@voyantjs/workflows 0.28.3 → 0.29.0
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/driver.d.ts +237 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +53 -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 +32 -0
- package/dist/events/manifest-builder.d.ts.map +1 -0
- package/dist/events/manifest-builder.js +66 -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 +8 -0
- package/dist/handler/index.d.ts.map +1 -1
- package/dist/handler/index.js +1 -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/protocol/index.d.ts +17 -2
- package/dist/protocol/index.d.ts.map +1 -1
- package/dist/runtime/ctx.d.ts +9 -0
- package/dist/runtime/ctx.d.ts.map +1 -1
- package/dist/runtime/ctx.js +17 -0
- package/dist/runtime/executor.d.ts +7 -0
- package/dist/runtime/executor.d.ts.map +1 -1
- package/dist/runtime/executor.js +1 -0
- package/dist/trigger.d.ts +28 -14
- package/dist/trigger.d.ts.map +1 -1
- package/dist/trigger.js +4 -4
- package/dist/workflow.d.ts +10 -0
- package/dist/workflow.d.ts.map +1 -1
- package/package.json +14 -2
- package/src/driver.ts +277 -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 +97 -0
- package/src/events/payload-hash.ts +110 -0
- package/src/events/predicate.ts +390 -0
- package/src/events/registry.ts +88 -0
- package/src/handler/index.ts +9 -0
- package/src/http-ingest.ts +299 -0
- package/src/protocol/index.ts +17 -2
- package/src/runtime/ctx.ts +29 -0
- package/src/runtime/executor.ts +8 -0
- package/src/trigger.ts +31 -15
- package/src/workflow.ts +11 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Input mapper DSL — projects a workflow input from an event envelope.
|
|
2
|
+
//
|
|
3
|
+
// Each `EventFilterDeclaration` carries an optional `input: InputMapper`. At
|
|
4
|
+
// ingest time, after the predicate matches, the mapper builds the actual
|
|
5
|
+
// input value passed to `driver.trigger(target, input, ...)`. Same path
|
|
6
|
+
// roots as the predicate evaluator (`data`, `metadata`, `name`, `emittedAt`).
|
|
7
|
+
//
|
|
8
|
+
// Variants:
|
|
9
|
+
// * `undefined` → pass through `envelope.data`
|
|
10
|
+
// * `{ passthrough: true }` → explicit pass-through of `envelope.data`
|
|
11
|
+
// * `{ path: string }` → workflow input = the resolved path value
|
|
12
|
+
// * `{ object: {...} }` → build an object by projecting each key;
|
|
13
|
+
// each value is itself an InputMapper or a
|
|
14
|
+
// `PathOrLit` for terminal projections
|
|
15
|
+
//
|
|
16
|
+
// Architecture: docs/architecture/workflows-runtime-architecture.md §13.2.
|
|
17
|
+
import { resolvePath } from "./predicate.js";
|
|
18
|
+
// ---- Public API ----
|
|
19
|
+
/**
|
|
20
|
+
* Project a workflow input from an event envelope. Mirrors the predicate
|
|
21
|
+
* evaluator's no-throw contract — missing paths produce `undefined` in the
|
|
22
|
+
* output, registration-time linting catches structural errors.
|
|
23
|
+
*
|
|
24
|
+
* Throws `InputMapperError` only on unexpected shape errors (the mapper
|
|
25
|
+
* itself was constructed wrong). Drivers catch and surface this as
|
|
26
|
+
* `IngestMatch.status === "skipped"` with reason `"input_projection_error"`.
|
|
27
|
+
*/
|
|
28
|
+
export function projectInput(mapper, envelope) {
|
|
29
|
+
if (mapper === undefined)
|
|
30
|
+
return envelope.data;
|
|
31
|
+
if (typeof mapper !== "object" || mapper === null) {
|
|
32
|
+
throw new InputMapperError(`input mapper must be undefined, {passthrough}, {path}, or {object}, got ${typeof mapper}`);
|
|
33
|
+
}
|
|
34
|
+
if ("passthrough" in mapper) {
|
|
35
|
+
if (mapper.passthrough !== true) {
|
|
36
|
+
throw new InputMapperError(`{ passthrough } must be true`);
|
|
37
|
+
}
|
|
38
|
+
return envelope.data;
|
|
39
|
+
}
|
|
40
|
+
if ("path" in mapper) {
|
|
41
|
+
if (typeof mapper.path !== "string" || mapper.path.length === 0) {
|
|
42
|
+
throw new InputMapperError(`{ path } must be a non-empty string`);
|
|
43
|
+
}
|
|
44
|
+
return resolvePath(mapper.path, envelope);
|
|
45
|
+
}
|
|
46
|
+
if ("object" in mapper) {
|
|
47
|
+
if (typeof mapper.object !== "object" || mapper.object === null) {
|
|
48
|
+
throw new InputMapperError(`{ object } must map keys to InputMapper | PathOrLit`);
|
|
49
|
+
}
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [key, child] of Object.entries(mapper.object)) {
|
|
52
|
+
out[key] = projectChild(child, envelope);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
throw new InputMapperError(`input mapper must contain one of passthrough | path | object, got keys: ${Object.keys(mapper).join(", ")}`);
|
|
57
|
+
}
|
|
58
|
+
export function validateInputMapper(mapper) {
|
|
59
|
+
const errors = [];
|
|
60
|
+
walkValidate(mapper, errors, []);
|
|
61
|
+
return { ok: errors.length === 0, errors };
|
|
62
|
+
}
|
|
63
|
+
// ---- Internals ----
|
|
64
|
+
class InputMapperError extends Error {
|
|
65
|
+
constructor(message) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = "InputMapperError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve one entry inside `{ object: { ... } }`. The value is either a
|
|
72
|
+
* nested mapper (recurse) or a `PathOrLit` (terminal projection).
|
|
73
|
+
*/
|
|
74
|
+
function projectChild(child, envelope) {
|
|
75
|
+
if (child === undefined)
|
|
76
|
+
return envelope.data;
|
|
77
|
+
if (typeof child !== "object" || child === null) {
|
|
78
|
+
throw new InputMapperError(`nested mapper entry must be an object, got ${typeof child}`);
|
|
79
|
+
}
|
|
80
|
+
// Distinguish PathOrLit (`{ path }` or `{ lit }`) from nested mapper.
|
|
81
|
+
if ("lit" in child) {
|
|
82
|
+
return child.lit;
|
|
83
|
+
}
|
|
84
|
+
// `{ path: "..." }` is shared between PathOrLit and InputMapper — resolve directly.
|
|
85
|
+
if ("path" in child) {
|
|
86
|
+
if (typeof child.path !== "string" || child.path.length === 0) {
|
|
87
|
+
throw new InputMapperError(`nested { path } must be a non-empty string`);
|
|
88
|
+
}
|
|
89
|
+
return resolvePath(child.path, envelope);
|
|
90
|
+
}
|
|
91
|
+
// Otherwise: a nested InputMapper (passthrough / object).
|
|
92
|
+
return projectInput(child, envelope);
|
|
93
|
+
}
|
|
94
|
+
function walkValidate(mapper, errors, path) {
|
|
95
|
+
if (mapper === undefined)
|
|
96
|
+
return;
|
|
97
|
+
if (typeof mapper !== "object" || mapper === null) {
|
|
98
|
+
errors.push(`${pathLabel(path)}: input mapper must be undefined or an object, got ${typeof mapper}`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const keys = Object.keys(mapper);
|
|
102
|
+
if (keys.length === 0) {
|
|
103
|
+
errors.push(`${pathLabel(path)}: input mapper must specify passthrough | path | object`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if ("passthrough" in mapper) {
|
|
107
|
+
if (mapper.passthrough !== true) {
|
|
108
|
+
errors.push(`${pathLabel(path)}: { passthrough } must be true`);
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if ("path" in mapper) {
|
|
113
|
+
if (typeof mapper.path !== "string" || mapper.path.length === 0) {
|
|
114
|
+
errors.push(`${pathLabel(path)}: { path } must be a non-empty string`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
validatePathRoot(mapper.path, errors, path);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if ("object" in mapper) {
|
|
121
|
+
if (typeof mapper.object !== "object" || mapper.object === null) {
|
|
122
|
+
errors.push(`${pathLabel(path)}: { object } must map keys to InputMapper | PathOrLit`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
for (const [k, child] of Object.entries(mapper.object)) {
|
|
126
|
+
validateChild(child, errors, [...path, k]);
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
errors.push(`${pathLabel(path)}: unknown mapper variant — keys: ${keys.join(", ")}`);
|
|
131
|
+
}
|
|
132
|
+
function validateChild(child, errors, path) {
|
|
133
|
+
if (child === undefined)
|
|
134
|
+
return;
|
|
135
|
+
if (typeof child !== "object" || child === null) {
|
|
136
|
+
errors.push(`${pathLabel(path)}: nested entry must be an object`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if ("lit" in child) {
|
|
140
|
+
const t = typeof child.lit;
|
|
141
|
+
if (t !== "string" &&
|
|
142
|
+
t !== "number" &&
|
|
143
|
+
t !== "boolean" &&
|
|
144
|
+
child.lit !== null) {
|
|
145
|
+
errors.push(`${pathLabel(path)}: { lit } must be string | number | boolean | null`);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if ("path" in child) {
|
|
150
|
+
if (typeof child.path !== "string" || child.path.length === 0) {
|
|
151
|
+
errors.push(`${pathLabel(path)}: { path } must be a non-empty string`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
validatePathRoot(child.path, errors, path);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
walkValidate(child, errors, path);
|
|
158
|
+
}
|
|
159
|
+
function validatePathRoot(path, errors, errorPath) {
|
|
160
|
+
// Match the predicate path roots exactly so the two DSLs stay aligned.
|
|
161
|
+
const firstSegment = path.split(".")[0] ?? "";
|
|
162
|
+
const root = firstSegment.split("[")[0] ?? "";
|
|
163
|
+
if (root !== "data" && root !== "metadata" && root !== "name" && root !== "emittedAt") {
|
|
164
|
+
errors.push(`${pathLabel(errorPath)}: path root "${root}" is not one of data | metadata | name | emittedAt`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function pathLabel(path) {
|
|
168
|
+
return path.length === 0 ? "(root)" : path.join(".");
|
|
169
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { WorkflowManifest } from "../protocol/index.js";
|
|
2
|
+
import type { EventFilterRuntimeEntry } from "./registry.js";
|
|
3
|
+
export interface BuildManifestArgs {
|
|
4
|
+
/** Project / tenant identifier. Single-tenant runtimes pass `"default"`. */
|
|
5
|
+
projectId?: string;
|
|
6
|
+
/** Deployment environment. */
|
|
7
|
+
environment: "production" | "preview" | "development";
|
|
8
|
+
/** Workflow definitions collected from modules + plugins. */
|
|
9
|
+
workflows: ReadonlyArray<{
|
|
10
|
+
id: string;
|
|
11
|
+
config?: {
|
|
12
|
+
defaultRuntime?: "edge" | "node";
|
|
13
|
+
retry?: unknown;
|
|
14
|
+
timeout?: unknown;
|
|
15
|
+
};
|
|
16
|
+
}>;
|
|
17
|
+
/** Event-filter entries from `getEventFilterRegistry()`. */
|
|
18
|
+
eventFilters: ReadonlyArray<EventFilterRuntimeEntry>;
|
|
19
|
+
/** Wall-clock build time, ms-since-epoch. Defaults to `Date.now()`. */
|
|
20
|
+
builtAt?: number;
|
|
21
|
+
/** Source-code version of the manifest builder. */
|
|
22
|
+
builderVersion?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build a deterministic `WorkflowManifest`. Same inputs always produce
|
|
26
|
+
* byte-identical output, including `versionId`.
|
|
27
|
+
*
|
|
28
|
+
* Does NOT write the manifest anywhere — that's the driver's
|
|
29
|
+
* `registerManifest(...)` responsibility. This function is pure.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildManifest(args: BuildManifestArgs): Promise<WorkflowManifest>;
|
|
32
|
+
//# sourceMappingURL=manifest-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-builder.d.ts","sourceRoot":"","sources":["../../src/events/manifest-builder.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAEV,gBAAgB,EAEjB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AAE5D,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8BAA8B;IAC9B,WAAW,EAAE,YAAY,GAAG,SAAS,GAAG,aAAa,CAAA;IACrD,6DAA6D;IAC7D,SAAS,EAAE,aAAa,CAAC;QACvB,EAAE,EAAE,MAAM,CAAA;QACV,MAAM,CAAC,EAAE;YAAE,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,OAAO,CAAC;YAAC,OAAO,CAAC,EAAE,OAAO,CAAA;SAAE,CAAA;KAClF,CAAC,CAAA;IACF,4DAA4D;IAC5D,YAAY,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAA;IACpD,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,mDAAmD;IACnD,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsDtF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Build a `WorkflowManifest` from collected workflow + event-filter entries.
|
|
2
|
+
//
|
|
3
|
+
// Called once at `createApp()` boot (PR4). The resulting manifest is
|
|
4
|
+
// content-addressed: byte-identical inputs produce byte-identical
|
|
5
|
+
// `versionId`s, so concurrent registration calls don't race meaningfully —
|
|
6
|
+
// the second caller sees the same versionId the first did.
|
|
7
|
+
//
|
|
8
|
+
// Architecture: docs/architecture/workflows-runtime-architecture.md §14.1.
|
|
9
|
+
import { canonicalJson, shortHash } from "./payload-hash.js";
|
|
10
|
+
/**
|
|
11
|
+
* Build a deterministic `WorkflowManifest`. Same inputs always produce
|
|
12
|
+
* byte-identical output, including `versionId`.
|
|
13
|
+
*
|
|
14
|
+
* Does NOT write the manifest anywhere — that's the driver's
|
|
15
|
+
* `registerManifest(...)` responsibility. This function is pure.
|
|
16
|
+
*/
|
|
17
|
+
export async function buildManifest(args) {
|
|
18
|
+
const builtAt = args.builtAt ?? Date.now();
|
|
19
|
+
const builderVersion = args.builderVersion ?? "@voyantjs/workflows@manifest-builder/v1";
|
|
20
|
+
const projectId = args.projectId ?? "default";
|
|
21
|
+
const workflows = args.workflows
|
|
22
|
+
.map((wf) => ({
|
|
23
|
+
id: wf.id,
|
|
24
|
+
version: "v1",
|
|
25
|
+
steps: [],
|
|
26
|
+
schedules: [],
|
|
27
|
+
defaultRuntime: wf.config?.defaultRuntime ?? "edge",
|
|
28
|
+
hasCompensation: false,
|
|
29
|
+
sourceLocation: { file: "<runtime>", line: 0 },
|
|
30
|
+
}))
|
|
31
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
32
|
+
// Sort filters by id so the canonical form is order-independent.
|
|
33
|
+
const eventFilters = args.eventFilters
|
|
34
|
+
.map((entry) => entry.manifest)
|
|
35
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
36
|
+
const draft = {
|
|
37
|
+
schemaVersion: 1,
|
|
38
|
+
projectId,
|
|
39
|
+
builtAt,
|
|
40
|
+
builderVersion,
|
|
41
|
+
capabilities: ["events:v1"],
|
|
42
|
+
workflows,
|
|
43
|
+
eventFilters,
|
|
44
|
+
bindings: {},
|
|
45
|
+
environments: { production: {}, preview: {}, development: {} },
|
|
46
|
+
};
|
|
47
|
+
// versionId is the cryptographic short hash of the canonical manifest
|
|
48
|
+
// body (excluding builtAt + versionId itself, which are non-load-bearing
|
|
49
|
+
// for content identity).
|
|
50
|
+
const identityBody = {
|
|
51
|
+
schemaVersion: draft.schemaVersion,
|
|
52
|
+
projectId: draft.projectId,
|
|
53
|
+
builderVersion: draft.builderVersion,
|
|
54
|
+
capabilities: draft.capabilities,
|
|
55
|
+
workflows: draft.workflows,
|
|
56
|
+
eventFilters: draft.eventFilters,
|
|
57
|
+
bindings: draft.bindings,
|
|
58
|
+
environments: draft.environments,
|
|
59
|
+
};
|
|
60
|
+
const versionId = await shortHash(identityBody);
|
|
61
|
+
void canonicalJson; // referenced via shortHash; keep the import surface stable
|
|
62
|
+
return {
|
|
63
|
+
...draft,
|
|
64
|
+
versionId,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively alphabetize object keys, leaving arrays and primitives intact.
|
|
3
|
+
* `undefined` is converted to `null` so canonical JSON shape is stable
|
|
4
|
+
* (JSON.stringify drops `undefined` from objects).
|
|
5
|
+
*/
|
|
6
|
+
export declare function canonicalize(value: unknown): unknown;
|
|
7
|
+
/**
|
|
8
|
+
* Stable canonical JSON string for `value`. Two values that are deeply
|
|
9
|
+
* equal modulo key order produce identical strings; two values that
|
|
10
|
+
* differ in any way produce different strings. Used as the input to
|
|
11
|
+
* `sha256(...)` for content-derived ids.
|
|
12
|
+
*/
|
|
13
|
+
export declare function canonicalJson(value: unknown): string;
|
|
14
|
+
/**
|
|
15
|
+
* SHA-256 hex digest of an arbitrary value (canonicalized first). Async
|
|
16
|
+
* — callers await once during manifest build.
|
|
17
|
+
*
|
|
18
|
+
* @returns lowercase hex string, 64 chars long.
|
|
19
|
+
*/
|
|
20
|
+
export declare function sha256(value: unknown): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Short content-derived id, used as `EventFilterManifestEntry.payloadHash`
|
|
23
|
+
* and `WorkflowManifest.versionId`. 16 hex chars (~64 bits) — collision
|
|
24
|
+
* space is fine for human-friendly ids in dashboards/logs; the canonical
|
|
25
|
+
* full hash is `sha256(...)` if you need it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function shortHash(value: unknown): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Derive a stable event id from an envelope when `metadata.eventId` is
|
|
30
|
+
* absent. Mirrors the formula from architecture doc §15.2:
|
|
31
|
+
*
|
|
32
|
+
* `${name}:${emittedAt}:${sha256(canonical(data)).slice(0, 12)}`
|
|
33
|
+
*
|
|
34
|
+
* Same envelope content always produces the same id — concurrent retries
|
|
35
|
+
* of the same external HTTP delivery dedupe at the driver's
|
|
36
|
+
* `${filterId}:${eventId}` idempotency key derivation.
|
|
37
|
+
*
|
|
38
|
+
* Returns a fallback id of the form `evt_<name>_<emittedAt>_<hash12>`
|
|
39
|
+
* (URL-safe; no colons in case the id flows through path segments).
|
|
40
|
+
*/
|
|
41
|
+
export declare function deriveStableEventId(envelope: {
|
|
42
|
+
name: string;
|
|
43
|
+
data: unknown;
|
|
44
|
+
emittedAt: string;
|
|
45
|
+
}): Promise<string>;
|
|
46
|
+
//# sourceMappingURL=payload-hash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payload-hash.d.ts","sourceRoot":"","sources":["../../src/events/payload-hash.ts"],"names":[],"mappings":"AAWA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAYpD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAED;;;;;GAKG;AACH,wBAAsB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAK5D;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAG/D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;CAClB,GAAG,OAAO,CAAC,MAAM,CAAC,CAKlB"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Canonical JSON + SHA-256 helpers used to derive `payloadHash` ids for
|
|
2
|
+
// `EventFilterRuntimeEntry` and `WorkflowManifest.versionId`.
|
|
3
|
+
//
|
|
4
|
+
// Canonicalization recursively alphabetizes object keys before
|
|
5
|
+
// JSON.stringify. SHA-256 uses Web Crypto (`globalThis.crypto.subtle`),
|
|
6
|
+
// available on Node ≥ 19, all modern browsers, and Cloudflare Workers.
|
|
7
|
+
// Async because Web Crypto's digest is async — callers `await` once
|
|
8
|
+
// at registration time.
|
|
9
|
+
//
|
|
10
|
+
// Architecture: docs/architecture/workflows-runtime-architecture.md §13.3.
|
|
11
|
+
/**
|
|
12
|
+
* Recursively alphabetize object keys, leaving arrays and primitives intact.
|
|
13
|
+
* `undefined` is converted to `null` so canonical JSON shape is stable
|
|
14
|
+
* (JSON.stringify drops `undefined` from objects).
|
|
15
|
+
*/
|
|
16
|
+
export function canonicalize(value) {
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
if (value === null || typeof value !== "object")
|
|
20
|
+
return value;
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return value.map(canonicalize);
|
|
23
|
+
}
|
|
24
|
+
const sorted = {};
|
|
25
|
+
const keys = Object.keys(value).sort();
|
|
26
|
+
for (const k of keys) {
|
|
27
|
+
sorted[k] = canonicalize(value[k]);
|
|
28
|
+
}
|
|
29
|
+
return sorted;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Stable canonical JSON string for `value`. Two values that are deeply
|
|
33
|
+
* equal modulo key order produce identical strings; two values that
|
|
34
|
+
* differ in any way produce different strings. Used as the input to
|
|
35
|
+
* `sha256(...)` for content-derived ids.
|
|
36
|
+
*/
|
|
37
|
+
export function canonicalJson(value) {
|
|
38
|
+
return JSON.stringify(canonicalize(value));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* SHA-256 hex digest of an arbitrary value (canonicalized first). Async
|
|
42
|
+
* — callers await once during manifest build.
|
|
43
|
+
*
|
|
44
|
+
* @returns lowercase hex string, 64 chars long.
|
|
45
|
+
*/
|
|
46
|
+
export async function sha256(value) {
|
|
47
|
+
const text = canonicalJson(value);
|
|
48
|
+
const bytes = new TextEncoder().encode(text);
|
|
49
|
+
const digest = await getCrypto().subtle.digest("SHA-256", bytes);
|
|
50
|
+
return bytesToHex(new Uint8Array(digest));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Short content-derived id, used as `EventFilterManifestEntry.payloadHash`
|
|
54
|
+
* and `WorkflowManifest.versionId`. 16 hex chars (~64 bits) — collision
|
|
55
|
+
* space is fine for human-friendly ids in dashboards/logs; the canonical
|
|
56
|
+
* full hash is `sha256(...)` if you need it.
|
|
57
|
+
*/
|
|
58
|
+
export async function shortHash(value) {
|
|
59
|
+
const full = await sha256(value);
|
|
60
|
+
return full.slice(0, 16);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Derive a stable event id from an envelope when `metadata.eventId` is
|
|
64
|
+
* absent. Mirrors the formula from architecture doc §15.2:
|
|
65
|
+
*
|
|
66
|
+
* `${name}:${emittedAt}:${sha256(canonical(data)).slice(0, 12)}`
|
|
67
|
+
*
|
|
68
|
+
* Same envelope content always produces the same id — concurrent retries
|
|
69
|
+
* of the same external HTTP delivery dedupe at the driver's
|
|
70
|
+
* `${filterId}:${eventId}` idempotency key derivation.
|
|
71
|
+
*
|
|
72
|
+
* Returns a fallback id of the form `evt_<name>_<emittedAt>_<hash12>`
|
|
73
|
+
* (URL-safe; no colons in case the id flows through path segments).
|
|
74
|
+
*/
|
|
75
|
+
export async function deriveStableEventId(envelope) {
|
|
76
|
+
const dataHash = (await sha256(envelope.data)).slice(0, 12);
|
|
77
|
+
const safeName = envelope.name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
78
|
+
const safeAt = envelope.emittedAt.replace(/[^a-zA-Z0-9.]/g, "_");
|
|
79
|
+
return `evt_${safeName}_${safeAt}_${dataHash}`;
|
|
80
|
+
}
|
|
81
|
+
// ---- Internal ----
|
|
82
|
+
function getCrypto() {
|
|
83
|
+
// `globalThis.crypto` is available on Node 19+, Workers, browsers.
|
|
84
|
+
// Any environment older than that needs a polyfill at the consumer level.
|
|
85
|
+
const c = globalThis.crypto;
|
|
86
|
+
if (!c?.subtle) {
|
|
87
|
+
throw new Error("@voyantjs/workflows/events: globalThis.crypto.subtle is required for payload-hash. " +
|
|
88
|
+
"Polyfill via webcrypto on legacy runtimes.");
|
|
89
|
+
}
|
|
90
|
+
return c;
|
|
91
|
+
}
|
|
92
|
+
function bytesToHex(bytes) {
|
|
93
|
+
let out = "";
|
|
94
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
95
|
+
out += (bytes[i] ?? 0).toString(16).padStart(2, "0");
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Either a path into the envelope or an inline literal value. */
|
|
2
|
+
export type PathOrLit = {
|
|
3
|
+
path: string;
|
|
4
|
+
} | {
|
|
5
|
+
lit: string | number | boolean | null;
|
|
6
|
+
};
|
|
7
|
+
export type PredicateExpr = {
|
|
8
|
+
eq: [PathOrLit, PathOrLit];
|
|
9
|
+
} | {
|
|
10
|
+
neq: [PathOrLit, PathOrLit];
|
|
11
|
+
} | {
|
|
12
|
+
in: [PathOrLit, PathOrLit[]];
|
|
13
|
+
} | {
|
|
14
|
+
gt: [PathOrLit, PathOrLit];
|
|
15
|
+
} | {
|
|
16
|
+
gte: [PathOrLit, PathOrLit];
|
|
17
|
+
} | {
|
|
18
|
+
lt: [PathOrLit, PathOrLit];
|
|
19
|
+
} | {
|
|
20
|
+
lte: [PathOrLit, PathOrLit];
|
|
21
|
+
} | {
|
|
22
|
+
exists: PathOrLit;
|
|
23
|
+
} | {
|
|
24
|
+
not: PredicateExpr;
|
|
25
|
+
} | {
|
|
26
|
+
and: PredicateExpr[];
|
|
27
|
+
} | {
|
|
28
|
+
or: PredicateExpr[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Minimal structural envelope the evaluator reads. Matches the standard
|
|
32
|
+
* `EventEnvelope` from `@voyantjs/core`. Declared structurally here so the
|
|
33
|
+
* SDK package stays a leaf.
|
|
34
|
+
*/
|
|
35
|
+
export interface PredicateEnvelope<TData = unknown> {
|
|
36
|
+
name: string;
|
|
37
|
+
data: TData;
|
|
38
|
+
metadata?: Record<string, unknown> | undefined;
|
|
39
|
+
emittedAt: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Evaluate a predicate against an event envelope. Returns `true` / `false`.
|
|
43
|
+
* Path resolution against missing keys yields `undefined`, which makes
|
|
44
|
+
* comparison ops `false` (not throw). The evaluator never throws on data
|
|
45
|
+
* mismatches — registration-time linting catches structural errors via
|
|
46
|
+
* {@link validatePredicate}.
|
|
47
|
+
*
|
|
48
|
+
* Throws `PredicateEvalError` only on unexpected shape errors (the predicate
|
|
49
|
+
* itself was constructed wrong, e.g. malformed operator). Drivers catch
|
|
50
|
+
* this and surface it as `IngestMatch.status === "skipped"` with reason
|
|
51
|
+
* `"where_eval_error"`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function evaluatePredicate(expr: PredicateExpr, envelope: PredicateEnvelope): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a path string against an envelope. Public so consumers (input
|
|
56
|
+
* mapper, manifest builder) can share the same path semantics without
|
|
57
|
+
* re-implementing them.
|
|
58
|
+
*
|
|
59
|
+
* Path syntax: dot-separated; `[N]` for array index; missing intermediate
|
|
60
|
+
* keys produce `undefined`. Roots: `data`, `metadata`, `name`, `emittedAt`.
|
|
61
|
+
*
|
|
62
|
+
* Returns `undefined` on any shape mismatch — that's how runtime evaluation
|
|
63
|
+
* stays no-throw.
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolvePath(path: string, envelope: PredicateEnvelope): unknown;
|
|
66
|
+
export interface PredicateValidationResult {
|
|
67
|
+
ok: boolean;
|
|
68
|
+
errors: string[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Static structural check on a `PredicateExpr`. Catches path roots that
|
|
72
|
+
* aren't `data`/`metadata`/`name`/`emittedAt`, type mismatches on
|
|
73
|
+
* comparison operators (number vs string lhs/rhs), and malformed grammars.
|
|
74
|
+
* Surfaced at `trigger.on()` registration so authoring errors fail fast.
|
|
75
|
+
*/
|
|
76
|
+
export declare function validatePredicate(expr: PredicateExpr): PredicateValidationResult;
|
|
77
|
+
//# sourceMappingURL=predicate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"predicate.d.ts","sourceRoot":"","sources":["../../src/events/predicate.ts"],"names":[],"mappings":"AAkBA,kEAAkE;AAClE,MAAM,MAAM,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;CAAE,CAAA;AAIpF,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;CAAE,GAC9B;IAAE,GAAG,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;CAAE,GAC/B;IAAE,EAAE,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC,CAAA;CAAE,GAChC;IAAE,EAAE,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;CAAE,GAC9B;IAAE,GAAG,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;CAAE,GAC/B;IAAE,EAAE,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;CAAE,GAC9B;IAAE,GAAG,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;CAAE,GAC/B;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,GAAG,EAAE,aAAa,CAAA;CAAE,GACtB;IAAE,GAAG,EAAE,aAAa,EAAE,CAAA;CAAE,GACxB;IAAE,EAAE,EAAE,aAAa,EAAE,CAAA;CAAE,CAAA;AAI3B;;;;GAIG;AACH,MAAM,WAAW,iBAAiB,CAAC,KAAK,GAAG,OAAO;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,KAAK,CAAA;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;IAC9C,SAAS,EAAE,MAAM,CAAA;CAClB;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAiC3F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAkC9E;AAID,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,yBAAyB,CAIhF"}
|