@voyant-travel/workflows 0.107.10

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.
Files changed (120) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +52 -0
  3. package/README.md +79 -0
  4. package/dist/auth/index.d.ts +125 -0
  5. package/dist/auth/index.d.ts.map +1 -0
  6. package/dist/auth/index.js +352 -0
  7. package/dist/bindings.d.ts +119 -0
  8. package/dist/bindings.d.ts.map +1 -0
  9. package/dist/bindings.js +19 -0
  10. package/dist/client.d.ts +135 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +305 -0
  13. package/dist/conditions.d.ts +29 -0
  14. package/dist/conditions.d.ts.map +1 -0
  15. package/dist/conditions.js +5 -0
  16. package/dist/config.d.ts +93 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +7 -0
  19. package/dist/driver.d.ts +237 -0
  20. package/dist/driver.d.ts.map +1 -0
  21. package/dist/driver.js +53 -0
  22. package/dist/errors.d.ts +58 -0
  23. package/dist/errors.d.ts.map +1 -0
  24. package/dist/errors.js +76 -0
  25. package/dist/events/compile.d.ts +34 -0
  26. package/dist/events/compile.d.ts.map +1 -0
  27. package/dist/events/compile.js +204 -0
  28. package/dist/events/index.d.ts +8 -0
  29. package/dist/events/index.d.ts.map +1 -0
  30. package/dist/events/index.js +11 -0
  31. package/dist/events/input-mapper.d.ts +24 -0
  32. package/dist/events/input-mapper.d.ts.map +1 -0
  33. package/dist/events/input-mapper.js +169 -0
  34. package/dist/events/manifest-builder.d.ts +42 -0
  35. package/dist/events/manifest-builder.d.ts.map +1 -0
  36. package/dist/events/manifest-builder.js +313 -0
  37. package/dist/events/payload-hash.d.ts +46 -0
  38. package/dist/events/payload-hash.d.ts.map +1 -0
  39. package/dist/events/payload-hash.js +98 -0
  40. package/dist/events/predicate.d.ts +77 -0
  41. package/dist/events/predicate.d.ts.map +1 -0
  42. package/dist/events/predicate.js +347 -0
  43. package/dist/events/registry.d.ts +37 -0
  44. package/dist/events/registry.d.ts.map +1 -0
  45. package/dist/events/registry.js +47 -0
  46. package/dist/handler/index.d.ts +114 -0
  47. package/dist/handler/index.d.ts.map +1 -0
  48. package/dist/handler/index.js +267 -0
  49. package/dist/handler/resume.d.ts +41 -0
  50. package/dist/handler/resume.d.ts.map +1 -0
  51. package/dist/handler/resume.js +44 -0
  52. package/dist/http-ingest.d.ts +54 -0
  53. package/dist/http-ingest.d.ts.map +1 -0
  54. package/dist/http-ingest.js +214 -0
  55. package/dist/index.d.ts +6 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +10 -0
  58. package/dist/protocol/index.d.ts +345 -0
  59. package/dist/protocol/index.d.ts.map +1 -0
  60. package/dist/protocol/index.js +110 -0
  61. package/dist/rate-limit/index.d.ts +40 -0
  62. package/dist/rate-limit/index.d.ts.map +1 -0
  63. package/dist/rate-limit/index.js +139 -0
  64. package/dist/runtime/ctx.d.ts +111 -0
  65. package/dist/runtime/ctx.d.ts.map +1 -0
  66. package/dist/runtime/ctx.js +624 -0
  67. package/dist/runtime/determinism.d.ts +19 -0
  68. package/dist/runtime/determinism.d.ts.map +1 -0
  69. package/dist/runtime/determinism.js +61 -0
  70. package/dist/runtime/errors.d.ts +21 -0
  71. package/dist/runtime/errors.d.ts.map +1 -0
  72. package/dist/runtime/errors.js +45 -0
  73. package/dist/runtime/executor.d.ts +166 -0
  74. package/dist/runtime/executor.d.ts.map +1 -0
  75. package/dist/runtime/executor.js +226 -0
  76. package/dist/runtime/journal.d.ts +56 -0
  77. package/dist/runtime/journal.d.ts.map +1 -0
  78. package/dist/runtime/journal.js +28 -0
  79. package/dist/testing/index.d.ts +117 -0
  80. package/dist/testing/index.d.ts.map +1 -0
  81. package/dist/testing/index.js +599 -0
  82. package/dist/trigger.d.ts +37 -0
  83. package/dist/trigger.d.ts.map +1 -0
  84. package/dist/trigger.js +11 -0
  85. package/dist/types.d.ts +63 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +3 -0
  88. package/dist/workflow.d.ts +222 -0
  89. package/dist/workflow.d.ts.map +1 -0
  90. package/dist/workflow.js +55 -0
  91. package/package.json +120 -0
  92. package/src/auth/index.ts +398 -0
  93. package/src/bindings.ts +135 -0
  94. package/src/client.ts +498 -0
  95. package/src/conditions.ts +43 -0
  96. package/src/config.ts +114 -0
  97. package/src/driver.ts +277 -0
  98. package/src/errors.ts +109 -0
  99. package/src/events/compile.ts +268 -0
  100. package/src/events/index.ts +42 -0
  101. package/src/events/input-mapper.ts +201 -0
  102. package/src/events/manifest-builder.ts +372 -0
  103. package/src/events/payload-hash.ts +110 -0
  104. package/src/events/predicate.ts +390 -0
  105. package/src/events/registry.ts +86 -0
  106. package/src/handler/index.ts +413 -0
  107. package/src/handler/resume.ts +100 -0
  108. package/src/http-ingest.ts +299 -0
  109. package/src/index.ts +18 -0
  110. package/src/protocol/index.ts +483 -0
  111. package/src/rate-limit/index.ts +181 -0
  112. package/src/runtime/ctx.ts +876 -0
  113. package/src/runtime/determinism.ts +75 -0
  114. package/src/runtime/errors.ts +58 -0
  115. package/src/runtime/executor.ts +442 -0
  116. package/src/runtime/journal.ts +80 -0
  117. package/src/testing/index.ts +796 -0
  118. package/src/trigger.ts +63 -0
  119. package/src/types.ts +80 -0
  120. package/src/workflow.ts +328 -0
