1import * as Effect from "effect/Effect";
2import type * as Redacted from "effect/Redacted";
3import type * as HttpClient from "effect/http/HttpClient";
4import type { RuntimeContext } from "../../RuntimeContext.ts";
5import { AccountApiToken } from "../ApiToken/AccountApiToken.ts";
6import type { PermissionGroupRef } from "../ApiToken/Common.ts";
7import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
8import type { Credentials } from "../Credentials.ts";
9import { authorizeWith } from "../HttpClientUtils.ts";
10import { 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 */
20export 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 */
57export 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}. */
65export 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 */
75export 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 */
87export 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 });