@voyantjs/workflows-orchestrator-cloudflare 0.6.7 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -26,30 +26,30 @@
26
26
  // See docs/runtime-protocol.md §2 and docs/design.md §6 for the
27
27
  // design this adapter implements.
28
28
 
29
- export * from "./types.js";
30
- export { createDurableObjectRunStore } from "./do-store.js";
29
+ export {
30
+ type BundleLocation,
31
+ type CfContainerRunnerDeps,
32
+ type ContainerNamespaceLike,
33
+ createCfContainerStepRunner,
34
+ } from "./cf-container-runner.js"
31
35
  export {
32
36
  createDispatchStepHandler,
33
37
  type DispatchHandlerDeps,
34
- } from "./dispatch-handler.js";
38
+ } from "./dispatch-handler.js"
39
+ export { createDurableObjectRunStore } from "./do-store.js"
35
40
  export {
36
- handleDurableObjectRequest,
37
- handleDurableObjectAlarm,
38
41
  type DurableObjectDeps,
39
- } from "./durable-object.js";
40
- export {
41
- handleWorkerRequest,
42
- type WorkerFetchDeps,
43
- type DurableObjectNamespaceLike,
44
- } from "./worker.js";
45
- export {
46
- createCfContainerStepRunner,
47
- type BundleLocation,
48
- type CfContainerRunnerDeps,
49
- type ContainerNamespaceLike,
50
- } from "./cf-container-runner.js";
42
+ handleDurableObjectAlarm,
43
+ handleDurableObjectRequest,
44
+ } from "./durable-object.js"
51
45
  export {
52
46
  createR2Presigner,
53
- type R2PresignerOptions,
54
47
  type PresignArgs,
55
- } from "./r2-sign.js";
48
+ type R2PresignerOptions,
49
+ } from "./r2-sign.js"
50
+ export * from "./types.js"
51
+ export {
52
+ type DurableObjectNamespaceLike,
53
+ handleWorkerRequest,
54
+ type WorkerFetchDeps,
55
+ } from "./worker.js"
package/src/r2-sign.ts CHANGED
@@ -17,42 +17,44 @@
17
17
 
18
18
  export interface R2PresignerOptions {
19
19
  /** Cloudflare account id (32-char hex). */
20
- accountId: string;
20
+ accountId: string
21
21
  /** R2 Access Key ID from your CF dashboard — scope read-only. */
22
- accessKeyId: string;
22
+ accessKeyId: string
23
23
  /** R2 Secret Access Key. Store as a Worker Secret. */
24
- secretAccessKey: string;
24
+ secretAccessKey: string
25
25
  /** R2 bucket name. */
26
- bucket: string;
26
+ bucket: string
27
27
  }
28
28
 
29
29
  export interface PresignArgs {
30
30
  /** Object key, e.g. `"prj_42/v1/container.mjs"`. Leading `/` is optional. */
31
- key: string;
31
+ key: string
32
32
  /** Seconds until the URL stops being valid. Min 1, max 604800. */
33
- expiresIn: number;
33
+ expiresIn: number
34
34
  }
35
35
 