@@ -0,0 +1,398 @@
1
+ // @voyant-travel/workflows/auth
2
+ //
3
+ // Paired HMAC signer + verifier for the `X-Voyant-Dispatch-Auth`
4
+ // header on `POST /__voyant/workflow-step`. Both sides share a
5
+ // symmetric secret — suitable for local dev and single-region
6
+ // deployments; asymmetric signing (control-plane issuer + tenant
7
+ // public-key) is a later upgrade that keeps the same header shape.
8
+ //
9
+ // Built on Web Crypto (`crypto.subtle`), so it works unchanged in
10
+ // Node 20+, Cloudflare Workers, Deno, Bun, and browsers.
11
+ //
12
+ // Usage on the orchestrator side:
13
+ //
14
+ // import { createHmacSigner } from "@voyant-travel/workflows/auth";
15
+ // const sign = await createHmacSigner(process.env.VOYANT_SIGNING_KEY!);
16
+ // createDispatchStepHandler(script, { dispatcher, sign });
17
+ //
18
+ // Usage on the tenant side:
19
+ //
20
+ // import { createHmacVerifier } from "@voyant-travel/workflows/auth";
21
+ // import { createStepHandler } from "@voyant-travel/workflows/handler";
22
+ // const verify = await createHmacVerifier(env.VOYANT_SIGNING_KEY);
23
+ // export default { fetch: createStepHandler({ verifyRequest: verify }) };
24
+
25
+ export const AUTH_HEADER = "x-voyant-dispatch-auth" as const
26
+
27
+ /**
28
+ * HMAC header carried on orchestrator → node-step-container dispatches
29
+ * (`POST /step`). Signed over the raw request body with the shared
30
+ * `VOYANT_WORKFLOW_STEP_AUTH_SECRET`. See `createCfContainerStepRunner`
31
+ * (signing side) and `apps/workflows-node-step-container` (verifying side).
32
+ */
33
+ export const STEP_AUTH_HEADER = "x-voyant-step-auth" as const
34
+
35
+ /**
36
+ * HMAC header returned by the node-step-container on successful `/step`
37
+ * responses. Signed over the raw JSON response body with the same
38
+ * `VOYANT_WORKFLOW_STEP_AUTH_SECRET` so the orchestrator can reject
39
+ * forged step journal entries before committing run state.
40
+ */
41
+ export const STEP_RESPONSE_AUTH_HEADER = "x-voyant-step-response-auth" as const
42
+
43
+ /** Parse a comma-separated token/origin list env var into trimmed, non-empty entries. */
44
+ export function parseTokenList(raw: string | null | undefined): string[] {
45
+ return (raw ?? "")
46
+ .split(",")
47
+ .map((s) => s.trim())
48
+ .filter((s) => s.length > 0)
49
+ }
50
+
51
+ /**
52
+ * Returns a verifier that accepts `Authorization: Bearer <token>`
53
+ * where `<token>` matches any of the `validTokens` (case-sensitive,
54
+ * constant-time compared). Usable as the `verifyRequest` dep on
55
+ * `handleWorkerRequest` / `createStepHandler` for the public-facing
56
+ * surface of an orchestrator or tenant Worker.
57
+ *
58
+ * Intended for dev + single-tenant deployments. Production should
59
+ * issue per-tenant, short-lived tokens from a control plane.
60
+ */
61
+ export function createBearerVerifier(validTokens: readonly string[]): (req: Request) => void {
62
+ if (validTokens.length === 0) {
63
+ throw new Error("createBearerVerifier: need at least one valid token")
64
+ }
65
+ return (req) => {
66
+ const header = req.headers.get("authorization")
67
+ if (!header) throw new Error("missing Authorization header")
68
+ const match = /^Bearer (.+)$/.exec(header)
69
+ if (!match) {
70
+ throw new Error("Authorization header must use the Bearer scheme")
71
+ }
72
+ const presented = match[1]!
73
+ for (const valid of validTokens) {
74
+ if (constantTimeEquals(presented, valid)) return
75
+ }
76
+ throw new Error("bearer token does not match any configured value")
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Constant-time string comparison. Does NOT early-return on length
82
+ * mismatch: the loop always runs over the longer input (out-of-range
83
+ * `charCodeAt` is NaN, which coerces to 0 under bitwise ops), and a
84
+ * length mismatch only sets a diff bit. This avoids leaking how many
85
+ * leading characters of a secret matched, or whether the length matched.
86
+ */
87
+ function constantTimeEquals(a: string, b: string): boolean {
88
+ const length = Math.max(a.length, b.length, 1)
89
+ let diff = a.length === b.length ? 0 : 1
90
+ for (let i = 0; i < length; i++) {
91
+ diff |= (a.charCodeAt(i) | 0) ^ (b.charCodeAt(i) | 0)
92
+ }
93
+ return diff === 0
94
+ }
95
+
96
+ // ---- Fail-closed bearer auth resolution ----
97
+
98
+ /**
99
+ * Error thrown by verifiers produced in this module. Carries an HTTP
100
+ * `status` + machine-readable `code` so HTTP surfaces
101
+ * (`handleWorkerRequest`, the node dashboard server, step handlers)
102
+ * can map auth failures to the right response instead of a blanket 401.
103
+ */
104
+ export class RequestAuthError extends Error {
105
+ readonly status: number
106
+ readonly code: string
107
+
108
+ constructor(status: number, code: string, message: string) {
109
+ super(message)
110
+ this.name = "RequestAuthError"
111
+ this.status = status
112
+ this.code = code
113
+ }
114
+ }
115
+
116
+ export type BearerAuthDecision =
117
+ | { ok: true }
118
+ | { ok: false; status: number; error: string; message: string }
119
+
120
+ export interface BearerAuthOptions {
121
+ /** Accepted bearer tokens. Empty/undefined = no auth configured. */
122
+ tokens?: readonly string[]
123
+ /**
124
+ * Explicit local-dev opt-out. When `true` AND no tokens are
125
+ * configured, requests are allowed through with a loud warning.
126
+ * When `false`/unset and no tokens are configured, every request is
127
+ * rejected with 503 `auth_not_configured` (fail closed).
128
+ */
129
+ allowUnauthenticated?: boolean
130
+ /** Warning sink for the unauthenticated opt-out. Defaults to `console.warn`. */
131
+ warn?: (message: string) => void
132
+ }
133
+
134
+ const UNAUTHENTICATED_WARNING =
135
+ "[voyant-workflows] AUTH DISABLED: no bearer tokens are configured and " +
136
+ "VOYANT_WORKFLOWS_ALLOW_UNAUTHENTICATED is set. Every caller can trigger, read, " +
137
+ "resume, and cancel workflow runs. This mode is for LOCAL DEVELOPMENT ONLY — " +
138
+ "configure VOYANT_API_TOKENS before deploying."
139
+
140
+ /**
141
+ * Build a transport-agnostic bearer authorizer from a raw
142
+ * `Authorization` header value. Fail-closed semantics:
143
+ *
144
+ * - tokens configured → exact (constant-time) Bearer match or 401
145
+ * - no tokens + opt-out → always allowed (warns loudly once, at creation)
146
+ * - no tokens, no opt-out → always 503 `auth_not_configured`
147
+ */
148
+ export function createBearerAuthorizer(
149
+ opts: BearerAuthOptions,
150
+ ): (authorizationHeader: string | null | undefined) => BearerAuthDecision {
151
+ const tokens = (opts.tokens ?? []).filter((t) => t.length > 0)
152
+ if (tokens.length === 0) {
153
+ if (opts.allowUnauthenticated) {
154
+ ;(opts.warn ?? console.warn)(UNAUTHENTICATED_WARNING)
155
+ return () => ({ ok: true })
156
+ }
157
+ return () => ({
158
+ ok: false,
159
+ status: 503,
160
+ error: "auth_not_configured",
161
+ message:
162
+ "no bearer tokens are configured for this workflows API; set VOYANT_API_TOKENS " +
163
+ "(or pass tokens explicitly), or opt out for local development with " +
164
+ "VOYANT_WORKFLOWS_ALLOW_UNAUTHENTICATED=1",
165
+ })
166
+ }
167
+ return (header) => {
168
+ if (!header) {
169
+ return {
170
+ ok: false,
171
+ status: 401,
172
+ error: "unauthorized",
173
+ message: "missing Authorization header",
174
+ }
175
+ }
176
+ const match = /^Bearer (.+)$/.exec(header)
177
+ if (!match) {
178
+ return {
179
+ ok: false,
180
+ status: 401,
181
+ error: "unauthorized",
182
+ message: "Authorization header must use the Bearer scheme",
183
+ }
184
+ }
185
+ const presented = match[1]!
186
+ for (const valid of tokens) {
187
+ if (constantTimeEquals(presented, valid)) return { ok: true }
188
+ }
189
+ return {
190
+ ok: false,
191
+ status: 401,
192
+ error: "unauthorized",
193
+ message: "bearer token does not match any configured value",
194
+ }
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Resolve a `verifyRequest` hook (the dep consumed by
200
+ * `handleWorkerRequest` / `createStepHandler`) with fail-closed
201
+ * defaults — the missing-token case yields a verifier that rejects
202
+ * every request with a 503 `auth_not_configured` `RequestAuthError`
203
+ * instead of `undefined` (which would skip auth entirely).
204
+ *
205
+ * Returns `undefined` (auth skipped) ONLY when no tokens are
206
+ * configured AND `allowUnauthenticated` is explicitly set.
207
+ */
208
+ export function resolveRequestVerifier(
209
+ opts: BearerAuthOptions,
210
+ ): ((req: Request) => void) | undefined {
211
+ const tokens = (opts.tokens ?? []).filter((t) => t.length > 0)
212
+ if (tokens.length === 0 && opts.allowUnauthenticated) {
213
+ ;(opts.warn ?? console.warn)(UNAUTHENTICATED_WARNING)
214
+ return undefined
215
+ }
216
+ const authorize = createBearerAuthorizer({ ...opts, allowUnauthenticated: false })
217
+ return (req) => {
218
+ const decision = authorize(req.headers.get("authorization"))
219
+ if (!decision.ok) {
220
+ throw new RequestAuthError(decision.status, decision.error, decision.message)
221
+ }
222
+ }
223
+ }
224
+
225
+ /** Returns a signer: `(body: string) => Promise<string>` (base64 HMAC-SHA256). */
226
+ export async function createHmacSigner(secret: string): Promise<(body: string) => Promise<string>> {
227
+ const key = await importKey(secret, ["sign"])
228
+ return async (body) => {
229
+ const sig = await crypto.subtle.sign("HMAC", key, encode(body))
230
+ return toBase64(sig)
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Returns a verifier: `(req: Request) => Promise<void>`. Throws if:
236
+ * - the header is missing,
237
+ * - the signature is malformed,
238
+ * - the signature does not match the current body.
239
+ *
240
+ * The verifier consumes `req.body` via `req.text()`. Callers that
241
+ * need the body downstream should pre-clone: `req.clone()` before
242
+ * passing in.
243
+ */
244
+ export async function createHmacVerifier(secret: string): Promise<(req: Request) => Promise<void>> {
245
+ const key = await importKey(secret, ["verify"])
246
+ return async (req) => {
247
+ const header = req.headers.get(AUTH_HEADER)
248
+ if (!header) {
249
+ throw new Error(`missing ${AUTH_HEADER} header`)
250
+ }
251
+ let sig: ArrayBuffer
252
+ try {
253
+ sig = fromBase64(header)
254
+ } catch {
255
+ throw new Error(`malformed ${AUTH_HEADER} header (expected base64)`)
256
+ }
257
+ const body = await req.clone().text()
258
+ const ok = await crypto.subtle.verify("HMAC", key, sig, encode(body))
259
+ if (!ok) {
260
+ throw new Error(`${AUTH_HEADER} signature does not match request body`)
261
+ }
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Returns a verifier over a raw body string + detached base64 HMAC
267
+ * signature (the value of `STEP_AUTH_HEADER`). Suitable for servers
268
+ * that have already buffered the request body (e.g. the node step
269
+ * container). `crypto.subtle.verify` is constant-time; malformed
270
+ * base64 or a missing signature simply yields `false`.
271
+ */
272
+ export async function createHmacBodyVerifier(
273
+ secret: string,
274
+ ): Promise<(body: string, signatureBase64: string | null | undefined) => Promise<boolean>> {
275
+ const key = await importKey(secret, ["verify"])
276
+ return async (body, signatureBase64) => {
277
+ if (!signatureBase64) return false
278
+ let sig: ArrayBuffer
279
+ try {
280
+ sig = fromBase64(signatureBase64)
281
+ } catch {
282
+ return false
283
+ }
284
+ return crypto.subtle.verify("HMAC", key, sig, encode(body))
285
+ }
286
+ }
287
+
288
+ // ---- Bundle URL origin policy ----
289
+
290
+ export type BundleUrlDecision = { ok: true } | { ok: false; message: string }
291
+
292
+ /**
293
+ * Default bundle source: Cloudflare R2's S3 endpoint
294
+ * (`<account>.r2.cloudflarestorage.com`), which is where deploy
295
+ * pipelines stage `container.mjs` and the orchestrator mints
296
+ * short-lived signed URLs. Anything else must be explicitly
297
+ * allowlisted via `allowedOrigins`.
298
+ */
299
+ const R2_HOST_SUFFIX = ".r2.cloudflarestorage.com"
300
+
301
+ /**
302
+ * Build an origin allowlist check for caller-supplied `bundle.url`
303
+ * values (the node step container dynamically `import()`s the fetched
304
+ * bytes, so an unrestricted URL is remote-code-execution-adjacent —
305
+ * the SHA-256 hash pin is supplied by the same caller and is not a
306
+ * security control on its own).
307
+ *
308
+ * - `allowedOrigins` set (e.g. from `VOYANT_BUNDLE_ALLOWED_ORIGINS`):
309
+ * the URL's origin must match one of the entries exactly.
310
+ * Invalid entries throw at creation time (misconfiguration should
311
+ * fail loudly, not silently widen).
312
+ * - unset/empty: only HTTPS URLs on `*.r2.cloudflarestorage.com`
313
+ * are allowed — the production bundle source.
314
+ */
315
+ export function createBundleUrlPolicy(opts?: {
316
+ allowedOrigins?: readonly string[]
317
+ }): (url: string) => BundleUrlDecision {
318
+ const entries = (opts?.allowedOrigins ?? []).filter((entry) => entry.length > 0)
319
+ const allowed = new Set<string>()
320
+ for (const entry of entries) {
321
+ let parsed: URL
322
+ try {
323
+ parsed = new URL(entry)
324
+ } catch {
325
+ throw new Error(`createBundleUrlPolicy: invalid origin in allowlist: "${entry}"`)
326
+ }
327
+ allowed.add(parsed.origin)
328
+ }
329
+ return (url) => {
330
+ let parsed: URL
331
+ try {
332
+ parsed = new URL(url)
333
+ } catch {
334
+ return { ok: false, message: `bundle url is not a valid URL: "${url}"` }
335
+ }
336
+ if (allowed.size > 0) {
337
+ if (allowed.has(parsed.origin)) return { ok: true }
338
+ return {
339
+ ok: false,
340
+ message:
341
+ `bundle url origin "${parsed.origin}" is not in the configured allowlist ` +
342
+ "(VOYANT_BUNDLE_ALLOWED_ORIGINS)",
343
+ }
344
+ }
345
+ if (parsed.protocol === "https:" && parsed.hostname.endsWith(R2_HOST_SUFFIX)) {
346
+ return { ok: true }
347
+ }
348
+ return {
349
+ ok: false,
350
+ message:
351
+ `bundle url origin "${parsed.origin}" rejected: with no VOYANT_BUNDLE_ALLOWED_ORIGINS ` +
352
+ `configured, only https://*${R2_HOST_SUFFIX} bundle sources are allowed`,
353
+ }
354
+ }
355
+ }
356
+
357
+ // ---- Internals ----
358
+
359
+ async function importKey(
360
+ secret: string,
361
+ usages: readonly ("sign" | "verify")[],
362
+ ): Promise<CryptoKey> {
363
+ if (secret.length === 0) {
364
+ throw new Error("HMAC secret must be a non-empty string")
365
+ }
366
+ return crypto.subtle.importKey("raw", encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
367
+ ...usages,
368
+ ])
369
+ }
370
+
371
+ /**
372
+ * Encode to a freshly-allocated ArrayBuffer. TextEncoder's Uint8Array
373
+ * is typed as `Uint8Array<ArrayBufferLike>` under recent TS lib, which
374
+ * doesn't satisfy the `BufferSource` param of `subtle.sign/verify`.
375
+ * Copying into a new ArrayBuffer sidesteps the nominal mismatch.
376
+ */
377
+ function encode(s: string): ArrayBuffer {
378
+ const view = new TextEncoder().encode(s)
379
+ const buf = new ArrayBuffer(view.byteLength)
380
+ new Uint8Array(buf).set(view)
381
+ return buf
382
+ }
383
+
384
+ function toBase64(buffer: ArrayBuffer): string {
385
+ // btoa is available in every modern runtime (Node 16+, Workers, browsers).
386
+ const bytes = new Uint8Array(buffer)
387
+ let bin = ""
388
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!)
389
+ return btoa(bin)
390
+ }
391
+
392
+ function fromBase64(s: string): ArrayBuffer {
393
+ const bin = atob(s)
394
+ const buf = new ArrayBuffer(bin.length)
395
+ const view = new Uint8Array(buf)
396
+ for (let i = 0; i < bin.length; i++) view[i] = bin.charCodeAt(i)
397
+ return buf
398
+ }
@@ -0,0 +1,135 @@
1
+ // @voyant-travel/workflows/bindings
2
+ //
3
+ // Runtime binding shim. Edge runtime passes through to native CF bindings;
4
+ // container runtime makes authenticated HTTPS calls to CF's per-binding APIs.
5
+ //
6
+ // Contract defined in docs/sdk-surface.md §9 and docs/design.md §5.2.
7
+
8
+ // Types intentionally mirror Cloudflare's native binding shapes so the
9
+ // same code runs on edge and container runtimes. Runtime behavior is
10
+ // provided by the platform (native bindings on Workers; an HTTPS shim
11
+ // on the container runtime) — this package is the shared type surface.
12
+
13
+ export interface Env {
14
+ [key: string]: Binding
15
+ }
16
+
17
+ export type Binding = D1Database | R2Bucket | KVNamespace | Queue<unknown> | string // secrets
18
+
19
+ export interface D1Database {
20
+ prepare(sql: string): D1PreparedStatement
21
+ batch(statements: D1PreparedStatement[]): Promise<D1Result[]>
22
+ exec(sql: string): Promise<D1ExecResult>
23
+ }
24
+
25
+ export interface D1PreparedStatement {
26
+ bind(...values: unknown[]): D1PreparedStatement
27
+ first<T = unknown>(column?: string): Promise<T | null>
28
+ run(): Promise<D1Result>
29
+ all<T = unknown>(): Promise<{ results: T[]; meta: D1Meta }>
30
+ /** Per-step-invocation read cache. */
31
+ memoize(): D1PreparedStatement
32
+ }
33
+
34
+ export interface D1Result {
35
+ success: boolean
36
+ meta: D1Meta
37
+ results?: unknown[]
38
+ }
39
+
40
+ export interface D1ExecResult {
41
+ count: number
42
+ duration: number
43
+ }
44
+
45
+ export interface D1Meta {
46
+ duration: number
47
+ rows_read: number
48
+ rows_written: number
49
+ }
50
+
51
+ export interface R2Bucket {
52
+ get(key: string, opts?: R2GetOptions): Promise<R2Object | null>
53
+ put(key: string, value: R2PutBody, opts?: R2PutOptions): Promise<R2Object>
54
+ delete(keys: string | string[]): Promise<void>
55
+ list(opts?: R2ListOptions): Promise<R2Objects>
56
+ head(key: string): Promise<R2Object | null>
57
+ }
58
+
59
+ export interface R2GetOptions {
60
+ range?: { offset?: number; length?: number }
61
+ onlyIf?: { etagMatches?: string }
62
+ }
63
+ export interface R2PutOptions {
64
+ httpMetadata?: Record<string, string>
65
+ customMetadata?: Record<string, string>
66
+ }
67
+ export type R2PutBody = ReadableStream | ArrayBuffer | string
68
+ export interface R2Object {
69
+ key: string
70
+ size: number
71
+ etag: string
72
+ httpMetadata?: Record<string, string>
73
+ customMetadata?: Record<string, string>
74
+ body?: ReadableStream
75
+ arrayBuffer(): Promise<ArrayBuffer>
76
+ text(): Promise<string>
77
+ json<T = unknown>(): Promise<T>
78
+ }
79
+ export interface R2ListOptions {
80
+ prefix?: string
81
+ cursor?: string
82
+ limit?: number
83
+ }
84
+ export interface R2Objects {
85
+ objects: R2Object[]
86
+ truncated: boolean
87
+ cursor?: string
88
+ }
89
+
90
+ export interface KVNamespace {
91
+ get<T = string>(key: string, opts?: KVGetOptions<T>): Promise<T | null>
92
+ put(key: string, value: string | ArrayBuffer, opts?: KVPutOptions): Promise<void>
93
+ delete(key: string): Promise<void>
94
+ list(opts?: KVListOptions): Promise<KVList>
95
+ }
96
+
97
+ export interface KVGetOptions<T> {
98
+ type?: T extends string ? "text" : "json" | "arrayBuffer" | "stream"
99
+ }
100
+ export interface KVPutOptions {
101
+ expiration?: number
102
+ expirationTtl?: number
103
+ metadata?: Record<string, unknown>
104
+ }
105
+ export interface KVListOptions {
106
+ prefix?: string
107
+ cursor?: string
108
+ limit?: number
109
+ }
110
+ export interface KVList {
111
+ keys: { name: string; expiration?: number; metadata?: unknown }[]
112
+ list_complete: boolean
113
+ cursor?: string
114
+ }
115
+
116
+ export interface Queue<T> {
117
+ send(message: T, opts?: { delaySeconds?: number }): Promise<void>
118
+ sendBatch(messages: readonly T[]): Promise<void>
119
+ }
120
+
121
+ /**
122
+ * The environment object tenant code reads bindings from.
123
+ *
124
+ * On the edge runtime, this is a pass-through to the tenant worker's
125
+ * `env` parameter (CF Workers). On the container runtime, the
126
+ * platform injects HTTP-based clients that mimic the shapes above.
127
+ */
128
+ export const env: Env = new Proxy({} as Env, {
129
+ get(_, key: string): Binding {
130
+ throw new Error(
131
+ `@voyant-travel/workflows/bindings: env.${key} was accessed outside a workflow / step body. ` +
132
+ `Bindings are injected by the runtime — see docs/sdk-surface.md §9.`,
133
+ )
134
+ },
135
+ })