@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,299 @@
|
|
|
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
|
+
|
|
15
|
+
import type { IngestEventArgs, WorkflowDriver } from "./driver.js"
|
|
16
|
+
import type { EnvironmentName } from "./types.js"
|
|
17
|
+
|
|
18
|
+
const ALLOWED_ENVS = new Set<EnvironmentName>(["production", "preview", "development"])
|
|
19
|
+
|
|
20
|
+
// ---- Public types ----
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Minimum interface a Hono-shaped app exposes that we use. `app.post(...)`
|
|
24
|
+
* and `app.get(...)` register handlers; the handler signature mirrors
|
|
25
|
+
* Hono's `Context`-style callback for portability — we only read the
|
|
26
|
+
* request body and request params via the framework's response helpers.
|
|
27
|
+
*/
|
|
28
|
+
export interface HttpAppLike {
|
|
29
|
+
post(path: string, handler: HttpHandler): unknown
|
|
30
|
+
get(path: string, handler: HttpHandler): unknown
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Minimum context shape we read off Hono. Restricted to body parsing,
|
|
35
|
+
* route params, and JSON response helpers.
|
|
36
|
+
*/
|
|
37
|
+
export interface HttpContextLike {
|
|
38
|
+
req: {
|
|
39
|
+
json(): Promise<unknown>
|
|
40
|
+
param(name: string): string | undefined
|
|
41
|
+
header(name: string): string | undefined
|
|
42
|
+
raw: Request
|
|
43
|
+
}
|
|
44
|
+
json(body: unknown, status?: number): Response
|
|
45
|
+
text(body: string, status?: number): Response
|
|
46
|
+
status(code: number): unknown
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type HttpHandler = (ctx: HttpContextLike) => Promise<Response> | Response
|
|
50
|
+
|
|
51
|
+
export interface MountHttpIngestAdapterOptions {
|
|
52
|
+
/**
|
|
53
|
+
* Driver the adapter forwards into. Typically the same instance
|
|
54
|
+
* `createApp({ workflows: { driver } })` constructed.
|
|
55
|
+
*/
|
|
56
|
+
driver: WorkflowDriver
|
|
57
|
+
/** Mount path. Defaults to `"/api/workflows"`. */
|
|
58
|
+
basePath?: string
|
|
59
|
+
/**
|
|
60
|
+
* Optional auth check. Receives the original `Request` and returns
|
|
61
|
+
* `void` on success / throws on failure. Reuse
|
|
62
|
+
* `createBearerVerifier(...)` from `@voyant-travel/workflows/auth` for the
|
|
63
|
+
* canonical bearer-token shape.
|
|
64
|
+
*/
|
|
65
|
+
verifyRequest?: (req: Request) => void | Promise<void>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---- Mount ----
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Mount the adapter onto a Hono-shaped app. Registers:
|
|
72
|
+
*
|
|
73
|
+
* POST {basePath}/events → driver.ingestEvent
|
|
74
|
+
* POST {basePath}/manifests → driver.registerManifest
|
|
75
|
+
* GET {basePath}/manifests/:env → driver.getManifest
|
|
76
|
+
*
|
|
77
|
+
* Returns the mounted base path so callers can log it.
|
|
78
|
+
*/
|
|
79
|
+
export function mountHttpIngestAdapter(
|
|
80
|
+
app: HttpAppLike,
|
|
81
|
+
opts: MountHttpIngestAdapterOptions,
|
|
82
|
+
): string {
|
|
83
|
+
const base = (opts.basePath ?? "/api/workflows").replace(/\/$/, "")
|
|
84
|
+
|
|
85
|
+
app.post(`${base}/events`, async (ctx) => {
|
|
86
|
+
if (opts.verifyRequest) {
|
|
87
|
+
try {
|
|
88
|
+
await opts.verifyRequest(ctx.req.raw)
|
|
89
|
+
} catch (err) {
|
|
90
|
+
return ctx.json({ error: "unauthorized", message: errMessage(err) }, 401)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let raw: unknown
|
|
95
|
+
try {
|
|
96
|
+
raw = await ctx.req.json()
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return ctx.json({ error: "invalid_json", message: errMessage(err) }, 400)
|
|
99
|
+
}
|
|
100
|
+
const validation = validateIngestBody(raw)
|
|
101
|
+
if (!validation.ok) return ctx.json(validation.error, 400)
|
|
102
|
+
|
|
103
|
+
const args: IngestEventArgs = {
|
|
104
|
+
environment: validation.body.environment,
|
|
105
|
+
envelope: validation.body.envelope,
|
|
106
|
+
idempotencyKey: validation.body.idempotencyKey,
|
|
107
|
+
}
|
|
108
|
+
const result = await opts.driver.ingestEvent(args)
|
|
109
|
+
if (!result.ok && result.reason === "manifest_not_registered") {
|
|
110
|
+
return ctx.json(result, 200)
|
|
111
|
+
}
|
|
112
|
+
if (!result.ok) {
|
|
113
|
+
return ctx.json(result, 502)
|
|
114
|
+
}
|
|
115
|
+
return ctx.json(result, 200)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
app.post(`${base}/manifests`, async (ctx) => {
|
|
119
|
+
if (opts.verifyRequest) {
|
|
120
|
+
try {
|
|
121
|
+
await opts.verifyRequest(ctx.req.raw)
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return ctx.json({ error: "unauthorized", message: errMessage(err) }, 401)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
let raw: unknown
|
|
127
|
+
try {
|
|
128
|
+
raw = await ctx.req.json()
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return ctx.json({ error: "invalid_json", message: errMessage(err) }, 400)
|
|
131
|
+
}
|
|
132
|
+
const validation = validateRegisterBody(raw)
|
|
133
|
+
if (!validation.ok) return ctx.json(validation.error, 400)
|
|
134
|
+
try {
|
|
135
|
+
const result = await opts.driver.registerManifest({
|
|
136
|
+
environment: validation.body.environment,
|
|
137
|
+
manifest: validation.body.manifest as never, // structurally compatible
|
|
138
|
+
})
|
|
139
|
+
return ctx.json({ ok: true, versionId: result.versionId }, 200)
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return ctx.json({ error: "register_failed", message: errMessage(err) }, 500)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
app.get(`${base}/manifests/:env`, async (ctx) => {
|
|
146
|
+
if (opts.verifyRequest) {
|
|
147
|
+
try {
|
|
148
|
+
await opts.verifyRequest(ctx.req.raw)
|
|
149
|
+
} catch (err) {
|
|
150
|
+
return ctx.json({ error: "unauthorized", message: errMessage(err) }, 401)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const env = ctx.req.param("env")
|
|
154
|
+
if (!env || !ALLOWED_ENVS.has(env as EnvironmentName)) {
|
|
155
|
+
return ctx.json(
|
|
156
|
+
{
|
|
157
|
+
error: "invalid_environment",
|
|
158
|
+
message: `environment must be one of ${[...ALLOWED_ENVS].join(", ")}`,
|
|
159
|
+
},
|
|
160
|
+
400,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
const manifest = await opts.driver.getManifest({ environment: env as EnvironmentName })
|
|
164
|
+
if (!manifest) {
|
|
165
|
+
return ctx.json({ error: "not_found", environment: env }, 404)
|
|
166
|
+
}
|
|
167
|
+
return ctx.json({ environment: env, versionId: manifest.versionId, manifest }, 200)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
return base
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---- Validation ----
|
|
174
|
+
|
|
175
|
+
interface IngestBody {
|
|
176
|
+
environment: EnvironmentName
|
|
177
|
+
envelope: {
|
|
178
|
+
name: string
|
|
179
|
+
data: unknown
|
|
180
|
+
metadata?: Record<string, unknown> & { eventId?: string }
|
|
181
|
+
emittedAt: string
|
|
182
|
+
}
|
|
183
|
+
idempotencyKey?: string
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function validateIngestBody(
|
|
187
|
+
raw: unknown,
|
|
188
|
+
): { ok: true; body: IngestBody } | { ok: false; error: { error: string; message: string } } {
|
|
189
|
+
if (typeof raw !== "object" || raw === null) {
|
|
190
|
+
return { ok: false, error: { error: "invalid_body", message: "expected JSON object" } }
|
|
191
|
+
}
|
|
192
|
+
const r = raw as Record<string, unknown>
|
|
193
|
+
if (typeof r.environment !== "string" || !ALLOWED_ENVS.has(r.environment as EnvironmentName)) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
error: {
|
|
197
|
+
error: "invalid_body",
|
|
198
|
+
message: `"environment" must be one of ${[...ALLOWED_ENVS].join(", ")}`,
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (typeof r.envelope !== "object" || r.envelope === null) {
|
|
203
|
+
return { ok: false, error: { error: "invalid_body", message: '"envelope" must be an object' } }
|
|
204
|
+
}
|
|
205
|
+
const envelope = r.envelope as Record<string, unknown>
|
|
206
|
+
if (typeof envelope.name !== "string" || envelope.name.length === 0) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
error: { error: "invalid_body", message: '"envelope.name" must be a non-empty string' },
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (typeof envelope.emittedAt !== "string" || envelope.emittedAt.length === 0) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
error: {
|
|
216
|
+
error: "invalid_body",
|
|
217
|
+
message: '"envelope.emittedAt" must be an ISO timestamp string',
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (
|
|
222
|
+
envelope.metadata !== undefined &&
|
|
223
|
+
(typeof envelope.metadata !== "object" || envelope.metadata === null)
|
|
224
|
+
) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
error: {
|
|
228
|
+
error: "invalid_body",
|
|
229
|
+
message: '"envelope.metadata" must be an object when supplied',
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (r.idempotencyKey !== undefined && typeof r.idempotencyKey !== "string") {
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
error: { error: "invalid_body", message: '"idempotencyKey" must be a string when supplied' },
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
ok: true,
|
|
241
|
+
body: {
|
|
242
|
+
environment: r.environment as EnvironmentName,
|
|
243
|
+
envelope: {
|
|
244
|
+
name: envelope.name,
|
|
245
|
+
data: envelope.data,
|
|
246
|
+
metadata: envelope.metadata as Record<string, unknown> | undefined,
|
|
247
|
+
emittedAt: envelope.emittedAt,
|
|
248
|
+
},
|
|
249
|
+
idempotencyKey: r.idempotencyKey as string | undefined,
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interface RegisterBody {
|
|
255
|
+
environment: EnvironmentName
|
|
256
|
+
manifest: Record<string, unknown> & { versionId: string }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function validateRegisterBody(
|
|
260
|
+
raw: unknown,
|
|
261
|
+
): { ok: true; body: RegisterBody } | { ok: false; error: { error: string; message: string } } {
|
|
262
|
+
if (typeof raw !== "object" || raw === null) {
|
|
263
|
+
return { ok: false, error: { error: "invalid_body", message: "expected JSON object" } }
|
|
264
|
+
}
|
|
265
|
+
const r = raw as Record<string, unknown>
|
|
266
|
+
if (typeof r.environment !== "string" || !ALLOWED_ENVS.has(r.environment as EnvironmentName)) {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
error: {
|
|
270
|
+
error: "invalid_body",
|
|
271
|
+
message: `"environment" must be one of ${[...ALLOWED_ENVS].join(", ")}`,
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (typeof r.manifest !== "object" || r.manifest === null) {
|
|
276
|
+
return { ok: false, error: { error: "invalid_body", message: '"manifest" must be an object' } }
|
|
277
|
+
}
|
|
278
|
+
const manifest = r.manifest as Record<string, unknown>
|
|
279
|
+
if (typeof manifest.versionId !== "string" || manifest.versionId.length === 0) {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
error: {
|
|
283
|
+
error: "invalid_body",
|
|
284
|
+
message: '"manifest.versionId" must be a non-empty string',
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
ok: true,
|
|
290
|
+
body: {
|
|
291
|
+
environment: r.environment as EnvironmentName,
|
|
292
|
+
manifest: manifest as Record<string, unknown> & { versionId: string },
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function errMessage(err: unknown): string {
|
|
298
|
+
return err instanceof Error ? err.message : String(err)
|
|
299
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
|
|
7
|
+
export * from "./conditions.js"
|
|
8
|
+
export {
|
|
9
|
+
FatalError,
|
|
10
|
+
HookConflictError,
|
|
11
|
+
QuotaExceededError,
|
|
12
|
+
RetryableError,
|
|
13
|
+
TimeoutError,
|
|
14
|
+
ValidationError,
|
|
15
|
+
} from "./errors.js"
|
|
16
|
+
export * from "./trigger.js"
|
|
17
|
+
export * from "./types.js"
|
|
18
|
+
export * from "./workflow.js"
|