36
- export function createR2Presigner(opts: R2PresignerOptions): (args: PresignArgs) => Promise<string> {
37
- const host = `${opts.accountId}.r2.cloudflarestorage.com`;
38
- const region = "auto"; // R2's SigV4 region convention.
39
- const service = "s3";
36
+ export function createR2Presigner(
37
+ opts: R2PresignerOptions,
38
+ ): (args: PresignArgs) => Promise<string> {
39
+ const host = `${opts.accountId}.r2.cloudflarestorage.com`
40
+ const region = "auto" // R2's SigV4 region convention.
41
+ const service = "s3"
40
42
 
41
43
  return async ({ key, expiresIn }) => {
42
44
  if (expiresIn < 1 || expiresIn > 604_800) {
43
- throw new Error(`R2 presign: expiresIn must be 1..604800, got ${expiresIn}`);
45
+ throw new Error(`R2 presign: expiresIn must be 1..604800, got ${expiresIn}`)
44
46
  }
45
- const normalizedKey = key.replace(/^\/+/, "");
47
+ const normalizedKey = key.replace(/^\/+/, "")
46
48
  const encodedKey = normalizedKey
47
49
  .split("/")
48
50
  .map((seg) => encodeURIComponent(seg))
49
- .join("/");
51
+ .join("/")
50
52
 
51
- const now = new Date();
52
- const amzDate = toAmzDate(now);
53
- const shortDate = amzDate.slice(0, 8);
54
- const credentialScope = `${shortDate}/${region}/${service}/aws4_request`;
55
- const credential = `${opts.accessKeyId}/${credentialScope}`;
53
+ const now = new Date()
54
+ const amzDate = toAmzDate(now)
55
+ const shortDate = amzDate.slice(0, 8)
56
+ const credentialScope = `${shortDate}/${region}/${service}/aws4_request`
57
+ const credential = `${opts.accessKeyId}/${credentialScope}`
56
58
 
57
59
  const params: Array<[string, string]> = [
58
60
  ["X-Amz-Algorithm", "AWS4-HMAC-SHA256"],
@@ -60,11 +62,11 @@ export function createR2Presigner(opts: R2PresignerOptions): (args: PresignArgs)
60
62
  ["X-Amz-Date", amzDate],
61
63
  ["X-Amz-Expires", String(expiresIn)],
62
64
  ["X-Amz-SignedHeaders", "host"],
63
- ];
64
- params.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
65
+ ]
66
+ params.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
65
67
  const canonicalQuery = params
66
68
  .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
67
- .join("&");
69
+ .join("&")
68
70
 
69
71
  const canonicalRequest = [
70
72
  "GET",
@@ -73,62 +75,57 @@ export function createR2Presigner(opts: R2PresignerOptions): (args: PresignArgs)
73
75
  `host:${host}\n`,
74
76
  "host",
75
77
  "UNSIGNED-PAYLOAD",
76
- ].join("\n");
78
+ ].join("\n")
77
79
 
78
80
  const stringToSign = [
79
81
  "AWS4-HMAC-SHA256",
80
82
  amzDate,
81
83
  credentialScope,
82
84
  await sha256Hex(canonicalRequest),
83
- ].join("\n");
84
-
85
- const signingKey = await deriveSigningKey(
86
- opts.secretAccessKey,
87
- shortDate,
88
- region,
89
- service,
90
- );
91
- const signature = toHex(await hmac(signingKey, stringToSign));
92
-
93
- return `https://${host}/${opts.bucket}/${encodedKey}?${canonicalQuery}&X-Amz-Signature=${signature}`;
94
- };
85
+ ].join("\n")
86
+
87
+ const signingKey = await deriveSigningKey(opts.secretAccessKey, shortDate, region, service)
88
+ const signature = toHex(await hmac(signingKey, stringToSign))
89
+
90
+ return `https://${host}/${opts.bucket}/${encodedKey}?${canonicalQuery}&X-Amz-Signature=${signature}`
91
+ }
95
92
  }
96
93
 
97
94
  // ---- Crypto helpers ----
98
95
 
99
96
  function toAmzDate(d: Date): string {
100
- const yyyy = d.getUTCFullYear();
101
- const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
102
- const dd = String(d.getUTCDate()).padStart(2, "0");
103
- const hh = String(d.getUTCHours()).padStart(2, "0");
104
- const mi = String(d.getUTCMinutes()).padStart(2, "0");
105
- const ss = String(d.getUTCSeconds()).padStart(2, "0");
106
- return `${yyyy}${mm}${dd}T${hh}${mi}${ss}Z`;
97
+ const yyyy = d.getUTCFullYear()
98
+ const mm = String(d.getUTCMonth() + 1).padStart(2, "0")
99
+ const dd = String(d.getUTCDate()).padStart(2, "0")
100
+ const hh = String(d.getUTCHours()).padStart(2, "0")
101
+ const mi = String(d.getUTCMinutes()).padStart(2, "0")
102
+ const ss = String(d.getUTCSeconds()).padStart(2, "0")
103
+ return `${yyyy}${mm}${dd}T${hh}${mi}${ss}Z`
107
104
  }
108
105
 
109
106
  function toHex(bytes: ArrayBuffer): string {
110
- const arr = new Uint8Array(bytes);
111
- let out = "";
112
- for (const b of arr) out += b.toString(16).padStart(2, "0");
113
- return out;
107
+ const arr = new Uint8Array(bytes)
108
+ let out = ""
109
+ for (const b of arr) out += b.toString(16).padStart(2, "0")
110
+ return out
114
111
  }
115
112
 
116
113
  async function sha256Hex(input: string): Promise<string> {
117
- const bytes = new TextEncoder().encode(input);
118
- const digest = await crypto.subtle.digest("SHA-256", bytes);
119
- return toHex(digest);
114
+ const bytes = new TextEncoder().encode(input)
115
+ const digest = await crypto.subtle.digest("SHA-256", bytes)
116
+ return toHex(digest)
120
117
  }
