| 1 | import * as Effect from "effect/Effect"; |
| 2 | import type * as Redacted from "effect/Redacted"; |
| 3 | import type * as HttpClient from "effect/http/HttpClient"; |
| 4 | import type { RuntimeContext } from "../../RuntimeContext.ts"; |
| 5 | import { AccountApiToken } from "../ApiToken/AccountApiToken.ts"; |
| 6 | import type { PermissionGroupRef } from "../ApiToken/Common.ts"; |
| 7 | import { CloudflareEnvironment } from "../CloudflareEnvironment.ts"; |
| 8 | import type { Credentials } from "../Credentials.ts"; |
| 9 | import { authorizeWith } from "../HttpClientUtils.ts"; |
| 10 | import { Worker } from "../Workers/Worker.ts"; |
| 11 | |
| 12 | /** |
| 13 | * Shared runtime body for a tunnel binding. Mints a scoped |
| 14 | * {@link AccountApiToken} (with the given permission groups), attaches the |
| 15 | * narrow allow-policy to it (guarded by the runtime flag so it is a no-op once |
| 16 | * deployed), binds the token's outputs into the Worker, then builds the client. |
| 17 | * |
| 18 | * Pass the result to `Layer.effect(<Callable>, ...)`. |
| 19 | */ |
| 20 | export const makeTunnelClient = <C>( |
| 21 | sid: string, |
| 22 | permissionGroups: PermissionGroupRef[], |
| 23 | makeClient: (auth: TunnelAuth) => C, |
| 24 | ) => |
| 25 | Effect.gen(function* () { |
| 26 | const Token = yield* AccountApiToken; |
| 27 | const env = yield* CloudflareEnvironment; |
| 28 | |
| 29 | return Effect.fn(function* () { |
| 30 | const ctx = yield* Worker; |
| 31 | const token = yield* Token(`${ctx.LogicalId}Token`); |
| 32 | if (!globalThis.__ALCHEMY_RUNTIME__) { |
| 33 | const { accountId } = yield* env; |
| 34 | yield* token.bind(sid, { |
| 35 | policies: [ |
| 36 | { |
| 37 | effect: "allow", |
| 38 | permissionGroups, |
| 39 | resources: { |
| 40 | [`com.cloudflare.api.account.${accountId}`]: "*", |
| 41 | }, |
| 42 | }, |
| 43 | ], |
| 44 | }); |
| 45 | } |
| 46 | return makeClient(makeTunnelAuth(yield* bindTunnelToken(token))); |
| 47 | }); |
| 48 | }); |
| 49 | |
| 50 | /** |
| 51 | * Injectable auth for the tunnel client builders. Both the scoped-token |
| 52 | * (`*Binding`) and current-credentials (`*Local`) variants supply an |
| 53 | * `authorize` (which provides `Credentials` + `HttpClient` to a raw SDK op) |
| 54 | * and an `accountId`, so the client builders are agnostic to how creds are |
| 55 | * obtained. |
| 56 | */ |
| 57 | export interface TunnelAuth { |
| 58 | authorize: <A, E>( |
| 59 | eff: Effect.Effect<A, E, Credentials | HttpClient.HttpClient>, |
| 60 | ) => Effect.Effect<A, E, RuntimeContext>; |
| 61 | accountId: Effect.Effect<string>; |
| 62 | } |
| 63 | |
| 64 | /** Build a scoped-token {@link TunnelAuth} from a bound {@link Token}. */ |
| 65 | export const makeTunnelAuth = (token: Token): TunnelAuth => ({ |
| 66 | authorize: authorizeWith(token), |
| 67 | accountId: token.accountId, |
| 68 | }); |
| 69 | |
| 70 | /** |
| 71 | * Runtime accessors for a tunnel binding's token, obtained by binding the |
| 72 | * {@link AccountApiToken}'s outputs in the Worker's Init phase. Each accessor |
| 73 | * reads the value back from the Worker's environment at runtime. |
| 74 | */ |
| 75 | export interface Token { |
| 76 | /** The token's plaintext value (injected as a `secret_text` binding). */ |
| 77 | value: Effect.Effect<Redacted.Redacted<string>>; |
| 78 | /** The account id the token is scoped to. */ |
| 79 | accountId: Effect.Effect<string>; |
| 80 | } |
| 81 | |
| 82 | /** |
| 83 | * Bind an {@link AccountApiToken}'s outputs into the Worker so they can be read |
| 84 | * at runtime: `token.value` is injected as a `secret_text` binding and |
| 85 | * `token.accountId` as `plain_text`. Returns the {@link Token} accessors. |
| 86 | */ |
| 87 | export const bindTunnelToken = (token: AccountApiToken) => |
| 88 | Effect.gen(function* () { |
| 89 | const value = yield* token.value; |
| 90 | const accountId = yield* token.accountId; |
| 91 | return { value, accountId } satisfies Token; |
| 92 | }); |