@emilia-protocol/gate 0.14.0 → 0.15.1

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +60 -1
  2. package/README.md +32 -1
  3. package/aeb-consumption-store.js +3 -0
  4. package/dist/aeb-consumption-store.d.ts +88 -0
  5. package/dist/aeb-consumption-store.d.ts.map +1 -0
  6. package/dist/aeb-consumption-store.js +471 -0
  7. package/dist/aeb-consumption-store.js.map +1 -0
  8. package/dist/capability-receipt.d.ts +8 -0
  9. package/dist/capability-receipt.d.ts.map +1 -1
  10. package/dist/capability-receipt.js +17 -0
  11. package/dist/capability-receipt.js.map +1 -1
  12. package/dist/index.d.ts +13 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +16 -4
  15. package/dist/index.js.map +1 -1
  16. package/dist/proposal-to-effect-postgres.d.ts +144 -0
  17. package/dist/proposal-to-effect-postgres.d.ts.map +1 -0
  18. package/dist/proposal-to-effect-postgres.js +1578 -0
  19. package/dist/proposal-to-effect-postgres.js.map +1 -0
  20. package/dist/proposal-to-effect-status-head-store.d.ts +72 -0
  21. package/dist/proposal-to-effect-status-head-store.d.ts.map +1 -0
  22. package/dist/proposal-to-effect-status-head-store.js +355 -0
  23. package/dist/proposal-to-effect-status-head-store.js.map +1 -0
  24. package/dist/proposal-to-effect-status.d.ts +55 -0
  25. package/dist/proposal-to-effect-status.d.ts.map +1 -0
  26. package/dist/proposal-to-effect-status.js +294 -0
  27. package/dist/proposal-to-effect-status.js.map +1 -0
  28. package/dist/proposal-to-effect.d.ts +151 -3
  29. package/dist/proposal-to-effect.d.ts.map +1 -1
  30. package/dist/proposal-to-effect.js +589 -46
  31. package/dist/proposal-to-effect.js.map +1 -1
  32. package/dist/remedy-case-set-postgres.d.ts +73 -0
  33. package/dist/remedy-case-set-postgres.d.ts.map +1 -0
  34. package/dist/remedy-case-set-postgres.js +846 -0
  35. package/dist/remedy-case-set-postgres.js.map +1 -0
  36. package/dist/remedy-case-set.d.ts +53 -0
  37. package/dist/remedy-case-set.d.ts.map +1 -0
  38. package/dist/remedy-case-set.js +571 -0
  39. package/dist/remedy-case-set.js.map +1 -0
  40. package/dist/remedy-program-adapters.d.ts +137 -0
  41. package/dist/remedy-program-adapters.d.ts.map +1 -0
  42. package/dist/remedy-program-adapters.js +590 -0
  43. package/dist/remedy-program-adapters.js.map +1 -0
  44. package/dist/trust-program-revocation.js.map +1 -1
  45. package/package.json +283 -63
  46. package/proposal-to-effect-postgres.js +3 -0
  47. package/proposal-to-effect-status-head-store.js +2 -0
  48. package/proposal-to-effect-status.js +2 -0
  49. package/remedy-case-set-postgres.js +3 -0
  50. package/remedy-case-set.js +3 -0
  51. package/remedy-program-adapters.js +3 -0
  52. package/src/aeb-consumption-store.ts +568 -0
  53. package/src/capability-receipt.ts +17 -0
  54. package/src/index.ts +66 -3
  55. package/src/proposal-to-effect-postgres.ts +1833 -0
  56. package/src/proposal-to-effect-status-head-store.ts +449 -0
  57. package/src/proposal-to-effect-status.ts +388 -0
  58. package/src/proposal-to-effect.ts +755 -50
  59. package/src/remedy-case-set-postgres.ts +1008 -0
  60. package/src/remedy-case-set.ts +603 -0
  61. package/src/remedy-program-adapters.ts +657 -0
  62. package/src/trust-program-revocation.ts +1 -1