121
118
 
122
119
  async function hmac(key: ArrayBuffer | Uint8Array, msg: string): Promise<ArrayBuffer> {
123
- const keyBuf = key instanceof Uint8Array ? key.slice().buffer : key;
120
+ const keyBuf = key instanceof Uint8Array ? key.slice().buffer : key
124
121
  const cryptoKey = await crypto.subtle.importKey(
125
122
  "raw",
126
123
  keyBuf,
127
124
  { name: "HMAC", hash: "SHA-256" },
128
125
  false,
129
126
  ["sign"],
130
- );
131
- return crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(msg));
127
+ )
128
+ return crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(msg))
132
129
  }
133
130
 
134
131
  async function deriveSigningKey(
@@ -137,9 +134,9 @@ async function deriveSigningKey(
137
134
  region: string,
138
135
  service: string,
139
136
  ): Promise<ArrayBuffer> {
140
- const kDate = await hmac(new TextEncoder().encode(`AWS4${secret}`), shortDate);
141
- const kRegion = await hmac(kDate, region);
142
- const kService = await hmac(kRegion, service);
143
- const kSigning = await hmac(kService, "aws4_request");
144
- return kSigning;
137
+ const kDate = await hmac(new TextEncoder().encode(`AWS4${secret}`), shortDate)
138
+ const kRegion = await hmac(kDate, region)
139
+ const kService = await hmac(kRegion, service)
140
+ const kSigning = await hmac(kService, "aws4_request")
141
+ return kSigning
145
142
  }
package/src/types.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // a hard dependency on `@cloudflare/workers-types` — matching the
3
3
  // shape is enough, and tests can pass plain objects.
4
4
 
5
- import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator";
5
+ import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator"
6
6
 
7
7
  /**
8
8
  * Subset of Cloudflare's `DurableObjectStorage` we actually use.
@@ -15,16 +15,16 @@ import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator";
15
15
  * due — see `handleDurableObjectAlarm`.
16
16
  */
17
17
  export interface DurableObjectStorageLike {
18
- get<T>(key: string): Promise<T | undefined>;
19
- put<T>(key: string, value: T): Promise<void>;
20
- delete(key: string): Promise<boolean>;
21
- list<T>(options?: { prefix?: string; limit?: number }): Promise<Map<string, T>>;
18
+ get<T>(key: string): Promise<T | undefined>
19
+ put<T>(key: string, value: T): Promise<void>
20
+ delete(key: string): Promise<boolean>
21
+ list<T>(options?: { prefix?: string; limit?: number }): Promise<Map<string, T>>
22
22
  /** ms-since-epoch of the scheduled alarm, or null if none. */
23
- getAlarm?(): Promise<number | null>;
23
+ getAlarm?(): Promise<number | null>
24
24
  /** Schedule the DO's alarm() method to fire at `wakeAt`. */
25
- setAlarm?(wakeAt: number): Promise<void>;
25
+ setAlarm?(wakeAt: number): Promise<void>
26
26
  /** Cancel any pending alarm. */
27
- deleteAlarm?(): Promise<void>;
27
+ deleteAlarm?(): Promise<void>
28
28
  }
29
29
 
30
30
  /**
@@ -33,42 +33,45 @@ export interface DurableObjectStorageLike {
33
33
  * registered under that name.
34
34
  */
35
35
  export interface DispatchNamespaceLike {
36
- get(name: string, args?: Record<string, unknown>): {
37
- fetch(request: Request): Promise<Response>;
38
- };
36
+ get(
37
+ name: string,
38
+ args?: Record<string, unknown>,
39
+ ): {
40
+ fetch(request: Request): Promise<Response>
41
+ }
39
42
  }
40
43
 
41
44
  /** Args the Worker passes when routing to a run DO. */
42
45
  export interface RunOperation {
43
- op: "trigger" | "resume" | "cancel" | "get";
44
- payload?: unknown;
46
+ op: "trigger" | "resume" | "cancel" | "get"
47
+ payload?: unknown
45
48
  }
46
49
 
47
50
  /** Injection payload on resume ops. */
48
51
  export interface ResumePayload {
49
- injection: WaitpointInjection;
52
+ injection: WaitpointInjection
50
53
  }
51
54
 
52
55
  /** Trigger payload. */
53
56
  export interface TriggerPayload {
54
- workflowId: string;
55
- workflowVersion: string;
56
- input: unknown;
57
+ workflowId: string
58
+ workflowVersion: string
59
+ input: unknown
57
60
  tenantMeta: {
58
- tenantId: string;
59
- projectId: string;
60
- organizationId: string;
61
+ tenantId: string
62
+ projectId: string
63
+ organizationId: string
61
64
  /** Dispatch-namespace name to forward step requests to. */
62
- tenantScript: string;
63
- };
64
- environment?: "production" | "preview" | "development";
65
- tags?: string[];
66
- runId?: string;
65
+ tenantScript: string
66
+ }
67
+ environment?: "production" | "preview" | "development"
68
+ tags?: string[]
69
+ runId?: string
67
70
  }
