@stigmer/runner 3.10.0 → 3.11.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/README.md +12 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/call-llm.js +9 -10
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +2 -1
- package/dist/activities/classify-tool-approvals.js +28 -2
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +32 -0
- package/dist/activities/discover-mcp-server.js +162 -27
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +8 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +28 -2
- package/dist/activities/execute-cursor/approval-state.js +7 -1
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.d.ts +14 -0
- package/dist/activities/execute-cursor/attachment-resolver.js +18 -4
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
- package/dist/activities/execute-cursor/blueprint-resolver.js +6 -22
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/env-resolver.js +3 -1
- package/dist/activities/execute-cursor/env-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/error-classifier.d.ts +40 -3
- package/dist/activities/execute-cursor/error-classifier.js +81 -3
- package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
- package/dist/activities/execute-cursor/extract-structured-output.d.ts +29 -0
- package/dist/activities/execute-cursor/extract-structured-output.js +58 -0
- package/dist/activities/execute-cursor/extract-structured-output.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +14 -3
- package/dist/activities/execute-cursor/hook-script.js +72 -10
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +5 -1
- package/dist/activities/execute-cursor/index.js +51 -57
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/mcp-resolver.d.ts +24 -1
- package/dist/activities/execute-cursor/mcp-resolver.js +5 -2
- package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +18 -4
- package/dist/activities/execute-cursor/prompt-builder.js +12 -7
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.js +4 -1
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +18 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +68 -23
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/environment.js +3 -1
- package/dist/activities/execute-deep-agent/environment.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +15 -0
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -7
- package/dist/activities/execute-deep-agent/prompt-builder.js +8 -2
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +10 -0
- package/dist/activities/execute-deep-agent/setup.js +55 -23
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +18 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +8 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +11 -4
- package/dist/activities/execute-deep-agent/subagent-wiring.js +13 -4
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +3 -1
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/activities/workflow-event-activities.d.ts +28 -10
- package/dist/activities/workflow-event-activities.js +87 -58
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +21 -1
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +9 -4
- package/dist/client/stigmer-client.js +28 -15
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/encryption/config.d.ts +32 -0
- package/dist/encryption/config.js +68 -0
- package/dist/encryption/config.js.map +1 -0
- package/dist/encryption/index.d.ts +3 -0
- package/dist/encryption/index.js +3 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/encryption/payload-codec.d.ts +41 -0
- package/dist/encryption/payload-codec.js +130 -0
- package/dist/encryption/payload-codec.js.map +1 -0
- package/dist/payload-codecs.d.ts +16 -0
- package/dist/payload-codecs.js +38 -0
- package/dist/payload-codecs.js.map +1 -0
- package/dist/preflight.d.ts +31 -0
- package/dist/preflight.js +43 -0
- package/dist/preflight.js.map +1 -1
- package/dist/runner-manager.js +5 -15
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +5 -16
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +9 -3
- package/dist/shared/approval-policy.js +15 -6
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/attachment-naming.d.ts +53 -0
- package/dist/shared/attachment-naming.js +59 -0
- package/dist/shared/attachment-naming.js.map +1 -0
- package/dist/shared/caller-identity.d.ts +23 -2
- package/dist/shared/caller-identity.js +36 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +1 -0
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/checkpointer/http-saver.d.ts +42 -1
- package/dist/shared/checkpointer/http-saver.js +96 -8
- package/dist/shared/checkpointer/http-saver.js.map +1 -1
- package/dist/shared/conversation-attachment.js +1 -0
- package/dist/shared/conversation-attachment.js.map +1 -1
- package/dist/shared/datastore-attachment.d.ts +50 -7
- package/dist/shared/datastore-attachment.js +93 -11
- package/dist/shared/datastore-attachment.js.map +1 -1
- package/dist/shared/http-retry.d.ts +43 -0
- package/dist/shared/http-retry.js +50 -0
- package/dist/shared/http-retry.js.map +1 -0
- package/dist/shared/llm-backend.d.ts +275 -0
- package/dist/shared/llm-backend.js +425 -0
- package/dist/shared/llm-backend.js.map +1 -0
- package/dist/shared/llm-proxy.d.ts +8 -0
- package/dist/shared/llm-proxy.js +15 -0
- package/dist/shared/llm-proxy.js.map +1 -1
- package/dist/shared/mcp-enabled-tools.d.ts +57 -0
- package/dist/shared/mcp-enabled-tools.js +86 -0
- package/dist/shared/mcp-enabled-tools.js.map +1 -0
- package/dist/shared/mcp-manager.d.ts +3 -1
- package/dist/shared/mcp-manager.js +17 -4
- package/dist/shared/mcp-manager.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +39 -2
- package/dist/shared/mcp-resolver.js +38 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/model-client.d.ts +12 -5
- package/dist/shared/model-client.js +138 -18
- package/dist/shared/model-client.js.map +1 -1
- package/dist/shared/model-error.js +198 -5
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +26 -0
- package/dist/shared/plan-mode-permissions.js +28 -0
- package/dist/shared/plan-mode-permissions.js.map +1 -0
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +2 -4
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +18 -0
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
- package/dist/workflows/call-agent-orchestrator.js +1 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.js +7 -0
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/engine-core.js +23 -2
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +1 -1
- package/dist/workflows/execute-from-execution.js +11 -1
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/package.json +8 -2
- package/src/__tests__/claimcheck-codec.test.ts +36 -0
- package/src/__tests__/encryption-codec.test.ts +234 -0
- package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
- package/src/__tests__/history-encryption-e2e.test.ts +243 -0
- package/src/__tests__/preflight.test.ts +50 -2
- package/src/activities/__tests__/call-llm.test.ts +75 -0
- package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +203 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
- package/src/activities/call-llm.ts +9 -16
- package/src/activities/classify-tool-approvals.ts +34 -4
- package/src/activities/discover-mcp-server.ts +190 -32
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +53 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +40 -14
- package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
- package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
- package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
- package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
- package/src/activities/execute-cursor/approval-state.ts +30 -1
- package/src/activities/execute-cursor/attachment-resolver.ts +31 -3
- package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
- package/src/activities/execute-cursor/env-resolver.ts +3 -1
- package/src/activities/execute-cursor/error-classifier.ts +91 -4
- package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
- package/src/activities/execute-cursor/hook-script.ts +74 -10
- package/src/activities/execute-cursor/index.ts +55 -71
- package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
- package/src/activities/execute-cursor/prompt-builder.ts +34 -9
- package/src/activities/execute-cursor/turn-stream.ts +5 -2
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +110 -8
- package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +173 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
- package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
- package/src/activities/execute-deep-agent/environment.ts +3 -1
- package/src/activities/execute-deep-agent/index.ts +20 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
- package/src/activities/execute-deep-agent/setup.ts +76 -28
- package/src/activities/execute-deep-agent/subagent-transformer.ts +23 -1
- package/src/activities/execute-deep-agent/subagent-wiring.ts +14 -4
- package/src/activities/hydrate-workflow-execution.ts +3 -1
- package/src/activities/workflow-event-activities.ts +96 -69
- package/src/claimcheck/payload-codec.ts +33 -1
- package/src/client/__tests__/stigmer-client.test.ts +8 -8
- package/src/client/stigmer-client.ts +32 -18
- package/src/encryption/config.ts +91 -0
- package/src/encryption/index.ts +3 -0
- package/src/encryption/payload-codec.ts +152 -0
- package/src/payload-codecs.ts +56 -0
- package/src/preflight.ts +45 -0
- package/src/runner-manager.ts +6 -24
- package/src/runner.ts +6 -25
- package/src/shared/__tests__/approval-policy.test.ts +82 -39
- package/src/shared/__tests__/attachment-naming.test.ts +159 -0
- package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
- package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
- package/src/shared/__tests__/caller-identity.test.ts +25 -0
- package/src/shared/__tests__/channel-attachment.test.ts +1 -1
- package/src/shared/__tests__/connect-backfill.test.ts +1 -0
- package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
- package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
- package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
- package/src/shared/__tests__/foundry-seam.test.ts +482 -0
- package/src/shared/__tests__/http-retry.test.ts +67 -0
- package/src/shared/__tests__/llm-backend.test.ts +616 -0
- package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
- package/src/shared/__tests__/mcp-manager.test.ts +84 -2
- package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
- package/src/shared/__tests__/model-client.test.ts +154 -0
- package/src/shared/__tests__/model-error.test.ts +289 -1
- package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
- package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
- package/src/shared/__tests__/vertex-seam.test.ts +295 -0
- package/src/shared/approval-policy.ts +14 -7
- package/src/shared/attachment-naming.ts +78 -0
- package/src/shared/caller-identity.ts +40 -5
- package/src/shared/channel-attachment.ts +1 -0
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
- package/src/shared/checkpointer/http-saver.ts +117 -9
- package/src/shared/conversation-attachment.ts +1 -0
- package/src/shared/datastore-attachment.ts +106 -11
- package/src/shared/http-retry.ts +50 -0
- package/src/shared/llm-backend.ts +544 -0
- package/src/shared/llm-proxy.ts +15 -0
- package/src/shared/mcp-enabled-tools.ts +105 -0
- package/src/shared/mcp-manager.ts +21 -4
- package/src/shared/mcp-resolver.ts +73 -2
- package/src/shared/model-client.ts +161 -19
- package/src/shared/model-error.ts +222 -4
- package/src/shared/plan-mode-permissions.ts +30 -0
- package/src/worker.ts +4 -5
- package/src/workflow-engine/types.ts +18 -0
- package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
- package/src/workflows/call-agent-orchestrator.ts +10 -0
- package/src/workflows/connect-mcp-server.ts +7 -0
- package/src/workflows/engine-core.ts +23 -2
- package/src/workflows/execute-from-execution.ts +12 -2
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Converts plain event descriptors (safe for the Temporal deterministic
|
|
5
5
|
* sandbox) into WorkflowExecutionEvent proto objects and sends them
|
|
6
|
-
* alongside a status update via gRPC.
|
|
7
|
-
*
|
|
6
|
+
* alongside a status update via gRPC. Failures propagate so the local
|
|
7
|
+
* activity's retry policy fires — safe because sequence numbers are
|
|
8
|
+
* workflow-assigned (stable across attempts) and the store skips
|
|
9
|
+
* already-persisted sequences, making retries idempotent. The workflow's
|
|
10
|
+
* emit wrappers catch after retries are exhausted; event emission never
|
|
11
|
+
* fails a run.
|
|
8
12
|
*
|
|
9
13
|
* Registered as proxyLocalActivities in engine-core.ts so the workflow
|
|
10
14
|
* sandbox can emit events at task boundaries and approval gates.
|
|
@@ -140,6 +144,17 @@ function buildClient(): StigmerClient {
|
|
|
140
144
|
});
|
|
141
145
|
}
|
|
142
146
|
|
|
147
|
+
/**
|
|
148
|
+
* LEGACY sequence assignment — only for descriptors that arrive without a
|
|
149
|
+
* workflow-assigned `sequenceNumber`, i.e. replays of histories recorded
|
|
150
|
+
* before the "workflow-assigned-event-sequences" patch (see
|
|
151
|
+
* engine-core.ts). This counter is process-global: it is shared across
|
|
152
|
+
* every execution on the worker and resets on worker restart, which is
|
|
153
|
+
* the root cause of oss#308's silent event loss. Deliberately NOT fixed —
|
|
154
|
+
* pre-patch executions keep their old (flawed) behavior for the
|
|
155
|
+
* migration window. Delete together with the patch gate once pre-patch
|
|
156
|
+
* executions have drained.
|
|
157
|
+
*/
|
|
143
158
|
let sequenceCounter = 0;
|
|
144
159
|
|
|
145
160
|
function nextSequence(): bigint {
|
|
@@ -148,33 +163,44 @@ function nextSequence(): bigint {
|
|
|
148
163
|
}
|
|
149
164
|
|
|
150
165
|
/**
|
|
151
|
-
*
|
|
166
|
+
* Return the persisted event-log high-water mark for the execution, and
|
|
167
|
+
* seed the legacy process-global counter from it.
|
|
152
168
|
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
169
|
+
* The workflow seeds its own sequence counter from the returned value
|
|
170
|
+
* (patched path); the legacy global seed remains for pre-patch replays,
|
|
171
|
+
* whose emit path still assigns sequences here in the activity.
|
|
155
172
|
*
|
|
156
|
-
* On
|
|
157
|
-
*
|
|
158
|
-
*
|
|
173
|
+
* On first run (no prior events) returns 0, so the first event gets
|
|
174
|
+
* sequence_number = 1. On recovery (events already persisted from a
|
|
175
|
+
* failed run) returns the highest persisted sequence_number, so new
|
|
176
|
+
* events continue from N+1.
|
|
159
177
|
*
|
|
160
178
|
* When executionId is empty (direct executeServerlessWorkflow without a
|
|
161
|
-
* persisted execution),
|
|
179
|
+
* persisted execution), returns 0 as a safe fallback.
|
|
180
|
+
*
|
|
181
|
+
* Returns a plain number — the value crosses back into the workflow
|
|
182
|
+
* through Temporal's JSON payload converter, which cannot carry BigInt.
|
|
162
183
|
*/
|
|
163
|
-
export async function initSequenceFromEventLog(executionId: string): Promise<
|
|
184
|
+
export async function initSequenceFromEventLog(executionId: string): Promise<number> {
|
|
164
185
|
if (!executionId) {
|
|
165
186
|
sequenceCounter = 0;
|
|
166
|
-
return;
|
|
187
|
+
return 0;
|
|
167
188
|
}
|
|
168
189
|
const client = buildClient();
|
|
169
190
|
const highWaterMark = await client.getEventLogHighWaterMark(executionId);
|
|
170
191
|
sequenceCounter = Number(highWaterMark);
|
|
192
|
+
return sequenceCounter;
|
|
171
193
|
}
|
|
172
194
|
|
|
173
195
|
/** @internal Exported for unit testing only. */
|
|
174
196
|
export function toProtoEvent(desc: WorkflowEventDescriptor): WorkflowExecutionEvent {
|
|
175
197
|
const base = create(WorkflowExecutionEventSchema, {
|
|
176
198
|
eventId: crypto.randomUUID(),
|
|
177
|
-
|
|
199
|
+
// Workflow-assigned sequence when present (stable across activity
|
|
200
|
+
// retries); legacy activity-side assignment only for pre-patch replays.
|
|
201
|
+
sequenceNumber: desc.sequenceNumber !== undefined
|
|
202
|
+
? BigInt(desc.sequenceNumber)
|
|
203
|
+
: nextSequence(),
|
|
178
204
|
occurredAt: desc.occurredAt,
|
|
179
205
|
taskName: desc.taskName ?? "",
|
|
180
206
|
});
|
|
@@ -382,6 +408,14 @@ export function toProtoEvent(desc: WorkflowEventDescriptor): WorkflowExecutionEv
|
|
|
382
408
|
return base;
|
|
383
409
|
}
|
|
384
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Emit workflow events (with the accompanying status snapshot) to the
|
|
413
|
+
* server. Errors propagate so the local activity retry policy fires;
|
|
414
|
+
* retries are idempotent because workflow-assigned sequence numbers are
|
|
415
|
+
* stable across attempts and the store skips already-persisted ones. The
|
|
416
|
+
* workflow-side emit wrappers absorb the failure after retries are
|
|
417
|
+
* exhausted, so a broken timeline write never fails the run.
|
|
418
|
+
*/
|
|
385
419
|
export async function emitWorkflowEvents(
|
|
386
420
|
executionId: string,
|
|
387
421
|
events: WorkflowEventDescriptor[],
|
|
@@ -389,64 +423,57 @@ export async function emitWorkflowEvents(
|
|
|
389
423
|
): Promise<void> {
|
|
390
424
|
if (!executionId || events.length === 0) return;
|
|
391
425
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
statusFields.error = failedEvent.error;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const input = create(WorkflowExecutionUpdateStatusInputSchema, {
|
|
439
|
-
executionId,
|
|
440
|
-
status: create(WorkflowExecutionStatusSchema, statusFields as Parameters<typeof create>[1]),
|
|
441
|
-
events: protoEvents,
|
|
442
|
-
});
|
|
443
|
-
await client.workflowExecutionCommand.updateStatus(input);
|
|
444
|
-
} catch (err) {
|
|
445
|
-
console.error(
|
|
446
|
-
`Failed to emit ${events.length} workflow event(s) for ${executionId}:`,
|
|
447
|
-
err,
|
|
448
|
-
);
|
|
426
|
+
const client = buildClient();
|
|
427
|
+
const protoEvents = events.map(toProtoEvent);
|
|
428
|
+
|
|
429
|
+
const protoTasks = (taskStatuses ?? []).map(ts =>
|
|
430
|
+
create(WorkflowTaskSchema, {
|
|
431
|
+
taskId: ts.taskId ?? "",
|
|
432
|
+
taskName: ts.taskName,
|
|
433
|
+
taskType: TASK_KIND_TO_TYPE_MAP[ts.taskKind] ?? WorkflowTaskType.WORKFLOW_TASK_TYPE_UNSPECIFIED,
|
|
434
|
+
status: TASK_STATUS_MAP[ts.status],
|
|
435
|
+
startedAt: ts.startedAt ?? "",
|
|
436
|
+
completedAt: ts.completedAt ?? "",
|
|
437
|
+
error: ts.error ?? "",
|
|
438
|
+
input: toJsonObject(ts.input),
|
|
439
|
+
output: toJsonObject(ts.output),
|
|
440
|
+
metadata: toJsonObject(ts.metadata),
|
|
441
|
+
costMicros: BigInt(ts.costMicros ?? 0),
|
|
442
|
+
inputTokens: BigInt(ts.inputTokens ?? 0),
|
|
443
|
+
outputTokens: BigInt(ts.outputTokens ?? 0),
|
|
444
|
+
uiHint: ts.uiHint ?? "",
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
const startedEvent = events.find(e => e.type === "execution_started");
|
|
449
|
+
const completedEvent = events.find(e => e.type === "execution_completed");
|
|
450
|
+
const failedEvent = events.find(e => e.type === "execution_failed");
|
|
451
|
+
|
|
452
|
+
const statusFields: Record<string, unknown> = { tasks: protoTasks };
|
|
453
|
+
|
|
454
|
+
if (startedEvent) {
|
|
455
|
+
statusFields.phase = ExecutionPhase.EXECUTION_IN_PROGRESS;
|
|
456
|
+
statusFields.startedAt = startedEvent.occurredAt;
|
|
457
|
+
}
|
|
458
|
+
if (completedEvent && completedEvent.type === "execution_completed") {
|
|
459
|
+
statusFields.phase = ExecutionPhase.EXECUTION_COMPLETED;
|
|
460
|
+
statusFields.completedAt = completedEvent.occurredAt;
|
|
461
|
+
statusFields.totalCostMicros = BigInt(completedEvent.totalCostMicros);
|
|
462
|
+
statusFields.totalInputTokens = BigInt(completedEvent.totalInputTokens ?? 0);
|
|
463
|
+
statusFields.totalOutputTokens = BigInt(completedEvent.totalOutputTokens ?? 0);
|
|
464
|
+
}
|
|
465
|
+
if (failedEvent && failedEvent.type === "execution_failed") {
|
|
466
|
+
statusFields.phase = ExecutionPhase.EXECUTION_FAILED;
|
|
467
|
+
statusFields.completedAt = failedEvent.occurredAt;
|
|
468
|
+
statusFields.error = failedEvent.error;
|
|
449
469
|
}
|
|
470
|
+
|
|
471
|
+
const input = create(WorkflowExecutionUpdateStatusInputSchema, {
|
|
472
|
+
executionId,
|
|
473
|
+
status: create(WorkflowExecutionStatusSchema, statusFields as Parameters<typeof create>[1]),
|
|
474
|
+
events: protoEvents,
|
|
475
|
+
});
|
|
476
|
+
await client.workflowExecutionCommand.updateStatus(input);
|
|
450
477
|
}
|
|
451
478
|
|
|
452
479
|
const PROTO_STATUS_TO_STRING: Record<number, string> = {
|
|
@@ -22,6 +22,14 @@ interface ClaimcheckMarker {
|
|
|
22
22
|
key: string;
|
|
23
23
|
size: number;
|
|
24
24
|
compressed: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The relocated payload's original metadata, base64-encoded per value.
|
|
27
|
+
* Without it the restored payload would carry the marker's own
|
|
28
|
+
* "binary/claimcheck" encoding and no payload converter could interpret
|
|
29
|
+
* it. Absent on markers written before this field existed; those decode
|
|
30
|
+
* with the legacy (metadata-less) behavior.
|
|
31
|
+
*/
|
|
32
|
+
metadata?: Record<string, string>;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
export class ClaimcheckPayloadCodec implements PayloadCodec {
|
|
@@ -63,6 +71,7 @@ export class ClaimcheckPayloadCodec implements PayloadCodec {
|
|
|
63
71
|
key,
|
|
64
72
|
size: data.length,
|
|
65
73
|
compressed,
|
|
74
|
+
metadata: serializeMetadata(payload.metadata),
|
|
66
75
|
};
|
|
67
76
|
|
|
68
77
|
return {
|
|
@@ -94,7 +103,9 @@ export class ClaimcheckPayloadCodec implements PayloadCodec {
|
|
|
94
103
|
const dataBuf = marker.compressed ? decompress(rawBuf) : rawBuf;
|
|
95
104
|
|
|
96
105
|
return {
|
|
97
|
-
metadata:
|
|
106
|
+
metadata: marker.metadata
|
|
107
|
+
? deserializeMetadata(marker.metadata)
|
|
108
|
+
: payload.metadata,
|
|
98
109
|
data: dataBuf,
|
|
99
110
|
};
|
|
100
111
|
}
|
|
@@ -105,3 +116,24 @@ export class ClaimcheckPayloadCodec implements PayloadCodec {
|
|
|
105
116
|
return Buffer.from(encoding).toString("utf-8") === MARKER_ENCODING_VALUE;
|
|
106
117
|
}
|
|
107
118
|
}
|
|
119
|
+
|
|
120
|
+
function serializeMetadata(
|
|
121
|
+
metadata: Payload["metadata"],
|
|
122
|
+
): Record<string, string> | undefined {
|
|
123
|
+
if (!metadata) return undefined;
|
|
124
|
+
const out: Record<string, string> = {};
|
|
125
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
126
|
+
if (value) out[key] = Buffer.from(value).toString("base64");
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function deserializeMetadata(
|
|
132
|
+
metadata: Record<string, string>,
|
|
133
|
+
): Record<string, Uint8Array> {
|
|
134
|
+
const out: Record<string, Uint8Array> = {};
|
|
135
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
136
|
+
out[key] = Buffer.from(value, "base64");
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
@@ -320,22 +320,22 @@ describe("StigmerClient", () => {
|
|
|
320
320
|
expect(spy).not.toHaveBeenCalled();
|
|
321
321
|
});
|
|
322
322
|
|
|
323
|
-
it("
|
|
323
|
+
it("throws when the server mints no token — the bootstrap credential no longer decrypts (stigmer-cloud#218)", async () => {
|
|
324
324
|
const client = clientWithRunnerCredential(fakeTokenOfType("embedded_runner"));
|
|
325
325
|
vi.spyOn(client, "getRunnerScopedToken").mockResolvedValue(undefined);
|
|
326
326
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
327
|
+
await expect(
|
|
328
|
+
client.acquireScopedRunnerToken({ agentExecutionId: "aex_1" }),
|
|
329
|
+
).rejects.toThrow(/minted no scoped runner token for agent execution aex_1/);
|
|
330
330
|
});
|
|
331
331
|
|
|
332
|
-
it("
|
|
332
|
+
it("throws when the exchange fails, naming the scope and preserving the cause", async () => {
|
|
333
333
|
const client = clientWithRunnerCredential(fakeTokenOfType("embedded_runner"));
|
|
334
334
|
vi.spyOn(client, "getRunnerScopedToken").mockRejectedValue(new Error("boom"));
|
|
335
335
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
336
|
+
await expect(
|
|
337
|
+
client.acquireScopedRunnerToken({ workflowExecutionId: "wfx_1" }),
|
|
338
|
+
).rejects.toThrow(/exchange failed for workflow execution wfx_1: boom/);
|
|
339
339
|
});
|
|
340
340
|
});
|
|
341
341
|
|
|
@@ -360,10 +360,15 @@ export class StigmerClient {
|
|
|
360
360
|
* OSS/local runner holds no runner-class credential at all (skip — the
|
|
361
361
|
* server neither mints nor redacts).
|
|
362
362
|
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
363
|
+
* A failed exchange is a hard error, not a fallback: since the #156 item-3
|
|
364
|
+
* flip (stigmer-cloud#218) the bootstrap credential no longer decrypts, so
|
|
365
|
+
* a read that "fell back" would silently receive redacted placeholders and
|
|
366
|
+
* the execution would run against junk secret values — strictly worse than
|
|
367
|
+
* failing here with the real reason. The secret-delivery call sites let
|
|
368
|
+
* this error fail the activity; opportunistic consumers that can genuinely
|
|
369
|
+
* proceed without a scoped token (attachment credential, channel
|
|
370
|
+
* discovery) catch it at the call site, where their degrade-to-empty
|
|
371
|
+
* contract lives.
|
|
367
372
|
*/
|
|
368
373
|
async acquireScopedRunnerToken(
|
|
369
374
|
scope: RunnerScopedTokenScope,
|
|
@@ -371,24 +376,33 @@ export class StigmerClient {
|
|
|
371
376
|
if (!isEmbeddedRunnerToken(this.runnerTokenRef?.current)) {
|
|
372
377
|
return undefined;
|
|
373
378
|
}
|
|
379
|
+
const scopeDescription =
|
|
380
|
+
"agentExecutionId" in scope
|
|
381
|
+
? `agent execution ${scope.agentExecutionId}`
|
|
382
|
+
: "workflowExecutionId" in scope
|
|
383
|
+
? `workflow execution ${scope.workflowExecutionId}`
|
|
384
|
+
: JSON.stringify(scope);
|
|
385
|
+
let scoped: RunnerScopedToken | undefined;
|
|
374
386
|
try {
|
|
375
|
-
|
|
376
|
-
if (!scoped) {
|
|
377
|
-
console.warn(
|
|
378
|
-
"[stigmer-client] Server minted no scoped runner token; " +
|
|
379
|
-
"falling back to the bootstrap credential for the ExecutionContext read",
|
|
380
|
-
);
|
|
381
|
-
return undefined;
|
|
382
|
-
}
|
|
383
|
-
return scoped.token;
|
|
387
|
+
scoped = await this.getRunnerScopedToken(scope);
|
|
384
388
|
} catch (err) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
+
throw new Error(
|
|
390
|
+
`Scoped runner token exchange failed for ${scopeDescription}: ` +
|
|
391
|
+
`${err instanceof Error ? err.message : String(err)}. ` +
|
|
392
|
+
"The bootstrap credential cannot read ExecutionContext secrets " +
|
|
393
|
+
"(stigmer-cloud#218), so this runner cannot serve the execution " +
|
|
394
|
+
"until the exchange succeeds.",
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (!scoped) {
|
|
398
|
+
throw new Error(
|
|
399
|
+
`Server minted no scoped runner token for ${scopeDescription}. ` +
|
|
400
|
+
"The bootstrap credential cannot read ExecutionContext secrets " +
|
|
401
|
+
"(stigmer-cloud#218), so this runner cannot serve the execution " +
|
|
402
|
+
"until the control plane mints scoped tokens.",
|
|
389
403
|
);
|
|
390
|
-
return undefined;
|
|
391
404
|
}
|
|
405
|
+
return scoped.token;
|
|
392
406
|
}
|
|
393
407
|
|
|
394
408
|
/**
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload-encryption configuration (stigmer-cloud#227).
|
|
3
|
+
*
|
|
4
|
+
* Encryption is enabled iff STIGMER_PAYLOAD_ENCRYPTION_KEY is present —
|
|
5
|
+
* the same enabled-iff-configured pattern as the claim-check codec. A
|
|
6
|
+
* malformed key fails the boot rather than silently running plaintext:
|
|
7
|
+
* an operator who set the key intended history to be encrypted.
|
|
8
|
+
*
|
|
9
|
+
* Key rotation: payloads carry the id of the key that encrypted them.
|
|
10
|
+
* During a rotation window the previous key stays readable via
|
|
11
|
+
* STIGMER_PAYLOAD_ENCRYPTION_SECONDARY_KEY(_ID) while new payloads are
|
|
12
|
+
* written under the primary key.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface EncryptionKey {
|
|
16
|
+
readonly keyId: string;
|
|
17
|
+
/** 32-byte AES-256 key. */
|
|
18
|
+
readonly key: Buffer;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PayloadEncryptionConfig {
|
|
22
|
+
/** Key used to encrypt outgoing payloads (and decrypt its own). */
|
|
23
|
+
readonly primary: EncryptionKey;
|
|
24
|
+
/** Decrypt-only key accepted during rotation windows. */
|
|
25
|
+
readonly secondary?: EncryptionKey;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const KEY_ENV = "STIGMER_PAYLOAD_ENCRYPTION_KEY";
|
|
29
|
+
const KEY_ID_ENV = "STIGMER_PAYLOAD_ENCRYPTION_KEY_ID";
|
|
30
|
+
const SECONDARY_KEY_ENV = "STIGMER_PAYLOAD_ENCRYPTION_SECONDARY_KEY";
|
|
31
|
+
const SECONDARY_KEY_ID_ENV = "STIGMER_PAYLOAD_ENCRYPTION_SECONDARY_KEY_ID";
|
|
32
|
+
|
|
33
|
+
const AES_256_KEY_BYTES = 32;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns the encryption config, or undefined when encryption is not
|
|
37
|
+
* configured (the codec is then simply not installed).
|
|
38
|
+
*
|
|
39
|
+
* @throws when a key is present but malformed, or a key id is missing —
|
|
40
|
+
* key misconfiguration must stop the boot, not degrade to plaintext.
|
|
41
|
+
*/
|
|
42
|
+
export function loadPayloadEncryptionConfig(): PayloadEncryptionConfig | undefined {
|
|
43
|
+
const rawKey = process.env[KEY_ENV];
|
|
44
|
+
if (!rawKey) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const primary: EncryptionKey = {
|
|
49
|
+
keyId: requireKeyId(KEY_ID_ENV),
|
|
50
|
+
key: parseKey(rawKey, KEY_ENV),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const rawSecondary = process.env[SECONDARY_KEY_ENV];
|
|
54
|
+
const secondary: EncryptionKey | undefined = rawSecondary
|
|
55
|
+
? {
|
|
56
|
+
keyId: requireKeyId(SECONDARY_KEY_ID_ENV),
|
|
57
|
+
key: parseKey(rawSecondary, SECONDARY_KEY_ENV),
|
|
58
|
+
}
|
|
59
|
+
: undefined;
|
|
60
|
+
|
|
61
|
+
return { primary, secondary };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function requireKeyId(envName: string): string {
|
|
65
|
+
const keyId = process.env[envName];
|
|
66
|
+
// An explicit id is required (no default): during rotation two keys
|
|
67
|
+
// coexist, and payloads must name which one encrypted them.
|
|
68
|
+
if (!keyId) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Payload encryption misconfigured: ${envName} is required when the ` +
|
|
71
|
+
`corresponding key is set`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return keyId;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseKey(rawBase64: string, envName: string): Buffer {
|
|
78
|
+
let key: Buffer;
|
|
79
|
+
try {
|
|
80
|
+
key = Buffer.from(rawBase64, "base64");
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error(`Payload encryption misconfigured: ${envName} is not valid base64`);
|
|
83
|
+
}
|
|
84
|
+
if (key.length !== AES_256_KEY_BYTES) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Payload encryption misconfigured: ${envName} must decode to ` +
|
|
87
|
+
`${AES_256_KEY_BYTES} bytes (AES-256), got ${key.length}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return key;
|
|
91
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporal PayloadCodec that encrypts payloads at rest in workflow
|
|
3
|
+
* history (stigmer-cloud#227).
|
|
4
|
+
*
|
|
5
|
+
* Why: the workflow engine runs inside the Temporal deterministic
|
|
6
|
+
* sandbox, so decrypted execution-context values cross the history
|
|
7
|
+
* boundary in many places — the hydrate activity result, the runtime
|
|
8
|
+
* env passed as an input to every per-task activity, and expression
|
|
9
|
+
* results recorded as local-activity markers. Encrypting at the payload
|
|
10
|
+
* codec layer closes the entire class with one mechanism instead of
|
|
11
|
+
* chasing each crossing.
|
|
12
|
+
*
|
|
13
|
+
* Envelope (cross-SDK contract — the Java decode-only codec in
|
|
14
|
+
* stigmer-cloud's temporal-starter must match it byte-for-byte, pinned
|
|
15
|
+
* by the conformance fixture in __tests__/fixtures/):
|
|
16
|
+
*
|
|
17
|
+
* metadata: encoding = "binary/encrypted"
|
|
18
|
+
* encryption-key-id = <key id that encrypted this payload>
|
|
19
|
+
* data: iv (12 bytes) ‖ AES-256-GCM(ciphertext ‖ tag (16 bytes))
|
|
20
|
+
*
|
|
21
|
+
* The plaintext is the serialized ORIGINAL Payload proto (metadata AND
|
|
22
|
+
* data), so decode restores the payload exactly — including its
|
|
23
|
+
* original encoding — with no side channel.
|
|
24
|
+
*
|
|
25
|
+
* Decode passes through payloads it did not encode. This is what keeps
|
|
26
|
+
* plaintext signals from the Java/Go orchestrators and pre-rollout
|
|
27
|
+
* in-flight histories working with zero migration. Everything else
|
|
28
|
+
* fails closed: unknown key id, missing key id, and ciphertext tampering
|
|
29
|
+
* all throw rather than surfacing bogus payloads.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
33
|
+
import type { Payload, PayloadCodec } from "@temporalio/common";
|
|
34
|
+
// Default-import + destructure, NOT `import { temporal } from …`:
|
|
35
|
+
// @temporalio/proto is CommonJS and its `temporal` export defeats Node's
|
|
36
|
+
// cjs-module-lexer named-export detection, so the named form loads under
|
|
37
|
+
// tsx/vitest (their interop is looser) but crashes plain `node dist/main.js`
|
|
38
|
+
// at startup with "Named export 'temporal' not found" (stigmer/stigmer#399
|
|
39
|
+
// boot regression). Pinned by scripts/verify-dist-boot.mjs in CI.
|
|
40
|
+
import proto from "@temporalio/proto";
|
|
41
|
+
import type { EncryptionKey, PayloadEncryptionConfig } from "./config.js";
|
|
42
|
+
|
|
43
|
+
const { temporal } = proto;
|
|
44
|
+
|
|
45
|
+
const ENCODING_METADATA_KEY = "encoding";
|
|
46
|
+
const ENCRYPTED_ENCODING_VALUE = "binary/encrypted";
|
|
47
|
+
const KEY_ID_METADATA_KEY = "encryption-key-id";
|
|
48
|
+
|
|
49
|
+
/** AES-GCM parameters shared with the Java implementation. */
|
|
50
|
+
const IV_BYTES = 12;
|
|
51
|
+
const AUTH_TAG_BYTES = 16;
|
|
52
|
+
|
|
53
|
+
export class EncryptionPayloadCodec implements PayloadCodec {
|
|
54
|
+
private readonly decryptKeysById: Map<string, Buffer>;
|
|
55
|
+
|
|
56
|
+
constructor(private readonly config: PayloadEncryptionConfig) {
|
|
57
|
+
this.decryptKeysById = new Map([[config.primary.keyId, config.primary.key]]);
|
|
58
|
+
if (config.secondary) {
|
|
59
|
+
this.decryptKeysById.set(config.secondary.keyId, config.secondary.key);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async encode(payloads: Payload[]): Promise<Payload[]> {
|
|
64
|
+
return payloads.map((p) => this.encodePayload(p));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async decode(payloads: Payload[]): Promise<Payload[]> {
|
|
68
|
+
return payloads.map((p) => this.decodePayload(p));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private encodePayload(payload: Payload): Payload {
|
|
72
|
+
// Data-less payloads (binary/null from void results) stay as-is:
|
|
73
|
+
// there is nothing to protect, and the cross-language parents that
|
|
74
|
+
// await our workflows as void must be able to read them without a key.
|
|
75
|
+
if (!payload.data || payload.data.length === 0) {
|
|
76
|
+
return payload;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const plaintext = temporal.api.common.v1.Payload.encode(payload).finish();
|
|
80
|
+
return {
|
|
81
|
+
metadata: {
|
|
82
|
+
[ENCODING_METADATA_KEY]: Buffer.from(ENCRYPTED_ENCODING_VALUE),
|
|
83
|
+
[KEY_ID_METADATA_KEY]: Buffer.from(this.config.primary.keyId),
|
|
84
|
+
},
|
|
85
|
+
data: encrypt(plaintext, this.config.primary),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private decodePayload(payload: Payload): Payload {
|
|
90
|
+
if (!isEncryptedPayload(payload)) {
|
|
91
|
+
return payload;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const keyIdBytes = payload.metadata?.[KEY_ID_METADATA_KEY];
|
|
95
|
+
if (!keyIdBytes) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"Encrypted payload is missing its encryption-key-id metadata — refusing to decode",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const keyId = Buffer.from(keyIdBytes).toString("utf-8");
|
|
101
|
+
const key = this.decryptKeysById.get(keyId);
|
|
102
|
+
if (!key) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Encrypted payload uses unknown key id '${keyId}' — configure it as the ` +
|
|
105
|
+
`primary or secondary payload encryption key (rotation window?)`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const plaintext = decrypt(payload.data!, key, keyId);
|
|
110
|
+
return temporal.api.common.v1.Payload.decode(plaintext);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isEncryptedPayload(payload: Payload): boolean {
|
|
115
|
+
const encoding = payload.metadata?.[ENCODING_METADATA_KEY];
|
|
116
|
+
if (!encoding) return false;
|
|
117
|
+
return Buffer.from(encoding).toString("utf-8") === ENCRYPTED_ENCODING_VALUE;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function encrypt(plaintext: Uint8Array, key: EncryptionKey): Buffer {
|
|
121
|
+
const iv = randomBytes(IV_BYTES);
|
|
122
|
+
const cipher = createCipheriv("aes-256-gcm", key.key, iv);
|
|
123
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
124
|
+
// Layout iv ‖ ciphertext ‖ tag matches Java's AES/GCM/NoPadding, whose
|
|
125
|
+
// doFinal() output is ciphertext ‖ tag.
|
|
126
|
+
return Buffer.concat([iv, ciphertext, cipher.getAuthTag()]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function decrypt(data: Uint8Array, key: Buffer, keyId: string): Buffer {
|
|
130
|
+
if (data.length < IV_BYTES + AUTH_TAG_BYTES) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Encrypted payload under key id '${keyId}' is truncated (${data.length} bytes)`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const buf = Buffer.from(data);
|
|
136
|
+
const iv = buf.subarray(0, IV_BYTES);
|
|
137
|
+
const ciphertext = buf.subarray(IV_BYTES, buf.length - AUTH_TAG_BYTES);
|
|
138
|
+
const tag = buf.subarray(buf.length - AUTH_TAG_BYTES);
|
|
139
|
+
|
|
140
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
141
|
+
decipher.setAuthTag(tag);
|
|
142
|
+
try {
|
|
143
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
144
|
+
} catch {
|
|
145
|
+
// GCM auth failure: tampered ciphertext or a key that does not match
|
|
146
|
+
// its advertised id. Never surface partially decrypted bytes.
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Failed to decrypt payload under key id '${keyId}' — ciphertext is ` +
|
|
149
|
+
`corrupt or the configured key does not match`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|