@voyantjs/workflows 0.0.0 → 0.6.8
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 +26 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +137 -0
- package/dist/conditions.d.ts +29 -0
- package/dist/conditions.d.ts.map +1 -0
- package/dist/conditions.js +5 -0
- package/dist/handler/index.d.ts +104 -0
- package/dist/handler/index.d.ts.map +1 -0
- package/dist/handler/index.js +238 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/protocol/index.d.ts +187 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +7 -0
- package/dist/rate-limit/index.d.ts +40 -0
- package/dist/rate-limit/index.d.ts.map +1 -0
- package/dist/rate-limit/index.js +139 -0
- package/dist/runtime/ctx.d.ts +102 -0
- package/dist/runtime/ctx.d.ts.map +1 -0
- package/dist/runtime/ctx.js +607 -0
- package/dist/runtime/determinism.d.ts +19 -0
- package/dist/runtime/determinism.d.ts.map +1 -0
- package/dist/runtime/determinism.js +61 -0
- package/dist/runtime/errors.d.ts +21 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +45 -0
- package/dist/runtime/executor.d.ts +159 -0
- package/dist/runtime/executor.d.ts.map +1 -0
- package/dist/runtime/executor.js +225 -0
- package/dist/runtime/journal.d.ts +55 -0
- package/dist/runtime/journal.d.ts.map +1 -0
- package/dist/runtime/journal.js +28 -0
- package/dist/testing/index.d.ts +117 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +595 -0
- package/dist/trigger.d.ts +122 -0
- package/dist/trigger.d.ts.map +1 -0
- package/dist/trigger.js +23 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/workflow.d.ts +212 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +46 -0
- package/package.json +30 -30
- package/src/auth/index.ts +46 -52
- package/src/conditions.ts +13 -13
- package/src/handler/index.ts +110 -106
- package/src/index.ts +7 -7
- package/src/protocol/index.ts +137 -71
- package/src/rate-limit/index.ts +77 -78
- package/src/runtime/ctx.ts +354 -342
- package/src/runtime/determinism.ts +27 -27
- package/src/runtime/errors.ts +17 -17
- package/src/runtime/executor.ts +179 -172
- package/src/runtime/journal.ts +25 -25
- package/src/testing/index.ts +268 -202
- package/src/trigger.ts +64 -71
- package/src/types.ts +16 -18
- package/src/workflow.ts +154 -152
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare const AUTH_HEADER: "x-voyant-dispatch-auth";
|
|
2
|
+
/**
|
|
3
|
+
* Returns a verifier that accepts `Authorization: Bearer <token>`
|
|
4
|
+
* where `<token>` matches any of the `validTokens` (case-sensitive,
|
|
5
|
+
* constant-time compared). Usable as the `verifyRequest` dep on
|
|
6
|
+
* `handleWorkerRequest` / `createStepHandler` for the public-facing
|
|
7
|
+
* surface of an orchestrator or tenant Worker.
|
|
8
|
+
*
|
|
9
|
+
* Intended for dev + single-tenant deployments. Production should
|
|
10
|
+
* issue per-tenant, short-lived tokens from a control plane.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createBearerVerifier(validTokens: readonly string[]): (req: Request) => void;
|
|
13
|
+
/** Returns a signer: `(body: string) => Promise<string>` (base64 HMAC-SHA256). */
|
|
14
|
+
export declare function createHmacSigner(secret: string): Promise<(body: string) => Promise<string>>;
|
|
15
|
+
/**
|
|
16
|
+
* Returns a verifier: `(req: Request) => Promise<void>`. Throws if:
|
|
17
|
+
* - the header is missing,
|
|
18
|
+
* - the signature is malformed,
|
|
19
|
+
* - the signature does not match the current body.
|
|
20
|
+
*
|
|
21
|
+
* The verifier consumes `req.body` via `req.text()`. Callers that
|
|
22
|
+
* need the body downstream should pre-clone: `req.clone()` before
|
|
23
|
+
* passing in.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createHmacVerifier(secret: string): Promise<(req: Request) => Promise<void>>;
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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;AAWD,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"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// @voyantjs/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 "@voyantjs/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 "@voyantjs/workflows/auth";
|
|
21
|
+
// import { createStepHandler } from "@voyantjs/workflows/handler";
|
|
22
|
+
// const verify = await createHmacVerifier(env.VOYANT_SIGNING_KEY);
|
|
23
|
+
// export default { fetch: createStepHandler({ verifyRequest: verify }) };
|
|
24
|
+
export const AUTH_HEADER = "x-voyant-dispatch-auth";
|
|
25
|
+
/**
|
|
26
|
+
* Returns a verifier that accepts `Authorization: Bearer <token>`
|
|
27
|
+
* where `<token>` matches any of the `validTokens` (case-sensitive,
|
|
28
|
+
* constant-time compared). Usable as the `verifyRequest` dep on
|
|
29
|
+
* `handleWorkerRequest` / `createStepHandler` for the public-facing
|
|
30
|
+
* surface of an orchestrator or tenant Worker.
|
|
31
|
+
*
|
|
32
|
+
* Intended for dev + single-tenant deployments. Production should
|
|
33
|
+
* issue per-tenant, short-lived tokens from a control plane.
|
|
34
|
+
*/
|
|
35
|
+
export function createBearerVerifier(validTokens) {
|
|
36
|
+
if (validTokens.length === 0) {
|
|
37
|
+
throw new Error("createBearerVerifier: need at least one valid token");
|
|
38
|
+
}
|
|
39
|
+
return (req) => {
|
|
40
|
+
const header = req.headers.get("authorization");
|
|
41
|
+
if (!header)
|
|
42
|
+
throw new Error("missing Authorization header");
|
|
43
|
+
const match = /^Bearer (.+)$/.exec(header);
|
|
44
|
+
if (!match) {
|
|
45
|
+
throw new Error("Authorization header must use the Bearer scheme");
|
|
46
|
+
}
|
|
47
|
+
const presented = match[1];
|
|
48
|
+
for (const valid of validTokens) {
|
|
49
|
+
if (constantTimeEquals(presented, valid))
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
throw new Error("bearer token does not match any configured value");
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function constantTimeEquals(a, b) {
|
|
56
|
+
if (a.length !== b.length)
|
|
57
|
+
return false;
|
|
58
|
+
let diff = 0;
|
|
59
|
+
for (let i = 0; i < a.length; i++) {
|
|
60
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
61
|
+
}
|
|
62
|
+
return diff === 0;
|
|
63
|
+
}
|
|
64
|
+
/** Returns a signer: `(body: string) => Promise<string>` (base64 HMAC-SHA256). */
|
|
65
|
+
export async function createHmacSigner(secret) {
|
|
66
|
+
const key = await importKey(secret, ["sign"]);
|
|
67
|
+
return async (body) => {
|
|
68
|
+
const sig = await crypto.subtle.sign("HMAC", key, encode(body));
|
|
69
|
+
return toBase64(sig);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Returns a verifier: `(req: Request) => Promise<void>`. Throws if:
|
|
74
|
+
* - the header is missing,
|
|
75
|
+
* - the signature is malformed,
|
|
76
|
+
* - the signature does not match the current body.
|
|
77
|
+
*
|
|
78
|
+
* The verifier consumes `req.body` via `req.text()`. Callers that
|
|
79
|
+
* need the body downstream should pre-clone: `req.clone()` before
|
|
80
|
+
* passing in.
|
|
81
|
+
*/
|
|
82
|
+
export async function createHmacVerifier(secret) {
|
|
83
|
+
const key = await importKey(secret, ["verify"]);
|
|
84
|
+
return async (req) => {
|
|
85
|
+
const header = req.headers.get(AUTH_HEADER);
|
|
86
|
+
if (!header) {
|
|
87
|
+
throw new Error(`missing ${AUTH_HEADER} header`);
|
|
88
|
+
}
|
|
89
|
+
let sig;
|
|
90
|
+
try {
|
|
91
|
+
sig = fromBase64(header);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw new Error(`malformed ${AUTH_HEADER} header (expected base64)`);
|
|
95
|
+
}
|
|
96
|
+
const body = await req.clone().text();
|
|
97
|
+
const ok = await crypto.subtle.verify("HMAC", key, sig, encode(body));
|
|
98
|
+
if (!ok) {
|
|
99
|
+
throw new Error(`${AUTH_HEADER} signature does not match request body`);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// ---- Internals ----
|
|
104
|
+
async function importKey(secret, usages) {
|
|
105
|
+
if (secret.length === 0) {
|
|
106
|
+
throw new Error("HMAC secret must be a non-empty string");
|
|
107
|
+
}
|
|
108
|
+
return crypto.subtle.importKey("raw", encode(secret), { name: "HMAC", hash: "SHA-256" }, false, usages);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Encode to a freshly-allocated ArrayBuffer. TextEncoder's Uint8Array
|
|
112
|
+
* is typed as `Uint8Array<ArrayBufferLike>` under recent TS lib, which
|
|
113
|
+
* doesn't satisfy the `BufferSource` param of `subtle.sign/verify`.
|
|
114
|
+
* Copying into a new ArrayBuffer sidesteps the nominal mismatch.
|
|
115
|
+
*/
|
|
116
|
+
function encode(s) {
|
|
117
|
+
const view = new TextEncoder().encode(s);
|
|
118
|
+
const buf = new ArrayBuffer(view.byteLength);
|
|
119
|
+
new Uint8Array(buf).set(view);
|
|
120
|
+
return buf;
|
|
121
|
+
}
|
|
122
|
+
function toBase64(buffer) {
|
|
123
|
+
// btoa is available in every modern runtime (Node 16+, Workers, browsers).
|
|
124
|
+
const bytes = new Uint8Array(buffer);
|
|
125
|
+
let bin = "";
|
|
126
|
+
for (let i = 0; i < bytes.length; i++)
|
|
127
|
+
bin += String.fromCharCode(bytes[i]);
|
|
128
|
+
return btoa(bin);
|
|
129
|
+
}
|
|
130
|
+
function fromBase64(s) {
|
|
131
|
+
const bin = atob(s);
|
|
132
|
+
const buf = new ArrayBuffer(bin.length);
|
|
133
|
+
const view = new Uint8Array(buf);
|
|
134
|
+
for (let i = 0; i < bin.length; i++)
|
|
135
|
+
view[i] = bin.charCodeAt(i);
|
|
136
|
+
return buf;
|
|
137
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Duration, RunStatus } from "./types.js";
|
|
2
|
+
export interface EventCondition {
|
|
3
|
+
event: string;
|
|
4
|
+
match?: Record<string, unknown> | ((payload: unknown) => boolean);
|
|
5
|
+
}
|
|
6
|
+
export interface SignalCondition {
|
|
7
|
+
signal: string;
|
|
8
|
+
match?: Record<string, unknown> | ((payload: unknown) => boolean);
|
|
9
|
+
}
|
|
10
|
+
export interface TimeCondition {
|
|
11
|
+
after?: Duration | Date;
|
|
12
|
+
before?: Duration | Date;
|
|
13
|
+
}
|
|
14
|
+
export interface RunStatusCondition {
|
|
15
|
+
run: {
|
|
16
|
+
id: string;
|
|
17
|
+
status: RunStatus[];
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface OrCondition {
|
|
21
|
+
or: Condition[];
|
|
22
|
+
}
|
|
23
|
+
export interface AndCondition {
|
|
24
|
+
and: Condition[];
|
|
25
|
+
}
|
|
26
|
+
export type Condition = EventCondition | SignalCondition | TimeCondition | RunStatusCondition | OrCondition | AndCondition;
|
|
27
|
+
export declare const or: (...conditions: Condition[]) => OrCondition;
|
|
28
|
+
export declare const and: (...conditions: Condition[]) => AndCondition;
|
|
29
|
+
//# sourceMappingURL=conditions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditions.d.ts","sourceRoot":"","sources":["../src/conditions.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAErD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,CAAA;CAClE;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,CAAA;CAClE;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;IACvB,MAAM,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,SAAS,EAAE,CAAA;KAAE,CAAA;CACzC;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,SAAS,EAAE,CAAA;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,SAAS,EAAE,CAAA;CACjB;AAED,MAAM,MAAM,SAAS,GACjB,cAAc,GACd,eAAe,GACf,aAAa,GACb,kBAAkB,GAClB,WAAW,GACX,YAAY,CAAA;AAEhB,eAAO,MAAM,EAAE,GAAI,GAAG,YAAY,SAAS,EAAE,KAAG,WAAmC,CAAA;AACnF,eAAO,MAAM,GAAG,GAAI,GAAG,YAAY,SAAS,EAAE,KAAG,YAAqC,CAAA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Condition types used in step options (waitFor / cancelIf / skipIf)
|
|
2
|
+
// and in event-filter match expressions.
|
|
3
|
+
// Authoritative contract in docs/sdk-surface.md §4.
|
|
4
|
+
export const or = (...conditions) => ({ or: conditions });
|
|
5
|
+
export const and = (...conditions) => ({ and: conditions });
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type ExecuteWorkflowStepRequest, type ExecuteWorkflowStepResponse, executeWorkflowStep, type StepRunner } from "../runtime/executor.js";
|
|
2
|
+
export type { StepJournalEntry } from "../runtime/journal.js";
|
|
3
|
+
export type { ExecuteWorkflowStepRequest, ExecuteWorkflowStepResponse, StepRunner };
|
|
4
|
+
export { executeWorkflowStep };
|
|
5
|
+
import { type ProtocolVersion } from "../protocol/index.js";
|
|
6
|
+
import type { RateLimiter } from "../rate-limit/index.js";
|
|
7
|
+
import type { JournalSlice } from "../runtime/journal.js";
|
|
8
|
+
import type { RunTrigger } from "../types.js";
|
|
9
|
+
export interface StepHandlerDeps {
|
|
10
|
+
/**
|
|
11
|
+
* Optional. Called before parsing the body. Should throw / reject
|
|
12
|
+
* if the request is not from a trusted orchestrator. In production
|
|
13
|
+
* this verifies the `X-Voyant-Dispatch-Auth` HMAC against a public
|
|
14
|
+
* key embedded by `voyant build`.
|
|
15
|
+
*/
|
|
16
|
+
verifyRequest?: (req: Request) => void | Promise<void>;
|
|
17
|
+
/** Injectable clock. Defaults to Date.now. */
|
|
18
|
+
now?: () => number;
|
|
19
|
+
/** Optional structured logger. */
|
|
20
|
+
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Rate limiter shared across step invocations. Required when any
|
|
23
|
+
* registered workflow declares `options.rateLimit` on a step; see
|
|
24
|
+
* `createInMemoryRateLimiter` in `@voyantjs/workflows/rate-limit` for
|
|
25
|
+
* the reference impl. One instance per Worker process is the
|
|
26
|
+
* intended cardinality — state is kept in the limiter's closure.
|
|
27
|
+
*/
|
|
28
|
+
rateLimiter?: RateLimiter;
|
|
29
|
+
/**
|
|
30
|
+
* Runner for steps declared with `options.runtime === "node"`.
|
|
31
|
+
* Leave unset for handlers that only run edge steps; any node step
|
|
32
|
+
* will then fail with `NODE_RUNTIME_UNAVAILABLE`.
|
|
33
|
+
*
|
|
34
|
+
* Typical impl dispatches to a separate sandboxed context:
|
|
35
|
+
* - Local dev: an in-process passthrough (same Node process).
|
|
36
|
+
* - CF production: a Cloudflare Container binding, via
|
|
37
|
+
* `createCfContainerStepRunner` from `@voyantjs/workflows-orchestrator-cloudflare`.
|
|
38
|
+
*
|
|
39
|
+
* This is bring-your-own because the right dispatch shape depends on
|
|
40
|
+
* the target runtime; the executor only cares that a runner exists.
|
|
41
|
+
*/
|
|
42
|
+
nodeStepRunner?: StepRunner;
|
|
43
|
+
}
|
|
44
|
+
/** The HTTP request body the orchestrator sends. */
|
|
45
|
+
export interface WorkflowStepRequest {
|
|
46
|
+
protocolVersion: ProtocolVersion;
|
|
47
|
+
runId: string;
|
|
48
|
+
workflowId: string;
|
|
49
|
+
workflowVersion: string;
|
|
50
|
+
invocationCount: number;
|
|
51
|
+
input: unknown;
|
|
52
|
+
journal: JournalSlice;
|
|
53
|
+
environment: "production" | "preview" | "development";
|
|
54
|
+
deadline: number;
|
|
55
|
+
tenantMeta: {
|
|
56
|
+
tenantId: string;
|
|
57
|
+
projectId: string;
|
|
58
|
+
organizationId: string;
|
|
59
|
+
projectSlug?: string;
|
|
60
|
+
organizationSlug?: string;
|
|
61
|
+
};
|
|
62
|
+
runMeta: {
|
|
63
|
+
number: number;
|
|
64
|
+
attempt: number;
|
|
65
|
+
triggeredBy: RunTrigger;
|
|
66
|
+
tags: string[];
|
|
67
|
+
startedAt: number;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** The JSON response body the tenant returns. */
|
|
71
|
+
export type WorkflowStepResponse = ExecuteWorkflowStepResponse;
|
|
72
|
+
/** Error-response envelope used for HTTP 4xx/5xx. */
|
|
73
|
+
export interface StepHandlerError {
|
|
74
|
+
error: string;
|
|
75
|
+
message: string;
|
|
76
|
+
details?: unknown;
|
|
77
|
+
}
|
|
78
|
+
/** Build an HTTP fetch-style handler. */
|
|
79
|
+
export declare function createStepHandler(deps?: StepHandlerDeps): (req: Request) => Promise<Response>;
|
|
80
|
+
/** Per-invocation options available to callers of the transport-free entry point. */
|
|
81
|
+
export interface StepRequestOptions {
|
|
82
|
+
/** AbortSignal forwarded to `ctx.signal` inside the step body. */
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
/**
|
|
85
|
+
* Fires synchronously from `ctx.stream.*` as each chunk is produced.
|
|
86
|
+
* Used by orchestrators that want to broadcast chunks live
|
|
87
|
+
* (dashboards, queues) before the invocation returns.
|
|
88
|
+
*/
|
|
89
|
+
onStreamChunk?: (chunk: import("../runtime/executor.js").StreamChunk) => void;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Transport-free entry point. Callers that already parsed the body
|
|
93
|
+
* (e.g. local orchestrator in-memory, tests) invoke this directly.
|
|
94
|
+
* Returns either the step response or an error envelope with the HTTP
|
|
95
|
+
* status the caller should use.
|
|
96
|
+
*/
|
|
97
|
+
export declare function handleStepRequest(raw: unknown, deps?: StepHandlerDeps, opts?: StepRequestOptions): Promise<{
|
|
98
|
+
status: number;
|
|
99
|
+
body: WorkflowStepResponse;
|
|
100
|
+
} | {
|
|
101
|
+
status: number;
|
|
102
|
+
body: StepHandlerError;
|
|
103
|
+
}>;
|
|
104
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handler/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// @voyantjs/workflows/handler
|
|
2
|
+
//
|
|
3
|
+
// The tenant side of the runtime protocol (see docs/runtime-protocol.md §2).
|
|
4
|
+
// The orchestrator invokes `POST /__voyant/workflow-step`; the tenant
|
|
5
|
+
// Worker responds by running the workflow body once (one invocation of
|
|
6
|
+
// the executor) and returning the executor response as JSON.
|
|
7
|
+
//
|
|
8
|
+
// export default { fetch: createStepHandler() }
|
|
9
|
+
//
|
|
10
|
+
// is enough to make a tenant Worker protocol-conformant. Auth is
|
|
11
|
+
// optional at the SDK level: in production, wire the HMAC verifier
|
|
12
|
+
// bundled by `voyant build`; for local dev, leave it unset.
|
|
13
|
+
//
|
|
14
|
+
// The executor's native response shape is returned verbatim — the wire
|
|
15
|
+
// document calls for compensated/compensation_failed to be folded into
|
|
16
|
+
// "failed" for the first draft, but since the draft is not yet locked
|
|
17
|
+
// and the executor-shape already round-trips losslessly, we keep the
|
|
18
|
+
// full discriminated union here. The orchestrator adapter can collapse.
|
|
19
|
+
import { executeWorkflowStep, } from "../runtime/executor.js";
|
|
20
|
+
export { executeWorkflowStep };
|
|
21
|
+
import { PROTOCOL_VERSION } from "../protocol/index.js";
|
|
22
|
+
import { getWorkflow } from "../workflow.js";
|
|
23
|
+
/** Build an HTTP fetch-style handler. */
|
|
24
|
+
export function createStepHandler(deps = {}) {
|
|
25
|
+
return async (req) => {
|
|
26
|
+
if (req.method !== "POST") {
|
|
27
|
+
return jsonResponse(405, errorBody("method_not_allowed", "POST required"));
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
if (deps.verifyRequest)
|
|
31
|
+
await deps.verifyRequest(req);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
deps.logger?.("warn", "step handler: auth rejected", {
|
|
35
|
+
error: err instanceof Error ? err.message : String(err),
|
|
36
|
+
});
|
|
37
|
+
return jsonResponse(401, errorBody("unauthorized", errMessage(err)));
|
|
38
|
+
}
|
|
39
|
+
let raw;
|
|
40
|
+
try {
|
|
41
|
+
raw = await req.json();
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return jsonResponse(400, errorBody("invalid_json", errMessage(err)));
|
|
45
|
+
}
|
|
46
|
+
// The incoming Request carries its own AbortSignal; threading it
|
|
47
|
+
// through lets `ctx.signal` observe client-side aborts (orchestrator
|
|
48
|
+
// cancellations, closed fetches, etc.) during step execution.
|
|
49
|
+
const out = await runStepInner(raw, deps, { signal: req.signal });
|
|
50
|
+
return jsonResponse(out.status, out.body);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Transport-free entry point. Callers that already parsed the body
|
|
55
|
+
* (e.g. local orchestrator in-memory, tests) invoke this directly.
|
|
56
|
+
* Returns either the step response or an error envelope with the HTTP
|
|
57
|
+
* status the caller should use.
|
|
58
|
+
*/
|
|
59
|
+
export async function handleStepRequest(raw, deps = {}, opts = {}) {
|
|
60
|
+
return runStepInner(raw, deps, opts);
|
|
61
|
+
}
|
|
62
|
+
async function runStepInner(raw, deps, opts = {}) {
|
|
63
|
+
const parsed = parseRequest(raw);
|
|
64
|
+
if (!parsed.ok)
|
|
65
|
+
return { status: 400, body: errorBody("invalid_request", parsed.message) };
|
|
66
|
+
const reqBody = parsed.value;
|
|
67
|
+
if (reqBody.protocolVersion !== PROTOCOL_VERSION) {
|
|
68
|
+
return {
|
|
69
|
+
status: 426,
|
|
70
|
+
body: errorBody("protocol_version_mismatch", `tenant supports protocol ${PROTOCOL_VERSION}, got ${String(reqBody.protocolVersion)}`),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const def = getWorkflow(reqBody.workflowId);
|
|
74
|
+
if (!def) {
|
|
75
|
+
return {
|
|
76
|
+
status: 404,
|
|
77
|
+
body: errorBody("workflow_not_found", `workflow "${reqBody.workflowId}" is not registered in this bundle`),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const now = deps.now ?? (() => Date.now());
|
|
81
|
+
const stepRunner = createInProcessStepRunner(now);
|
|
82
|
+
const runtimeEnv = {
|
|
83
|
+
run: {
|
|
84
|
+
id: reqBody.runId,
|
|
85
|
+
number: reqBody.runMeta.number,
|
|
86
|
+
attempt: reqBody.runMeta.attempt,
|
|
87
|
+
triggeredBy: reqBody.runMeta.triggeredBy,
|
|
88
|
+
tags: reqBody.runMeta.tags,
|
|
89
|
+
startedAt: reqBody.runMeta.startedAt,
|
|
90
|
+
},
|
|
91
|
+
workflow: { id: reqBody.workflowId, version: reqBody.workflowVersion },
|
|
92
|
+
environment: { name: reqBody.environment },
|
|
93
|
+
project: {
|
|
94
|
+
id: reqBody.tenantMeta.projectId,
|
|
95
|
+
slug: reqBody.tenantMeta.projectSlug ?? reqBody.tenantMeta.projectId,
|
|
96
|
+
},
|
|
97
|
+
organization: {
|
|
98
|
+
id: reqBody.tenantMeta.organizationId,
|
|
99
|
+
slug: reqBody.tenantMeta.organizationSlug ?? reqBody.tenantMeta.organizationId,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
const response = await executeWorkflowStep(def, {
|
|
104
|
+
runId: reqBody.runId,
|
|
105
|
+
workflowId: reqBody.workflowId,
|
|
106
|
+
workflowVersion: reqBody.workflowVersion,
|
|
107
|
+
input: reqBody.input,
|
|
108
|
+
journal: reqBody.journal,
|
|
109
|
+
invocationCount: reqBody.invocationCount,
|
|
110
|
+
environment: runtimeEnv,
|
|
111
|
+
triggeredBy: reqBody.runMeta.triggeredBy,
|
|
112
|
+
runStartedAt: reqBody.runMeta.startedAt,
|
|
113
|
+
tags: reqBody.runMeta.tags,
|
|
114
|
+
stepRunner,
|
|
115
|
+
nodeStepRunner: deps.nodeStepRunner,
|
|
116
|
+
rateLimiter: deps.rateLimiter,
|
|
117
|
+
now,
|
|
118
|
+
abortSignal: opts.signal,
|
|
119
|
+
onStreamChunk: opts.onStreamChunk,
|
|
120
|
+
});
|
|
121
|
+
return { status: 200, body: response };
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
deps.logger?.("error", "step handler: executor threw", {
|
|
125
|
+
error: err instanceof Error ? err.message : String(err),
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
status: 500,
|
|
129
|
+
body: errorBody("executor_error", errMessage(err)),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Build a step runner that executes the step body in the same
|
|
135
|
+
* process. Suitable for `runtime: "edge"`. Container-runtime steps
|
|
136
|
+
* will swap this for a dispatching runner that POSTs to a pod.
|
|
137
|
+
*/
|
|
138
|
+
function createInProcessStepRunner(now) {
|
|
139
|
+
return async ({ stepId: _stepId, attempt, fn, stepCtx }) => {
|
|
140
|
+
const startedAt = now();
|
|
141
|
+
try {
|
|
142
|
+
const output = await fn(stepCtx);
|
|
143
|
+
return {
|
|
144
|
+
attempt,
|
|
145
|
+
status: "ok",
|
|
146
|
+
output,
|
|
147
|
+
startedAt,
|
|
148
|
+
finishedAt: now(),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
const e = err;
|
|
153
|
+
const code = typeof err.code === "string"
|
|
154
|
+
? err.code
|
|
155
|
+
: "UNKNOWN";
|
|
156
|
+
const retryAfter = err.retryAfter;
|
|
157
|
+
return {
|
|
158
|
+
attempt,
|
|
159
|
+
status: "err",
|
|
160
|
+
error: {
|
|
161
|
+
category: "USER_ERROR",
|
|
162
|
+
code,
|
|
163
|
+
message: e?.message ?? String(err),
|
|
164
|
+
name: e?.name,
|
|
165
|
+
stack: e?.stack,
|
|
166
|
+
data: retryAfter !== undefined ? { retryAfter } : undefined,
|
|
167
|
+
},
|
|
168
|
+
startedAt,
|
|
169
|
+
finishedAt: now(),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// ---- Parsing ----
|
|
175
|
+
function parseRequest(raw) {
|
|
176
|
+
if (raw === null || typeof raw !== "object") {
|
|
177
|
+
return { ok: false, message: "body must be a JSON object" };
|
|
178
|
+
}
|
|
179
|
+
const r = raw;
|
|
180
|
+
const required = [
|
|
181
|
+
"protocolVersion",
|
|
182
|
+
"runId",
|
|
183
|
+
"workflowId",
|
|
184
|
+
"workflowVersion",
|
|
185
|
+
"invocationCount",
|
|
186
|
+
"journal",
|
|
187
|
+
"environment",
|
|
188
|
+
"deadline",
|
|
189
|
+
"tenantMeta",
|
|
190
|
+
"runMeta",
|
|
191
|
+
];
|
|
192
|
+
for (const k of required) {
|
|
193
|
+
if (!(k in r))
|
|
194
|
+
return { ok: false, message: `missing required field "${k}"` };
|
|
195
|
+
}
|
|
196
|
+
if (typeof r.protocolVersion !== "number") {
|
|
197
|
+
return { ok: false, message: "`protocolVersion` must be a number" };
|
|
198
|
+
}
|
|
199
|
+
if (typeof r.runId !== "string" || r.runId.length === 0) {
|
|
200
|
+
return { ok: false, message: "`runId` must be a non-empty string" };
|
|
201
|
+
}
|
|
202
|
+
if (typeof r.workflowId !== "string" || r.workflowId.length === 0) {
|
|
203
|
+
return { ok: false, message: "`workflowId` must be a non-empty string" };
|
|
204
|
+
}
|
|
205
|
+
if (typeof r.invocationCount !== "number" || r.invocationCount < 1) {
|
|
206
|
+
return { ok: false, message: "`invocationCount` must be >= 1" };
|
|
207
|
+
}
|
|
208
|
+
if (!r.journal || typeof r.journal !== "object") {
|
|
209
|
+
return { ok: false, message: "`journal` must be an object" };
|
|
210
|
+
}
|
|
211
|
+
const env = r.environment;
|
|
212
|
+
if (env !== "production" && env !== "preview" && env !== "development") {
|
|
213
|
+
return { ok: false, message: "`environment` must be production | preview | development" };
|
|
214
|
+
}
|
|
215
|
+
if (!r.tenantMeta || typeof r.tenantMeta !== "object") {
|
|
216
|
+
return { ok: false, message: "`tenantMeta` must be an object" };
|
|
217
|
+
}
|
|
218
|
+
if (!r.runMeta || typeof r.runMeta !== "object") {
|
|
219
|
+
return { ok: false, message: "`runMeta` must be an object" };
|
|
220
|
+
}
|
|
221
|
+
return { ok: true, value: r };
|
|
222
|
+
}
|
|
223
|
+
// ---- Helpers ----
|
|
224
|
+
function jsonResponse(status, body) {
|
|
225
|
+
return new Response(JSON.stringify(body), {
|
|
226
|
+
status,
|
|
227
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function errorBody(error, message, details) {
|
|
231
|
+
const out = { error, message };
|
|
232
|
+
if (details !== undefined)
|
|
233
|
+
out.details = details;
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
function errMessage(err) {
|
|
237
|
+
return err instanceof Error ? err.message : String(err);
|
|
238
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { FatalError, HookConflictError, QuotaExceededError, RetryableError, TimeoutError, ValidationError, } from "@voyantjs/workflows-errors";
|
|
2
|
+
export * from "./conditions.js";
|
|
3
|
+
export * from "./trigger.js";
|
|
4
|
+
export * from "./types.js";
|
|
5
|
+
export * from "./workflow.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,eAAe,GAChB,MAAM,4BAA4B,CAAA;AACnC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @voyantjs/workflows
|
|
2
|
+
//
|
|
3
|
+
// Authoring SDK for Voyant Workflows. Full contract in:
|
|
4
|
+
// docs/sdk-surface.md §2–§8
|
|
5
|
+
// docs/design.md §3–§4
|
|
6
|
+
export { FatalError, HookConflictError, QuotaExceededError, RetryableError, TimeoutError, ValidationError, } from "@voyantjs/workflows-errors";
|
|
7
|
+
export * from "./conditions.js";
|
|
8
|
+
export * from "./trigger.js";
|
|
9
|
+
export * from "./types.js";
|
|
10
|
+
export * from "./workflow.js";
|