68
71
 
69
72
  /** Cancel payload. */
70
73
  export interface CancelPayload {
71
- reason?: string;
74
+ reason?: string
72
75
  }
73
76
 
74
77
  /**
@@ -77,7 +80,7 @@ export interface CancelPayload {
77
80
  */
78
81
  export interface AdapterEnv<DONamespace = unknown, DispatchNS = DispatchNamespaceLike> {
79
82
  /** Durable Object namespace holding one DO per run. Typed loosely to avoid a CF types dep. */
80
- WORKFLOW_RUN_DO: DONamespace;
83
+ WORKFLOW_RUN_DO: DONamespace
81
84
  /** Dispatch namespace containing tenant Workers. */
82
- DISPATCHER: DispatchNS;
85
+ DISPATCHER: DispatchNS
83
86
  }
package/src/worker.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  // POST /api/runs/:id/tokens/:token → inject a MANUAL (token) waitpoint
12
12
  // POST /api/runs/:id/cancel → cancel a run
13
13
 
14
- import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator";
14
+ import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator"
15
15
 
16
16
  /**
17
17
  * Minimal shape of a DO namespace. `idFromName` returns an opaque id;
@@ -19,80 +19,80 @@ import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator";
19
19
  * Typed loosely so tests can pass any matching object.
20
20
  */
21
21
  export interface DurableObjectNamespaceLike<Id = unknown> {
22
- idFromName(name: string): Id;
23
- get(id: Id): { fetch(req: Request): Promise<Response> };
22
+ idFromName(name: string): Id
23
+ get(id: Id): { fetch(req: Request): Promise<Response> }
24
24
  }
25
25
 
26
26
  export interface WorkerFetchDeps<Id = unknown> {
27
- runDO: DurableObjectNamespaceLike<Id>;
27
+ runDO: DurableObjectNamespaceLike<Id>
28
28
  /**
29
29
  * Called before any routing. Throws/rejects to reject the request.
30
30
  * Typical implementation validates a tenant access token.
31
31
  */
32
- verifyRequest?: (req: Request) => void | Promise<void>;
32
+ verifyRequest?: (req: Request) => void | Promise<void>
33
33
  /** Optional logger. */
34
- logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void;
34
+ logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void
35
35
  /** id generator for new triggers; defaults to `run_<random>`. */
36
- idGenerator?: () => string;
36
+ idGenerator?: () => string
37
37
  /** Injectable clock for id generation. */
38
- now?: () => number;
38
+ now?: () => number
39
39
  }
40
40
 
41
41
  export async function handleWorkerRequest<Id>(
42
42
  req: Request,
43
43
  deps: WorkerFetchDeps<Id>,
44
44
  ): Promise<Response> {
45
- const url = new URL(req.url);
45
+ const url = new URL(req.url)
46
46
 
47
47
  if (req.method === "OPTIONS") {
48
48
  return new Response(null, {
49
49
  status: 204,
50
50
  headers: corsHeaders("GET,POST,OPTIONS"),
51
- });
51
+ })
52
52
  }
53
53
 
54
54
  try {
55
- if (deps.verifyRequest) await deps.verifyRequest(req);
55
+ if (deps.verifyRequest) await deps.verifyRequest(req)
56
56
  } catch (err) {
57
57
  return json(401, {
58
58
  error: "unauthorized",
59
59
  message: err instanceof Error ? err.message : String(err),
60
- });
60
+ })
61
61
  }
62
62
 
63
63
  // POST /api/runs — trigger a new run.
