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 * as Output from "../../Output.ts";
6import { Self } from "../../Self.ts";
7import { AccountApiToken } from "../ApiToken/AccountApiToken.ts";
8import type { PermissionGroupRef } from "../ApiToken/Common.ts";
9import type { Credentials } from "../Credentials.ts";
10import { authorizeWith } from "../HttpClientUtils.ts";
11import type { Zone } from "../Zone/Zone.ts";
12
13/**
14 * Shared scaffolding for the HTTP-backed DNS bindings.
15 *
16 * Creates a least-privilege {@link AccountApiToken} scoped to the requested
17 * zone, binds its `value` into the host Worker at deploy time (guarded by
18 * `__ALCHEMY_RUNTIME__` so it is a no-op once running inside the deployed
19 * Worker), then delegates to `makeClient` with the bound token and the zone's
20 * `zoneId`.
21 *
22 * The zone's `zoneId` is bound into the Worker at init time, so the resulting
23 * client closes over it and callers never pass it per request — the
24 * provisioned token only grants access to that one zone anyway.
25 */
26export const makeHttpDnsBinding = <Client>(options: {
27 permissionGroups: PermissionGroupRef[];
28 makeClient: (auth: DnsAuth, zoneId: Effect.Effect<string>) => Client;
29}) =>
30 Effect.gen(function* () {
31 const Token = yield* AccountApiToken;
32 const self = yield* Self;
33
34 return Effect.fn(function* (zone: Zone) {
35 const token = yield* Token(`${self.LogicalId}Token`);
36 if (!globalThis.__ALCHEMY_RUNTIME__) {
37 yield* token.bind`${self.LogicalId}(${zone})`({
38 policies: [
39 {
40 effect: "allow",
41 permissionGroups: options.permissionGroups,
42 resources: zone.zoneId.pipe(
43 Output.flatMap(
44 (zoneId) =>
45 Output.interpolate`com.cloudflare.api.account.zone.${zoneId}`,
46 ),
47 Output.map((zoneId) => ({
48 [zoneId]: "*",
49 })),
50 ),
51 },
52 ],
53 });
54 }
55 const bound = {
56 value: yield* token.value,
57 } satisfies Token;
58 const zoneId = yield* zone.zoneId;
59 return options.makeClient(makeDnsAuth(bound), zoneId);
60 });
61 });
62
63/**
64 * Injectable auth for the DNS HTTP client builders. Both the scoped-token
65 * (`*Http`) and current-credentials (`*Local`) variants supply an `authorize`
66 * that provides `Credentials` + `HttpClient` to a raw SDK op, so the client
67 * builders are agnostic to how creds are obtained. DNS record ops are
68 * zone-scoped (the `zoneId` is passed alongside), so no `accountId` is needed.
69 */
70export interface DnsAuth {
71 authorize: <A, E>(
72 eff: Effect.Effect<A, E, Credentials | HttpClient.HttpClient>,
73 ) => Effect.Effect<A, E, RuntimeContext>;
74}
75
76/** Build a scoped-token {@link DnsAuth} from a bound {@link Token}. */
77export const makeDnsAuth = (token: Token): DnsAuth => ({
78 authorize: authorizeWith(token),
79});
80
81/**
82 * Runtime accessor for a DNS binding's token, obtained by binding the
83 * {@link AccountApiToken}'s `value` output in the Worker's Init phase. Reads the
84 * value back from the Worker's environment at runtime. DNS record operations
85 * are zone-scoped (the `zoneId` is passed per call), so the account id is not
86 * needed at runtime.
87 */
88export interface Token {
89 /** The token's plaintext value (injected as a `secret_text` binding). */
90 value: Effect.Effect<Redacted.Redacted<string>>;
91}