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 { Self } from "../../Self.ts";
6import { AccountApiToken } from "../ApiToken/AccountApiToken.ts";
7import type { PermissionGroupRef } from "../ApiToken/Common.ts";
8import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
9import type { Credentials } from "../Credentials.ts";
10import type { Queue } from "./Queue.ts";
11import { SendError } from "./QueueTypes.ts";
12
13/**
14 * Injectable auth used by the Queue HTTP client builder. Both the
15 * scoped-token HTTP variant ({@link makeWriteQueueHttpClient}) and the
16 * current-credentials Local variant build this so they share the exact
17 * same request path — only the way credentials reach the SDK op differs.
18 *
19 * - `authorize` runs a raw distilled op (which needs
20 * `Credentials | HttpClient`) and discharges those requirements down to
21 * {@link RuntimeContext}. The HTTP variant provides a minted token; the
22 * Local variant provides the ambient current-credentials context.
23 * - `accountId` resolves the Cloudflare account the queue lives in.
24 */
25export interface QueueAuth {
26 authorize: <A, E>(
27 eff: Effect.Effect<A, E, Credentials | HttpClient.HttpClient>,
28 ) => Effect.Effect<A, E, RuntimeContext>;
29 accountId: Effect.Effect<string>;
30}
31
32/**
33 * Shared scaffolding for the HTTP-backed Queue services.
34 *
35 * Creates a scoped {@link AccountApiToken}, binds its `value` /
36 * `accountId` into the host Worker at deploy time, then delegates to
37 * `makeClient` with the bound token and the queue's `queueId`.
38 */
39export const makeHttpQueueBinding = <Client>(options: {
40 permissionGroups: PermissionGroup[];
41 makeClient: (token: HttpToken, queueId: Effect.Effect<string>) => Client;
42}) =>
43 Effect.gen(function* () {
44 const Token = yield* AccountApiToken;
45 const self = yield* Self;
46 const env = yield* CloudflareEnvironment;
47
48 return Effect.fn(function* (queue: Queue) {
49 const { accountId } = yield* env;
50 const token = yield* Token(`${self.LogicalId}Token`);
51 if (!globalThis.__ALCHEMY_RUNTIME__) {
52 yield* token.bind`${queue.LogicalId}`({
53 policies: [
54 {
55 effect: "allow",
56 permissionGroups: options.permissionGroups,
57 resources: {
58 [`com.cloudflare.api.account.${accountId}`]: "*",
59 },
60 },
61 ],
62 });
63 }
64 const bound = {
65 value: yield* token.value,
66 accountId: yield* token.accountId,
67 } satisfies HttpToken;
68 const queueId = yield* queue.queueId;
69 return options.makeClient(bound, queueId);
70 });
71 });
72
73/** Resolve the account and queue id once per operation. */
74export const makeQueueHttpScope = (
75 auth: QueueAuth,
76 queueId: Effect.Effect<string>,
77): Effect.Effect<HttpScope> =>
78 Effect.gen(function* () {
79 const accountId = yield* auth.accountId;
80 const id = yield* queueId;
81 return { accountId, queueId: id };
82 });
83
84export const toQueueSendError = (error: unknown): SendError =>
85 new SendError({
86 message:
87 typeof error === "object" && error !== null && "message" in error
88 ? String((error as { message: unknown }).message)
89 : "Unknown queue error",
90 cause: error,
91 });
92
93export interface HttpToken {
94 value: Effect.Effect<Redacted.Redacted<string>>;
95 accountId: Effect.Effect<string>;
96}
97
98export interface HttpScope {
99 accountId: string;
100 queueId: string;
101}
102
103const QUEUE_HTTP_PERMISSION_GROUPS: PermissionGroupRef[] = [
104 "Queues Read",
105 "Queues Write",
106];
107
108type PermissionGroup = (typeof QUEUE_HTTP_PERMISSION_GROUPS)[number];