64
64
  if (req.method === "POST" && url.pathname === "/api/runs") {
65
- let payload: Record<string, unknown>;
65
+ let payload: Record<string, unknown>
66
66
  try {
67
- payload = (await req.json()) as Record<string, unknown>;
67
+ payload = (await req.json()) as Record<string, unknown>
68
68
  } catch (err) {
69
- return json(400, { error: "invalid_json", message: errMsg(err) });
69
+ return json(400, { error: "invalid_json", message: errMsg(err) })
70
70
  }
71
- const runId = typeof payload.runId === "string" ? payload.runId : defaultRunId(deps);
71
+ const runId = typeof payload.runId === "string" ? payload.runId : defaultRunId(deps)
72
72
  const forward = new Request(`https://do-internal/trigger`, {
73
73
  method: "POST",
74
74
  headers: { "content-type": "application/json" },
75
75
  body: JSON.stringify({ ...payload, runId }),
76
- });
77
- return forwardToRunDO(runId, forward, deps);
76
+ })
77
+ return forwardToRunDO(runId, forward, deps)
78
78
  }
79
79
 
80
80
  // Everything below operates on a specific runId.
81
- const runMatch = url.pathname.match(/^\/api\/runs\/([^/]+)(\/.+)?$/);
81
+ const runMatch = url.pathname.match(/^\/api\/runs\/([^/]+)(\/.+)?$/)
82
82
  if (!runMatch) {
83
- return json(404, { error: "route_not_found", path: url.pathname });
83
+ return json(404, { error: "route_not_found", path: url.pathname })
84
84
  }
85
- const runId = decodeURIComponent(runMatch[1]!);
86
- const tail = runMatch[2] ?? "";
85
+ const runId = decodeURIComponent(runMatch[1]!)
86
+ const tail = runMatch[2] ?? ""
87
87
 
88
88
  if (req.method === "GET" && tail === "") {
89
- const forward = new Request(`https://do-internal/get`, { method: "GET" });
90
- return forwardToRunDO(runId, forward, deps);
89
+ const forward = new Request(`https://do-internal/get`, { method: "GET" })
90
+ return forwardToRunDO(runId, forward, deps)
91
91
  }
92
92
 
93
93
  if (req.method === "POST" && tail === "/cancel") {
94
- const body = await safeJson(req);
95
- if (isErrorBody(body)) return json(400, body);
94
+ const body = await safeJson(req)
95
+ if (isErrorBody(body)) return json(400, body)
96
96
  return forwardToRunDO(
97
97
  runId,
98
98
  new Request(`https://do-internal/cancel`, {
@@ -101,14 +101,14 @@ export async function handleWorkerRequest<Id>(
101
101
  body: JSON.stringify(body),
102
102
  }),
103
103
  deps,
104
- );
104
+ )
105
105
  }
106
106
 
107
107
  // Waitpoint injections: events, signals, tokens.
108
- const body = await safeJson(req);
109
- if (isErrorBody(body)) return json(400, body);
110
- const injection = parseInjection(tail, body);
111
- if ("error" in injection) return json(400, injection);
108
+ const body = await safeJson(req)
109
+ if (isErrorBody(body)) return json(400, body)
110
+ const injection = parseInjection(tail, body)
111
+ if ("error" in injection) return json(400, injection)
112
112
  return forwardToRunDO(
113
113
  runId,
114
114
  new Request(`https://do-internal/resume`, {
@@ -117,13 +117,13 @@ export async function handleWorkerRequest<Id>(
117
117
  body: JSON.stringify({ injection: injection.injection }),
118
118
  }),
119
119
  deps,
120
- );
120
+ )
121
121
  }
122
122
 
123
123
  function isErrorBody(
124
124
  body: Record<string, unknown> | { error: string; message: string },
125
125
  ): body is { error: string; message: string } {
126
- return typeof (body as { error?: unknown }).error === "string";
126
+ return typeof (body as { error?: unknown }).error === "string"
127
127
  }
128
128
 
