@voyantjs/workflows 0.107.5 → 0.107.6
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/dist/auth/index.d.ts +99 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +221 -6
- package/dist/events/registry.d.ts.map +1 -1
- package/dist/handler/index.js +22 -1
- package/dist/runtime/ctx.d.ts.map +1 -1
- package/dist/runtime/ctx.js +14 -14
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +6 -2
- package/dist/workflow.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/auth/index.ts +259 -11
- package/src/events/registry.ts +2 -4
- package/src/handler/index.ts +22 -1
- package/src/runtime/ctx.ts +19 -22
- package/src/testing/index.ts +20 -19
- package/src/workflow.ts +2 -4
package/dist/auth/index.d.ts
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
export declare const AUTH_HEADER: "x-voyant-dispatch-auth";
|
|
2
|
+
/**
|
|
3
|
+
* HMAC header carried on orchestrator → node-step-container dispatches
|
|
4
|
+
* (`POST /step`). Signed over the raw request body with the shared
|
|
5
|
+
* `VOYANT_WORKFLOW_STEP_AUTH_SECRET`. See `createCfContainerStepRunner`
|
|
6
|
+
* (signing side) and `apps/workflows-node-step-container` (verifying side).
|
|
7
|
+
*/
|
|
8
|
+
export declare const STEP_AUTH_HEADER: "x-voyant-step-auth";
|
|
9
|
+
/**
|
|
10
|
+
* HMAC header returned by the node-step-container on successful `/step`
|
|
11
|
+
* responses. Signed over the raw JSON response body with the same
|
|
12
|
+
* `VOYANT_WORKFLOW_STEP_AUTH_SECRET` so the orchestrator can reject
|
|
13
|
+
* forged step journal entries before committing run state.
|
|
14
|
+
*/
|
|
15
|
+
export declare const STEP_RESPONSE_AUTH_HEADER: "x-voyant-step-response-auth";
|
|
16
|
+
/** Parse a comma-separated token/origin list env var into trimmed, non-empty entries. */
|
|
17
|
+
export declare function parseTokenList(raw: string | null | undefined): string[];
|
|
2
18
|
/**
|
|
3
19
|
* Returns a verifier that accepts `Authorization: Bearer <token>`
|
|
4
20
|
* where `<token>` matches any of the `validTokens` (case-sensitive,
|
|
@@ -10,6 +26,58 @@ export declare const AUTH_HEADER: "x-voyant-dispatch-auth";
|
|
|
10
26
|
* issue per-tenant, short-lived tokens from a control plane.
|
|
11
27
|
*/
|
|
12
28
|
export declare function createBearerVerifier(validTokens: readonly string[]): (req: Request) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Error thrown by verifiers produced in this module. Carries an HTTP
|
|
31
|
+
* `status` + machine-readable `code` so HTTP surfaces
|
|
32
|
+
* (`handleWorkerRequest`, the node dashboard server, step handlers)
|
|
33
|
+
* can map auth failures to the right response instead of a blanket 401.
|
|
34
|
+
*/
|
|
35
|
+
export declare class RequestAuthError extends Error {
|
|
36
|
+
readonly status: number;
|
|
37
|
+
readonly code: string;
|
|
38
|
+
constructor(status: number, code: string, message: string);
|
|
39
|
+
}
|
|
40
|
+
export type BearerAuthDecision = {
|
|
41
|
+
ok: true;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
status: number;
|
|
45
|
+
error: string;
|
|
46
|
+
message: string;
|
|
47
|
+
};
|
|
48
|
+
export interface BearerAuthOptions {
|
|
49
|
+
/** Accepted bearer tokens. Empty/undefined = no auth configured. */
|
|
50
|
+
tokens?: readonly string[];
|
|
51
|
+
/**
|
|
52
|
+
* Explicit local-dev opt-out. When `true` AND no tokens are
|
|
53
|
+
* configured, requests are allowed through with a loud warning.
|
|
54
|
+
* When `false`/unset and no tokens are configured, every request is
|
|
55
|
+
* rejected with 503 `auth_not_configured` (fail closed).
|
|
56
|
+
*/
|
|
57
|
+
allowUnauthenticated?: boolean;
|
|
58
|
+
/** Warning sink for the unauthenticated opt-out. Defaults to `console.warn`. */
|
|
59
|
+
warn?: (message: string) => void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Build a transport-agnostic bearer authorizer from a raw
|
|
63
|
+
* `Authorization` header value. Fail-closed semantics:
|
|
64
|
+
*
|
|
65
|
+
* - tokens configured → exact (constant-time) Bearer match or 401
|
|
66
|
+
* - no tokens + opt-out → always allowed (warns loudly once, at creation)
|
|
67
|
+
* - no tokens, no opt-out → always 503 `auth_not_configured`
|
|
68
|
+
*/
|
|
69
|
+
export declare function createBearerAuthorizer(opts: BearerAuthOptions): (authorizationHeader: string | null | undefined) => BearerAuthDecision;
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a `verifyRequest` hook (the dep consumed by
|
|
72
|
+
* `handleWorkerRequest` / `createStepHandler`) with fail-closed
|
|
73
|
+
* defaults — the missing-token case yields a verifier that rejects
|
|
74
|
+
* every request with a 503 `auth_not_configured` `RequestAuthError`
|
|
75
|
+
* instead of `undefined` (which would skip auth entirely).
|
|
76
|
+
*
|
|
77
|
+
* Returns `undefined` (auth skipped) ONLY when no tokens are
|
|
78
|
+
* configured AND `allowUnauthenticated` is explicitly set.
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveRequestVerifier(opts: BearerAuthOptions): ((req: Request) => void) | undefined;
|
|
13
81
|
/** Returns a signer: `(body: string) => Promise<string>` (base64 HMAC-SHA256). */
|
|
14
82
|
export declare function createHmacSigner(secret: string): Promise<(body: string) => Promise<string>>;
|
|
15
83
|
/**
|
|
@@ -23,4 +91,35 @@ export declare function createHmacSigner(secret: string): Promise<(body: string)
|
|
|
23
91
|
* passing in.
|
|
24
92
|
*/
|
|
25
93
|
export declare function createHmacVerifier(secret: string): Promise<(req: Request) => Promise<void>>;
|
|
94
|
+
/**
|
|
95
|
+
* Returns a verifier over a raw body string + detached base64 HMAC
|
|
96
|
+
* signature (the value of `STEP_AUTH_HEADER`). Suitable for servers
|
|
97
|
+
* that have already buffered the request body (e.g. the node step
|
|
98
|
+
* container). `crypto.subtle.verify` is constant-time; malformed
|
|
99
|
+
* base64 or a missing signature simply yields `false`.
|
|
100
|
+
*/
|
|
101
|
+
export declare function createHmacBodyVerifier(secret: string): Promise<(body: string, signatureBase64: string | null | undefined) => Promise<boolean>>;
|
|
102
|
+
export type BundleUrlDecision = {
|
|
103
|
+
ok: true;
|
|
104
|
+
} | {
|
|
105
|
+
ok: false;
|
|
106
|
+
message: string;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Build an origin allowlist check for caller-supplied `bundle.url`
|
|
110
|
+
* values (the node step container dynamically `import()`s the fetched
|
|
111
|
+
* bytes, so an unrestricted URL is remote-code-execution-adjacent —
|
|
112
|
+
* the SHA-256 hash pin is supplied by the same caller and is not a
|
|
113
|
+
* security control on its own).
|
|
114
|
+
*
|
|
115
|
+
* - `allowedOrigins` set (e.g. from `VOYANT_BUNDLE_ALLOWED_ORIGINS`):
|
|
116
|
+
* the URL's origin must match one of the entries exactly.
|
|
117
|
+
* Invalid entries throw at creation time (misconfiguration should
|
|
118
|
+
* fail loudly, not silently widen).
|
|
119
|
+
* - unset/empty: only HTTPS URLs on `*.r2.cloudflarestorage.com`
|
|
120
|
+
* are allowed — the production bundle source.
|
|
121
|
+
*/
|
|
122
|
+
export declare function createBundleUrlPolicy(opts?: {
|
|
123
|
+
allowedOrigins?: readonly string[];
|
|
124
|
+
}): (url: string) => BundleUrlDecision;
|
|
26
125
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/auth/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAwBA,eAAO,MAAM,WAAW,EAAG,wBAAiC,CAAA;AAE5D;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAiB3F;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAwBA,eAAO,MAAM,WAAW,EAAG,wBAAiC,CAAA;AAE5D;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAG,oBAA6B,CAAA;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,EAAG,6BAAsC,CAAA;AAE/E,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CAKvE;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAiB3F;AAoBD;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;gBAET,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAM1D;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjE,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1B;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,gFAAgF;IAChF,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC;AAQD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,iBAAiB,GACtB,CAAC,mBAAmB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,KAAK,kBAAkB,CA8CxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,iBAAiB,GACtB,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,SAAS,CAatC;AAED,kFAAkF;AAClF,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,CAMjG;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAmBjG;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,CAYzF;AAID,MAAM,MAAM,iBAAiB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAW7E;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,CAAC,EAAE;IAC3C,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACnC,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,iBAAiB,CAsCrC"}
|
package/dist/auth/index.js
CHANGED
|
@@ -22,6 +22,27 @@
|
|
|
22
22
|
// const verify = await createHmacVerifier(env.VOYANT_SIGNING_KEY);
|
|
23
23
|
// export default { fetch: createStepHandler({ verifyRequest: verify }) };
|
|
24
24
|
export const AUTH_HEADER = "x-voyant-dispatch-auth";
|
|
25
|
+
/**
|
|
26
|
+
* HMAC header carried on orchestrator → node-step-container dispatches
|
|
27
|
+
* (`POST /step`). Signed over the raw request body with the shared
|
|
28
|
+
* `VOYANT_WORKFLOW_STEP_AUTH_SECRET`. See `createCfContainerStepRunner`
|
|
29
|
+
* (signing side) and `apps/workflows-node-step-container` (verifying side).
|
|
30
|
+
*/
|
|
31
|
+
export const STEP_AUTH_HEADER = "x-voyant-step-auth";
|
|
32
|
+
/**
|
|
33
|
+
* HMAC header returned by the node-step-container on successful `/step`
|
|
34
|
+
* responses. Signed over the raw JSON response body with the same
|
|
35
|
+
* `VOYANT_WORKFLOW_STEP_AUTH_SECRET` so the orchestrator can reject
|
|
36
|
+
* forged step journal entries before committing run state.
|
|
37
|
+
*/
|
|
38
|
+
export const STEP_RESPONSE_AUTH_HEADER = "x-voyant-step-response-auth";
|
|
39
|
+
/** Parse a comma-separated token/origin list env var into trimmed, non-empty entries. */
|
|
40
|
+
export function parseTokenList(raw) {
|
|
41
|
+
return (raw ?? "")
|
|
42
|
+
.split(",")
|
|
43
|
+
.map((s) => s.trim())
|
|
44
|
+
.filter((s) => s.length > 0);
|
|
45
|
+
}
|
|
25
46
|
/**
|
|
26
47
|
* Returns a verifier that accepts `Authorization: Bearer <token>`
|
|
27
48
|
* where `<token>` matches any of the `validTokens` (case-sensitive,
|
|
@@ -52,15 +73,123 @@ export function createBearerVerifier(validTokens) {
|
|
|
52
73
|
throw new Error("bearer token does not match any configured value");
|
|
53
74
|
};
|
|
54
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Constant-time string comparison. Does NOT early-return on length
|
|
78
|
+
* mismatch: the loop always runs over the longer input (out-of-range
|
|
79
|
+
* `charCodeAt` is NaN, which coerces to 0 under bitwise ops), and a
|
|
80
|
+
* length mismatch only sets a diff bit. This avoids leaking how many
|
|
81
|
+
* leading characters of a secret matched, or whether the length matched.
|
|
82
|
+
*/
|
|
55
83
|
function constantTimeEquals(a, b) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let
|
|
59
|
-
|
|
60
|
-
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
84
|
+
const length = Math.max(a.length, b.length, 1);
|
|
85
|
+
let diff = a.length === b.length ? 0 : 1;
|
|
86
|
+
for (let i = 0; i < length; i++) {
|
|
87
|
+
diff |= (a.charCodeAt(i) | 0) ^ (b.charCodeAt(i) | 0);
|
|
61
88
|
}
|
|
62
89
|
return diff === 0;
|
|
63
90
|
}
|
|
91
|
+
// ---- Fail-closed bearer auth resolution ----
|
|
92
|
+
/**
|
|
93
|
+
* Error thrown by verifiers produced in this module. Carries an HTTP
|
|
94
|
+
* `status` + machine-readable `code` so HTTP surfaces
|
|
95
|
+
* (`handleWorkerRequest`, the node dashboard server, step handlers)
|
|
96
|
+
* can map auth failures to the right response instead of a blanket 401.
|
|
97
|
+
*/
|
|
98
|
+
export class RequestAuthError extends Error {
|
|
99
|
+
status;
|
|
100
|
+
code;
|
|
101
|
+
constructor(status, code, message) {
|
|
102
|
+
super(message);
|
|
103
|
+
this.name = "RequestAuthError";
|
|
104
|
+
this.status = status;
|
|
105
|
+
this.code = code;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const UNAUTHENTICATED_WARNING = "[voyant-workflows] AUTH DISABLED: no bearer tokens are configured and " +
|
|
109
|
+
"VOYANT_WORKFLOWS_ALLOW_UNAUTHENTICATED is set. Every caller can trigger, read, " +
|
|
110
|
+
"resume, and cancel workflow runs. This mode is for LOCAL DEVELOPMENT ONLY — " +
|
|
111
|
+
"configure VOYANT_API_TOKENS before deploying.";
|
|
112
|
+
/**
|
|
113
|
+
* Build a transport-agnostic bearer authorizer from a raw
|
|
114
|
+
* `Authorization` header value. Fail-closed semantics:
|
|
115
|
+
*
|
|
116
|
+
* - tokens configured → exact (constant-time) Bearer match or 401
|
|
117
|
+
* - no tokens + opt-out → always allowed (warns loudly once, at creation)
|
|
118
|
+
* - no tokens, no opt-out → always 503 `auth_not_configured`
|
|
119
|
+
*/
|
|
120
|
+
export function createBearerAuthorizer(opts) {
|
|
121
|
+
const tokens = (opts.tokens ?? []).filter((t) => t.length > 0);
|
|
122
|
+
if (tokens.length === 0) {
|
|
123
|
+
if (opts.allowUnauthenticated) {
|
|
124
|
+
;
|
|
125
|
+
(opts.warn ?? console.warn)(UNAUTHENTICATED_WARNING);
|
|
126
|
+
return () => ({ ok: true });
|
|
127
|
+
}
|
|
128
|
+
return () => ({
|
|
129
|
+
ok: false,
|
|
130
|
+
status: 503,
|
|
131
|
+
error: "auth_not_configured",
|
|
132
|
+
message: "no bearer tokens are configured for this workflows API; set VOYANT_API_TOKENS " +
|
|
133
|
+
"(or pass tokens explicitly), or opt out for local development with " +
|
|
134
|
+
"VOYANT_WORKFLOWS_ALLOW_UNAUTHENTICATED=1",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return (header) => {
|
|
138
|
+
if (!header) {
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
status: 401,
|
|
142
|
+
error: "unauthorized",
|
|
143
|
+
message: "missing Authorization header",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const match = /^Bearer (.+)$/.exec(header);
|
|
147
|
+
if (!match) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
status: 401,
|
|
151
|
+
error: "unauthorized",
|
|
152
|
+
message: "Authorization header must use the Bearer scheme",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const presented = match[1];
|
|
156
|
+
for (const valid of tokens) {
|
|
157
|
+
if (constantTimeEquals(presented, valid))
|
|
158
|
+
return { ok: true };
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
status: 401,
|
|
163
|
+
error: "unauthorized",
|
|
164
|
+
message: "bearer token does not match any configured value",
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Resolve a `verifyRequest` hook (the dep consumed by
|
|
170
|
+
* `handleWorkerRequest` / `createStepHandler`) with fail-closed
|
|
171
|
+
* defaults — the missing-token case yields a verifier that rejects
|
|
172
|
+
* every request with a 503 `auth_not_configured` `RequestAuthError`
|
|
173
|
+
* instead of `undefined` (which would skip auth entirely).
|
|
174
|
+
*
|
|
175
|
+
* Returns `undefined` (auth skipped) ONLY when no tokens are
|
|
176
|
+
* configured AND `allowUnauthenticated` is explicitly set.
|
|
177
|
+
*/
|
|
178
|
+
export function resolveRequestVerifier(opts) {
|
|
179
|
+
const tokens = (opts.tokens ?? []).filter((t) => t.length > 0);
|
|
180
|
+
if (tokens.length === 0 && opts.allowUnauthenticated) {
|
|
181
|
+
;
|
|
182
|
+
(opts.warn ?? console.warn)(UNAUTHENTICATED_WARNING);
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
const authorize = createBearerAuthorizer({ ...opts, allowUnauthenticated: false });
|
|
186
|
+
return (req) => {
|
|
187
|
+
const decision = authorize(req.headers.get("authorization"));
|
|
188
|
+
if (!decision.ok) {
|
|
189
|
+
throw new RequestAuthError(decision.status, decision.error, decision.message);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
64
193
|
/** Returns a signer: `(body: string) => Promise<string>` (base64 HMAC-SHA256). */
|
|
65
194
|
export async function createHmacSigner(secret) {
|
|
66
195
|
const key = await importKey(secret, ["sign"]);
|
|
@@ -100,12 +229,98 @@ export async function createHmacVerifier(secret) {
|
|
|
100
229
|
}
|
|
101
230
|
};
|
|
102
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Returns a verifier over a raw body string + detached base64 HMAC
|
|
234
|
+
* signature (the value of `STEP_AUTH_HEADER`). Suitable for servers
|
|
235
|
+
* that have already buffered the request body (e.g. the node step
|
|
236
|
+
* container). `crypto.subtle.verify` is constant-time; malformed
|
|
237
|
+
* base64 or a missing signature simply yields `false`.
|
|
238
|
+
*/
|
|
239
|
+
export async function createHmacBodyVerifier(secret) {
|
|
240
|
+
const key = await importKey(secret, ["verify"]);
|
|
241
|
+
return async (body, signatureBase64) => {
|
|
242
|
+
if (!signatureBase64)
|
|
243
|
+
return false;
|
|
244
|
+
let sig;
|
|
245
|
+
try {
|
|
246
|
+
sig = fromBase64(signatureBase64);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
return crypto.subtle.verify("HMAC", key, sig, encode(body));
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Default bundle source: Cloudflare R2's S3 endpoint
|
|
256
|
+
* (`<account>.r2.cloudflarestorage.com`), which is where deploy
|
|
257
|
+
* pipelines stage `container.mjs` and the orchestrator mints
|
|
258
|
+
* short-lived signed URLs. Anything else must be explicitly
|
|
259
|
+
* allowlisted via `allowedOrigins`.
|
|
260
|
+
*/
|
|
261
|
+
const R2_HOST_SUFFIX = ".r2.cloudflarestorage.com";
|
|
262
|
+
/**
|
|
263
|
+
* Build an origin allowlist check for caller-supplied `bundle.url`
|
|
264
|
+
* values (the node step container dynamically `import()`s the fetched
|
|
265
|
+
* bytes, so an unrestricted URL is remote-code-execution-adjacent —
|
|
266
|
+
* the SHA-256 hash pin is supplied by the same caller and is not a
|
|
267
|
+
* security control on its own).
|
|
268
|
+
*
|
|
269
|
+
* - `allowedOrigins` set (e.g. from `VOYANT_BUNDLE_ALLOWED_ORIGINS`):
|
|
270
|
+
* the URL's origin must match one of the entries exactly.
|
|
271
|
+
* Invalid entries throw at creation time (misconfiguration should
|
|
272
|
+
* fail loudly, not silently widen).
|
|
273
|
+
* - unset/empty: only HTTPS URLs on `*.r2.cloudflarestorage.com`
|
|
274
|
+
* are allowed — the production bundle source.
|
|
275
|
+
*/
|
|
276
|
+
export function createBundleUrlPolicy(opts) {
|
|
277
|
+
const entries = (opts?.allowedOrigins ?? []).filter((entry) => entry.length > 0);
|
|
278
|
+
const allowed = new Set();
|
|
279
|
+
for (const entry of entries) {
|
|
280
|
+
let parsed;
|
|
281
|
+
try {
|
|
282
|
+
parsed = new URL(entry);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
throw new Error(`createBundleUrlPolicy: invalid origin in allowlist: "${entry}"`);
|
|
286
|
+
}
|
|
287
|
+
allowed.add(parsed.origin);
|
|
288
|
+
}
|
|
289
|
+
return (url) => {
|
|
290
|
+
let parsed;
|
|
291
|
+
try {
|
|
292
|
+
parsed = new URL(url);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return { ok: false, message: `bundle url is not a valid URL: "${url}"` };
|
|
296
|
+
}
|
|
297
|
+
if (allowed.size > 0) {
|
|
298
|
+
if (allowed.has(parsed.origin))
|
|
299
|
+
return { ok: true };
|
|
300
|
+
return {
|
|
301
|
+
ok: false,
|
|
302
|
+
message: `bundle url origin "${parsed.origin}" is not in the configured allowlist ` +
|
|
303
|
+
"(VOYANT_BUNDLE_ALLOWED_ORIGINS)",
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (parsed.protocol === "https:" && parsed.hostname.endsWith(R2_HOST_SUFFIX)) {
|
|
307
|
+
return { ok: true };
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
ok: false,
|
|
311
|
+
message: `bundle url origin "${parsed.origin}" rejected: with no VOYANT_BUNDLE_ALLOWED_ORIGINS ` +
|
|
312
|
+
`configured, only https://*${R2_HOST_SUFFIX} bundle sources are allowed`,
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
}
|
|
103
316
|
// ---- Internals ----
|
|
104
317
|
async function importKey(secret, usages) {
|
|
105
318
|
if (secret.length === 0) {
|
|
106
319
|
throw new Error("HMAC secret must be a non-empty string");
|
|
107
320
|
}
|
|
108
|
-
return crypto.subtle.importKey("raw", encode(secret), { name: "HMAC", hash: "SHA-256" }, false,
|
|
321
|
+
return crypto.subtle.importKey("raw", encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
|
|
322
|
+
...usages,
|
|
323
|
+
]);
|
|
109
324
|
}
|
|
110
325
|
/**
|
|
111
326
|
* Encode to a freshly-allocated ArrayBuffer. TextEncoder's Uint8Array
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/events/registry.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAA;AACpE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAI3D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAA;IAC3C,qCAAqC;IACrC,QAAQ,CAAC,WAAW,EAAE,sBAAsB,CAAC,OAAO,CAAC,CAAA;IACrD,yDAAyD;IACzD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;CAClC;AAED,UAAU,mBAAmB;IAC3B,GAAG,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAAA;IACzC,IAAI,IAAI,uBAAuB,EAAE,CAAA;IACjC,oEAAoE;IACpE,KAAK,IAAI,IAAI,CAAA;CACd;
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/events/registry.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAA;AACpE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAI3D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAA;IAC3C,qCAAqC;IACrC,QAAQ,CAAC,WAAW,EAAE,sBAAsB,CAAC,OAAO,CAAC,CAAA;IACrD,yDAAyD;IACzD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;CAClC;AAED,UAAU,mBAAmB;IAC3B,GAAG,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAAA;IACzC,IAAI,IAAI,uBAAuB,EAAE,CAAA;IACjC,oEAAoE;IACpE,KAAK,IAAI,IAAI,CAAA;CACd;AA8BD,gEAAgE;AAChE,wBAAgB,sBAAsB,IAAI,mBAAmB,CAE5D;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD"}
|
package/dist/handler/index.js
CHANGED
|
@@ -203,9 +203,15 @@ function parseRequest(raw) {
|
|
|
203
203
|
if (typeof r.workflowId !== "string" || r.workflowId.length === 0) {
|
|
204
204
|
return { ok: false, message: "`workflowId` must be a non-empty string" };
|
|
205
205
|
}
|
|
206
|
+
if (typeof r.workflowVersion !== "string" || r.workflowVersion.length === 0) {
|
|
207
|
+
return { ok: false, message: "`workflowVersion` must be a non-empty string" };
|
|
208
|
+
}
|
|
206
209
|
if (typeof r.invocationCount !== "number" || r.invocationCount < 1) {
|
|
207
210
|
return { ok: false, message: "`invocationCount` must be >= 1" };
|
|
208
211
|
}
|
|
212
|
+
if (typeof r.deadline !== "number") {
|
|
213
|
+
return { ok: false, message: "`deadline` must be a number" };
|
|
214
|
+
}
|
|
209
215
|
if (!r.journal || typeof r.journal !== "object") {
|
|
210
216
|
return { ok: false, message: "`journal` must be an object" };
|
|
211
217
|
}
|
|
@@ -219,7 +225,22 @@ function parseRequest(raw) {
|
|
|
219
225
|
if (!r.runMeta || typeof r.runMeta !== "object") {
|
|
220
226
|
return { ok: false, message: "`runMeta` must be an object" };
|
|
221
227
|
}
|
|
222
|
-
return {
|
|
228
|
+
return {
|
|
229
|
+
ok: true,
|
|
230
|
+
value: {
|
|
231
|
+
protocolVersion: r.protocolVersion,
|
|
232
|
+
runId: r.runId,
|
|
233
|
+
workflowId: r.workflowId,
|
|
234
|
+
workflowVersion: r.workflowVersion,
|
|
235
|
+
invocationCount: r.invocationCount,
|
|
236
|
+
input: r.input,
|
|
237
|
+
journal: r.journal,
|
|
238
|
+
environment: env,
|
|
239
|
+
deadline: r.deadline,
|
|
240
|
+
tenantMeta: r.tenantMeta,
|
|
241
|
+
runMeta: r.runMeta,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
223
244
|
}
|
|
224
245
|
// ---- Helpers ----
|
|
225
246
|
function jsonResponse(status, body) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ctx.d.ts","sourceRoot":"","sources":["../../src/runtime/ctx.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ctx.d.ts","sourceRoot":"","sources":["../../src/runtime/ctx.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime/ctx.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Builds the `ctx` object passed to the workflow body.
|
|
2
|
+
// agent-quality: file-size exception -- Central ctx API wiring remains together until wait/stream/group runtime slices can be extracted safely.
|
|
2
3
|
//
|
|
3
4
|
// The executor owns the waitpoint-pending queue and the callbacks
|
|
4
5
|
// into the orchestrator; ctx is a thin shell that delegates.
|
|
@@ -380,22 +381,21 @@ export function buildCtx(args) {
|
|
|
380
381
|
throw err;
|
|
381
382
|
}
|
|
382
383
|
}
|
|
383
|
-
const streamImpl = async (streamId,
|
|
384
|
-
const source =
|
|
385
|
-
? sourceOrFn()
|
|
386
|
-
: sourceOrFn;
|
|
384
|
+
const streamImpl = async (streamId, sourceFn) => {
|
|
385
|
+
const source = sourceFn();
|
|
387
386
|
await consumeStream(streamId, source, inferEncoding(source));
|
|
388
387
|
};
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
388
|
+
const stream = Object.assign(streamImpl, {
|
|
389
|
+
text: async (id, source) => {
|
|
390
|
+
await consumeStream(id, source, "text");
|
|
391
|
+
},
|
|
392
|
+
json: async (id, source) => {
|
|
393
|
+
await consumeStream(id, source, "json");
|
|
394
|
+
},
|
|
395
|
+
bytes: async (id, source) => {
|
|
396
|
+
await consumeStream(id, source, "base64");
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
399
|
// ---- groups ----
|
|
400
400
|
// `ctx.group(name, fn)` creates a compensation scope. Implementation
|
|
401
401
|
// strategy: the outer compensable list is the single source of truth;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"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"}
|
package/dist/testing/index.js
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
//
|
|
3
3
|
// In-process test harness with mocked steps, waits, and invokes.
|
|
4
4
|
// Contract in docs/sdk-surface.md §11.
|
|
5
|
+
// agent-quality: file-size exception -- legacy test harness stays co-located until pause/resume helpers are split.
|
|
5
6
|
//
|
|
6
7
|
// Drives `executeWorkflowStep` across resumptions. Steps resolve
|
|
7
8
|
// from a user-supplied stub map; waitpoints resolve from fixtures.
|
|
8
9
|
import { executeWorkflowStep, } from "../runtime/executor.js";
|
|
9
10
|
import { emptyJournal } from "../runtime/journal.js";
|
|
10
11
|
import { getWorkflow } from "../workflow.js";
|
|
12
|
+
function workflowForExecutor(def) {
|
|
13
|
+
return def;
|
|
14
|
+
}
|
|
11
15
|
export async function runWorkflowForTest(workflow, input, opts = {}) {
|
|
12
16
|
const def = workflow;
|
|
13
17
|
const now = opts.now ?? (() => Date.now());
|
|
@@ -33,7 +37,7 @@ export async function runWorkflowForTest(workflow, input, opts = {}) {
|
|
|
33
37
|
const cursors = { event: new Map(), signal: new Map() };
|
|
34
38
|
while (invocationCount < maxInvocations) {
|
|
35
39
|
invocationCount += 1;
|
|
36
|
-
const response = await executeWorkflowStep(def, {
|
|
40
|
+
const response = await executeWorkflowStep(workflowForExecutor(def), {
|
|
37
41
|
runId: `run_test_${def.id}`,
|
|
38
42
|
workflowId: def.id,
|
|
39
43
|
workflowVersion: "test",
|
|
@@ -219,7 +223,7 @@ export async function resumeWorkflowForTest(workflow, input, opts) {
|
|
|
219
223
|
let carryover = opts.pause.pendingWaitpoints.filter((w) => w.clientWaitpointId !== matched.clientWaitpointId);
|
|
220
224
|
while (invocationCount < maxInvocations) {
|
|
221
225
|
invocationCount += 1;
|
|
222
|
-
const response = await executeWorkflowStep(def, {
|
|
226
|
+
const response = await executeWorkflowStep(workflowForExecutor(def), {
|
|
223
227
|
runId: `run_test_${def.id}`,
|
|
224
228
|
workflowId: def.id,
|
|
225
229
|
workflowVersion: "test",
|
package/dist/workflow.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../src/workflow.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,KAAK,EACV,QAAQ,EACR,eAAe,EACf,WAAW,EACX,aAAa,EACb,WAAW,EACX,SAAS,EACT,UAAU,EACX,MAAM,YAAY,CAAA;AAInB,MAAM,WAAW,cAAc,CAAC,MAAM,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO;IACjE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,MAAM,WAAW,cAAc,CAAC,MAAM,EAAE,OAAO;IAC7C,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,EAAE,CAAA;IACtD,WAAW,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAA;IACvC,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,OAAO,CAAC,EAAE,QAAQ,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAChC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CACvE;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB,CAAC,MAAM,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO,CACrE,SAAQ,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjD;AAED,MAAM,MAAM,mBAAmB,GAAG,CAC9B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,KAAK,EAAE,QAAQ,CAAA;CAAE,GACnB;IAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CACxB,GAAG;IACF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAA;IACpD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAA;IACpC,YAAY,CAAC,EAAE,eAAe,EAAE,CAAA;IAChC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,WAAW,iBAAiB,CAAC,MAAM;IACvC,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,CAAA;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,GAAG,oBAAoB,GAAG,eAAe,GAAG,aAAa,CAAA;CAC5E;
|
|
1
|
+
{"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../src/workflow.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/auth/index.ts
CHANGED
|
@@ -24,6 +24,30 @@
|
|
|
24
24
|
|
|
25
25
|
export const AUTH_HEADER = "x-voyant-dispatch-auth" as const
|
|
26
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
|
+
|
|
27
51
|
/**
|
|
28
52
|
* Returns a verifier that accepts `Authorization: Bearer <token>`
|
|
29
53
|
* where `<token>` matches any of the `validTokens` (case-sensitive,
|
|
@@ -53,15 +77,151 @@ export function createBearerVerifier(validTokens: readonly string[]): (req: Requ
|
|
|
53
77
|
}
|
|
54
78
|
}
|
|
55
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
|
+
*/
|
|
56
87
|
function constantTimeEquals(a: string, b: string): boolean {
|
|
57
|
-
|
|
58
|
-
let diff = 0
|
|
59
|
-
for (let i = 0; i <
|
|
60
|
-
diff |= a.charCodeAt(i) ^ b.charCodeAt(i)
|
|
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)
|
|
61
92
|
}
|
|
62
93
|
return diff === 0
|
|
63
94
|
}
|
|
64
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
|
+
|
|
65
225
|
/** Returns a signer: `(body: string) => Promise<string>` (base64 HMAC-SHA256). */
|
|
66
226
|
export async function createHmacSigner(secret: string): Promise<(body: string) => Promise<string>> {
|
|
67
227
|
const key = await importKey(secret, ["sign"])
|
|
@@ -102,6 +262,98 @@ export async function createHmacVerifier(secret: string): Promise<(req: Request)
|
|
|
102
262
|
}
|
|
103
263
|
}
|
|
104
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
|
+
|
|
105
357
|
// ---- Internals ----
|
|
106
358
|
|
|
107
359
|
async function importKey(
|
|
@@ -111,13 +363,9 @@ async function importKey(
|
|
|
111
363
|
if (secret.length === 0) {
|
|
112
364
|
throw new Error("HMAC secret must be a non-empty string")
|
|
113
365
|
}
|
|
114
|
-
return crypto.subtle.importKey(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
118
|
-
false,
|
|
119
|
-
usages as KeyUsage[],
|
|
120
|
-
)
|
|
366
|
+
return crypto.subtle.importKey("raw", encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
|
|
367
|
+
...usages,
|
|
368
|
+
])
|
|
121
369
|
}
|
|
122
370
|
|
|
123
371
|
/**
|
package/src/events/registry.ts
CHANGED
|
@@ -44,10 +44,8 @@ interface EventFilterRegistry {
|
|
|
44
44
|
reset(): void
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const globalRef = globalThis as
|
|
48
|
-
typeof REGISTRY_KEY,
|
|
49
|
-
Map<string, EventFilterRuntimeEntry> | undefined
|
|
50
|
-
>
|
|
47
|
+
const globalRef = globalThis as typeof globalThis &
|
|
48
|
+
Record<typeof REGISTRY_KEY, Map<string, EventFilterRuntimeEntry> | undefined>
|
|
51
49
|
|
|
52
50
|
const REGISTRY: Map<string, EventFilterRuntimeEntry> =
|
|
53
51
|
globalRef[REGISTRY_KEY] ?? new Map<string, EventFilterRuntimeEntry>()
|
package/src/handler/index.ts
CHANGED
|
@@ -335,9 +335,15 @@ function parseRequest(
|
|
|
335
335
|
if (typeof r.workflowId !== "string" || r.workflowId.length === 0) {
|
|
336
336
|
return { ok: false, message: "`workflowId` must be a non-empty string" }
|
|
337
337
|
}
|
|
338
|
+
if (typeof r.workflowVersion !== "string" || r.workflowVersion.length === 0) {
|
|
339
|
+
return { ok: false, message: "`workflowVersion` must be a non-empty string" }
|
|
340
|
+
}
|
|
338
341
|
if (typeof r.invocationCount !== "number" || r.invocationCount < 1) {
|
|
339
342
|
return { ok: false, message: "`invocationCount` must be >= 1" }
|
|
340
343
|
}
|
|
344
|
+
if (typeof r.deadline !== "number") {
|
|
345
|
+
return { ok: false, message: "`deadline` must be a number" }
|
|
346
|
+
}
|
|
341
347
|
if (!r.journal || typeof r.journal !== "object") {
|
|
342
348
|
return { ok: false, message: "`journal` must be an object" }
|
|
343
349
|
}
|
|
@@ -351,7 +357,22 @@ function parseRequest(
|
|
|
351
357
|
if (!r.runMeta || typeof r.runMeta !== "object") {
|
|
352
358
|
return { ok: false, message: "`runMeta` must be an object" }
|
|
353
359
|
}
|
|
354
|
-
return {
|
|
360
|
+
return {
|
|
361
|
+
ok: true,
|
|
362
|
+
value: {
|
|
363
|
+
protocolVersion: r.protocolVersion as ProtocolVersion,
|
|
364
|
+
runId: r.runId,
|
|
365
|
+
workflowId: r.workflowId,
|
|
366
|
+
workflowVersion: r.workflowVersion,
|
|
367
|
+
invocationCount: r.invocationCount,
|
|
368
|
+
input: r.input,
|
|
369
|
+
journal: r.journal as JournalSlice,
|
|
370
|
+
environment: env,
|
|
371
|
+
deadline: r.deadline,
|
|
372
|
+
tenantMeta: r.tenantMeta as WorkflowStepRequest["tenantMeta"],
|
|
373
|
+
runMeta: r.runMeta as WorkflowStepRequest["runMeta"],
|
|
374
|
+
},
|
|
375
|
+
}
|
|
355
376
|
}
|
|
356
377
|
|
|
357
378
|
// ---- Helpers ----
|
package/src/runtime/ctx.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Builds the `ctx` object passed to the workflow body.
|
|
2
|
+
// agent-quality: file-size exception -- Central ctx API wiring remains together until wait/stream/group runtime slices can be extracted safely.
|
|
2
3
|
//
|
|
3
4
|
// The executor owns the waitpoint-pending queue and the callbacks
|
|
4
5
|
// into the orchestrator; ctx is a thin shell that delegates.
|
|
@@ -360,7 +361,7 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
360
361
|
let closed = false
|
|
361
362
|
return {
|
|
362
363
|
async next(): Promise<IteratorResult<T>> {
|
|
363
|
-
if (closed) return { value: undefined
|
|
364
|
+
if (closed) return { value: undefined, done: true }
|
|
364
365
|
checkCancel()
|
|
365
366
|
const iterId = `${iterIdPrefix}:iter:${nextClientId()}`
|
|
366
367
|
const resolvedIter = lookupWaitpoint(iterId)
|
|
@@ -373,7 +374,7 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
373
374
|
const payload = resolvedIter.payload as unknown
|
|
374
375
|
if (isStreamEnd(payload)) {
|
|
375
376
|
closed = true
|
|
376
|
-
return { value: undefined
|
|
377
|
+
return { value: undefined, done: true }
|
|
377
378
|
}
|
|
378
379
|
if (payload === undefined && onTimeout === "throw") {
|
|
379
380
|
throw new Error(`waitpoint ${iterId} timed out`)
|
|
@@ -382,7 +383,7 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
382
383
|
},
|
|
383
384
|
async return(): Promise<IteratorResult<T>> {
|
|
384
385
|
closed = true
|
|
385
|
-
return { value: undefined
|
|
386
|
+
return { value: undefined, done: true }
|
|
386
387
|
},
|
|
387
388
|
[Symbol.asyncIterator]() {
|
|
388
389
|
return this
|
|
@@ -611,29 +612,25 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
611
612
|
}
|
|
612
613
|
}
|
|
613
614
|
|
|
614
|
-
const streamImpl = async (
|
|
615
|
+
const streamImpl = async <T>(
|
|
615
616
|
streamId: string,
|
|
616
|
-
|
|
617
|
+
sourceFn: () => AsyncGenerator<T>,
|
|
617
618
|
): Promise<void> => {
|
|
618
|
-
const source =
|
|
619
|
-
typeof sourceOrFn === "function"
|
|
620
|
-
? (sourceOrFn as () => AsyncGenerator<unknown>)()
|
|
621
|
-
: sourceOrFn
|
|
619
|
+
const source = sourceFn()
|
|
622
620
|
await consumeStream(streamId, source, inferEncoding(source))
|
|
623
621
|
}
|
|
624
622
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const stream = streamImpl as unknown as StreamApi
|
|
623
|
+
const stream: StreamApi = Object.assign(streamImpl, {
|
|
624
|
+
text: async (id, source) => {
|
|
625
|
+
await consumeStream(id, source, "text")
|
|
626
|
+
},
|
|
627
|
+
json: async (id, source) => {
|
|
628
|
+
await consumeStream(id, source, "json")
|
|
629
|
+
},
|
|
630
|
+
bytes: async (id, source) => {
|
|
631
|
+
await consumeStream(id, source, "base64")
|
|
632
|
+
},
|
|
633
|
+
} satisfies Pick<StreamApi, "text" | "json" | "bytes">)
|
|
637
634
|
|
|
638
635
|
// ---- groups ----
|
|
639
636
|
|
|
@@ -769,7 +766,7 @@ function normalizeChunk(value: unknown, encoding: "text" | "json" | "base64"): u
|
|
|
769
766
|
function toBase64(bytes: Uint8Array): string {
|
|
770
767
|
// Node + modern runtimes provide Buffer or btoa. Use Buffer when
|
|
771
768
|
// available for efficiency; fall back to manual encode for isolates.
|
|
772
|
-
const g = globalThis as
|
|
769
|
+
const g = globalThis as typeof globalThis & {
|
|
773
770
|
Buffer?: { from(b: Uint8Array): { toString(enc: "base64"): string } }
|
|
774
771
|
btoa?: (s: string) => string
|
|
775
772
|
}
|
package/src/testing/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// In-process test harness with mocked steps, waits, and invokes.
|
|
4
4
|
// Contract in docs/sdk-surface.md §11.
|
|
5
|
+
// agent-quality: file-size exception -- legacy test harness stays co-located until pause/resume helpers are split.
|
|
5
6
|
//
|
|
6
7
|
// Drives `executeWorkflowStep` across resumptions. Steps resolve
|
|
7
8
|
// from a user-supplied stub map; waitpoints resolve from fixtures.
|
|
@@ -31,6 +32,10 @@ import type {
|
|
|
31
32
|
} from "../workflow.js"
|
|
32
33
|
import { getWorkflow } from "../workflow.js"
|
|
33
34
|
|
|
35
|
+
function workflowForExecutor<TIn, TOut>(def: WorkflowDefinition<TIn, TOut>): WorkflowDefinition {
|
|
36
|
+
return def as WorkflowDefinition
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
export interface TestOptions<_TIn> {
|
|
35
40
|
/** Map of stepId → function run in place of the real step body. */
|
|
36
41
|
steps?: Record<string, (stepCtx: StepContext) => unknown | Promise<unknown>>
|
|
@@ -128,7 +133,7 @@ export async function runWorkflowForTest<TIn, TOut>(
|
|
|
128
133
|
while (invocationCount < maxInvocations) {
|
|
129
134
|
invocationCount += 1
|
|
130
135
|
|
|
131
|
-
const response = await executeWorkflowStep(def
|
|
136
|
+
const response = await executeWorkflowStep(workflowForExecutor(def), {
|
|
132
137
|
runId: `run_test_${def.id}`,
|
|
133
138
|
workflowId: def.id,
|
|
134
139
|
workflowVersion: "test",
|
|
@@ -363,7 +368,7 @@ export async function resumeWorkflowForTest<TIn, TOut>(
|
|
|
363
368
|
while (invocationCount < maxInvocations) {
|
|
364
369
|
invocationCount += 1
|
|
365
370
|
|
|
366
|
-
const response = await executeWorkflowStep(def
|
|
371
|
+
const response = await executeWorkflowStep(workflowForExecutor(def), {
|
|
367
372
|
runId: `run_test_${def.id}`,
|
|
368
373
|
workflowId: def.id,
|
|
369
374
|
workflowVersion: "test",
|
|
@@ -681,23 +686,19 @@ async function resolveWaitpoint(
|
|
|
681
686
|
at: now(),
|
|
682
687
|
data: { childWorkflowId, input: childInput },
|
|
683
688
|
})
|
|
684
|
-
const childResult = await runWorkflowForTest(
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
maxInvocations: opts.maxInvocations,
|
|
698
|
-
pauseOnWait: detach || opts.pauseOnWait,
|
|
699
|
-
},
|
|
700
|
-
)
|
|
689
|
+
const childResult = await runWorkflowForTest(child, childInput, {
|
|
690
|
+
steps: opts.steps,
|
|
691
|
+
waitForEvent: opts.waitForEvent,
|
|
692
|
+
waitForSignal: opts.waitForSignal,
|
|
693
|
+
waitForToken: opts.waitForToken,
|
|
694
|
+
invoke: opts.invoke,
|
|
695
|
+
env: opts.env,
|
|
696
|
+
environment: opts.environment,
|
|
697
|
+
now: opts.now,
|
|
698
|
+
random: opts.random,
|
|
699
|
+
maxInvocations: opts.maxInvocations,
|
|
700
|
+
pauseOnWait: detach || opts.pauseOnWait,
|
|
701
|
+
})
|
|
701
702
|
parentEvents.push({
|
|
702
703
|
type: "child.finished",
|
|
703
704
|
at: now(),
|
package/src/workflow.ts
CHANGED
|
@@ -72,10 +72,8 @@ export interface ConcurrencyPolicy<TInput> {
|
|
|
72
72
|
* would create a private map per bundle copy.
|
|
73
73
|
*/
|
|
74
74
|
const REGISTRY_KEY = "__voyantWorkflowRegistry" as const
|
|
75
|
-
const globalRef = globalThis as
|
|
76
|
-
typeof REGISTRY_KEY,
|
|
77
|
-
Map<string, WorkflowDefinition> | undefined
|
|
78
|
-
>
|
|
75
|
+
const globalRef = globalThis as typeof globalThis &
|
|
76
|
+
Record<typeof REGISTRY_KEY, Map<string, WorkflowDefinition> | undefined>
|
|
79
77
|
const REGISTRY: Map<string, WorkflowDefinition> =
|
|
80
78
|
globalRef[REGISTRY_KEY] ?? new Map<string, WorkflowDefinition>()
|
|
81
79
|
globalRef[REGISTRY_KEY] = REGISTRY
|