@voyant-travel/workflows 0.107.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +52 -0
- package/README.md +79 -0
- package/dist/auth/index.d.ts +125 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +352 -0
- package/dist/bindings.d.ts +119 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +19 -0
- package/dist/client.d.ts +135 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +305 -0
- package/dist/conditions.d.ts +29 -0
- package/dist/conditions.d.ts.map +1 -0
- package/dist/conditions.js +5 -0
- package/dist/config.d.ts +93 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +7 -0
- package/dist/driver.d.ts +237 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +53 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +76 -0
- package/dist/events/compile.d.ts +34 -0
- package/dist/events/compile.d.ts.map +1 -0
- package/dist/events/compile.js +204 -0
- package/dist/events/index.d.ts +8 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +11 -0
- package/dist/events/input-mapper.d.ts +24 -0
- package/dist/events/input-mapper.d.ts.map +1 -0
- package/dist/events/input-mapper.js +169 -0
- package/dist/events/manifest-builder.d.ts +42 -0
- package/dist/events/manifest-builder.d.ts.map +1 -0
- package/dist/events/manifest-builder.js +313 -0
- package/dist/events/payload-hash.d.ts +46 -0
- package/dist/events/payload-hash.d.ts.map +1 -0
- package/dist/events/payload-hash.js +98 -0
- package/dist/events/predicate.d.ts +77 -0
- package/dist/events/predicate.d.ts.map +1 -0
- package/dist/events/predicate.js +347 -0
- package/dist/events/registry.d.ts +37 -0
- package/dist/events/registry.d.ts.map +1 -0
- package/dist/events/registry.js +47 -0
- package/dist/handler/index.d.ts +114 -0
- package/dist/handler/index.d.ts.map +1 -0
- package/dist/handler/index.js +267 -0
- package/dist/handler/resume.d.ts +41 -0
- package/dist/handler/resume.d.ts.map +1 -0
- package/dist/handler/resume.js +44 -0
- package/dist/http-ingest.d.ts +54 -0
- package/dist/http-ingest.d.ts.map +1 -0
- package/dist/http-ingest.js +214 -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 +345 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +110 -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 +111 -0
- package/dist/runtime/ctx.d.ts.map +1 -0
- package/dist/runtime/ctx.js +624 -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 +166 -0
- package/dist/runtime/executor.d.ts.map +1 -0
- package/dist/runtime/executor.js +226 -0
- package/dist/runtime/journal.d.ts +56 -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 +599 -0
- package/dist/trigger.d.ts +37 -0
- package/dist/trigger.d.ts.map +1 -0
- package/dist/trigger.js +11 -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 +222 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +55 -0
- package/package.json +120 -0
- package/src/auth/index.ts +398 -0
- package/src/bindings.ts +135 -0
- package/src/client.ts +498 -0
- package/src/conditions.ts +43 -0
- package/src/config.ts +114 -0
- package/src/driver.ts +277 -0
- package/src/errors.ts +109 -0
- package/src/events/compile.ts +268 -0
- package/src/events/index.ts +42 -0
- package/src/events/input-mapper.ts +201 -0
- package/src/events/manifest-builder.ts +372 -0
- package/src/events/payload-hash.ts +110 -0
- package/src/events/predicate.ts +390 -0
- package/src/events/registry.ts +86 -0
- package/src/handler/index.ts +413 -0
- package/src/handler/resume.ts +100 -0
- package/src/http-ingest.ts +299 -0
- package/src/index.ts +18 -0
- package/src/protocol/index.ts +483 -0
- package/src/rate-limit/index.ts +181 -0
- package/src/runtime/ctx.ts +876 -0
- package/src/runtime/determinism.ts +75 -0
- package/src/runtime/errors.ts +58 -0
- package/src/runtime/executor.ts +442 -0
- package/src/runtime/journal.ts +80 -0
- package/src/testing/index.ts +796 -0
- package/src/trigger.ts +63 -0
- package/src/types.ts +80 -0
- package/src/workflow.ts +328 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// @voyant-travel/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
|
+
export { buildResumeStepRequest, } from "./resume.js";
|
|
24
|
+
/** Build an HTTP fetch-style handler. */
|
|
25
|
+
export function createStepHandler(deps = {}) {
|
|
26
|
+
return async (req) => {
|
|
27
|
+
if (req.method !== "POST") {
|
|
28
|
+
return jsonResponse(405, errorBody("method_not_allowed", "POST required"));
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
if (deps.verifyRequest)
|
|
32
|
+
await deps.verifyRequest(req);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
deps.logger?.("warn", "step handler: auth rejected", {
|
|
36
|
+
error: err instanceof Error ? err.message : String(err),
|
|
37
|
+
});
|
|
38
|
+
return jsonResponse(401, errorBody("unauthorized", errMessage(err)));
|
|
39
|
+
}
|
|
40
|
+
let raw;
|
|
41
|
+
try {
|
|
42
|
+
raw = await req.json();
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
return jsonResponse(400, errorBody("invalid_json", errMessage(err)));
|
|
46
|
+
}
|
|
47
|
+
// The incoming Request carries its own AbortSignal; threading it
|
|
48
|
+
// through lets `ctx.signal` observe client-side aborts (orchestrator
|
|
49
|
+
// cancellations, closed fetches, etc.) during step execution.
|
|
50
|
+
const out = await runStepInner(raw, deps, { signal: req.signal });
|
|
51
|
+
return jsonResponse(out.status, out.body);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Transport-free entry point. Callers that already parsed the body
|
|
56
|
+
* (e.g. local orchestrator in-memory, tests) invoke this directly.
|
|
57
|
+
* Returns either the step response or an error envelope with the HTTP
|
|
58
|
+
* status the caller should use.
|
|
59
|
+
*/
|
|
60
|
+
export async function handleStepRequest(raw, deps = {}, opts = {}) {
|
|
61
|
+
return runStepInner(raw, deps, opts);
|
|
62
|
+
}
|
|
63
|
+
async function runStepInner(raw, deps, opts = {}) {
|
|
64
|
+
const parsed = parseRequest(raw);
|
|
65
|
+
if (!parsed.ok)
|
|
66
|
+
return { status: 400, body: errorBody("invalid_request", parsed.message) };
|
|
67
|
+
const reqBody = parsed.value;
|
|
68
|
+
if (reqBody.protocolVersion !== PROTOCOL_VERSION) {
|
|
69
|
+
return {
|
|
70
|
+
status: 426,
|
|
71
|
+
body: errorBody("protocol_version_mismatch", `tenant supports protocol ${PROTOCOL_VERSION}, got ${String(reqBody.protocolVersion)}`),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const def = getWorkflow(reqBody.workflowId);
|
|
75
|
+
if (!def) {
|
|
76
|
+
return {
|
|
77
|
+
status: 404,
|
|
78
|
+
body: errorBody("workflow_not_found", `workflow "${reqBody.workflowId}" is not registered in this bundle`),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const now = deps.now ?? (() => Date.now());
|
|
82
|
+
const stepRunner = createInProcessStepRunner(now);
|
|
83
|
+
const runtimeEnv = {
|
|
84
|
+
run: {
|
|
85
|
+
id: reqBody.runId,
|
|
86
|
+
number: reqBody.runMeta.number,
|
|
87
|
+
attempt: reqBody.runMeta.attempt,
|
|
88
|
+
triggeredBy: reqBody.runMeta.triggeredBy,
|
|
89
|
+
tags: reqBody.runMeta.tags,
|
|
90
|
+
startedAt: reqBody.runMeta.startedAt,
|
|
91
|
+
},
|
|
92
|
+
workflow: { id: reqBody.workflowId, version: reqBody.workflowVersion },
|
|
93
|
+
environment: { name: reqBody.environment },
|
|
94
|
+
project: {
|
|
95
|
+
id: reqBody.tenantMeta.projectId,
|
|
96
|
+
slug: reqBody.tenantMeta.projectSlug ?? reqBody.tenantMeta.projectId,
|
|
97
|
+
},
|
|
98
|
+
organization: {
|
|
99
|
+
id: reqBody.tenantMeta.organizationId,
|
|
100
|
+
slug: reqBody.tenantMeta.organizationSlug ?? reqBody.tenantMeta.organizationId,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
try {
|
|
104
|
+
const response = await executeWorkflowStep(def, {
|
|
105
|
+
runId: reqBody.runId,
|
|
106
|
+
workflowId: reqBody.workflowId,
|
|
107
|
+
workflowVersion: reqBody.workflowVersion,
|
|
108
|
+
input: reqBody.input,
|
|
109
|
+
journal: reqBody.journal,
|
|
110
|
+
invocationCount: reqBody.invocationCount,
|
|
111
|
+
environment: runtimeEnv,
|
|
112
|
+
triggeredBy: reqBody.runMeta.triggeredBy,
|
|
113
|
+
runStartedAt: reqBody.runMeta.startedAt,
|
|
114
|
+
tags: reqBody.runMeta.tags,
|
|
115
|
+
stepRunner,
|
|
116
|
+
nodeStepRunner: deps.nodeStepRunner,
|
|
117
|
+
rateLimiter: deps.rateLimiter,
|
|
118
|
+
services: deps.services,
|
|
119
|
+
now,
|
|
120
|
+
abortSignal: opts.signal,
|
|
121
|
+
onStreamChunk: opts.onStreamChunk,
|
|
122
|
+
});
|
|
123
|
+
return { status: 200, body: response };
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
deps.logger?.("error", "step handler: executor threw", {
|
|
127
|
+
error: err instanceof Error ? err.message : String(err),
|
|
128
|
+
});
|
|
129
|
+
return {
|
|
130
|
+
status: 500,
|
|
131
|
+
body: errorBody("executor_error", errMessage(err)),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build a step runner that executes the step body in the same
|
|
137
|
+
* process. Suitable for `runtime: "edge"`. Container-runtime steps
|
|
138
|
+
* will swap this for a dispatching runner that POSTs to a pod.
|
|
139
|
+
*/
|
|
140
|
+
function createInProcessStepRunner(now) {
|
|
141
|
+
return async ({ stepId: _stepId, attempt, fn, stepCtx }) => {
|
|
142
|
+
const startedAt = now();
|
|
143
|
+
try {
|
|
144
|
+
const output = await fn(stepCtx);
|
|
145
|
+
return {
|
|
146
|
+
attempt,
|
|
147
|
+
status: "ok",
|
|
148
|
+
output,
|
|
149
|
+
startedAt,
|
|
150
|
+
finishedAt: now(),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const e = err;
|
|
155
|
+
const code = typeof err.code === "string"
|
|
156
|
+
? err.code
|
|
157
|
+
: "UNKNOWN";
|
|
158
|
+
const retryAfter = err.retryAfter;
|
|
159
|
+
return {
|
|
160
|
+
attempt,
|
|
161
|
+
status: "err",
|
|
162
|
+
error: {
|
|
163
|
+
category: "USER_ERROR",
|
|
164
|
+
code,
|
|
165
|
+
message: e?.message ?? String(err),
|
|
166
|
+
name: e?.name,
|
|
167
|
+
stack: e?.stack,
|
|
168
|
+
data: retryAfter !== undefined ? { retryAfter } : undefined,
|
|
169
|
+
},
|
|
170
|
+
startedAt,
|
|
171
|
+
finishedAt: now(),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// ---- Parsing ----
|
|
177
|
+
function parseRequest(raw) {
|
|
178
|
+
if (raw === null || typeof raw !== "object") {
|
|
179
|
+
return { ok: false, message: "body must be a JSON object" };
|
|
180
|
+
}
|
|
181
|
+
const r = raw;
|
|
182
|
+
const required = [
|
|
183
|
+
"protocolVersion",
|
|
184
|
+
"runId",
|
|
185
|
+
"workflowId",
|
|
186
|
+
"workflowVersion",
|
|
187
|
+
"invocationCount",
|
|
188
|
+
"journal",
|
|
189
|
+
"environment",
|
|
190
|
+
"deadline",
|
|
191
|
+
"tenantMeta",
|
|
192
|
+
"runMeta",
|
|
193
|
+
];
|
|
194
|
+
for (const k of required) {
|
|
195
|
+
if (!(k in r))
|
|
196
|
+
return { ok: false, message: `missing required field "${k}"` };
|
|
197
|
+
}
|
|
198
|
+
if (typeof r.protocolVersion !== "number") {
|
|
199
|
+
return { ok: false, message: "`protocolVersion` must be a number" };
|
|
200
|
+
}
|
|
201
|
+
if (typeof r.runId !== "string" || r.runId.length === 0) {
|
|
202
|
+
return { ok: false, message: "`runId` must be a non-empty string" };
|
|
203
|
+
}
|
|
204
|
+
if (typeof r.workflowId !== "string" || r.workflowId.length === 0) {
|
|
205
|
+
return { ok: false, message: "`workflowId` must be a non-empty string" };
|
|
206
|
+
}
|
|
207
|
+
if (typeof r.workflowVersion !== "string" || r.workflowVersion.length === 0) {
|
|
208
|
+
return { ok: false, message: "`workflowVersion` must be a non-empty string" };
|
|
209
|
+
}
|
|
210
|
+
if (typeof r.invocationCount !== "number" || r.invocationCount < 1) {
|
|
211
|
+
return { ok: false, message: "`invocationCount` must be >= 1" };
|
|
212
|
+
}
|
|
213
|
+
if (typeof r.deadline !== "number") {
|
|
214
|
+
return { ok: false, message: "`deadline` must be a number" };
|
|
215
|
+
}
|
|
216
|
+
if (!r.journal || typeof r.journal !== "object") {
|
|
217
|
+
return { ok: false, message: "`journal` must be an object" };
|
|
218
|
+
}
|
|
219
|
+
const env = r.environment;
|
|
220
|
+
if (env !== "production" && env !== "preview" && env !== "development") {
|
|
221
|
+
return { ok: false, message: "`environment` must be production | preview | development" };
|
|
222
|
+
}
|
|
223
|
+
if (!r.tenantMeta || typeof r.tenantMeta !== "object") {
|
|
224
|
+
return { ok: false, message: "`tenantMeta` must be an object" };
|
|
225
|
+
}
|
|
226
|
+
if (!r.runMeta || typeof r.runMeta !== "object") {
|
|
227
|
+
return { ok: false, message: "`runMeta` must be an object" };
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
ok: true,
|
|
231
|
+
value: {
|
|
232
|
+
protocolVersion: r.protocolVersion,
|
|
233
|
+
runId: r.runId,
|
|
234
|
+
workflowId: r.workflowId,
|
|
235
|
+
workflowVersion: r.workflowVersion,
|
|
236
|
+
invocationCount: r.invocationCount,
|
|
237
|
+
input: r.input,
|
|
238
|
+
journal: r.journal,
|
|
239
|
+
environment: env,
|
|
240
|
+
deadline: r.deadline,
|
|
241
|
+
tenantMeta: r.tenantMeta,
|
|
242
|
+
runMeta: r.runMeta,
|
|
243
|
+
activation: isObjectRecord(r.activation)
|
|
244
|
+
? r.activation
|
|
245
|
+
: undefined,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// ---- Helpers ----
|
|
250
|
+
function jsonResponse(status, body) {
|
|
251
|
+
return new Response(JSON.stringify(body), {
|
|
252
|
+
status,
|
|
253
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
function errorBody(error, message, details) {
|
|
257
|
+
const out = { error, message };
|
|
258
|
+
if (details !== undefined)
|
|
259
|
+
out.details = details;
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
function errMessage(err) {
|
|
263
|
+
return err instanceof Error ? err.message : String(err);
|
|
264
|
+
}
|
|
265
|
+
function isObjectRecord(value) {
|
|
266
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
267
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ProtocolVersion, type WorkflowActivationFreshness, type WorkflowBundleReference, type WorkflowJournalReference, type WorkflowPayloadReference, type WorkflowWaitpointResumeTarget, type WorkflowWaitpointSnapshot } from "../protocol/index.js";
|
|
2
|
+
import type { JournalSlice } from "../runtime/journal.js";
|
|
3
|
+
import type { WorkflowStepRequest } from "./index.js";
|
|
4
|
+
export interface BuildResumeStepRequestInput {
|
|
5
|
+
runId: string;
|
|
6
|
+
workflowId: string;
|
|
7
|
+
workflowVersion: string;
|
|
8
|
+
input: unknown;
|
|
9
|
+
journal: JournalSlice;
|
|
10
|
+
pendingWaitpoints: readonly WorkflowWaitpointResumeTarget[];
|
|
11
|
+
waitpointId?: string;
|
|
12
|
+
waitpointKey?: string;
|
|
13
|
+
parkedAt?: number;
|
|
14
|
+
resumePayload?: unknown;
|
|
15
|
+
resumePayloadRef?: WorkflowPayloadReference;
|
|
16
|
+
resolvedAt?: number;
|
|
17
|
+
matchedEventId?: string;
|
|
18
|
+
source?: "live" | "inbox" | "replay";
|
|
19
|
+
protocolVersion?: ProtocolVersion;
|
|
20
|
+
invocationCount: number;
|
|
21
|
+
environment: "production" | "preview" | "development";
|
|
22
|
+
deadline: number;
|
|
23
|
+
tenantMeta: WorkflowStepRequest["tenantMeta"];
|
|
24
|
+
runMeta: WorkflowStepRequest["runMeta"];
|
|
25
|
+
workflowReleaseId?: string;
|
|
26
|
+
releaseId?: string;
|
|
27
|
+
bundle?: WorkflowBundleReference;
|
|
28
|
+
journalRef?: WorkflowJournalReference;
|
|
29
|
+
freshness?: WorkflowActivationFreshness;
|
|
30
|
+
}
|
|
31
|
+
export type BuildResumeStepRequestResult = {
|
|
32
|
+
ok: true;
|
|
33
|
+
request: WorkflowStepRequest;
|
|
34
|
+
waitpoint: WorkflowWaitpointSnapshot;
|
|
35
|
+
} | {
|
|
36
|
+
ok: false;
|
|
37
|
+
code: "missing_waitpoint_selector" | "waitpoint_not_found";
|
|
38
|
+
message: string;
|
|
39
|
+
};
|
|
40
|
+
export declare function buildResumeStepRequest(input: BuildResumeStepRequestInput): BuildResumeStepRequestResult;
|
|
41
|
+
//# sourceMappingURL=resume.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resume.d.ts","sourceRoot":"","sources":["../../src/handler/resume.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,2BAA2B,EAChC,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC7B,KAAK,6BAA6B,EAClC,KAAK,yBAAyB,EAC/B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAErD,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,MAAM,CAAA;IACvB,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,EAAE,YAAY,CAAA;IACrB,iBAAiB,EAAE,SAAS,6BAA6B,EAAE,CAAA;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,gBAAgB,CAAC,EAAE,wBAAwB,CAAA;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;IACpC,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,eAAe,EAAE,MAAM,CAAA;IACvB,WAAW,EAAE,YAAY,GAAG,SAAS,GAAG,aAAa,CAAA;IACrD,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAA;IAC7C,OAAO,EAAE,mBAAmB,CAAC,SAAS,CAAC,CAAA;IACvC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,uBAAuB,CAAA;IAChC,UAAU,CAAC,EAAE,wBAAwB,CAAA;IACrC,SAAS,CAAC,EAAE,2BAA2B,CAAA;CACxC;AAED,MAAM,MAAM,4BAA4B,GACpC;IACE,EAAE,EAAE,IAAI,CAAA;IACR,OAAO,EAAE,mBAAmB,CAAA;IAC5B,SAAS,EAAE,yBAAyB,CAAA;CACrC,GACD;IACE,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,4BAA4B,GAAG,qBAAqB,CAAA;IAC1D,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAEL,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,2BAA2B,GACjC,4BAA4B,CA2C9B"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { applyWorkflowResumeToJournal, PROTOCOL_VERSION, } from "../protocol/index.js";
|
|
2
|
+
export function buildResumeStepRequest(input) {
|
|
3
|
+
const applied = applyWorkflowResumeToJournal({
|
|
4
|
+
journal: input.journal,
|
|
5
|
+
waitpoints: input.pendingWaitpoints,
|
|
6
|
+
waitpointId: input.waitpointId,
|
|
7
|
+
waitpointKey: input.waitpointKey,
|
|
8
|
+
parkedAt: input.parkedAt,
|
|
9
|
+
payload: input.resumePayload,
|
|
10
|
+
payloadRef: input.resumePayloadRef,
|
|
11
|
+
resolvedAt: input.resolvedAt,
|
|
12
|
+
matchedEventId: input.matchedEventId,
|
|
13
|
+
source: input.source,
|
|
14
|
+
});
|
|
15
|
+
if (!applied.ok)
|
|
16
|
+
return applied;
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
waitpoint: applied.waitpoint,
|
|
20
|
+
request: {
|
|
21
|
+
protocolVersion: input.protocolVersion ?? PROTOCOL_VERSION,
|
|
22
|
+
runId: input.runId,
|
|
23
|
+
workflowId: input.workflowId,
|
|
24
|
+
workflowVersion: input.workflowVersion,
|
|
25
|
+
invocationCount: input.invocationCount,
|
|
26
|
+
input: input.input,
|
|
27
|
+
journal: applied.journal,
|
|
28
|
+
environment: input.environment,
|
|
29
|
+
deadline: input.deadline,
|
|
30
|
+
tenantMeta: input.tenantMeta,
|
|
31
|
+
runMeta: input.runMeta,
|
|
32
|
+
activation: {
|
|
33
|
+
kind: "resume",
|
|
34
|
+
workflowReleaseId: input.workflowReleaseId,
|
|
35
|
+
releaseId: input.releaseId,
|
|
36
|
+
bundle: input.bundle,
|
|
37
|
+
journalRef: input.journalRef,
|
|
38
|
+
waitpoint: applied.waitpoint,
|
|
39
|
+
resumePayloadRef: input.resumePayloadRef,
|
|
40
|
+
freshness: input.freshness,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { WorkflowDriver } from "./driver.js";
|
|
2
|
+
/**
|
|
3
|
+
* Minimum interface a Hono-shaped app exposes that we use. `app.post(...)`
|
|
4
|
+
* and `app.get(...)` register handlers; the handler signature mirrors
|
|
5
|
+
* Hono's `Context`-style callback for portability — we only read the
|
|
6
|
+
* request body and request params via the framework's response helpers.
|
|
7
|
+
*/
|
|
8
|
+
export interface HttpAppLike {
|
|
9
|
+
post(path: string, handler: HttpHandler): unknown;
|
|
10
|
+
get(path: string, handler: HttpHandler): unknown;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Minimum context shape we read off Hono. Restricted to body parsing,
|
|
14
|
+
* route params, and JSON response helpers.
|
|
15
|
+
*/
|
|
16
|
+
export interface HttpContextLike {
|
|
17
|
+
req: {
|
|
18
|
+
json(): Promise<unknown>;
|
|
19
|
+
param(name: string): string | undefined;
|
|
20
|
+
header(name: string): string | undefined;
|
|
21
|
+
raw: Request;
|
|
22
|
+
};
|
|
23
|
+
json(body: unknown, status?: number): Response;
|
|
24
|
+
text(body: string, status?: number): Response;
|
|
25
|
+
status(code: number): unknown;
|
|
26
|
+
}
|
|
27
|
+
export type HttpHandler = (ctx: HttpContextLike) => Promise<Response> | Response;
|
|
28
|
+
export interface MountHttpIngestAdapterOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Driver the adapter forwards into. Typically the same instance
|
|
31
|
+
* `createApp({ workflows: { driver } })` constructed.
|
|
32
|
+
*/
|
|
33
|
+
driver: WorkflowDriver;
|
|
34
|
+
/** Mount path. Defaults to `"/api/workflows"`. */
|
|
35
|
+
basePath?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Optional auth check. Receives the original `Request` and returns
|
|
38
|
+
* `void` on success / throws on failure. Reuse
|
|
39
|
+
* `createBearerVerifier(...)` from `@voyant-travel/workflows/auth` for the
|
|
40
|
+
* canonical bearer-token shape.
|
|
41
|
+
*/
|
|
42
|
+
verifyRequest?: (req: Request) => void | Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Mount the adapter onto a Hono-shaped app. Registers:
|
|
46
|
+
*
|
|
47
|
+
* POST {basePath}/events → driver.ingestEvent
|
|
48
|
+
* POST {basePath}/manifests → driver.registerManifest
|
|
49
|
+
* GET {basePath}/manifests/:env → driver.getManifest
|
|
50
|
+
*
|
|
51
|
+
* Returns the mounted base path so callers can log it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function mountHttpIngestAdapter(app: HttpAppLike, opts: MountHttpIngestAdapterOptions): string;
|
|
54
|
+
//# sourceMappingURL=http-ingest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-ingest.d.ts","sourceRoot":"","sources":["../src/http-ingest.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAmB,cAAc,EAAE,MAAM,aAAa,CAAA;AAOlE;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAA;IACjD,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAA;CACjD;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE;QACH,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;QACxB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;QACvC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;QACxC,GAAG,EAAE,OAAO,CAAA;KACb,CAAA;IACD,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAA;IAC9C,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAA;IAC7C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;CAC9B;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAA;AAEhF,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,MAAM,EAAE,cAAc,CAAA;IACtB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvD;AAID;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,6BAA6B,GAClC,MAAM,CAyFR"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Optional HTTP ingest adapter — mounts `/api/manifests` and `/api/events`
|
|
2
|
+
// on a Hono-shaped app, forwarding into a `WorkflowDriver`.
|
|
3
|
+
//
|
|
4
|
+
// Self-host Mode 2 deployments mount this when external emitters need to
|
|
5
|
+
// fire events into the runtime (storefront BFF, third-party webhooks,
|
|
6
|
+
// sibling-process pairs across machines). voyant-cloud always mounts it
|
|
7
|
+
// at its HTTP boundary.
|
|
8
|
+
//
|
|
9
|
+
// Transport-agnostic: takes a minimal `HttpAppLike` interface so the SDK
|
|
10
|
+
// stays a leaf package (no `hono` dep). `@voyant-travel/voyant-hono`'s `Hono`
|
|
11
|
+
// instance satisfies the shape via TypeScript structural compat.
|
|
12
|
+
//
|
|
13
|
+
// Architecture: docs/architecture/workflows-runtime-architecture.md §15.4.
|
|
14
|
+
const ALLOWED_ENVS = new Set(["production", "preview", "development"]);
|
|
15
|
+
// ---- Mount ----
|
|
16
|
+
/**
|
|
17
|
+
* Mount the adapter onto a Hono-shaped app. Registers:
|
|
18
|
+
*
|
|
19
|
+
* POST {basePath}/events → driver.ingestEvent
|
|
20
|
+
* POST {basePath}/manifests → driver.registerManifest
|
|
21
|
+
* GET {basePath}/manifests/:env → driver.getManifest
|
|
22
|
+
*
|
|
23
|
+
* Returns the mounted base path so callers can log it.
|
|
24
|
+
*/
|
|
25
|
+
export function mountHttpIngestAdapter(app, opts) {
|
|
26
|
+
const base = (opts.basePath ?? "/api/workflows").replace(/\/$/, "");
|
|
27
|
+
app.post(`${base}/events`, async (ctx) => {
|
|
28
|
+
if (opts.verifyRequest) {
|
|
29
|
+
try {
|
|
30
|
+
await opts.verifyRequest(ctx.req.raw);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
return ctx.json({ error: "unauthorized", message: errMessage(err) }, 401);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
let raw;
|
|
37
|
+
try {
|
|
38
|
+
raw = await ctx.req.json();
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
return ctx.json({ error: "invalid_json", message: errMessage(err) }, 400);
|
|
42
|
+
}
|
|
43
|
+
const validation = validateIngestBody(raw);
|
|
44
|
+
if (!validation.ok)
|
|
45
|
+
return ctx.json(validation.error, 400);
|
|
46
|
+
const args = {
|
|
47
|
+
environment: validation.body.environment,
|
|
48
|
+
envelope: validation.body.envelope,
|
|
49
|
+
idempotencyKey: validation.body.idempotencyKey,
|
|
50
|
+
};
|
|
51
|
+
const result = await opts.driver.ingestEvent(args);
|
|
52
|
+
if (!result.ok && result.reason === "manifest_not_registered") {
|
|
53
|
+
return ctx.json(result, 200);
|
|
54
|
+
}
|
|
55
|
+
if (!result.ok) {
|
|
56
|
+
return ctx.json(result, 502);
|
|
57
|
+
}
|
|
58
|
+
return ctx.json(result, 200);
|
|
59
|
+
});
|
|
60
|
+
app.post(`${base}/manifests`, async (ctx) => {
|
|
61
|
+
if (opts.verifyRequest) {
|
|
62
|
+
try {
|
|
63
|
+
await opts.verifyRequest(ctx.req.raw);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
return ctx.json({ error: "unauthorized", message: errMessage(err) }, 401);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = await ctx.req.json();
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
return ctx.json({ error: "invalid_json", message: errMessage(err) }, 400);
|
|
75
|
+
}
|
|
76
|
+
const validation = validateRegisterBody(raw);
|
|
77
|
+
if (!validation.ok)
|
|
78
|
+
return ctx.json(validation.error, 400);
|
|
79
|
+
try {
|
|
80
|
+
const result = await opts.driver.registerManifest({
|
|
81
|
+
environment: validation.body.environment,
|
|
82
|
+
manifest: validation.body.manifest, // structurally compatible
|
|
83
|
+
});
|
|
84
|
+
return ctx.json({ ok: true, versionId: result.versionId }, 200);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return ctx.json({ error: "register_failed", message: errMessage(err) }, 500);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
app.get(`${base}/manifests/:env`, async (ctx) => {
|
|
91
|
+
if (opts.verifyRequest) {
|
|
92
|
+
try {
|
|
93
|
+
await opts.verifyRequest(ctx.req.raw);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
return ctx.json({ error: "unauthorized", message: errMessage(err) }, 401);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const env = ctx.req.param("env");
|
|
100
|
+
if (!env || !ALLOWED_ENVS.has(env)) {
|
|
101
|
+
return ctx.json({
|
|
102
|
+
error: "invalid_environment",
|
|
103
|
+
message: `environment must be one of ${[...ALLOWED_ENVS].join(", ")}`,
|
|
104
|
+
}, 400);
|
|
105
|
+
}
|
|
106
|
+
const manifest = await opts.driver.getManifest({ environment: env });
|
|
107
|
+
if (!manifest) {
|
|
108
|
+
return ctx.json({ error: "not_found", environment: env }, 404);
|
|
109
|
+
}
|
|
110
|
+
return ctx.json({ environment: env, versionId: manifest.versionId, manifest }, 200);
|
|
111
|
+
});
|
|
112
|
+
return base;
|
|
113
|
+
}
|
|
114
|
+
function validateIngestBody(raw) {
|
|
115
|
+
if (typeof raw !== "object" || raw === null) {
|
|
116
|
+
return { ok: false, error: { error: "invalid_body", message: "expected JSON object" } };
|
|
117
|
+
}
|
|
118
|
+
const r = raw;
|
|
119
|
+
if (typeof r.environment !== "string" || !ALLOWED_ENVS.has(r.environment)) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
error: {
|
|
123
|
+
error: "invalid_body",
|
|
124
|
+
message: `"environment" must be one of ${[...ALLOWED_ENVS].join(", ")}`,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (typeof r.envelope !== "object" || r.envelope === null) {
|
|
129
|
+
return { ok: false, error: { error: "invalid_body", message: '"envelope" must be an object' } };
|
|
130
|
+
}
|
|
131
|
+
const envelope = r.envelope;
|
|
132
|
+
if (typeof envelope.name !== "string" || envelope.name.length === 0) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
error: { error: "invalid_body", message: '"envelope.name" must be a non-empty string' },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (typeof envelope.emittedAt !== "string" || envelope.emittedAt.length === 0) {
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
error: {
|
|
142
|
+
error: "invalid_body",
|
|
143
|
+
message: '"envelope.emittedAt" must be an ISO timestamp string',
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (envelope.metadata !== undefined &&
|
|
148
|
+
(typeof envelope.metadata !== "object" || envelope.metadata === null)) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
error: {
|
|
152
|
+
error: "invalid_body",
|
|
153
|
+
message: '"envelope.metadata" must be an object when supplied',
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (r.idempotencyKey !== undefined && typeof r.idempotencyKey !== "string") {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
error: { error: "invalid_body", message: '"idempotencyKey" must be a string when supplied' },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
ok: true,
|
|
165
|
+
body: {
|
|
166
|
+
environment: r.environment,
|
|
167
|
+
envelope: {
|
|
168
|
+
name: envelope.name,
|
|
169
|
+
data: envelope.data,
|
|
170
|
+
metadata: envelope.metadata,
|
|
171
|
+
emittedAt: envelope.emittedAt,
|
|
172
|
+
},
|
|
173
|
+
idempotencyKey: r.idempotencyKey,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function validateRegisterBody(raw) {
|
|
178
|
+
if (typeof raw !== "object" || raw === null) {
|
|
179
|
+
return { ok: false, error: { error: "invalid_body", message: "expected JSON object" } };
|
|
180
|
+
}
|
|
181
|
+
const r = raw;
|
|
182
|
+
if (typeof r.environment !== "string" || !ALLOWED_ENVS.has(r.environment)) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
error: {
|
|
186
|
+
error: "invalid_body",
|
|
187
|
+
message: `"environment" must be one of ${[...ALLOWED_ENVS].join(", ")}`,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (typeof r.manifest !== "object" || r.manifest === null) {
|
|
192
|
+
return { ok: false, error: { error: "invalid_body", message: '"manifest" must be an object' } };
|
|
193
|
+
}
|
|
194
|
+
const manifest = r.manifest;
|
|
195
|
+
if (typeof manifest.versionId !== "string" || manifest.versionId.length === 0) {
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
error: {
|
|
199
|
+
error: "invalid_body",
|
|
200
|
+
message: '"manifest.versionId" must be a non-empty string',
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
body: {
|
|
207
|
+
environment: r.environment,
|
|
208
|
+
manifest: manifest,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function errMessage(err) {
|
|
213
|
+
return err instanceof Error ? err.message : String(err);
|
|
214
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "./conditions.js";
|
|
2
|
+
export { FatalError, HookConflictError, QuotaExceededError, RetryableError, TimeoutError, ValidationError, } from "./errors.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,cAAc,iBAAiB,CAAA;AAC/B,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,eAAe,GAChB,MAAM,aAAa,CAAA;AACpB,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @voyant-travel/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 * from "./conditions.js";
|
|
7
|
+
export { FatalError, HookConflictError, QuotaExceededError, RetryableError, TimeoutError, ValidationError, } from "./errors.js";
|
|
8
|
+
export * from "./trigger.js";
|
|
9
|
+
export * from "./types.js";
|
|
10
|
+
export * from "./workflow.js";
|