129
129
  function parseInjection(
@@ -132,19 +132,19 @@ function parseInjection(
132
132
  ): { injection: WaitpointInjection } | { error: string; message: string } {
133
133
  if (tail === "/events") {
134
134
  if (typeof body.eventType !== "string" || body.eventType.length === 0) {
135
- return { error: "invalid_body", message: "`eventType` (string) is required" };
135
+ return { error: "invalid_body", message: "`eventType` (string) is required" }
136
136
  }
137
137
  return {
138
138
  injection: { kind: "EVENT", eventType: body.eventType, payload: body.payload },
139
- };
139
+ }
140
140
  }
141
141
  if (tail === "/signals") {
142
142
  if (typeof body.name !== "string" || body.name.length === 0) {
143
- return { error: "invalid_body", message: "`name` (string) is required" };
143
+ return { error: "invalid_body", message: "`name` (string) is required" }
144
144
  }
145
- return { injection: { kind: "SIGNAL", name: body.name, payload: body.payload } };
145
+ return { injection: { kind: "SIGNAL", name: body.name, payload: body.payload } }
146
146
  }
147
- const tokenMatch = tail.match(/^\/tokens\/([^/]+)$/);
147
+ const tokenMatch = tail.match(/^\/tokens\/([^/]+)$/)
148
148
  if (tokenMatch) {
149
149
  return {
150
150
  injection: {
@@ -152,9 +152,9 @@ function parseInjection(
152
152
  tokenId: decodeURIComponent(tokenMatch[1]!),
153
153
  payload: body.payload,
154
154
  },
155
- };
155
+ }
156
156
  }
157
- return { error: "route_not_found", message: `unknown path suffix ${tail}` };
157
+ return { error: "route_not_found", message: `unknown path suffix ${tail}` }
158
158
  }
159
159
 
160
160
  async function forwardToRunDO<Id>(
@@ -162,36 +162,38 @@ async function forwardToRunDO<Id>(
162
162
  req: Request,
163
163
  deps: WorkerFetchDeps<Id>,
164
164
  ): Promise<Response> {
165
- const id = deps.runDO.idFromName(runId);
166
- const stub = deps.runDO.get(id);
167
- const resp = await stub.fetch(req);
165
+ const id = deps.runDO.idFromName(runId)
166
+ const stub = deps.runDO.get(id)
167
+ const resp = await stub.fetch(req)
168
168
  // Add CORS on outbound responses.
169
- const out = new Response(resp.body, resp);
169
+ const out = new Response(resp.body, resp)
170
170
  for (const [k, v] of Object.entries(corsHeaders("GET,POST,OPTIONS"))) {
171
- out.headers.set(k, v);
171
+ out.headers.set(k, v)
172
172
  }
173
- return out;
173
+ return out
174
174
  }
175
175
 
176
176
  function defaultRunId<Id>(deps: WorkerFetchDeps<Id>): string {
177
- if (deps.idGenerator) return deps.idGenerator();
178
- const now = deps.now ?? (() => Date.now());
179
- const ts = now().toString(36);
180
- const rand = Math.floor(Math.random() * 1_000_000).toString(36).padStart(4, "0");
181
- return `run_${ts}_${rand}`;
177
+ if (deps.idGenerator) return deps.idGenerator()
178
+ const now = deps.now ?? (() => Date.now())
179
+ const ts = now().toString(36)
180
+ const rand = Math.floor(Math.random() * 1_000_000)
181
+ .toString(36)
182
+ .padStart(4, "0")
183
+ return `run_${ts}_${rand}`
182
184
  }
183
185
 
184
186
  async function safeJson(
185
187
  req: Request,
186
188
  ): Promise<Record<string, unknown> | { error: string; message: string }> {
187
189
  // Some requests are bodyless (GET). Only parse when we have a body.
188
- if (req.method === "GET" || req.method === "HEAD") return {};
189
- const text = await req.text();
190
- if (text.length === 0) return {};
190
+ if (req.method === "GET" || req.method === "HEAD") return {}
191
+ const text = await req.text()
192
+ if (text.length === 0) return {}
191
193
  try {
192
- return JSON.parse(text) as Record<string, unknown>;
194
+ return JSON.parse(text) as Record<string, unknown>
193
195
  } catch (err) {
194
- return { error: "invalid_json", message: errMsg(err) };
196
+ return { error: "invalid_json", message: errMsg(err) }
195
197
  }
196
198
  }
197
199
 
@@ -200,7 +202,7 @@ function corsHeaders(methods: string): Record<string, string> {
200
202
  "access-control-allow-origin": "*",
201
203
  "access-control-allow-methods": methods,
202
204
  "access-control-allow-headers": "content-type, x-voyant-protocol",
203
- };
205
+ }
204
206
  }
205
207
 
206
208
  function json(status: number, body: unknown): Response {
@@ -210,9 +212,9 @@ function json(status: number, body: unknown): Response {
210
212
  "content-type": "application/json; charset=utf-8",
211
213
  ...corsHeaders("GET,POST,OPTIONS"),
212
214
  },
213
- });
215
+ })
214
216
  }
215
217
 
216
218
  function errMsg(err: unknown): string {
217
- return err instanceof Error ? err.message : String(err);
219
+ return err instanceof Error ? err.message : String(err)
218
220
  }