@@ -0,0 +1,603 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Durable coordinator for heterogeneous Remedy Program case sets.
4
+ *
5
+ * A case set is an immutable tenant-scoped manifest. Every leg names one exact
6
+ * child Remedy Program case and exact remedy bindings. The coordinator can
7
+ * become terminal only after the repository's signed child-receipt verifier
8
+ * accepts every leg against its full child state snapshot.
9
+ */
10
+ import crypto from 'node:crypto';
11
+
12
+ import { canonicalize } from '../execution-binding.js';
13
+ import { verifyRemedyProgramReceipt } from './remedy-program-receipt.js';
14
+
15
+ export const REMEDY_CASE_SET_VERSION = 'EP-GATE-REMEDY-CASE-SET-v1';
16
+
17
+ type DataRecord = Record<string, any>;
18
+ type Failure = Readonly<{ ok: false; reason: string }>;
19
+
20
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
21
+ const CAID = /^caid:1:[a-z][a-z0-9.-]*\.[1-9][0-9]*:jcs-sha256:[A-Za-z0-9_-]{43}$/;
22
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:/#@+-]{0,255}$/;
23
+ const CREATE_KEYS = new Set(['tenantId', 'caseSetId', 'ownerToken', 'legs']);
24
+ const RECORD_KEYS = new Set([
25
+ 'tenantId', 'caseSetId', 'ownerToken', 'expectedRevision', 'children',
26
+ ]);
27
+ const STATUS_KEYS = new Set(['tenantId', 'caseSetId']);
28
+ const LEG_KEYS = new Set([
29
+ 'leg_id', 'child_instance_id', 'remedy_profile_digest',
30
+ 'destination_binding_digest', 'max_remedy_units', 'unit', 'original', 'remedy',
31
+ ]);
32
+ const ORIGINAL_KEYS = new Set([
33
+ 'caid', 'action_digest', 'operation_id', 'consequence_mode',
34
+ 'consequence_digest', 'terminal_evidence_digest', 'outcome', 'occurred_at',
35
+ ]);
36
+ const REMEDY_KEYS = new Set([
37
+ 'operation_id', 'caid', 'action_digest', 'owner_mode', 'owner_digest',
38
+ ]);
39
+ const MANIFEST_KEYS = new Set(['version', 'tenant_id', 'case_set_id', 'legs']);
40
+ const CHILD_INPUT_KEYS = new Set(['legId', 'state', 'receipt']);
41
+ const OBSERVATION_KEYS = new Set([
42
+ 'leg_id', 'status', 'case_revision', 'receipt_content_digest',
43
+ 'state_snapshot_digest',
44
+ ]);
45
+ const STATE_KEYS = new Set([
46
+ 'version', 'tenant_id', 'case_set_id', 'status', 'revision', 'created_at',
47
+ 'updated_at', 'owner_token_digest', 'manifest', 'manifest_digest',
48
+ 'observations', 'create_request_digest', 'last_request_digest',
49
+ ]);
50
+ const ISSUER_KEYS = new Set(['issuer', 'tenant', 'environment', 'audience', 'key_id']);
51
+
52
+ export interface RemedyCaseSetState extends Record<string, unknown> {
53
+ version: string;
54
+ tenant_id: string;
55
+ case_set_id: string;
56
+ status: 'open' | 'indeterminate' | 'completed';
57
+ revision: number;
58
+ }
59
+
60
+ export interface RemedyCaseSetResult extends Record<string, unknown> {
61
+ ok: boolean;
62
+ reason?: string;
63
+ state?: RemedyCaseSetState;
64
+ }
65
+
66
+ export interface RemedyCaseSetStore {
67
+ readonly durable: boolean;
68
+ create(state: RemedyCaseSetState): unknown | Promise<unknown>;
69
+ get(input: Readonly<{ tenantId: string; caseSetId: string }>): unknown | Promise<unknown>;
70
+ compareAndSwap(input: Readonly<{
71
+ tenantId: string;
72
+ caseSetId: string;
73
+ expectedRevision: number;
74
+ ownerTokenDigest: string;
75
+ state: RemedyCaseSetState;
76
+ }>): unknown | Promise<unknown>;
77
+ }
78
+
79
+ export interface RemedyCaseSetCoordinatorOptions {
80
+ store: RemedyCaseSetStore;
81
+ tenantId: string;
82
+ trustedReceiptKeys: Record<string, string>;
83
+ expectedReceiptIssuer: {
84
+ issuer: string;
85
+ tenant: string;
86
+ environment: string;
87
+ audience: string;
88
+ key_id: string;
89
+ };
90
+ now?: () => number;
91
+ }
92
+
93
+ function isRecord(value: unknown): value is DataRecord {
94
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
95
+ const prototype = Object.getPrototypeOf(value);
96
+ return prototype === Object.prototype || prototype === null;
97
+ }
98
+
99
+ function isDataRecord(value: unknown): value is DataRecord {
100
+ if (!isRecord(value)) return false;
101
+ return Reflect.ownKeys(value).every((key) => {
102
+ if (typeof key !== 'string') return false;
103
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
104
+ return descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
105
+ });
106
+ }
107
+
108
+ function exactKeys(value: unknown, keys: Set<string>): value is DataRecord {
109
+ return isDataRecord(value)
110
+ && Object.keys(value).length === keys.size
111
+ && Object.keys(value).every((key) => keys.has(key));
112
+ }
113
+
114
+ function validId(value: unknown): value is string {
115
+ return typeof value === 'string' && ID.test(value);
116
+ }
117
+
118
+ function validContext(value: unknown): value is string {
119
+ return typeof value === 'string' && value.length > 0
120
+ && Buffer.byteLength(value, 'utf8') <= 512
121
+ && !/[\u0000-\u001f\u007f]/.test(value);
122
+ }
123
+
124
+ function instant(value: unknown): number {
125
+ if (typeof value !== 'string'
126
+ || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) return NaN;
127
+ const parsed = Date.parse(value);
128
+ return Number.isFinite(parsed) && new Date(parsed).toISOString() === value ? parsed : NaN;
129
+ }
130
+
131
+ function canonicalCopy<T>(value: T): T {
132
+ return JSON.parse(canonicalize(value));
133
+ }
134
+
135
+ function canonicalDigest(value: unknown): string {
136
+ return `sha256:${crypto.createHash('sha256').update(canonicalize(value)).digest('hex')}`;
137
+ }
138
+
139
+ function same(left: unknown, right: unknown): boolean {
140
+ try {
141
+ return canonicalize(left) === canonicalize(right);
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
146
+
147
+ function deepFreeze<T>(value: T): Readonly<T> {
148
+ if (value === null || typeof value !== 'object' || Object.isFrozen(value)) return value;
149
+ Object.freeze(value);
150
+ for (const child of Object.values(value as DataRecord)) deepFreeze(child);
151
+ return value;
152
+ }
153
+
154
+ function fail(reason: string): Failure {
155
+ return Object.freeze({ ok: false, reason });
156
+ }
157
+
158
+ function validOriginal(value: unknown): value is DataRecord {
159
+ return exactKeys(value, ORIGINAL_KEYS)
160
+ && typeof value.caid === 'string' && CAID.test(value.caid)
161
+ && typeof value.action_digest === 'string' && DIGEST.test(value.action_digest)
162
+ && validId(value.operation_id)
163
+ && ['receipt-program', 'action-escrow'].includes(value.consequence_mode)
164
+ && typeof value.consequence_digest === 'string' && DIGEST.test(value.consequence_digest)
165
+ && typeof value.terminal_evidence_digest === 'string' && DIGEST.test(value.terminal_evidence_digest)
166
+ && ['executed', 'indeterminate'].includes(value.outcome)
167
+ && Number.isFinite(instant(value.occurred_at));
168
+ }
169
+
170
+ function validRemedy(value: unknown): value is DataRecord {
171
+ return exactKeys(value, REMEDY_KEYS)
172
+ && validId(value.operation_id)
173
+ && typeof value.caid === 'string' && CAID.test(value.caid)
174
+ && typeof value.action_digest === 'string' && DIGEST.test(value.action_digest)
175
+ && ['receipt-program', 'action-escrow'].includes(value.owner_mode)
176
+ && typeof value.owner_digest === 'string' && DIGEST.test(value.owner_digest);
177
+ }
178
+
179
+ function validLeg(value: unknown): value is DataRecord {
180
+ return exactKeys(value, LEG_KEYS)
181
+ && validId(value.leg_id)
182
+ && validId(value.child_instance_id)
183
+ && typeof value.remedy_profile_digest === 'string' && DIGEST.test(value.remedy_profile_digest)
184
+ && typeof value.destination_binding_digest === 'string'
185
+ && DIGEST.test(value.destination_binding_digest)
186
+ && Number.isSafeInteger(value.max_remedy_units) && value.max_remedy_units > 0
187
+ && validContext(value.unit)
188
+ && validOriginal(value.original)
189
+ && validRemedy(value.remedy)
190
+ && value.remedy.operation_id !== value.original.operation_id
191
+ && value.remedy.action_digest !== value.original.action_digest;
192
+ }
193
+
194
+ function validManifest(value: unknown, tenantId?: string, caseSetId?: string): value is DataRecord {
195
+ if (!exactKeys(value, MANIFEST_KEYS)
196
+ || value.version !== REMEDY_CASE_SET_VERSION
197
+ || !validContext(value.tenant_id)
198
+ || (tenantId !== undefined && value.tenant_id !== tenantId)
199
+ || !validId(value.case_set_id)
200
+ || (caseSetId !== undefined && value.case_set_id !== caseSetId)
201
+ || !Array.isArray(value.legs) || value.legs.length === 0 || value.legs.length > 256
202
+ || !value.legs.every(validLeg)) return false;
203
+ const unique = (selector: (leg: DataRecord) => string) => (
204
+ new Set(value.legs.map(selector)).size === value.legs.length
205
+ );
206
+ return unique((leg) => leg.leg_id)
207
+ && unique((leg) => leg.child_instance_id)
208
+ && unique((leg) => leg.remedy.operation_id)
209
+ && unique((leg) => leg.remedy.action_digest)
210
+ && unique((leg) => leg.remedy.caid);
211
+ }
212
+
213
+ function validObservation(value: unknown): value is DataRecord {
214
+ if (!exactKeys(value, OBSERVATION_KEYS) || !validId(value.leg_id)
215
+ || !['pending', 'indeterminate', 'executed'].includes(value.status)) return false;
216
+ const pending = value.status === 'pending';
217
+ return pending
218
+ ? value.case_revision === null
219
+ && value.receipt_content_digest === null
220
+ && value.state_snapshot_digest === null
221
+ : Number.isSafeInteger(value.case_revision) && value.case_revision >= 0
222
+ && typeof value.receipt_content_digest === 'string'
223
+ && DIGEST.test(value.receipt_content_digest)
224
+ && typeof value.state_snapshot_digest === 'string'
225
+ && DIGEST.test(value.state_snapshot_digest);
226
+ }
227
+
228
+ function validState(value: unknown, tenantId?: string, caseSetId?: string): value is DataRecord {
229
+ if (!exactKeys(value, STATE_KEYS)
230
+ || value.version !== REMEDY_CASE_SET_VERSION
231
+ || !validContext(value.tenant_id)
232
+ || (tenantId !== undefined && value.tenant_id !== tenantId)
233
+ || !validId(value.case_set_id)
234
+ || (caseSetId !== undefined && value.case_set_id !== caseSetId)
235
+ || !['open', 'indeterminate', 'completed'].includes(value.status)
236
+ || !Number.isSafeInteger(value.revision) || value.revision < 0
237
+ || !Number.isFinite(instant(value.created_at))
238
+ || !Number.isFinite(instant(value.updated_at))
239
+ || instant(value.updated_at) < instant(value.created_at)
240
+ || typeof value.owner_token_digest !== 'string' || !DIGEST.test(value.owner_token_digest)
241
+ || !validManifest(value.manifest, value.tenant_id, value.case_set_id)
242
+ || typeof value.manifest_digest !== 'string'
243
+ || value.manifest_digest !== canonicalDigest(value.manifest)
244
+ || !Array.isArray(value.observations)
245
+ || value.observations.length !== value.manifest.legs.length
246
+ || !value.observations.every(validObservation)
247
+ || typeof value.create_request_digest !== 'string' || !DIGEST.test(value.create_request_digest)
248
+ || (value.last_request_digest !== null
249
+ && (typeof value.last_request_digest !== 'string' || !DIGEST.test(value.last_request_digest)))) {
250
+ return false;
251
+ }
252
+ const legIds = value.manifest.legs.map((leg: DataRecord) => leg.leg_id);
253
+ if (value.observations.some((entry: DataRecord, index: number) => entry.leg_id !== legIds[index])) {
254
+ return false;
255
+ }
256
+ const statuses = value.observations.map((entry: DataRecord) => entry.status);
257
+ if (value.status === 'open') return statuses.every((status: string) => status === 'pending');
258
+ if (value.status === 'completed') return statuses.every((status: string) => status === 'executed');
259
+ return statuses.includes('indeterminate') && statuses.every((status: string) => status !== 'pending');
260
+ }
261
+
262
+ function validIssuer(value: unknown): value is DataRecord {
263
+ return exactKeys(value, ISSUER_KEYS) && Object.values(value).every(validContext);
264
+ }
265
+
266
+ function publicKey(value: unknown): crypto.KeyObject | null {
267
+ try {
268
+ if (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/.test(value)) return null;
269
+ const bytes = Buffer.from(value, 'base64url');
270
+ if (bytes.toString('base64url') !== value) return null;
271
+ const key = crypto.createPublicKey({ key: bytes, format: 'der', type: 'spki' });
272
+ return key.asymmetricKeyType === 'ed25519' ? key : null;
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
277
+
278
+ function ownerDigest(value: string): string {
279
+ return canonicalDigest({ owner_token: value });
280
+ }
281
+
282
+ function originalWithoutEvidence(value: DataRecord): DataRecord {
283
+ const result = canonicalCopy(value);
284
+ delete result.evidence_digest;
285
+ return result;
286
+ }
287
+
288
+ /** Build a durable, ownership-fenced coordinator for one pinned tenant. */
289
+ export function createRemedyCaseSetCoordinator(options: RemedyCaseSetCoordinatorOptions) {
290
+ if (!isDataRecord(options)
291
+ || !options.store || options.store.durable !== true
292
+ || typeof options.store.create !== 'function'
293
+ || typeof options.store.get !== 'function'
294
+ || typeof options.store.compareAndSwap !== 'function'
295
+ || !validContext(options.tenantId)
296
+ || !validIssuer(options.expectedReceiptIssuer)
297
+ || options.expectedReceiptIssuer.tenant !== options.tenantId
298
+ || !isDataRecord(options.trustedReceiptKeys)
299
+ || Object.keys(options.trustedReceiptKeys).length === 0
300
+ || !Object.entries(options.trustedReceiptKeys).every(
301
+ ([keyId, key]) => validId(keyId) && publicKey(key) !== null,
302
+ )
303
+ || !Object.hasOwn(options.trustedReceiptKeys, options.expectedReceiptIssuer.key_id)
304
+ || (options.now !== undefined && typeof options.now !== 'function')) {
305
+ throw new TypeError('durable Remedy case-set coordinator configuration invalid');
306
+ }
307
+ const sourceStore = options.store;
308
+ const store = Object.freeze({
309
+ create: sourceStore.create.bind(sourceStore),
310
+ get: sourceStore.get.bind(sourceStore),
311
+ compareAndSwap: sourceStore.compareAndSwap.bind(sourceStore),
312
+ });
313
+ const tenantId = options.tenantId;
314
+ const trustedReceiptKeys = deepFreeze(canonicalCopy(options.trustedReceiptKeys));
315
+ const expectedReceiptIssuer = deepFreeze(canonicalCopy(options.expectedReceiptIssuer));
316
+ const now = options.now ?? Date.now;
317
+
318
+ const clock = (): number => {
319
+ try {
320
+ const value = now();
321
+ return Number.isSafeInteger(value) && value >= 0 ? value : NaN;
322
+ } catch {
323
+ return NaN;
324
+ }
325
+ };
326
+
327
+ const checkedState = (value: unknown, caseSetId: string): RemedyCaseSetResult => {
328
+ try {
329
+ const state = canonicalCopy(value);
330
+ return validState(state, tenantId, caseSetId)
331
+ ? { ok: true, state: deepFreeze(state) as RemedyCaseSetState }
332
+ : fail('store_state_invalid');
333
+ } catch {
334
+ return fail('store_state_invalid');
335
+ }
336
+ };
337
+
338
+ const load = async (caseSetId: string): Promise<RemedyCaseSetResult> => {
339
+ try {
340
+ const result: any = await store.get({ tenantId, caseSetId });
341
+ if (!isDataRecord(result)) return fail('store_response_invalid');
342
+ if (result.ok === true) return checkedState(result.state, caseSetId);
343
+ if (result.reason === 'case_set_not_found') return fail('case_set_not_found');
344
+ return fail('store_operation_failed');
345
+ } catch {
346
+ return fail('store_unavailable');
347
+ }
348
+ };
349
+
350
+ async function create(input: unknown): Promise<RemedyCaseSetResult> {
351
+ let value: DataRecord;
352
+ try {
353
+ value = canonicalCopy(input) as DataRecord;
354
+ } catch {
355
+ return fail('case_set_create_input_invalid');
356
+ }
357
+ if (!exactKeys(value, CREATE_KEYS)
358
+ || value.tenantId !== tenantId
359
+ || !validId(value.caseSetId)
360
+ || !validContext(value.ownerToken) || Buffer.byteLength(value.ownerToken, 'utf8') > 256
361
+ || !Array.isArray(value.legs)) return fail('case_set_create_input_invalid');
362
+ const manifest = {
363
+ version: REMEDY_CASE_SET_VERSION,
364
+ tenant_id: tenantId,
365
+ case_set_id: value.caseSetId,
366
+ legs: value.legs,
367
+ };
368
+ if (!validManifest(manifest, tenantId, value.caseSetId)) {
369
+ return fail('case_set_manifest_invalid');
370
+ }
371
+ const requestDigest = canonicalDigest(value);
372
+ const existing = await load(value.caseSetId);
373
+ if (existing.ok) {
374
+ return (existing.state as DataRecord).create_request_digest === requestDigest
375
+ ? { ok: true, idempotent: true, state: existing.state }
376
+ : fail('case_set_exists');
377
+ }
378
+ if (existing.reason !== 'case_set_not_found') return existing;
379
+ const at = clock();
380
+ if (!Number.isFinite(at)) return fail('clock_invalid');
381
+ const timestamp = new Date(at).toISOString();
382
+ const state: DataRecord = {
383
+ version: REMEDY_CASE_SET_VERSION,
384
+ tenant_id: tenantId,
385
+ case_set_id: value.caseSetId,
386
+ status: 'open',
387
+ revision: 0,
388
+ created_at: timestamp,
389
+ updated_at: timestamp,
390
+ owner_token_digest: ownerDigest(value.ownerToken),
391
+ manifest: canonicalCopy(manifest),
392
+ manifest_digest: canonicalDigest(manifest),
393
+ observations: manifest.legs.map((entry: DataRecord) => ({
394
+ leg_id: entry.leg_id,
395
+ status: 'pending',
396
+ case_revision: null,
397
+ receipt_content_digest: null,
398
+ state_snapshot_digest: null,
399
+ })),
400
+ create_request_digest: requestDigest,
401
+ last_request_digest: null,
402
+ };
403
+ if (!validState(state, tenantId, value.caseSetId)) return fail('case_set_manifest_invalid');
404
+ try {
405
+ const stored: any = await store.create(deepFreeze(canonicalCopy(state)) as RemedyCaseSetState);
406
+ if (!isDataRecord(stored)) return fail('store_response_invalid');
407
+ if (stored.ok === true) return checkedState(stored.state ?? state, value.caseSetId);
408
+ if (stored.reason !== 'case_set_exists') return fail('store_operation_failed');
409
+ const raced = await load(value.caseSetId);
410
+ return raced.ok && (raced.state as DataRecord).create_request_digest === requestDigest
411
+ ? { ok: true, idempotent: true, state: raced.state }
412
+ : fail('case_set_exists');
413
+ } catch {
414
+ return fail('store_unavailable');
415
+ }
416
+ }
417
+
418
+ function verifyChild(
419
+ child: DataRecord,
420
+ leg: DataRecord,
421
+ previous: DataRecord,
422
+ ): DataRecord | Failure {
423
+ try {
424
+ const state = canonicalCopy(child.state);
425
+ if (!isDataRecord(state)
426
+ || state.tenant_id !== tenantId
427
+ || state.instance_id !== leg.child_instance_id
428
+ || state.remedy_profile_digest !== leg.remedy_profile_digest
429
+ || state.destination_binding_digest !== leg.destination_binding_digest
430
+ || state.max_remedy_units !== leg.max_remedy_units
431
+ || state.unit !== leg.unit
432
+ || !same(originalWithoutEvidence(state.original), leg.original)) {
433
+ return fail('child_receipt_binding_mismatch');
434
+ }
435
+ const attempts = [
436
+ ...(state.active_remedy === null ? [] : [state.active_remedy]),
437
+ ...(Array.isArray(state.remedies) ? state.remedies : []),
438
+ ];
439
+ if (attempts.length !== 1) return fail('child_receipt_binding_mismatch');
440
+ const attempt = attempts[0];
441
+ if (!isDataRecord(attempt)
442
+ || attempt.remedy_operation_id !== leg.remedy.operation_id
443
+ || attempt.remedy_caid !== leg.remedy.caid
444
+ || attempt.remedy_action_digest !== leg.remedy.action_digest
445
+ || attempt.destination_binding_digest !== leg.destination_binding_digest
446
+ || attempt.units !== leg.max_remedy_units
447
+ || attempt.unit !== leg.unit) return fail('child_receipt_binding_mismatch');
448
+ const expected = {
449
+ original_operation_id: leg.original.operation_id,
450
+ original_action_digest: leg.original.action_digest,
451
+ original_terminal_evidence_digest: leg.original.terminal_evidence_digest,
452
+ case_instance_id: leg.child_instance_id,
453
+ case_revision: state.revision,
454
+ case_status: state.status,
455
+ remedy_operation_id: leg.remedy.operation_id,
456
+ remedy_action_digest: leg.remedy.action_digest,
457
+ remedy_caid: leg.remedy.caid,
458
+ destination_binding_digest: leg.destination_binding_digest,
459
+ units: leg.max_remedy_units,
460
+ unit: leg.unit,
461
+ owner_mode: leg.remedy.owner_mode,
462
+ owner_digest: leg.remedy.owner_digest,
463
+ };
464
+ const verified = verifyRemedyProgramReceipt(child.receipt, {
465
+ trustedKeys: trustedReceiptKeys,
466
+ expectedIssuer: expectedReceiptIssuer,
467
+ state,
468
+ expected,
469
+ });
470
+ if (verified.valid !== true || !isDataRecord(verified.payload)) {
471
+ return fail(verified.reason === 'receipt_expected_binding_mismatch'
472
+ || verified.reason === 'receipt_state_snapshot_mismatch'
473
+ ? 'child_receipt_binding_mismatch' : 'child_receipt_invalid');
474
+ }
475
+ const remedy = verified.payload.remedy;
476
+ const caseRecord = verified.payload.case;
477
+ let status: 'executed' | 'indeterminate';
478
+ if (remedy.status === 'executed' && remedy.outcome === 'executed'
479
+ && state.status === 'remedied' && state.active_remedy === null
480
+ && state.remedied_units === leg.max_remedy_units && state.remaining_units === 0) {
481
+ status = 'executed';
482
+ } else if (remedy.status === 'indeterminate' && remedy.outcome === 'indeterminate'
483
+ && state.status === 'remedy_indeterminate' && state.active_remedy !== null) {
484
+ status = 'indeterminate';
485
+ } else {
486
+ return fail('child_not_terminal_or_indeterminate');
487
+ }
488
+ const observation = {
489
+ leg_id: leg.leg_id,
490
+ status,
491
+ case_revision: state.revision,
492
+ receipt_content_digest: verified.content_digest,
493
+ state_snapshot_digest: caseRecord.state_snapshot_digest,
494
+ };
495
+ if (previous.status === 'executed' && !same(previous, observation)) {
496
+ return fail('completed_child_substitution');
497
+ }
498
+ if (previous.status !== 'pending') {
499
+ if (observation.case_revision < previous.case_revision) return fail('child_revision_regression');
500
+ if (observation.case_revision === previous.case_revision && !same(previous, observation)) {
501
+ return fail('child_revision_conflict');
502
+ }
503
+ }
504
+ return observation;
505
+ } catch {
506
+ return fail('child_receipt_invalid');
507
+ }
508
+ }
509
+
510
+ async function recordChildren(input: unknown): Promise<RemedyCaseSetResult> {
511
+ let value: DataRecord;
512
+ try {
513
+ value = canonicalCopy(input) as DataRecord;
514
+ } catch {
515
+ return fail('case_set_children_input_invalid');
516
+ }
517
+ if (!exactKeys(value, RECORD_KEYS)
518
+ || value.tenantId !== tenantId
519
+ || !validId(value.caseSetId)
520
+ || !validContext(value.ownerToken) || Buffer.byteLength(value.ownerToken, 'utf8') > 256
521
+ || !Number.isSafeInteger(value.expectedRevision) || value.expectedRevision < 0
522
+ || !Array.isArray(value.children)) return fail('case_set_children_input_invalid');
523
+ const loaded = await load(value.caseSetId);
524
+ if (!loaded.ok) return loaded;
525
+ const state = loaded.state as DataRecord;
526
+ const requestDigest = canonicalDigest(value);
527
+ if (state.last_request_digest === requestDigest) {
528
+ return { ok: true, idempotent: true, state: loaded.state };
529
+ }
530
+ if (state.status === 'completed') return fail('case_set_terminal');
531
+ if (state.owner_token_digest !== ownerDigest(value.ownerToken)) return fail('ownership_conflict');
532
+ if (state.revision !== value.expectedRevision) return fail('state_transition_conflict');
533
+ if (value.children.length !== state.manifest.legs.length) return fail('case_set_incomplete');
534
+ const byLeg = new Map<string, DataRecord>();
535
+ for (const child of value.children) {
536
+ if (!exactKeys(child, CHILD_INPUT_KEYS) || !validId(child.legId) || byLeg.has(child.legId)) {
537
+ return fail('case_set_child_set_invalid');
538
+ }
539
+ byLeg.set(child.legId, child);
540
+ }
541
+ if (state.manifest.legs.some((leg: DataRecord) => !byLeg.has(leg.leg_id))) {
542
+ return fail('case_set_incomplete');
543
+ }
544
+ const observations: DataRecord[] = [];
545
+ for (const [index, leg] of state.manifest.legs.entries()) {
546
+ const result = verifyChild(byLeg.get(leg.leg_id)!, leg, state.observations[index]);
547
+ if (result.ok === false && typeof result.reason === 'string') return result as Failure;
548
+ observations.push(result as DataRecord);
549
+ }
550
+ const receiptDigests = observations.map((entry) => entry.receipt_content_digest);
551
+ if (new Set(receiptDigests).size !== receiptDigests.length) {
552
+ return fail('child_evidence_replayed');
553
+ }
554
+ const at = clock();
555
+ if (!Number.isFinite(at)) return fail('clock_invalid');
556
+ if (at < instant(state.updated_at)) return fail('clock_regression');
557
+ const next = canonicalCopy(state);
558
+ next.revision += 1;
559
+ next.updated_at = new Date(at).toISOString();
560
+ next.observations = observations;
561
+ next.status = observations.every((entry) => entry.status === 'executed')
562
+ ? 'completed' : 'indeterminate';
563
+ next.last_request_digest = requestDigest;
564
+ if (next.manifest_digest !== state.manifest_digest || !same(next.manifest, state.manifest)
565
+ || !validState(next, tenantId, value.caseSetId)) return fail('case_set_state_invalid');
566
+ try {
567
+ const stored: any = await store.compareAndSwap({
568
+ tenantId,
569
+ caseSetId: value.caseSetId,
570
+ expectedRevision: state.revision,
571
+ ownerTokenDigest: state.owner_token_digest,
572
+ state: deepFreeze(canonicalCopy(next)) as RemedyCaseSetState,
573
+ });
574
+ if (!isDataRecord(stored)) return fail('store_response_invalid');
575
+ if (stored.ok === true) return checkedState(stored.state ?? next, value.caseSetId);
576
+ if (stored.reason === 'revision_conflict') return fail('state_transition_conflict');
577
+ if (stored.reason === 'ownership_conflict') return fail('ownership_conflict');
578
+ return fail('store_operation_failed');
579
+ } catch {
580
+ return fail('store_unavailable');
581
+ }
582
+ }
583
+
584
+ async function status(input: unknown): Promise<RemedyCaseSetResult> {
585
+ let value: DataRecord;
586
+ try {
587
+ value = canonicalCopy(input) as DataRecord;
588
+ } catch {
589
+ return fail('case_set_lookup_invalid');
590
+ }
591
+ if (!exactKeys(value, STATUS_KEYS) || value.tenantId !== tenantId || !validId(value.caseSetId)) {
592
+ return fail('case_set_lookup_invalid');
593
+ }
594
+ return load(value.caseSetId);
595
+ }
596
+
597
+ return Object.freeze({ create, recordChildren, status });
598
+ }
599
+
600
+ export default Object.freeze({
601
+ REMEDY_CASE_SET_VERSION,
602
+ createRemedyCaseSetCoordinator,
603
+ });