@voyantjs/workflows 0.6.7 → 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
package/src/auth/index.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// const verify = await createHmacVerifier(env.VOYANT_SIGNING_KEY);
|
|
23
23
|
// export default { fetch: createStepHandler({ verifyRequest: verify }) };
|
|
24
24
|
|
|
25
|
-
export const AUTH_HEADER = "x-voyant-dispatch-auth" as const
|
|
25
|
+
export const AUTH_HEADER = "x-voyant-dispatch-auth" as const
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Returns a verifier that accepts `Authorization: Bearer <token>`
|
|
@@ -34,45 +34,41 @@ export const AUTH_HEADER = "x-voyant-dispatch-auth" as const;
|
|
|
34
34
|
* Intended for dev + single-tenant deployments. Production should
|
|
35
35
|
* issue per-tenant, short-lived tokens from a control plane.
|
|
36
36
|
*/
|
|
37
|
-
export function createBearerVerifier(
|
|
38
|
-
validTokens: readonly string[],
|
|
39
|
-
): (req: Request) => void {
|
|
37
|
+
export function createBearerVerifier(validTokens: readonly string[]): (req: Request) => void {
|
|
40
38
|
if (validTokens.length === 0) {
|
|
41
|
-
throw new Error("createBearerVerifier: need at least one valid token")
|
|
39
|
+
throw new Error("createBearerVerifier: need at least one valid token")
|
|
42
40
|
}
|
|
43
41
|
return (req) => {
|
|
44
|
-
const header = req.headers.get("authorization")
|
|
45
|
-
if (!header) throw new Error("missing Authorization header")
|
|
46
|
-
const match = /^Bearer (.+)$/.exec(header)
|
|
42
|
+
const header = req.headers.get("authorization")
|
|
43
|
+
if (!header) throw new Error("missing Authorization header")
|
|
44
|
+
const match = /^Bearer (.+)$/.exec(header)
|
|
47
45
|
if (!match) {
|
|
48
|
-
throw new Error("Authorization header must use the Bearer scheme")
|
|
46
|
+
throw new Error("Authorization header must use the Bearer scheme")
|
|
49
47
|
}
|
|
50
|
-
const presented = match[1]
|
|
48
|
+
const presented = match[1]!
|
|
51
49
|
for (const valid of validTokens) {
|
|
52
|
-
if (constantTimeEquals(presented, valid)) return
|
|
50
|
+
if (constantTimeEquals(presented, valid)) return
|
|
53
51
|
}
|
|
54
|
-
throw new Error("bearer token does not match any configured value")
|
|
55
|
-
}
|
|
52
|
+
throw new Error("bearer token does not match any configured value")
|
|
53
|
+
}
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
function constantTimeEquals(a: string, b: string): boolean {
|
|
59
|
-
if (a.length !== b.length) return false
|
|
60
|
-
let diff = 0
|
|
57
|
+
if (a.length !== b.length) return false
|
|
58
|
+
let diff = 0
|
|
61
59
|
for (let i = 0; i < a.length; i++) {
|
|
62
|
-
diff |= a.charCodeAt(i) ^ b.charCodeAt(i)
|
|
60
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i)
|
|
63
61
|
}
|
|
64
|
-
return diff === 0
|
|
62
|
+
return diff === 0
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
/** Returns a signer: `(body: string) => Promise<string>` (base64 HMAC-SHA256). */
|
|
68
|
-
export async function createHmacSigner(
|
|
69
|
-
secret
|
|
70
|
-
): Promise<(body: string) => Promise<string>> {
|
|
71
|
-
const key = await importKey(secret, ["sign"]);
|
|
66
|
+
export async function createHmacSigner(secret: string): Promise<(body: string) => Promise<string>> {
|
|
67
|
+
const key = await importKey(secret, ["sign"])
|
|
72
68
|
return async (body) => {
|
|
73
|
-
const sig = await crypto.subtle.sign("HMAC", key, encode(body))
|
|
74
|
-
return toBase64(sig)
|
|
75
|
-
}
|
|
69
|
+
const sig = await crypto.subtle.sign("HMAC", key, encode(body))
|
|
70
|
+
return toBase64(sig)
|
|
71
|
+
}
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
/**
|
|
@@ -85,27 +81,25 @@ export async function createHmacSigner(
|
|
|
85
81
|
* need the body downstream should pre-clone: `req.clone()` before
|
|
86
82
|
* passing in.
|
|
87
83
|
*/
|
|
88
|
-
export async function createHmacVerifier(
|
|
89
|
-
secret
|
|
90
|
-
): Promise<(req: Request) => Promise<void>> {
|
|
91
|
-
const key = await importKey(secret, ["verify"]);
|
|
84
|
+
export async function createHmacVerifier(secret: string): Promise<(req: Request) => Promise<void>> {
|
|
85
|
+
const key = await importKey(secret, ["verify"])
|
|
92
86
|
return async (req) => {
|
|
93
|
-
const header = req.headers.get(AUTH_HEADER)
|
|
87
|
+
const header = req.headers.get(AUTH_HEADER)
|
|
94
88
|
if (!header) {
|
|
95
|
-
throw new Error(`missing ${AUTH_HEADER} header`)
|
|
89
|
+
throw new Error(`missing ${AUTH_HEADER} header`)
|
|
96
90
|
}
|
|
97
|
-
let sig: ArrayBuffer
|
|
91
|
+
let sig: ArrayBuffer
|
|
98
92
|
try {
|
|
99
|
-
sig = fromBase64(header)
|
|
93
|
+
sig = fromBase64(header)
|
|
100
94
|
} catch {
|
|
101
|
-
throw new Error(`malformed ${AUTH_HEADER} header (expected base64)`)
|
|
95
|
+
throw new Error(`malformed ${AUTH_HEADER} header (expected base64)`)
|
|
102
96
|
}
|
|
103
|
-
const body = await req.clone().text()
|
|
104
|
-
const ok = await crypto.subtle.verify("HMAC", key, sig, encode(body))
|
|
97
|
+
const body = await req.clone().text()
|
|
98
|
+
const ok = await crypto.subtle.verify("HMAC", key, sig, encode(body))
|
|
105
99
|
if (!ok) {
|
|
106
|
-
throw new Error(`${AUTH_HEADER} signature does not match request body`)
|
|
100
|
+
throw new Error(`${AUTH_HEADER} signature does not match request body`)
|
|
107
101
|
}
|
|
108
|
-
}
|
|
102
|
+
}
|
|
109
103
|
}
|
|
110
104
|
|
|
111
105
|
// ---- Internals ----
|
|
@@ -115,7 +109,7 @@ async function importKey(
|
|
|
115
109
|
usages: readonly ("sign" | "verify")[],
|
|
116
110
|
): Promise<CryptoKey> {
|
|
117
111
|
if (secret.length === 0) {
|
|
118
|
-
throw new Error("HMAC secret must be a non-empty string")
|
|
112
|
+
throw new Error("HMAC secret must be a non-empty string")
|
|
119
113
|
}
|
|
120
114
|
return crypto.subtle.importKey(
|
|
121
115
|
"raw",
|
|
@@ -123,7 +117,7 @@ async function importKey(
|
|
|
123
117
|
{ name: "HMAC", hash: "SHA-256" },
|
|
124
118
|
false,
|
|
125
119
|
usages as KeyUsage[],
|
|
126
|
-
)
|
|
120
|
+
)
|
|
127
121
|
}
|
|
128
122
|
|
|
129
123
|
/**
|
|
@@ -133,24 +127,24 @@ async function importKey(
|
|
|
133
127
|
* Copying into a new ArrayBuffer sidesteps the nominal mismatch.
|
|
134
128
|
*/
|
|
135
129
|
function encode(s: string): ArrayBuffer {
|
|
136
|
-
const view = new TextEncoder().encode(s)
|
|
137
|
-
const buf = new ArrayBuffer(view.byteLength)
|
|
138
|
-
new Uint8Array(buf).set(view)
|
|
139
|
-
return buf
|
|
130
|
+
const view = new TextEncoder().encode(s)
|
|
131
|
+
const buf = new ArrayBuffer(view.byteLength)
|
|
132
|
+
new Uint8Array(buf).set(view)
|
|
133
|
+
return buf
|
|
140
134
|
}
|
|
141
135
|
|
|
142
136
|
function toBase64(buffer: ArrayBuffer): string {
|
|
143
137
|
// btoa is available in every modern runtime (Node 16+, Workers, browsers).
|
|
144
|
-
const bytes = new Uint8Array(buffer)
|
|
145
|
-
let bin = ""
|
|
146
|
-
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!)
|
|
147
|
-
return btoa(bin)
|
|
138
|
+
const bytes = new Uint8Array(buffer)
|
|
139
|
+
let bin = ""
|
|
140
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!)
|
|
141
|
+
return btoa(bin)
|
|
148
142
|
}
|
|
149
143
|
|
|
150
144
|
function fromBase64(s: string): ArrayBuffer {
|
|
151
|
-
const bin = atob(s)
|
|
152
|
-
const buf = new ArrayBuffer(bin.length)
|
|
153
|
-
const view = new Uint8Array(buf)
|
|
154
|
-
for (let i = 0; i < bin.length; i++) view[i] = bin.charCodeAt(i)
|
|
155
|
-
return buf
|
|
145
|
+
const bin = atob(s)
|
|
146
|
+
const buf = new ArrayBuffer(bin.length)
|
|
147
|
+
const view = new Uint8Array(buf)
|
|
148
|
+
for (let i = 0; i < bin.length; i++) view[i] = bin.charCodeAt(i)
|
|
149
|
+
return buf
|
|
156
150
|
}
|
package/src/conditions.ts
CHANGED
|
@@ -2,33 +2,33 @@
|
|
|
2
2
|
// and in event-filter match expressions.
|
|
3
3
|
// Authoritative contract in docs/sdk-surface.md §4.
|
|
4
4
|
|
|
5
|
-
import type { Duration, RunStatus } from "./types.js"
|
|
5
|
+
import type { Duration, RunStatus } from "./types.js"
|
|
6
6
|
|
|
7
7
|
export interface EventCondition {
|
|
8
|
-
event: string
|
|
9
|
-
match?: Record<string, unknown> | ((payload: unknown) => boolean)
|
|
8
|
+
event: string
|
|
9
|
+
match?: Record<string, unknown> | ((payload: unknown) => boolean)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface SignalCondition {
|
|
13
|
-
signal: string
|
|
14
|
-
match?: Record<string, unknown> | ((payload: unknown) => boolean)
|
|
13
|
+
signal: string
|
|
14
|
+
match?: Record<string, unknown> | ((payload: unknown) => boolean)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface TimeCondition {
|
|
18
|
-
after?: Duration | Date
|
|
19
|
-
before?: Duration | Date
|
|
18
|
+
after?: Duration | Date
|
|
19
|
+
before?: Duration | Date
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export interface RunStatusCondition {
|
|
23
|
-
run: { id: string; status: RunStatus[] }
|
|
23
|
+
run: { id: string; status: RunStatus[] }
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface OrCondition {
|
|
27
|
-
or: Condition[]
|
|
27
|
+
or: Condition[]
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export interface AndCondition {
|
|
31
|
-
and: Condition[]
|
|
31
|
+
and: Condition[]
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export type Condition =
|
|
@@ -37,7 +37,7 @@ export type Condition =
|
|
|
37
37
|
| TimeCondition
|
|
38
38
|
| RunStatusCondition
|
|
39
39
|
| OrCondition
|
|
40
|
-
| AndCondition
|
|
40
|
+
| AndCondition
|
|
41
41
|
|
|
42
|
-
export const or = (...conditions: Condition[]): OrCondition => ({ or: conditions })
|
|
43
|
-
export const and = (...conditions: Condition[]): AndCondition => ({ and: conditions })
|
|
42
|
+
export const or = (...conditions: Condition[]): OrCondition => ({ or: conditions })
|
|
43
|
+
export const and = (...conditions: Condition[]): AndCondition => ({ and: conditions })
|
package/src/handler/index.ts
CHANGED
|
@@ -17,17 +17,23 @@
|
|
|
17
17
|
// and the executor-shape already round-trips losslessly, we keep the
|
|
18
18
|
// full discriminated union here. The orchestrator adapter can collapse.
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
type ExecuteWorkflowStepRequest,
|
|
22
|
+
type ExecuteWorkflowStepResponse,
|
|
23
|
+
executeWorkflowStep,
|
|
24
|
+
type StepRunner,
|
|
25
|
+
} from "../runtime/executor.js"
|
|
21
26
|
|
|
22
|
-
export {
|
|
23
|
-
export type {
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
import type
|
|
27
|
-
import {
|
|
28
|
-
import type {
|
|
29
|
-
import type {
|
|
30
|
-
import {
|
|
27
|
+
export type { StepJournalEntry } from "../runtime/journal.js"
|
|
28
|
+
export type { ExecuteWorkflowStepRequest, ExecuteWorkflowStepResponse, StepRunner }
|
|
29
|
+
export { executeWorkflowStep }
|
|
30
|
+
|
|
31
|
+
import { PROTOCOL_VERSION, type ProtocolVersion } from "../protocol/index.js"
|
|
32
|
+
import type { RateLimiter } from "../rate-limit/index.js"
|
|
33
|
+
import type { RuntimeEnvironment } from "../runtime/ctx.js"
|
|
34
|
+
import type { JournalSlice, StepJournalEntry } from "../runtime/journal.js"
|
|
35
|
+
import type { RunTrigger } from "../types.js"
|
|
36
|
+
import { getWorkflow } from "../workflow.js"
|
|
31
37
|
|
|
32
38
|
export interface StepHandlerDeps {
|
|
33
39
|
/**
|
|
@@ -36,11 +42,11 @@ export interface StepHandlerDeps {
|
|
|
36
42
|
* this verifies the `X-Voyant-Dispatch-Auth` HMAC against a public
|
|
37
43
|
* key embedded by `voyant build`.
|
|
38
44
|
*/
|
|
39
|
-
verifyRequest?: (req: Request) => void | Promise<void
|
|
45
|
+
verifyRequest?: (req: Request) => void | Promise<void>
|
|
40
46
|
/** Injectable clock. Defaults to Date.now. */
|
|
41
|
-
now?: () => number
|
|
47
|
+
now?: () => number
|
|
42
48
|
/** Optional structured logger. */
|
|
43
|
-
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void
|
|
49
|
+
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void
|
|
44
50
|
/**
|
|
45
51
|
* Rate limiter shared across step invocations. Required when any
|
|
46
52
|
* registered workflow declares `options.rateLimit` on a step; see
|
|
@@ -48,7 +54,7 @@ export interface StepHandlerDeps {
|
|
|
48
54
|
* the reference impl. One instance per Worker process is the
|
|
49
55
|
* intended cardinality — state is kept in the limiter's closure.
|
|
50
56
|
*/
|
|
51
|
-
rateLimiter?: RateLimiter
|
|
57
|
+
rateLimiter?: RateLimiter
|
|
52
58
|
/**
|
|
53
59
|
* Runner for steps declared with `options.runtime === "node"`.
|
|
54
60
|
* Leave unset for handlers that only run edge steps; any node step
|
|
@@ -62,88 +68,84 @@ export interface StepHandlerDeps {
|
|
|
62
68
|
* This is bring-your-own because the right dispatch shape depends on
|
|
63
69
|
* the target runtime; the executor only cares that a runner exists.
|
|
64
70
|
*/
|
|
65
|
-
nodeStepRunner?: StepRunner
|
|
71
|
+
nodeStepRunner?: StepRunner
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
/** The HTTP request body the orchestrator sends. */
|
|
69
75
|
export interface WorkflowStepRequest {
|
|
70
|
-
protocolVersion: ProtocolVersion
|
|
71
|
-
runId: string
|
|
72
|
-
workflowId: string
|
|
73
|
-
workflowVersion: string
|
|
74
|
-
invocationCount: number
|
|
75
|
-
input: unknown
|
|
76
|
-
journal: JournalSlice
|
|
77
|
-
environment: "production" | "preview" | "development"
|
|
78
|
-
deadline: number
|
|
76
|
+
protocolVersion: ProtocolVersion
|
|
77
|
+
runId: string
|
|
78
|
+
workflowId: string
|
|
79
|
+
workflowVersion: string
|
|
80
|
+
invocationCount: number
|
|
81
|
+
input: unknown
|
|
82
|
+
journal: JournalSlice
|
|
83
|
+
environment: "production" | "preview" | "development"
|
|
84
|
+
deadline: number
|
|
79
85
|
tenantMeta: {
|
|
80
|
-
tenantId: string
|
|
81
|
-
projectId: string
|
|
82
|
-
organizationId: string
|
|
83
|
-
projectSlug?: string
|
|
84
|
-
organizationSlug?: string
|
|
85
|
-
}
|
|
86
|
+
tenantId: string
|
|
87
|
+
projectId: string
|
|
88
|
+
organizationId: string
|
|
89
|
+
projectSlug?: string
|
|
90
|
+
organizationSlug?: string
|
|
91
|
+
}
|
|
86
92
|
runMeta: {
|
|
87
|
-
number: number
|
|
88
|
-
attempt: number
|
|
89
|
-
triggeredBy: RunTrigger
|
|
90
|
-
tags: string[]
|
|
91
|
-
startedAt: number
|
|
92
|
-
}
|
|
93
|
+
number: number
|
|
94
|
+
attempt: number
|
|
95
|
+
triggeredBy: RunTrigger
|
|
96
|
+
tags: string[]
|
|
97
|
+
startedAt: number
|
|
98
|
+
}
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
/** The JSON response body the tenant returns. */
|
|
96
|
-
export type WorkflowStepResponse = ExecuteWorkflowStepResponse
|
|
102
|
+
export type WorkflowStepResponse = ExecuteWorkflowStepResponse
|
|
97
103
|
|
|
98
104
|
/** Error-response envelope used for HTTP 4xx/5xx. */
|
|
99
105
|
export interface StepHandlerError {
|
|
100
|
-
error: string
|
|
101
|
-
message: string
|
|
102
|
-
details?: unknown
|
|
106
|
+
error: string
|
|
107
|
+
message: string
|
|
108
|
+
details?: unknown
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
/** Build an HTTP fetch-style handler. */
|
|
106
|
-
export function createStepHandler(
|
|
107
|
-
deps: StepHandlerDeps = {},
|
|
108
|
-
): (req: Request) => Promise<Response> {
|
|
112
|
+
export function createStepHandler(deps: StepHandlerDeps = {}): (req: Request) => Promise<Response> {
|
|
109
113
|
return async (req) => {
|
|
110
114
|
if (req.method !== "POST") {
|
|
111
|
-
return jsonResponse(405, errorBody("method_not_allowed", "POST required"))
|
|
115
|
+
return jsonResponse(405, errorBody("method_not_allowed", "POST required"))
|
|
112
116
|
}
|
|
113
117
|
try {
|
|
114
|
-
if (deps.verifyRequest) await deps.verifyRequest(req)
|
|
118
|
+
if (deps.verifyRequest) await deps.verifyRequest(req)
|
|
115
119
|
} catch (err) {
|
|
116
120
|
deps.logger?.("warn", "step handler: auth rejected", {
|
|
117
121
|
error: err instanceof Error ? err.message : String(err),
|
|
118
|
-
})
|
|
119
|
-
return jsonResponse(401, errorBody("unauthorized", errMessage(err)))
|
|
122
|
+
})
|
|
123
|
+
return jsonResponse(401, errorBody("unauthorized", errMessage(err)))
|
|
120
124
|
}
|
|
121
|
-
let raw: unknown
|
|
125
|
+
let raw: unknown
|
|
122
126
|
try {
|
|
123
|
-
raw = await req.json()
|
|
127
|
+
raw = await req.json()
|
|
124
128
|
} catch (err) {
|
|
125
|
-
return jsonResponse(400, errorBody("invalid_json", errMessage(err)))
|
|
129
|
+
return jsonResponse(400, errorBody("invalid_json", errMessage(err)))
|
|
126
130
|
}
|
|
127
131
|
// The incoming Request carries its own AbortSignal; threading it
|
|
128
132
|
// through lets `ctx.signal` observe client-side aborts (orchestrator
|
|
129
133
|
// cancellations, closed fetches, etc.) during step execution.
|
|
130
|
-
const out = await runStepInner(raw, deps, { signal: req.signal })
|
|
131
|
-
return jsonResponse(out.status, out.body)
|
|
132
|
-
}
|
|
134
|
+
const out = await runStepInner(raw, deps, { signal: req.signal })
|
|
135
|
+
return jsonResponse(out.status, out.body)
|
|
136
|
+
}
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
/** Per-invocation options available to callers of the transport-free entry point. */
|
|
136
140
|
export interface StepRequestOptions {
|
|
137
141
|
/** AbortSignal forwarded to `ctx.signal` inside the step body. */
|
|
138
|
-
signal?: AbortSignal
|
|
142
|
+
signal?: AbortSignal
|
|
139
143
|
/**
|
|
140
144
|
* Fires synchronously from `ctx.stream.*` as each chunk is produced.
|
|
141
145
|
* Used by orchestrators that want to broadcast chunks live
|
|
142
146
|
* (dashboards, queues) before the invocation returns.
|
|
143
147
|
*/
|
|
144
|
-
onStreamChunk?: (
|
|
145
|
-
chunk: import("../runtime/executor.js").StreamChunk,
|
|
146
|
-
) => void;
|
|
148
|
+
onStreamChunk?: (chunk: import("../runtime/executor.js").StreamChunk) => void
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
/**
|
|
@@ -157,10 +159,9 @@ export async function handleStepRequest(
|
|
|
157
159
|
deps: StepHandlerDeps = {},
|
|
158
160
|
opts: StepRequestOptions = {},
|
|
159
161
|
): Promise<
|
|
160
|
-
| { status: number; body:
|
|
161
|
-
| { status: number; body: StepHandlerError }
|
|
162
|
+
{ status: number; body: WorkflowStepResponse } | { status: number; body: StepHandlerError }
|
|
162
163
|
> {
|
|
163
|
-
return runStepInner(raw, deps, opts)
|
|
164
|
+
return runStepInner(raw, deps, opts)
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
async function runStepInner(
|
|
@@ -168,13 +169,12 @@ async function runStepInner(
|
|
|
168
169
|
deps: StepHandlerDeps,
|
|
169
170
|
opts: StepRequestOptions = {},
|
|
170
171
|
): Promise<
|
|
171
|
-
| { status: number; body:
|
|
172
|
-
| { status: number; body: StepHandlerError }
|
|
172
|
+
{ status: number; body: WorkflowStepResponse } | { status: number; body: StepHandlerError }
|
|
173
173
|
> {
|
|
174
|
-
const parsed = parseRequest(raw)
|
|
175
|
-
if (!parsed.ok) return { status: 400, body: errorBody("invalid_request", parsed.message) }
|
|
174
|
+
const parsed = parseRequest(raw)
|
|
175
|
+
if (!parsed.ok) return { status: 400, body: errorBody("invalid_request", parsed.message) }
|
|
176
176
|
|
|
177
|
-
const reqBody = parsed.value
|
|
177
|
+
const reqBody = parsed.value
|
|
178
178
|
if (reqBody.protocolVersion !== PROTOCOL_VERSION) {
|
|
179
179
|
return {
|
|
180
180
|
status: 426,
|
|
@@ -182,19 +182,22 @@ async function runStepInner(
|
|
|
182
182
|
"protocol_version_mismatch",
|
|
183
183
|
`tenant supports protocol ${PROTOCOL_VERSION}, got ${String(reqBody.protocolVersion)}`,
|
|
184
184
|
),
|
|
185
|
-
}
|
|
185
|
+
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
const def = getWorkflow(reqBody.workflowId)
|
|
188
|
+
const def = getWorkflow(reqBody.workflowId)
|
|
189
189
|
if (!def) {
|
|
190
190
|
return {
|
|
191
191
|
status: 404,
|
|
192
|
-
body: errorBody(
|
|
193
|
-
|
|
192
|
+
body: errorBody(
|
|
193
|
+
"workflow_not_found",
|
|
194
|
+
`workflow "${reqBody.workflowId}" is not registered in this bundle`,
|
|
195
|
+
),
|
|
196
|
+
}
|
|
194
197
|
}
|
|
195
198
|
|
|
196
|
-
const now = deps.now ?? (() => Date.now())
|
|
197
|
-
const stepRunner = createInProcessStepRunner(now)
|
|
199
|
+
const now = deps.now ?? (() => Date.now())
|
|
200
|
+
const stepRunner = createInProcessStepRunner(now)
|
|
198
201
|
|
|
199
202
|
const runtimeEnv: RuntimeEnvironment = {
|
|
200
203
|
run: {
|
|
@@ -215,7 +218,7 @@ async function runStepInner(
|
|
|
215
218
|
id: reqBody.tenantMeta.organizationId,
|
|
216
219
|
slug: reqBody.tenantMeta.organizationSlug ?? reqBody.tenantMeta.organizationId,
|
|
217
220
|
},
|
|
218
|
-
}
|
|
221
|
+
}
|
|
219
222
|
|
|
220
223
|
try {
|
|
221
224
|
const response = await executeWorkflowStep(def, {
|
|
@@ -235,16 +238,16 @@ async function runStepInner(
|
|
|
235
238
|
now,
|
|
236
239
|
abortSignal: opts.signal,
|
|
237
240
|
onStreamChunk: opts.onStreamChunk,
|
|
238
|
-
})
|
|
239
|
-
return { status: 200, body: response }
|
|
241
|
+
})
|
|
242
|
+
return { status: 200, body: response }
|
|
240
243
|
} catch (err) {
|
|
241
244
|
deps.logger?.("error", "step handler: executor threw", {
|
|
242
245
|
error: err instanceof Error ? err.message : String(err),
|
|
243
|
-
})
|
|
246
|
+
})
|
|
244
247
|
return {
|
|
245
248
|
status: 500,
|
|
246
249
|
body: errorBody("executor_error", errMessage(err)),
|
|
247
|
-
}
|
|
250
|
+
}
|
|
248
251
|
}
|
|
249
252
|
}
|
|
250
253
|
|
|
@@ -254,23 +257,24 @@ async function runStepInner(
|
|
|
254
257
|
* will swap this for a dispatching runner that POSTs to a pod.
|
|
255
258
|
*/
|
|
256
259
|
function createInProcessStepRunner(now: () => number): StepRunner {
|
|
257
|
-
return async ({ stepId, attempt, fn, stepCtx }): Promise<StepJournalEntry> => {
|
|
258
|
-
const startedAt = now()
|
|
260
|
+
return async ({ stepId: _stepId, attempt, fn, stepCtx }): Promise<StepJournalEntry> => {
|
|
261
|
+
const startedAt = now()
|
|
259
262
|
try {
|
|
260
|
-
const output = await fn(stepCtx)
|
|
263
|
+
const output = await fn(stepCtx)
|
|
261
264
|
return {
|
|
262
265
|
attempt,
|
|
263
266
|
status: "ok",
|
|
264
267
|
output,
|
|
265
268
|
startedAt,
|
|
266
269
|
finishedAt: now(),
|
|
267
|
-
}
|
|
270
|
+
}
|
|
268
271
|
} catch (err) {
|
|
269
|
-
const e = err as Error
|
|
270
|
-
const code =
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
const e = err as Error
|
|
273
|
+
const code =
|
|
274
|
+
typeof (err as { code?: unknown }).code === "string"
|
|
275
|
+
? (err as { code: string }).code
|
|
276
|
+
: "UNKNOWN"
|
|
277
|
+
const retryAfter = (err as { retryAfter?: unknown }).retryAfter
|
|
274
278
|
return {
|
|
275
279
|
attempt,
|
|
276
280
|
status: "err",
|
|
@@ -284,9 +288,9 @@ function createInProcessStepRunner(now: () => number): StepRunner {
|
|
|
284
288
|
},
|
|
285
289
|
startedAt,
|
|
286
290
|
finishedAt: now(),
|
|
287
|
-
}
|
|
291
|
+
}
|
|
288
292
|
}
|
|
289
|
-
}
|
|
293
|
+
}
|
|
290
294
|
}
|
|
291
295
|
|
|
292
296
|
// ---- Parsing ----
|
|
@@ -295,9 +299,9 @@ function parseRequest(
|
|
|
295
299
|
raw: unknown,
|
|
296
300
|
): { ok: true; value: WorkflowStepRequest } | { ok: false; message: string } {
|
|
297
301
|
if (raw === null || typeof raw !== "object") {
|
|
298
|
-
return { ok: false, message: "body must be a JSON object" }
|
|
302
|
+
return { ok: false, message: "body must be a JSON object" }
|
|
299
303
|
}
|
|
300
|
-
const r = raw as Record<string, unknown
|
|
304
|
+
const r = raw as Record<string, unknown>
|
|
301
305
|
const required: (keyof WorkflowStepRequest)[] = [
|
|
302
306
|
"protocolVersion",
|
|
303
307
|
"runId",
|
|
@@ -309,36 +313,36 @@ function parseRequest(
|
|
|
309
313
|
"deadline",
|
|
310
314
|
"tenantMeta",
|
|
311
315
|
"runMeta",
|
|
312
|
-
]
|
|
316
|
+
]
|
|
313
317
|
for (const k of required) {
|
|
314
|
-
if (!(k in r)) return { ok: false, message: `missing required field "${k}"` }
|
|
318
|
+
if (!(k in r)) return { ok: false, message: `missing required field "${k}"` }
|
|
315
319
|
}
|
|
316
320
|
if (typeof r.protocolVersion !== "number") {
|
|
317
|
-
return { ok: false, message: "`protocolVersion` must be a number" }
|
|
321
|
+
return { ok: false, message: "`protocolVersion` must be a number" }
|
|
318
322
|
}
|
|
319
323
|
if (typeof r.runId !== "string" || r.runId.length === 0) {
|
|
320
|
-
return { ok: false, message: "`runId` must be a non-empty string" }
|
|
324
|
+
return { ok: false, message: "`runId` must be a non-empty string" }
|
|
321
325
|
}
|
|
322
326
|
if (typeof r.workflowId !== "string" || r.workflowId.length === 0) {
|
|
323
|
-
return { ok: false, message: "`workflowId` must be a non-empty string" }
|
|
327
|
+
return { ok: false, message: "`workflowId` must be a non-empty string" }
|
|
324
328
|
}
|
|
325
329
|
if (typeof r.invocationCount !== "number" || r.invocationCount < 1) {
|
|
326
|
-
return { ok: false, message: "`invocationCount` must be >= 1" }
|
|
330
|
+
return { ok: false, message: "`invocationCount` must be >= 1" }
|
|
327
331
|
}
|
|
328
332
|
if (!r.journal || typeof r.journal !== "object") {
|
|
329
|
-
return { ok: false, message: "`journal` must be an object" }
|
|
333
|
+
return { ok: false, message: "`journal` must be an object" }
|
|
330
334
|
}
|
|
331
|
-
const env = r.environment
|
|
335
|
+
const env = r.environment
|
|
332
336
|
if (env !== "production" && env !== "preview" && env !== "development") {
|
|
333
|
-
return { ok: false, message: "`environment` must be production | preview | development" }
|
|
337
|
+
return { ok: false, message: "`environment` must be production | preview | development" }
|
|
334
338
|
}
|
|
335
339
|
if (!r.tenantMeta || typeof r.tenantMeta !== "object") {
|
|
336
|
-
return { ok: false, message: "`tenantMeta` must be an object" }
|
|
340
|
+
return { ok: false, message: "`tenantMeta` must be an object" }
|
|
337
341
|
}
|
|
338
342
|
if (!r.runMeta || typeof r.runMeta !== "object") {
|
|
339
|
-
return { ok: false, message: "`runMeta` must be an object" }
|
|
343
|
+
return { ok: false, message: "`runMeta` must be an object" }
|
|
340
344
|
}
|
|
341
|
-
return { ok: true, value: r as unknown as WorkflowStepRequest }
|
|
345
|
+
return { ok: true, value: r as unknown as WorkflowStepRequest }
|
|
342
346
|
}
|
|
343
347
|
|
|
344
348
|
// ---- Helpers ----
|
|
@@ -347,15 +351,15 @@ function jsonResponse(status: number, body: unknown): Response {
|
|
|
347
351
|
return new Response(JSON.stringify(body), {
|
|
348
352
|
status,
|
|
349
353
|
headers: { "content-type": "application/json; charset=utf-8" },
|
|
350
|
-
})
|
|
354
|
+
})
|
|
351
355
|
}
|
|
352
356
|
|
|
353
357
|
function errorBody(error: string, message: string, details?: unknown): StepHandlerError {
|
|
354
|
-
const out: StepHandlerError = { error, message }
|
|
355
|
-
if (details !== undefined) out.details = details
|
|
356
|
-
return out
|
|
358
|
+
const out: StepHandlerError = { error, message }
|
|
359
|
+
if (details !== undefined) out.details = details
|
|
360
|
+
return out
|
|
357
361
|
}
|
|
358
362
|
|
|
359
363
|
function errMessage(err: unknown): string {
|
|
360
|
-
return err instanceof Error ? err.message : String(err)
|
|
364
|
+
return err instanceof Error ? err.message : String(err)
|
|
361
365
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
// docs/sdk-surface.md §2–§8
|
|
5
5
|
// docs/design.md §3–§4
|
|
6
6
|
|
|
7
|
-
export * from "./types.js";
|
|
8
|
-
export * from "./workflow.js";
|
|
9
|
-
export * from "./trigger.js";
|
|
10
|
-
export * from "./conditions.js";
|
|
11
7
|
export {
|
|
12
8
|
FatalError,
|
|
13
|
-
RetryableError,
|
|
14
|
-
TimeoutError,
|
|
15
9
|
HookConflictError,
|
|
16
10
|
QuotaExceededError,
|
|
11
|
+
RetryableError,
|
|
12
|
+
TimeoutError,
|
|
17
13
|
ValidationError,
|
|
18
|
-
} from "@voyantjs/workflows-errors"
|
|
14
|
+
} from "@voyantjs/workflows-errors"
|
|
15
|
+
export * from "./conditions.js"
|
|
16
|
+
export * from "./trigger.js"
|
|
17
|
+
export * from "./types.js"
|
|
18
|
+
export * from "./workflow.js"
|