@voyantjs/workflows-orchestrator-cloudflare 0.107.4 → 0.107.6
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/cf-container-runner.d.ts.map +1 -1
- package/dist/cf-container-runner.js +16 -0
- package/dist/cloudflare-edge-driver.d.ts.map +1 -1
- package/dist/cloudflare-edge-driver.js +40 -3
- package/dist/event-handler.d.ts.map +1 -1
- package/dist/event-handler.js +34 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +1 -0
- package/package.json +3 -3
- package/src/__tests__/adapter-test-support.ts +107 -0
- package/src/cf-container-runner.ts +23 -0
- package/src/cloudflare-edge-driver.ts +59 -3
- package/src/event-handler.ts +52 -1
- package/src/worker.ts +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cf-container-runner.d.ts","sourceRoot":"","sources":["../src/cf-container-runner.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAoB,UAAU,EAAE,MAAM,6BAA6B,CAAA;
|
|
1
|
+
{"version":3,"file":"cf-container-runner.d.ts","sourceRoot":"","sources":["../src/cf-container-runner.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAoB,UAAU,EAAE,MAAM,6BAA6B,CAAA;AAI/E;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,QAAQ,IAAI,MAAM,CAAA;KAAE,CAAA;IAChD,GAAG,CAAC,EAAE,EAAE;QAAE,QAAQ,IAAI,MAAM,CAAA;KAAE,GAAG;QAAE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;CAChF;AAED,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAA;IACX;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,SAAS,EAAE,sBAAsB,CAAA;IACjC;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE;QACrB,KAAK,EAAE,MAAM,CAAA;QACb,UAAU,EAAE,MAAM,CAAA;QAClB,eAAe,EAAE,MAAM,CAAA;QACvB,SAAS,EAAE,MAAM,CAAA;QACjB,cAAc,EAAE,MAAM,CAAA;KACvB,KAAK,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAAA;IAC9C;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAA;IACjD,kCAAkC;IAClC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/E;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE;QACnB,KAAK,EAAE,MAAM,CAAA;QACb,UAAU,EAAE,MAAM,CAAA;QAClB,eAAe,EAAE,MAAM,CAAA;QACvB,MAAM,EAAE,MAAM,CAAA;QACd,OAAO,EAAE,MAAM,CAAA;KAChB,KAAK,MAAM,CAAA;CACb;AA2BD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,qBAAqB,GAAG,UAAU,CAuInF"}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
// `@voyantjs/workflows/handler`'s `executeWorkflowStep` with the
|
|
24
24
|
// workflow registry already loaded. See
|
|
25
25
|
// `apps/workflows-node-step-container/` for the reference image.
|
|
26
|
+
const STEP_RESPONSE_AUTH_HEADER = "x-voyant-step-response-auth";
|
|
26
27
|
/**
|
|
27
28
|
* Build a `StepRunner` that dispatches each step invocation to a
|
|
28
29
|
* Cloudflare Container in the given namespace.
|
|
@@ -124,6 +125,13 @@ export function createCfContainerStepRunner(deps) {
|
|
|
124
125
|
});
|
|
125
126
|
return failed(attempt, startedAt, "CONTAINER_HTTP_ERROR", new Error(`container returned HTTP ${response.status}: ${text}`));
|
|
126
127
|
}
|
|
128
|
+
if (deps.sign) {
|
|
129
|
+
const signature = response.headers.get(STEP_RESPONSE_AUTH_HEADER);
|
|
130
|
+
const expected = await deps.sign(text);
|
|
131
|
+
if (!signature || !constantTimeEquals(signature, expected)) {
|
|
132
|
+
return failed(attempt, startedAt, "CONTAINER_RESPONSE_SIGNATURE_INVALID", new Error("container response signature is missing or invalid"));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
127
135
|
try {
|
|
128
136
|
const entry = JSON.parse(text);
|
|
129
137
|
// Trust the container's own timestamps; they reflect the actual
|
|
@@ -135,6 +143,14 @@ export function createCfContainerStepRunner(deps) {
|
|
|
135
143
|
}
|
|
136
144
|
};
|
|
137
145
|
}
|
|
146
|
+
function constantTimeEquals(a, b) {
|
|
147
|
+
const length = Math.max(a.length, b.length, 1);
|
|
148
|
+
let diff = a.length === b.length ? 0 : 1;
|
|
149
|
+
for (let i = 0; i < length; i++) {
|
|
150
|
+
diff |= (a.charCodeAt(i) | 0) ^ (b.charCodeAt(i) | 0);
|
|
151
|
+
}
|
|
152
|
+
return diff === 0;
|
|
153
|
+
}
|
|
138
154
|
function failed(attempt, startedAt, code, err) {
|
|
139
155
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
140
156
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudflare-edge-driver.d.ts","sourceRoot":"","sources":["../src/cloudflare-edge-driver.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cloudflare-edge-driver.d.ts","sourceRoot":"","sources":["../src/cloudflare-edge-driver.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EACV,eAAe,EAOhB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EACV,aAAa,EAOd,MAAM,4BAA4B,CAAA;AAUnC,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,wBAAwB,CAAA;AAE/B,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAI7D,MAAM,WAAW,2BAA2B;IAC1C,uDAAuD;IACvD,qBAAqB,EAAE,0BAA0B,CAAA;IACjD,uFAAuF;IACvF,oBAAoB,CAAC,EAAE,0BAA0B,CAAA;IACjD,iDAAiD;IACjD,UAAU,EAAE,eAAe,CAAA;IAC3B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,eAAe,CAAA;IACpC,uFAAuF;IACvF,UAAU,CAAC,EAAE;QACX,QAAQ,EAAE,MAAM,CAAA;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,cAAc,EAAE,MAAM,CAAA;KACvB,CAAA;IACD,8CAA8C;IAC9C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,MAAM,CAAA;IAC1B,sEAAsE;IACtE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAChF;AAiED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,2BAA2B,GAAG,aAAa,CAqa3F"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Mode 1 driver — Cloudflare edge composition.
|
|
2
|
+
// agent-quality: file-size exception -- Public edge driver factory currently owns manifest, trigger, event-ingest, concurrency, and admin wiring; split only with a dedicated driver-surface refactor.
|
|
2
3
|
//
|
|
3
4
|
// `createApp({ workflows: { driver: createCloudflareEdgeDriver({ ... }) } })`
|
|
4
5
|
// is the entry point for any deployment that runs the orchestrator on
|
|
@@ -24,6 +25,42 @@ const DEFAULT_TENANT_META = {
|
|
|
24
25
|
projectId: "default",
|
|
25
26
|
organizationId: "default",
|
|
26
27
|
};
|
|
28
|
+
function serializeWorkflowManifest(manifest) {
|
|
29
|
+
return { ...manifest };
|
|
30
|
+
}
|
|
31
|
+
function deserializeWorkflowManifest(manifest) {
|
|
32
|
+
const { schemaVersion, projectId, versionId, builtAt, builderVersion, capabilities, workflows, eventFilters, bindings, environments, } = manifest;
|
|
33
|
+
if (schemaVersion !== 1 ||
|
|
34
|
+
typeof projectId !== "string" ||
|
|
35
|
+
typeof versionId !== "string" ||
|
|
36
|
+
typeof builtAt !== "number" ||
|
|
37
|
+
typeof builderVersion !== "string" ||
|
|
38
|
+
!isStringArray(capabilities) ||
|
|
39
|
+
!Array.isArray(workflows) ||
|
|
40
|
+
!Array.isArray(eventFilters) ||
|
|
41
|
+
!isRecord(bindings) ||
|
|
42
|
+
!isRecord(environments)) {
|
|
43
|
+
throw new Error("stored workflow manifest has an invalid shape");
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
schemaVersion,
|
|
47
|
+
projectId,
|
|
48
|
+
versionId,
|
|
49
|
+
builtAt,
|
|
50
|
+
builderVersion,
|
|
51
|
+
capabilities,
|
|
52
|
+
workflows: workflows,
|
|
53
|
+
eventFilters: eventFilters,
|
|
54
|
+
bindings: bindings,
|
|
55
|
+
environments: environments,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function isRecord(value) {
|
|
59
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
function isStringArray(value) {
|
|
62
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
63
|
+
}
|
|
27
64
|
// ---- Public factory ----
|
|
28
65
|
/**
|
|
29
66
|
* Build the Cloudflare-edge driver factory. The returned `DriverFactory`
|
|
@@ -137,14 +174,14 @@ export function createCloudflareEdgeDriver(opts) {
|
|
|
137
174
|
return manifestStore.registerManifest({
|
|
138
175
|
environment: args.environment,
|
|
139
176
|
versionId: args.manifest.versionId,
|
|
140
|
-
manifest: args.manifest,
|
|
177
|
+
manifest: serializeWorkflowManifest(args.manifest),
|
|
141
178
|
});
|
|
142
179
|
}
|
|
143
180
|
async function getManifest(args) {
|
|
144
181
|
const envelope = await manifestStore.getCurrent(args.environment);
|
|
145
182
|
if (!envelope)
|
|
146
183
|
return null;
|
|
147
|
-
return envelope.manifest;
|
|
184
|
+
return deserializeWorkflowManifest(envelope.manifest);
|
|
148
185
|
}
|
|
149
186
|
async function trigger(workflow, input, triggerOpts) {
|
|
150
187
|
assertNotShutdown();
|
|
@@ -192,7 +229,7 @@ export function createCloudflareEdgeDriver(opts) {
|
|
|
192
229
|
message: `No manifest is registered for environment "${args.environment}".`,
|
|
193
230
|
};
|
|
194
231
|
}
|
|
195
|
-
const manifest = stored.manifest;
|
|
232
|
+
const manifest = deserializeWorkflowManifest(stored.manifest);
|
|
196
233
|
const eventId = args.envelope.metadata?.eventId ?? (await deriveStableEventId(args.envelope));
|
|
197
234
|
const routed = routeEvent({
|
|
198
235
|
manifest,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-handler.d.ts","sourceRoot":"","sources":["../src/event-handler.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"event-handler.d.ts","sourceRoot":"","sources":["../src/event-handler.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAuD7D,MAAM,WAAW,gBAAgB,CAAC,EAAE,GAAG,OAAO;IAC5C,sDAAsD;IACtD,aAAa,EAAE,eAAe,CAAA;IAC9B,6DAA6D;IAC7D,KAAK,EAAE,0BAA0B,CAAC,EAAE,CAAC,CAAA;IACrC,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,MAAM,CAAA;IAC1B,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,sDAAsD;IACtD,UAAU,CAAC,EAAE;QACX,QAAQ,EAAE,MAAM,CAAA;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,cAAc,EAAE,MAAM,CAAA;QACtB,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,CAAA;IACD,uBAAuB;IACvB,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAChF;AAqBD,wBAAsB,iBAAiB,CAAC,EAAE,EACxC,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,gBAAgB,CAAC,EAAE,CAAC,GACzB,OAAO,CAAC,QAAQ,CAAC,CAqInB"}
|
package/dist/event-handler.js
CHANGED
|
@@ -14,6 +14,39 @@
|
|
|
14
14
|
import { deriveStableEventId } from "@voyantjs/workflows/events";
|
|
15
15
|
import { routeEvent } from "@voyantjs/workflows-orchestrator";
|
|
16
16
|
const ALLOWED_ENVS = new Set(["production", "preview", "development"]);
|
|
17
|
+
function deserializeWorkflowManifest(manifest) {
|
|
18
|
+
const { schemaVersion, projectId, versionId, builtAt, builderVersion, capabilities, workflows, eventFilters, bindings, environments, } = manifest;
|
|
19
|
+
if (schemaVersion !== 1 ||
|
|
20
|
+
typeof projectId !== "string" ||
|
|
21
|
+
typeof versionId !== "string" ||
|
|
22
|
+
typeof builtAt !== "number" ||
|
|
23
|
+
typeof builderVersion !== "string" ||
|
|
24
|
+
!isStringArray(capabilities) ||
|
|
25
|
+
!Array.isArray(workflows) ||
|
|
26
|
+
!Array.isArray(eventFilters) ||
|
|
27
|
+
!isRecord(bindings) ||
|
|
28
|
+
!isRecord(environments)) {
|
|
29
|
+
throw new Error("stored workflow manifest has an invalid shape");
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
schemaVersion,
|
|
33
|
+
projectId,
|
|
34
|
+
versionId,
|
|
35
|
+
builtAt,
|
|
36
|
+
builderVersion,
|
|
37
|
+
capabilities,
|
|
38
|
+
workflows: workflows,
|
|
39
|
+
eventFilters: eventFilters,
|
|
40
|
+
bindings: bindings,
|
|
41
|
+
environments: environments,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function isRecord(value) {
|
|
45
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
function isStringArray(value) {
|
|
48
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
49
|
+
}
|
|
17
50
|
const DEFAULT_TENANT_META = {
|
|
18
51
|
tenantId: "default",
|
|
19
52
|
projectId: "default",
|
|
@@ -44,7 +77,7 @@ export async function handleIngestEvent(req, deps) {
|
|
|
44
77
|
message: `No manifest is registered for environment "${body.environment}".`,
|
|
45
78
|
});
|
|
46
79
|
}
|
|
47
|
-
const manifest = manifestEnvelope.manifest;
|
|
80
|
+
const manifest = deserializeWorkflowManifest(manifestEnvelope.manifest);
|
|
48
81
|
// Event id derivation — use the caller-stamped one when present, fall
|
|
49
82
|
// back to a content-derived id so external callers without a forwarder
|
|
50
83
|
// still get sensible idempotency.
|
package/dist/worker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAE7D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAQrE;;;;GAIG;AACH,MAAM,WAAW,0BAA0B,CAAC,EAAE,GAAG,OAAO;IACtD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,EAAE,CAAA;IAC5B,GAAG,CAAC,EAAE,EAAE,EAAE,GAAG;QAAE,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;CACxD;AAED,MAAM,WAAW,eAAe,CAAC,EAAE,GAAG,OAAO;IAC3C,KAAK,EAAE,0BAA0B,CAAC,EAAE,CAAC,CAAA;IACrC;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,uBAAuB;IACvB,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/E,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,MAAM,CAAA;IAC1B,0CAA0C;IAC1C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,eAAe,CAAA;IAC/B;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,oBAAoB,CAAA;IACzC;;;;;OAKG;IACH,UAAU,CAAC,EAAE;QACX,QAAQ,EAAE,MAAM,CAAA;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,cAAc,EAAE,MAAM,CAAA;QACtB,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,CAAA;CACF;AAED,wBAAsB,mBAAmB,CAAC,EAAE,EAC1C,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,eAAe,CAAC,EAAE,CAAC,GACxB,OAAO,CAAC,QAAQ,CAAC,CA4OnB"}
|
package/dist/worker.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// agent-quality: file-size exception -- owner: workflows-orchestrator-cloudflare; existing module stays co-located until a dedicated split preserves behavior and tests.
|
|
1
2
|
// Public HTTP surface of the Cloudflare orchestrator. The outer
|
|
2
3
|
// Worker receives a request, resolves the run DO by id (or creates
|
|
3
4
|
// one for a new trigger), and forwards to the DO. This layer owns
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyantjs/workflows-orchestrator-cloudflare",
|
|
3
|
-
"version": "0.107.
|
|
3
|
+
"version": "0.107.6",
|
|
4
4
|
"description": "Cloudflare Worker + Durable Object adapter for @voyantjs/workflows-orchestrator. Dispatches workflow-step requests to tenant Workers via a Workers-for-Platforms dispatch namespace.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"NOTICE"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@voyantjs/workflows-orchestrator": "^0.107.
|
|
30
|
-
"@voyantjs/workflows": "^0.107.
|
|
29
|
+
"@voyantjs/workflows-orchestrator": "^0.107.6",
|
|
30
|
+
"@voyantjs/workflows": "^0.107.6"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@cloudflare/vitest-pool-workers": "^0.15.1",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { __resetRegistry } from "@voyantjs/workflows"
|
|
2
|
+
import { handleStepRequest } from "@voyantjs/workflows/handler"
|
|
3
|
+
import { beforeEach } from "vitest"
|
|
4
|
+
import {
|
|
5
|
+
createServiceBindingDispatcher,
|
|
6
|
+
type DurableObjectNamespaceLike,
|
|
7
|
+
type DurableObjectStorageLike,
|
|
8
|
+
handleDurableObjectRequest,
|
|
9
|
+
type ServiceBindingLike,
|
|
10
|
+
} from "../index.js"
|
|
11
|
+
|
|
12
|
+
export interface AlarmTrackingStorage extends DurableObjectStorageLike {
|
|
13
|
+
_alarm: number | null
|
|
14
|
+
_alarmCalls: number
|
|
15
|
+
_deleteAlarmCalls: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function makeStorage(): AlarmTrackingStorage {
|
|
19
|
+
const map = new Map<string, unknown>()
|
|
20
|
+
const s: AlarmTrackingStorage = {
|
|
21
|
+
_alarm: null,
|
|
22
|
+
_alarmCalls: 0,
|
|
23
|
+
_deleteAlarmCalls: 0,
|
|
24
|
+
async get<T>(key: string): Promise<T | undefined> {
|
|
25
|
+
return map.get(key) as T | undefined
|
|
26
|
+
},
|
|
27
|
+
async put<T>(key: string, value: T): Promise<void> {
|
|
28
|
+
map.set(key, value)
|
|
29
|
+
},
|
|
30
|
+
async delete(key) {
|
|
31
|
+
return map.delete(key)
|
|
32
|
+
},
|
|
33
|
+
async list<T>(options = {}) {
|
|
34
|
+
const out = new Map<string, T>()
|
|
35
|
+
for (const [k, v] of map) {
|
|
36
|
+
if (options.prefix && !k.startsWith(options.prefix)) continue
|
|
37
|
+
out.set(k, v as T)
|
|
38
|
+
if (options.limit && out.size >= options.limit) break
|
|
39
|
+
}
|
|
40
|
+
return out
|
|
41
|
+
},
|
|
42
|
+
async getAlarm() {
|
|
43
|
+
return s._alarm
|
|
44
|
+
},
|
|
45
|
+
async setAlarm(wakeAt) {
|
|
46
|
+
s._alarm = wakeAt
|
|
47
|
+
s._alarmCalls += 1
|
|
48
|
+
},
|
|
49
|
+
async deleteAlarm() {
|
|
50
|
+
s._alarm = null
|
|
51
|
+
s._deleteAlarmCalls += 1
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
return s
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function inProcessBinding(): ServiceBindingLike {
|
|
58
|
+
return {
|
|
59
|
+
async fetch(req: Request): Promise<Response> {
|
|
60
|
+
const body = await req.json()
|
|
61
|
+
const out = await handleStepRequest(body)
|
|
62
|
+
return new Response(JSON.stringify(out.body), {
|
|
63
|
+
status: out.status,
|
|
64
|
+
headers: { "content-type": "application/json" },
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function inProcessRunDONamespace(): DurableObjectNamespaceLike<string> & {
|
|
71
|
+
_storages: Map<string, DurableObjectStorageLike>
|
|
72
|
+
} {
|
|
73
|
+
const storages = new Map<string, DurableObjectStorageLike>()
|
|
74
|
+
const binding = inProcessBinding()
|
|
75
|
+
return {
|
|
76
|
+
_storages: storages,
|
|
77
|
+
idFromName(name) {
|
|
78
|
+
return name
|
|
79
|
+
},
|
|
80
|
+
get(id: string) {
|
|
81
|
+
let storage = storages.get(id)
|
|
82
|
+
if (!storage) {
|
|
83
|
+
storage = makeStorage()
|
|
84
|
+
storages.set(id, storage)
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
async fetch(req: Request): Promise<Response> {
|
|
88
|
+
return handleDurableObjectRequest(req, {
|
|
89
|
+
storage: storage!,
|
|
90
|
+
dispatcher: createServiceBindingDispatcher({ binding }),
|
|
91
|
+
})
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const tenantMeta = {
|
|
99
|
+
tenantId: "tnt_t",
|
|
100
|
+
projectId: "prj_t",
|
|
101
|
+
organizationId: "org_t",
|
|
102
|
+
tenantScript: "tenant-worker-a",
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
__resetRegistry()
|
|
107
|
+
})
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
import type { StepJournalEntry, StepRunner } from "@voyantjs/workflows/handler"
|
|
28
28
|
|
|
29
|
+
const STEP_RESPONSE_AUTH_HEADER = "x-voyant-step-response-auth"
|
|
30
|
+
|
|
29
31
|
/**
|
|
30
32
|
* Minimal subset of `DurableObjectNamespace` that the runner actually
|
|
31
33
|
* uses. Matches the shape exposed by `@cloudflare/containers`'
|
|
@@ -256,6 +258,18 @@ export function createCfContainerStepRunner(deps: CfContainerRunnerDeps): StepRu
|
|
|
256
258
|
new Error(`container returned HTTP ${response.status}: ${text}`),
|
|
257
259
|
)
|
|
258
260
|
}
|
|
261
|
+
if (deps.sign) {
|
|
262
|
+
const signature = response.headers.get(STEP_RESPONSE_AUTH_HEADER)
|
|
263
|
+
const expected = await deps.sign(text)
|
|
264
|
+
if (!signature || !constantTimeEquals(signature, expected)) {
|
|
265
|
+
return failed(
|
|
266
|
+
attempt,
|
|
267
|
+
startedAt,
|
|
268
|
+
"CONTAINER_RESPONSE_SIGNATURE_INVALID",
|
|
269
|
+
new Error("container response signature is missing or invalid"),
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
259
273
|
try {
|
|
260
274
|
const entry = JSON.parse(text) as StepJournalEntry
|
|
261
275
|
// Trust the container's own timestamps; they reflect the actual
|
|
@@ -272,6 +286,15 @@ export function createCfContainerStepRunner(deps: CfContainerRunnerDeps): StepRu
|
|
|
272
286
|
}
|
|
273
287
|
}
|
|
274
288
|
|
|
289
|
+
function constantTimeEquals(a: string, b: string): boolean {
|
|
290
|
+
const length = Math.max(a.length, b.length, 1)
|
|
291
|
+
let diff = a.length === b.length ? 0 : 1
|
|
292
|
+
for (let i = 0; i < length; i++) {
|
|
293
|
+
diff |= (a.charCodeAt(i) | 0) ^ (b.charCodeAt(i) | 0)
|
|
294
|
+
}
|
|
295
|
+
return diff === 0
|
|
296
|
+
}
|
|
297
|
+
|
|
275
298
|
function failed(attempt: number, startedAt: number, code: string, err: unknown): StepJournalEntry {
|
|
276
299
|
const e = err instanceof Error ? err : new Error(String(err))
|
|
277
300
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Mode 1 driver — Cloudflare edge composition.
|
|
2
|
+
// agent-quality: file-size exception -- Public edge driver factory currently owns manifest, trigger, event-ingest, concurrency, and admin wiring; split only with a dedicated driver-surface refactor.
|
|
2
3
|
//
|
|
3
4
|
// `createApp({ workflows: { driver: createCloudflareEdgeDriver({ ... }) } })`
|
|
4
5
|
// is the entry point for any deployment that runs the orchestrator on
|
|
@@ -91,6 +92,61 @@ const DEFAULT_TENANT_META = {
|
|
|
91
92
|
organizationId: "default",
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
function serializeWorkflowManifest(manifest: WorkflowManifest): Record<string, unknown> {
|
|
96
|
+
return { ...manifest }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function deserializeWorkflowManifest(manifest: Record<string, unknown>): WorkflowManifest {
|
|
100
|
+
const {
|
|
101
|
+
schemaVersion,
|
|
102
|
+
projectId,
|
|
103
|
+
versionId,
|
|
104
|
+
builtAt,
|
|
105
|
+
builderVersion,
|
|
106
|
+
capabilities,
|
|
107
|
+
workflows,
|
|
108
|
+
eventFilters,
|
|
109
|
+
bindings,
|
|
110
|
+
environments,
|
|
111
|
+
} = manifest
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
schemaVersion !== 1 ||
|
|
115
|
+
typeof projectId !== "string" ||
|
|
116
|
+
typeof versionId !== "string" ||
|
|
117
|
+
typeof builtAt !== "number" ||
|
|
118
|
+
typeof builderVersion !== "string" ||
|
|
119
|
+
!isStringArray(capabilities) ||
|
|
120
|
+
!Array.isArray(workflows) ||
|
|
121
|
+
!Array.isArray(eventFilters) ||
|
|
122
|
+
!isRecord(bindings) ||
|
|
123
|
+
!isRecord(environments)
|
|
124
|
+
) {
|
|
125
|
+
throw new Error("stored workflow manifest has an invalid shape")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
schemaVersion,
|
|
130
|
+
projectId,
|
|
131
|
+
versionId,
|
|
132
|
+
builtAt,
|
|
133
|
+
builderVersion,
|
|
134
|
+
capabilities,
|
|
135
|
+
workflows: workflows as WorkflowManifest["workflows"],
|
|
136
|
+
eventFilters: eventFilters as WorkflowManifest["eventFilters"],
|
|
137
|
+
bindings: bindings as WorkflowManifest["bindings"],
|
|
138
|
+
environments: environments as WorkflowManifest["environments"],
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
143
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isStringArray(value: unknown): value is string[] {
|
|
147
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
148
|
+
}
|
|
149
|
+
|
|
94
150
|
// ---- Public factory ----
|
|
95
151
|
|
|
96
152
|
/**
|
|
@@ -251,7 +307,7 @@ export function createCloudflareEdgeDriver(opts: CloudflareEdgeDriverOptions): D
|
|
|
251
307
|
return manifestStore.registerManifest({
|
|
252
308
|
environment: args.environment,
|
|
253
309
|
versionId: args.manifest.versionId,
|
|
254
|
-
manifest: args.manifest
|
|
310
|
+
manifest: serializeWorkflowManifest(args.manifest),
|
|
255
311
|
})
|
|
256
312
|
}
|
|
257
313
|
|
|
@@ -260,7 +316,7 @@ export function createCloudflareEdgeDriver(opts: CloudflareEdgeDriverOptions): D
|
|
|
260
316
|
}): Promise<WorkflowManifest | null> {
|
|
261
317
|
const envelope = await manifestStore.getCurrent(args.environment)
|
|
262
318
|
if (!envelope) return null
|
|
263
|
-
return envelope.manifest
|
|
319
|
+
return deserializeWorkflowManifest(envelope.manifest)
|
|
264
320
|
}
|
|
265
321
|
|
|
266
322
|
async function trigger<TIn, TOut>(
|
|
@@ -327,7 +383,7 @@ export function createCloudflareEdgeDriver(opts: CloudflareEdgeDriverOptions): D
|
|
|
327
383
|
message: `No manifest is registered for environment "${args.environment}".`,
|
|
328
384
|
}
|
|
329
385
|
}
|
|
330
|
-
const manifest = stored.manifest
|
|
386
|
+
const manifest = deserializeWorkflowManifest(stored.manifest)
|
|
331
387
|
const eventId = args.envelope.metadata?.eventId ?? (await deriveStableEventId(args.envelope))
|
|
332
388
|
const routed = routeEvent({
|
|
333
389
|
manifest,
|
package/src/event-handler.ts
CHANGED
|
@@ -21,6 +21,57 @@ import type { DurableObjectNamespaceLike } from "./worker.js"
|
|
|
21
21
|
|
|
22
22
|
const ALLOWED_ENVS = new Set(["production", "preview", "development"])
|
|
23
23
|
|
|
24
|
+
function deserializeWorkflowManifest(manifest: Record<string, unknown>): WorkflowManifest {
|
|
25
|
+
const {
|
|
26
|
+
schemaVersion,
|
|
27
|
+
projectId,
|
|
28
|
+
versionId,
|
|
29
|
+
builtAt,
|
|
30
|
+
builderVersion,
|
|
31
|
+
capabilities,
|
|
32
|
+
workflows,
|
|
33
|
+
eventFilters,
|
|
34
|
+
bindings,
|
|
35
|
+
environments,
|
|
36
|
+
} = manifest
|
|
37
|
+
|
|
38
|
+
if (
|
|
39
|
+
schemaVersion !== 1 ||
|
|
40
|
+
typeof projectId !== "string" ||
|
|
41
|
+
typeof versionId !== "string" ||
|
|
42
|
+
typeof builtAt !== "number" ||
|
|
43
|
+
typeof builderVersion !== "string" ||
|
|
44
|
+
!isStringArray(capabilities) ||
|
|
45
|
+
!Array.isArray(workflows) ||
|
|
46
|
+
!Array.isArray(eventFilters) ||
|
|
47
|
+
!isRecord(bindings) ||
|
|
48
|
+
!isRecord(environments)
|
|
49
|
+
) {
|
|
50
|
+
throw new Error("stored workflow manifest has an invalid shape")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
schemaVersion,
|
|
55
|
+
projectId,
|
|
56
|
+
versionId,
|
|
57
|
+
builtAt,
|
|
58
|
+
builderVersion,
|
|
59
|
+
capabilities,
|
|
60
|
+
workflows: workflows as WorkflowManifest["workflows"],
|
|
61
|
+
eventFilters: eventFilters as WorkflowManifest["eventFilters"],
|
|
62
|
+
bindings: bindings as WorkflowManifest["bindings"],
|
|
63
|
+
environments: environments as WorkflowManifest["environments"],
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
68
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isStringArray(value: unknown): value is string[] {
|
|
72
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
73
|
+
}
|
|
74
|
+
|
|
24
75
|
export interface EventHandlerDeps<Id = unknown> {
|
|
25
76
|
/** KV-backed manifest store (read-only path here). */
|
|
26
77
|
manifestStore: CfManifestStore
|
|
@@ -87,7 +138,7 @@ export async function handleIngestEvent<Id>(
|
|
|
87
138
|
message: `No manifest is registered for environment "${body.environment}".`,
|
|
88
139
|
})
|
|
89
140
|
}
|
|
90
|
-
const manifest = manifestEnvelope.manifest
|
|
141
|
+
const manifest = deserializeWorkflowManifest(manifestEnvelope.manifest)
|
|
91
142
|
|
|
92
143
|
// Event id derivation — use the caller-stamped one when present, fall
|
|
93
144
|
// back to a content-derived id so external callers without a forwarder
|
package/src/worker.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// agent-quality: file-size exception -- owner: workflows-orchestrator-cloudflare; existing module stays co-located until a dedicated split preserves behavior and tests.
|
|
1
2
|
// Public HTTP surface of the Cloudflare orchestrator. The outer
|
|
2
3
|
// Worker receives a request, resolves the run DO by id (or creates
|
|
3
4
|
// one for a new trigger), and forwards to the DO. This layer owns
|