@voyantjs/workflows-cloud-adapter 0.37.0

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/LICENSE ADDED
@@ -0,0 +1 @@
1
+ Apache-2.0
package/NOTICE ADDED
@@ -0,0 +1,4 @@
1
+ @voyantjs/workflows-cloud-adapter
2
+ Copyright 2026 Voyant
3
+
4
+ Licensed under the Apache License, Version 2.0.
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # @voyantjs/workflows-cloud-adapter
2
+
3
+ Tenant Worker adapter for Voyant Cloud Workflows projects. It wraps the
4
+ lower-level Cloudflare orchestrator primitives so a workflow Worker can
5
+ export the public `/api/*` run surface and `WorkflowRunDO` without
6
+ hand-wiring dispatchers, step handlers, R2 bundle signing, or local
7
+ fallback behavior.
8
+
9
+ ## Worker entry
10
+
11
+ ```ts
12
+ import "./workflows";
13
+ import { createCloudOrchestrator } from "@voyantjs/workflows-cloud-adapter";
14
+
15
+ export default createCloudOrchestrator();
16
+ export { WorkflowRunDO } from "@voyantjs/workflows-cloud-adapter";
17
+ ```
18
+
19
+ If your build exports a workflow bundle value, passing it is harmless;
20
+ workflow registration still happens through module imports:
21
+
22
+ ```ts
23
+ import workflows from "./workflows";
24
+ import { createCloudOrchestrator } from "@voyantjs/workflows-cloud-adapter";
25
+
26
+ export const { fetch, WorkflowRunDO } = createCloudOrchestrator(workflows);
27
+ ```
28
+
29
+ When passing adapter options such as `services`, `now`, or `logger`,
30
+ export `WorkflowRunDO` from the returned object as shown above. The
31
+ returned class is bound to the same options used by the Worker fetch
32
+ handler.
33
+
34
+ ## Hybrid apps
35
+
36
+ For Hono/itty-style apps, mount the workflows routes alongside your
37
+ existing routes:
38
+
39
+ ```ts
40
+ import { Hono } from "hono";
41
+ import "./workflows";
42
+ import { mountWorkflows } from "@voyantjs/workflows-cloud-adapter";
43
+
44
+ const app = new Hono<{ Bindings: Env }>();
45
+
46
+ app.get("/health", (c) => c.json({ ok: true }));
47
+ mountWorkflows(app);
48
+
49
+ export default app;
50
+ export { WorkflowRunDO } from "@voyantjs/workflows-cloud-adapter";
51
+ ```
52
+
53
+ `mountWorkflows(app)` registers `/api/*` when the app exposes
54
+ `all(path, handler)`. If the app only exposes `fetch`, the adapter wraps
55
+ that fetch method and intercepts `/api/*`.
56
+
57
+ ## Runtime behavior
58
+
59
+ - When `STEP_RUNNER` is present and bundle env vars are configured,
60
+ `runtime: "node"` steps dispatch to the platform step-runner
61
+ Container fleet. The binding may point at the shared
62
+ `voyant-step-runner` Worker or a platform-operated per-org dedicated
63
+ runner; the adapter does not distinguish between them.
64
+ - When `STEP_RUNNER` is absent, `runtime: "node"` steps run inline in
65
+ the tenant Worker isolate. This keeps `wrangler dev` usable without
66
+ Docker, R2, or platform-injected bindings.
67
+ - Edge steps always run in the tenant Worker isolate.
68
+
69
+ ## Tenant wrangler.jsonc
70
+
71
+ Tenants author the run Durable Object binding. Voyant Cloud overlays the
72
+ platform bindings and secrets at publish time.
73
+
74
+ ```jsonc
75
+ {
76
+ "name": "my-voyant-workflows",
77
+ "main": "src/worker.ts",
78
+ "compatibility_date": "2026-05-01",
79
+ "durable_objects": {
80
+ "bindings": [
81
+ {
82
+ "name": "WORKFLOW_RUN_DO",
83
+ "class_name": "WorkflowRunDO"
84
+ }
85
+ ]
86
+ },
87
+ "migrations": [
88
+ {
89
+ "tag": "v1",
90
+ "new_sqlite_classes": ["WorkflowRunDO"]
91
+ }
92
+ ]
93
+ }
94
+ ```
95
+
96
+ At publish time the platform injects the step-runner namespace binding:
97
+
98
+ ```jsonc
99
+ {
100
+ "durable_objects": {
101
+ "bindings": [
102
+ {
103
+ "name": "STEP_RUNNER",
104
+ "class_name": "StepRunner",
105
+ "script_name": "voyant-step-runner"
106
+ }
107
+ // Enterprise tenants may receive script_name:
108
+ // "voyant-step-runner-{org}".
109
+ ]
110
+ }
111
+ }
112
+ ```
113
+
114
+ ## Env contract
115
+
116
+ | Name | Required | Source | Purpose |
117
+ |---|---:|---|---|
118
+ | `WORKFLOW_RUN_DO` | yes | Tenant wrangler | Durable Object namespace for per-run state. |
119
+ | `STEP_RUNNER` | production node steps | Platform-injected | Durable Object namespace for the shared or dedicated step-runner Container fleet. |
120
+ | `WORKFLOW_MANIFESTS` | optional | Tenant/platform | KV namespace enabling `/api/manifests*` and `/api/events`. |
121
+ | `VOYANT_API_TOKENS` | production API | Tenant/platform | Comma-separated bearer tokens for public `/api/*` access. |
122
+ | `VOYANT_WORKFLOW_BUNDLE_URL_PREFIX` | with `STEP_RUNNER` | Platform-injected | R2 S3 API prefix: `https://<account>.r2.cloudflarestorage.com/<bucket>`. |
123
+ | `VOYANT_WORKFLOW_BUNDLE_KEY` | with `STEP_RUNNER` | Platform-injected | R2 object key for this version's `container.mjs`. |
124
+ | `VOYANT_WORKFLOW_BUNDLE_HASH` | with `STEP_RUNNER` | Platform-injected | SHA-256 hash for the bundle bytes. |
125
+ | `VOYANT_WORKFLOW_BUNDLE_R2_ACCESS_KEY_ID` | with `STEP_RUNNER` | Secret | Read-only R2 access key id. |
126
+ | `VOYANT_WORKFLOW_BUNDLE_R2_SECRET_ACCESS_KEY` | with `STEP_RUNNER` | Secret | Read-only R2 secret access key. |
127
+ | `VOYANT_WORKFLOW_STEP_AUTH_SECRET` | recommended with `STEP_RUNNER` | Secret | HMAC secret for `x-voyant-step-auth` on step dispatches. |
128
+ | `VOYANT_WORKFLOW_BUNDLE_URL_TTL_SECONDS` | optional | Platform-injected | Signed bundle URL TTL. Defaults to `300`. |
129
+
130
+ `VOYANT_WORKFLOW_BUNDLE_R2_ACCOUNT_ID` and
131
+ `VOYANT_WORKFLOW_BUNDLE_R2_BUCKET` can override the account id and
132
+ bucket parsed from `VOYANT_WORKFLOW_BUNDLE_URL_PREFIX`.
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@voyantjs/workflows-cloud-adapter",
3
+ "version": "0.37.0",
4
+ "description": "Tenant Worker adapter for Voyant Cloud Workflows deployments. Wires WorkflowRunDO, inline local dispatch, and platform step-runner bindings from env.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/voyantjs/voyant.git",
9
+ "directory": "packages/workflows-cloud-adapter"
10
+ },
11
+ "homepage": "https://voyant.cloud/workflows",
12
+ "type": "module",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./src/index.ts",
16
+ "import": "./src/index.ts"
17
+ }
18
+ },
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "files": [
22
+ "dist",
23
+ "src",
24
+ "!**/*.test.*",
25
+ "!**/*.spec.*",
26
+ "NOTICE"
27
+ ],
28
+ "scripts": {
29
+ "build": "pnpm -C ../.. --filter @voyantjs/workflows-orchestrator-cloudflare build && tsc -p tsconfig.json",
30
+ "check-types": "pnpm -C ../.. --filter @voyantjs/workflows-orchestrator-cloudflare build && tsc --noEmit",
31
+ "dev": "tsc -w -p tsconfig.json",
32
+ "test": "pnpm -C ../.. --filter @voyantjs/workflows-orchestrator-cloudflare build && vitest run",
33
+ "test:watch": "vitest"
34
+ },
35
+ "dependencies": {
36
+ "@voyantjs/workflows": "workspace:*",
37
+ "@voyantjs/workflows-orchestrator": "workspace:*",
38
+ "@voyantjs/workflows-orchestrator-cloudflare": "workspace:*"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.12.0",
42
+ "@voyantjs/voyant-typescript-config": "workspace:*",
43
+ "typescript": "^5.9.2",
44
+ "vitest": "^4.1.2"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "exports": {
49
+ ".": {
50
+ "types": "./dist/index.d.ts",
51
+ "import": "./dist/index.js"
52
+ }
53
+ },
54
+ "main": "./dist/index.js",
55
+ "types": "./dist/index.d.ts"
56
+ }
57
+ }
package/src/index.ts ADDED
@@ -0,0 +1,468 @@
1
+ // @voyantjs/workflows-cloud-adapter
2
+ //
3
+ // Tenant Worker adapter for Voyant Cloud's workflows runtime. The package
4
+ // keeps tenant entrypoints small while preserving the same Cloudflare
5
+ // Durable Object run model used by the lower-level orchestrator adapter.
6
+
7
+ import { createBearerVerifier, createHmacSigner } from "@voyantjs/workflows/auth"
8
+ import {
9
+ handleStepRequest,
10
+ type StepJournalEntry,
11
+ type StepRunner,
12
+ } from "@voyantjs/workflows/handler"
13
+ import { createInMemoryRateLimiter } from "@voyantjs/workflows/rate-limit"
14
+ import type { StepHandler } from "@voyantjs/workflows-orchestrator"
15
+ import {
16
+ type ContainerNamespaceLike,
17
+ createCfContainerStepRunner,
18
+ createInlineDispatcher,
19
+ createKvManifestStore,
20
+ createR2Presigner,
21
+ type DurableObjectNamespaceLike,
22
+ type DurableObjectStorageLike,
23
+ handleDurableObjectAlarm,
24
+ handleDurableObjectRequest,
25
+ handleWorkerRequest,
26
+ type KvNamespaceLike,
27
+ type StepDispatcher,
28
+ type WorkerFetchDeps,
29
+ } from "@voyantjs/workflows-orchestrator-cloudflare"
30
+
31
+ export interface CloudWorkflowsEnv {
32
+ /** Per-run Durable Object namespace declared by the tenant Worker. */
33
+ WORKFLOW_RUN_DO: DurableObjectNamespaceLike
34
+ /**
35
+ * Platform-injected namespace for the node step-runner Container fleet.
36
+ * The binding may target the shared platform fleet or a platform-operated
37
+ * per-org dedicated runner; the tenant adapter treats both the same.
38
+ */
39
+ STEP_RUNNER?: ContainerNamespaceLike
40
+ /**
41
+ * Optional KV namespace for workflow manifests. When present,
42
+ * `/api/manifests*` and `/api/events` are enabled.
43
+ */
44
+ WORKFLOW_MANIFESTS?: KvNamespaceLike
45
+ /**
46
+ * Comma-separated bearer tokens for public `/api/*` routes. Omit for local
47
+ * development only.
48
+ */
49
+ VOYANT_API_TOKENS?: string
50
+ /**
51
+ * Prefix for the R2 S3 API URL that hosts the container bundle.
52
+ * Expected form: `https://<account>.r2.cloudflarestorage.com/<bucket>`.
53
+ */
54
+ VOYANT_WORKFLOW_BUNDLE_URL_PREFIX?: string
55
+ /** R2 object key for this tenant Worker version's `container.mjs`. */
56
+ VOYANT_WORKFLOW_BUNDLE_KEY?: string
57
+ /** SHA-256 hex, or `sha256:<hex>`, for the container bundle bytes. */
58
+ VOYANT_WORKFLOW_BUNDLE_HASH?: string
59
+ /** R2 read-only access key id used to mint short-lived signed bundle URLs. */
60
+ VOYANT_WORKFLOW_BUNDLE_R2_ACCESS_KEY_ID?: string
61
+ /** R2 read-only secret access key used to mint short-lived signed bundle URLs. */
62
+ VOYANT_WORKFLOW_BUNDLE_R2_SECRET_ACCESS_KEY?: string
63
+ /** Optional explicit R2 account id. Defaults to parsing URL_PREFIX. */
64
+ VOYANT_WORKFLOW_BUNDLE_R2_ACCOUNT_ID?: string
65
+ /** Optional explicit R2 bucket. Defaults to parsing URL_PREFIX. */
66
+ VOYANT_WORKFLOW_BUNDLE_R2_BUCKET?: string
67
+ /** Optional signed URL TTL in seconds. Defaults to 300. */
68
+ VOYANT_WORKFLOW_BUNDLE_URL_TTL_SECONDS?: string
69
+ /** Shared secret used to sign dispatches to the platform step runner. */
70
+ VOYANT_WORKFLOW_STEP_AUTH_SECRET?: string
71
+ }
72
+
73
+ export interface DurableObjectStateLike {
74
+ storage: DurableObjectStorageLike
75
+ }
76
+
77
+ export interface CloudOrchestratorOptions<Env extends CloudWorkflowsEnv = CloudWorkflowsEnv> {
78
+ verifyRequest?: WorkerFetchDeps["verifyRequest"]
79
+ logger?: WorkerFetchDeps["logger"]
80
+ idGenerator?: WorkerFetchDeps["idGenerator"]
81
+ now?: () => number
82
+ tenantMeta?: WorkerFetchDeps["tenantMeta"]
83
+ services?: import("@voyantjs/workflows/driver").ServiceResolver
84
+ resolveEnv?: (env: Env) => Env
85
+ }
86
+
87
+ export interface CloudOrchestrator<Env extends CloudWorkflowsEnv = CloudWorkflowsEnv> {
88
+ fetch: (request: Request, env?: Env) => Promise<Response>
89
+ WorkflowRunDO: WorkflowRunDOClass<Env>
90
+ }
91
+
92
+ type EnvCache = {
93
+ dispatcher?: StepDispatcher
94
+ dispatcherOptions?: CloudExecutionOptions<CloudWorkflowsEnv>
95
+ stepHandler?: StepHandler
96
+ stepHandlerOptions?: CloudExecutionOptions<CloudWorkflowsEnv>
97
+ }
98
+
99
+ const envCache = new WeakMap<object, EnvCache>()
100
+ const defaultExecutionOptions: CloudExecutionOptions<CloudWorkflowsEnv> = {}
101
+
102
+ export type WorkflowRunDOClass<Env extends CloudWorkflowsEnv = CloudWorkflowsEnv> = new (
103
+ state: DurableObjectStateLike,
104
+ env: Env,
105
+ ) => WorkflowRunDO<Env>
106
+
107
+ type CloudExecutionOptions<Env extends CloudWorkflowsEnv = CloudWorkflowsEnv> = Pick<
108
+ CloudOrchestratorOptions<Env>,
109
+ "services" | "now" | "logger"
110
+ >
111
+
112
+ export function createCloudOrchestrator<Env extends CloudWorkflowsEnv = CloudWorkflowsEnv>(
113
+ workflows?: unknown,
114
+ boundEnv?: Env,
115
+ options: CloudOrchestratorOptions<Env> = {},
116
+ ): CloudOrchestrator<Env> {
117
+ void workflows
118
+ const WorkflowRunDOWithOptions = createWorkflowRunDOClass<Env>(options)
119
+
120
+ return {
121
+ fetch(request, requestEnv) {
122
+ const env = resolveBoundEnv(boundEnv, requestEnv, options)
123
+ return handleCloudFetch(request, env, options)
124
+ },
125
+ WorkflowRunDO: WorkflowRunDOWithOptions,
126
+ }
127
+ }
128
+
129
+ export function mountWorkflows<
130
+ App extends {
131
+ all?: (path: string, handler: (...args: unknown[]) => Response | Promise<Response>) => unknown
132
+ fetch?: (request: Request, env?: unknown, ctx?: unknown) => Response | Promise<Response>
133
+ },
134
+ Env extends CloudWorkflowsEnv = CloudWorkflowsEnv,
135
+ >(app: App, env?: Env, options: CloudOrchestratorOptions<Env> & { pathPrefix?: string } = {}): App {
136
+ const orchestrator = createCloudOrchestrator(undefined, env, options)
137
+ const pathPrefix = normalizePathPrefix(options.pathPrefix ?? "/api")
138
+
139
+ if (typeof app.all === "function") {
140
+ app.all(`${pathPrefix}/*`, (...args) => {
141
+ const request = extractRequest(args)
142
+ const requestEnv = extractEnv<Env>(args, env)
143
+ return orchestrator.fetch(request, requestEnv)
144
+ })
145
+ return app
146
+ }
147
+
148
+ if (typeof app.fetch === "function") {
149
+ const originalFetch = app.fetch.bind(app)
150
+ ;(app as { fetch: typeof app.fetch }).fetch = (request, requestEnv, ctx) => {
151
+ if (isMountedPath(new URL(request.url).pathname, pathPrefix)) {
152
+ return orchestrator.fetch(request, (requestEnv as Env | undefined) ?? env)
153
+ }
154
+ return originalFetch(request, requestEnv, ctx)
155
+ }
156
+ return app
157
+ }
158
+
159
+ throw new Error(
160
+ "mountWorkflows: app must expose either all(path, handler) or fetch(request, env)",
161
+ )
162
+ }
163
+
164
+ export async function handleCloudFetch<Env extends CloudWorkflowsEnv = CloudWorkflowsEnv>(
165
+ request: Request,
166
+ env: Env,
167
+ options: CloudOrchestratorOptions<Env> = {},
168
+ ): Promise<Response> {
169
+ const resolvedEnv = options.resolveEnv?.(env) ?? env
170
+ const tokens = (resolvedEnv.VOYANT_API_TOKENS ?? "")
171
+ .split(",")
172
+ .map((s) => s.trim())
173
+ .filter((s) => s.length > 0)
174
+
175
+ return handleWorkerRequest(request, {
176
+ runDO: resolvedEnv.WORKFLOW_RUN_DO,
177
+ verifyRequest:
178
+ options.verifyRequest ?? (tokens.length > 0 ? createBearerVerifier(tokens) : undefined),
179
+ logger: options.logger,
180
+ idGenerator: options.idGenerator,
181
+ now: options.now,
182
+ tenantMeta: options.tenantMeta,
183
+ manifestStore: resolvedEnv.WORKFLOW_MANIFESTS
184
+ ? createKvManifestStore({ kv: resolvedEnv.WORKFLOW_MANIFESTS })
185
+ : undefined,
186
+ })
187
+ }
188
+
189
+ export class WorkflowRunDO<Env extends CloudWorkflowsEnv = CloudWorkflowsEnv> {
190
+ private readonly state: DurableObjectStateLike
191
+ private readonly env: Env
192
+
193
+ constructor(state: DurableObjectStateLike, env: Env) {
194
+ this.state = state
195
+ this.env = env
196
+ }
197
+
198
+ async fetch(request: Request): Promise<Response> {
199
+ return handleDurableObjectRequest(request, {
200
+ storage: this.state.storage,
201
+ dispatcher: resolveDispatcher(this.env, this.executionOptions()),
202
+ now: this.executionOptions().now,
203
+ })
204
+ }
205
+
206
+ async alarm(): Promise<void> {
207
+ await handleDurableObjectAlarm({
208
+ storage: this.state.storage,
209
+ dispatcher: resolveDispatcher(this.env, this.executionOptions()),
210
+ now: this.executionOptions().now,
211
+ })
212
+ }
213
+
214
+ protected executionOptions(): CloudExecutionOptions<Env> {
215
+ return defaultExecutionOptions as CloudExecutionOptions<Env>
216
+ }
217
+ }
218
+
219
+ export function createCloudStepDispatcher<Env extends CloudWorkflowsEnv = CloudWorkflowsEnv>(
220
+ env: Env,
221
+ options: CloudExecutionOptions<Env> = defaultExecutionOptions as CloudExecutionOptions<Env>,
222
+ ): StepDispatcher {
223
+ return createInlineDispatcher(resolveStepHandler(env, options))
224
+ }
225
+
226
+ function createWorkflowRunDOClass<Env extends CloudWorkflowsEnv>(
227
+ options: CloudExecutionOptions<Env>,
228
+ ): WorkflowRunDOClass<Env> {
229
+ return class CloudWorkflowRunDO extends WorkflowRunDO<Env> {
230
+ protected override executionOptions(): CloudExecutionOptions<Env> {
231
+ return options
232
+ }
233
+ }
234
+ }
235
+
236
+ function resolveDispatcher<Env extends CloudWorkflowsEnv>(
237
+ env: Env,
238
+ options: CloudExecutionOptions<Env> = defaultExecutionOptions as CloudExecutionOptions<Env>,
239
+ ): StepDispatcher {
240
+ const cache = cacheFor(env)
241
+ if (!cache.dispatcher || cache.dispatcherOptions !== options) {
242
+ cache.dispatcherOptions = options
243
+ cache.dispatcher = createCloudStepDispatcher(env, options)
244
+ }
245
+ return cache.dispatcher
246
+ }
247
+
248
+ function resolveStepHandler<Env extends CloudWorkflowsEnv>(
249
+ env: Env,
250
+ options: CloudExecutionOptions<Env> = defaultExecutionOptions as CloudExecutionOptions<Env>,
251
+ ): StepHandler {
252
+ const cache = cacheFor(env)
253
+ if (!cache.stepHandler || cache.stepHandlerOptions !== options) {
254
+ cache.stepHandlerOptions = options
255
+ const handlerPromise = buildStepHandler(env, options)
256
+ cache.stepHandler = (req, stepOptions) =>
257
+ handlerPromise.then((handler) => handler(req, stepOptions))
258
+ }
259
+ return cache.stepHandler
260
+ }
261
+
262
+ async function buildStepHandler<Env extends CloudWorkflowsEnv>(
263
+ env: Env,
264
+ options: CloudExecutionOptions<Env> = defaultExecutionOptions as CloudExecutionOptions<Env>,
265
+ ): Promise<StepHandler> {
266
+ const nodeStepRunner = await createNodeStepRunner(env, options)
267
+ const rateLimiter = createInMemoryRateLimiter()
268
+
269
+ return (req, stepOptions) =>
270
+ handleStepRequest(
271
+ req,
272
+ {
273
+ rateLimiter,
274
+ nodeStepRunner,
275
+ services: options.services,
276
+ now: options.now,
277
+ logger: options.logger,
278
+ },
279
+ stepOptions,
280
+ )
281
+ }
282
+
283
+ async function createNodeStepRunner<Env extends CloudWorkflowsEnv>(
284
+ env: Env,
285
+ options: CloudExecutionOptions<Env>,
286
+ ): Promise<StepRunner> {
287
+ if (!env.STEP_RUNNER) {
288
+ return createInlineNodeStepRunner(options.now)
289
+ }
290
+
291
+ const bundle = resolveBundleConfig(env)
292
+ const presign = createR2Presigner({
293
+ accountId: bundle.accountId,
294
+ accessKeyId: bundle.accessKeyId,
295
+ secretAccessKey: bundle.secretAccessKey,
296
+ bucket: bundle.bucket,
297
+ })
298
+ const sign = env.VOYANT_WORKFLOW_STEP_AUTH_SECRET
299
+ ? await createHmacSigner(env.VOYANT_WORKFLOW_STEP_AUTH_SECRET)
300
+ : undefined
301
+
302
+ return createCfContainerStepRunner({
303
+ namespace: env.STEP_RUNNER,
304
+ sign,
305
+ logger: options.logger,
306
+ resolveBundle: async () => ({
307
+ url: await presign({
308
+ key: bundle.key,
309
+ expiresIn: bundle.expiresIn,
310
+ }),
311
+ hash: bundle.hash,
312
+ }),
313
+ })
314
+ }
315
+
316
+ function createInlineNodeStepRunner(now = () => Date.now()): StepRunner {
317
+ return async ({ attempt, fn, stepCtx }): Promise<StepJournalEntry> => {
318
+ const startedAt = now()
319
+ try {
320
+ return {
321
+ attempt,
322
+ status: "ok",
323
+ output: await fn(stepCtx),
324
+ startedAt,
325
+ finishedAt: now(),
326
+ }
327
+ } catch (err) {
328
+ const e = err as Error
329
+ return {
330
+ attempt,
331
+ status: "err",
332
+ startedAt,
333
+ finishedAt: now(),
334
+ error: {
335
+ category: "USER_ERROR",
336
+ code:
337
+ typeof (err as { code?: unknown }).code === "string"
338
+ ? (err as { code: string }).code
339
+ : "UNKNOWN",
340
+ message: e?.message ?? String(err),
341
+ name: e?.name,
342
+ stack: e?.stack,
343
+ },
344
+ }
345
+ }
346
+ }
347
+ }
348
+
349
+ function resolveBundleConfig(env: CloudWorkflowsEnv): {
350
+ accountId: string
351
+ bucket: string
352
+ accessKeyId: string
353
+ secretAccessKey: string
354
+ key: string
355
+ hash: string
356
+ expiresIn: number
357
+ } {
358
+ const parsedPrefix = parseBundleUrlPrefix(env.VOYANT_WORKFLOW_BUNDLE_URL_PREFIX)
359
+ const accountId = env.VOYANT_WORKFLOW_BUNDLE_R2_ACCOUNT_ID ?? parsedPrefix.accountId
360
+ const bucket = env.VOYANT_WORKFLOW_BUNDLE_R2_BUCKET ?? parsedPrefix.bucket
361
+ const accessKeyId = env.VOYANT_WORKFLOW_BUNDLE_R2_ACCESS_KEY_ID
362
+ const secretAccessKey = env.VOYANT_WORKFLOW_BUNDLE_R2_SECRET_ACCESS_KEY
363
+ const key = env.VOYANT_WORKFLOW_BUNDLE_KEY
364
+ const hash = env.VOYANT_WORKFLOW_BUNDLE_HASH
365
+
366
+ const missing = [
367
+ ["VOYANT_WORKFLOW_BUNDLE_R2_ACCESS_KEY_ID", accessKeyId],
368
+ ["VOYANT_WORKFLOW_BUNDLE_R2_SECRET_ACCESS_KEY", secretAccessKey],
369
+ ["VOYANT_WORKFLOW_BUNDLE_KEY", key],
370
+ ["VOYANT_WORKFLOW_BUNDLE_HASH", hash],
371
+ ].filter(([, value]) => typeof value !== "string" || value.length === 0)
372
+
373
+ if (!env.VOYANT_WORKFLOW_BUNDLE_URL_PREFIX && (!accountId || !bucket)) {
374
+ missing.push(["VOYANT_WORKFLOW_BUNDLE_URL_PREFIX", env.VOYANT_WORKFLOW_BUNDLE_URL_PREFIX])
375
+ }
376
+ if (!accountId) missing.push(["VOYANT_WORKFLOW_BUNDLE_R2_ACCOUNT_ID", accountId])
377
+ if (!bucket) missing.push(["VOYANT_WORKFLOW_BUNDLE_R2_BUCKET", bucket])
378
+ if (missing.length > 0) {
379
+ throw new Error(
380
+ `@voyantjs/workflows-cloud-adapter: STEP_RUNNER is configured but bundle env is incomplete: ${missing
381
+ .map(([name]) => name)
382
+ .join(", ")}`,
383
+ )
384
+ }
385
+
386
+ const expiresIn = Number(env.VOYANT_WORKFLOW_BUNDLE_URL_TTL_SECONDS ?? 300)
387
+ if (!Number.isFinite(expiresIn) || expiresIn < 1 || expiresIn > 604_800) {
388
+ throw new Error(
389
+ "@voyantjs/workflows-cloud-adapter: VOYANT_WORKFLOW_BUNDLE_URL_TTL_SECONDS must be 1..604800",
390
+ )
391
+ }
392
+
393
+ return {
394
+ accountId: accountId!,
395
+ bucket: bucket!,
396
+ accessKeyId: accessKeyId!,
397
+ secretAccessKey: secretAccessKey!,
398
+ key: key!,
399
+ hash: hash!,
400
+ expiresIn,
401
+ }
402
+ }
403
+
404
+ function parseBundleUrlPrefix(prefix: string | undefined): {
405
+ accountId?: string
406
+ bucket?: string
407
+ } {
408
+ if (!prefix) return {}
409
+ const url = new URL(prefix)
410
+ const suffix = ".r2.cloudflarestorage.com"
411
+ const accountId = url.hostname.endsWith(suffix)
412
+ ? url.hostname.slice(0, -suffix.length)
413
+ : undefined
414
+ const bucket = url.pathname.replace(/^\/+/, "").split("/")[0]
415
+ return {
416
+ accountId: accountId && accountId.length > 0 ? accountId : undefined,
417
+ bucket: bucket && bucket.length > 0 ? bucket : undefined,
418
+ }
419
+ }
420
+
421
+ function cacheFor(env: object): EnvCache {
422
+ let cache = envCache.get(env)
423
+ if (!cache) {
424
+ cache = {}
425
+ envCache.set(env, cache)
426
+ }
427
+ return cache
428
+ }
429
+
430
+ function resolveBoundEnv<Env extends CloudWorkflowsEnv>(
431
+ boundEnv: Env | undefined,
432
+ requestEnv: Env | undefined,
433
+ options: CloudOrchestratorOptions<Env>,
434
+ ): Env {
435
+ const env = requestEnv ?? boundEnv
436
+ if (!env) {
437
+ throw new Error(
438
+ "@voyantjs/workflows-cloud-adapter: env must be passed to fetch(request, env) or createCloudOrchestrator(workflows, env)",
439
+ )
440
+ }
441
+ return options.resolveEnv?.(env) ?? env
442
+ }
443
+
444
+ function normalizePathPrefix(prefix: string): string {
445
+ if (prefix === "/") return ""
446
+ return `/${prefix.replace(/^\/+|\/+$/g, "")}`
447
+ }
448
+
449
+ function isMountedPath(pathname: string, prefix: string): boolean {
450
+ return pathname === prefix || pathname.startsWith(`${prefix}/`)
451
+ }
452
+
453
+ function extractRequest(args: readonly unknown[]): Request {
454
+ const first = args[0]
455
+ if (first instanceof Request) return first
456
+ const raw = (first as { req?: { raw?: unknown } } | undefined)?.req?.raw
457
+ if (raw instanceof Request) return raw
458
+ throw new Error("mountWorkflows: could not resolve Request from route handler arguments")
459
+ }
460
+
461
+ function extractEnv<Env extends CloudWorkflowsEnv>(
462
+ args: readonly unknown[],
463
+ boundEnv: Env | undefined,
464
+ ): Env | undefined {
465
+ if (boundEnv) return boundEnv
466
+ const firstEnv = (args[0] as { env?: unknown } | undefined)?.env
467
+ return (firstEnv as Env | undefined) ?? (args[1] as Env | undefined)
468
+ }