@emilia-protocol/gate 0.13.0 → 0.15.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +89 -0
  3. package/aeb-consumption-store.js +3 -0
  4. package/dist/aeb-consumption-store.d.ts +80 -0
  5. package/dist/aeb-consumption-store.d.ts.map +1 -0
  6. package/dist/aeb-consumption-store.js +430 -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 +17 -4
  15. package/dist/index.js.map +1 -1
  16. package/dist/proposal-to-effect-postgres.d.ts +129 -0
  17. package/dist/proposal-to-effect-postgres.d.ts.map +1 -0
  18. package/dist/proposal-to-effect-postgres.js +1511 -0
  19. package/dist/proposal-to-effect-postgres.js.map +1 -0
  20. package/dist/proposal-to-effect-status.d.ts +55 -0
  21. package/dist/proposal-to-effect-status.d.ts.map +1 -0
  22. package/dist/proposal-to-effect-status.js +280 -0
  23. package/dist/proposal-to-effect-status.js.map +1 -0
  24. package/dist/proposal-to-effect.d.ts +294 -0
  25. package/dist/proposal-to-effect.d.ts.map +1 -0
  26. package/dist/proposal-to-effect.js +970 -0
  27. package/dist/proposal-to-effect.js.map +1 -0
  28. package/dist/remedy-case-set-postgres.d.ts +73 -0
  29. package/dist/remedy-case-set-postgres.d.ts.map +1 -0
  30. package/dist/remedy-case-set-postgres.js +846 -0
  31. package/dist/remedy-case-set-postgres.js.map +1 -0
  32. package/dist/remedy-case-set.d.ts +53 -0
  33. package/dist/remedy-case-set.d.ts.map +1 -0
  34. package/dist/remedy-case-set.js +571 -0
  35. package/dist/remedy-case-set.js.map +1 -0
  36. package/dist/remedy-program-adapters.d.ts +137 -0
  37. package/dist/remedy-program-adapters.d.ts.map +1 -0
  38. package/dist/remedy-program-adapters.js +590 -0
  39. package/dist/remedy-program-adapters.js.map +1 -0
  40. package/dist/trust-program-revocation.js.map +1 -1
  41. package/package.json +280 -63
  42. package/proposal-to-effect-postgres.js +3 -0
  43. package/proposal-to-effect-status.js +2 -0
  44. package/proposal-to-effect.js +4 -0
  45. package/remedy-case-set-postgres.js +3 -0
  46. package/remedy-case-set.js +3 -0
  47. package/remedy-program-adapters.js +3 -0
  48. package/src/aeb-consumption-store.ts +521 -0
  49. package/src/capability-receipt.ts +17 -0
  50. package/src/index.ts +57 -3
  51. package/src/proposal-to-effect-postgres.ts +1740 -0
  52. package/src/proposal-to-effect-status.ts +357 -0
  53. package/src/proposal-to-effect.ts +1276 -0
  54. package/src/remedy-case-set-postgres.ts +1008 -0
  55. package/src/remedy-case-set.ts +603 -0
  56. package/src/remedy-program-adapters.ts +657 -0
  57. package/src/trust-program-revocation.ts +1 -1
@@ -0,0 +1,657 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Constructor-pinned production adapters for the Remedy Program kernel.
4
+ *
5
+ * The kernel deliberately stores evidence references for every post-create
6
+ * transition. These adapters resolve those references from a relying-party
7
+ * evidence source and perform the concrete cryptographic verification here.
8
+ * Presenters cannot supply verifier functions, tenants, or trust keys.
9
+ */
10
+ import crypto from 'node:crypto';
11
+
12
+ import { verifyRevocation } from '@emilia-protocol/verify';
13
+
14
+ import { canonicalize } from '../execution-binding.js';
15
+ import { verifyActionEscrowStateStatement } from './action-escrow-state.js';
16
+
17
+ export const REMEDY_PROGRAM_EVIDENCE_VERSION = 'EP-GATE-REMEDY-EVIDENCE-v1';
18
+ export const REMEDY_PROGRAM_EVIDENCE_DOMAIN = `${REMEDY_PROGRAM_EVIDENCE_VERSION}\0`;
19
+
20
+ type DataRecord = Record<string, any>;
21
+
22
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
23
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:/#@+-]{0,255}$/;
24
+ const CAID = /^caid:1:[a-z][a-z0-9.-]*\.[1-9][0-9]*:jcs-sha256:[A-Za-z0-9_-]{43}$/;
25
+ const BASE64URL = /^[A-Za-z0-9_-]+$/;
26
+ const ORIGINAL_KEYS = new Set([
27
+ 'caid', 'action_digest', 'operation_id', 'consequence_mode',
28
+ 'consequence_digest', 'terminal_evidence_digest', 'outcome', 'occurred_at',
29
+ ]);
30
+ const STORED_ORIGINAL_KEYS = new Set([...ORIGINAL_KEYS, 'evidence_digest']);
31
+ const SIGNED_EVIDENCE_KEYS = new Set([
32
+ 'version', 'kind', 'issuer', 'payload', 'content_digest', 'signature',
33
+ ]);
34
+ const SIGNING_BODY_KEYS = new Set([
35
+ 'version', 'kind', 'issuer', 'payload', 'content_digest',
36
+ ]);
37
+ const ISSUER_KEYS = new Set(['authority_id', 'key_id']);
38
+ const SIGNATURE_KEYS = new Set(['algorithm', 'value']);
39
+ const AUTHORITY_KEYS = new Set(['authorityId', 'trustedKeys']);
40
+ const ORIGINAL_BINDING_KEYS = new Set([
41
+ 'agreementId', 'caid', 'bindingDigest', 'profileDigest', 'amendmentDigests',
42
+ ]);
43
+ const DISPUTE_PAYLOAD_KEYS = new Set([
44
+ 'evidence_id', 'tenant_id', 'instance_id', 'dispute_id', 'challenger_id',
45
+ 'requested_units', 'opened_at', 'original_operation_id', 'original_action_digest',
46
+ ]);
47
+ const AUTHORIZATION_PAYLOAD_KEYS = new Set([
48
+ 'evidence_id', 'tenant_id', 'instance_id', 'dispute_id',
49
+ 'original_operation_id', 'original_action_digest', 'remedy_operation_id',
50
+ 'remedy_caid', 'remedy_action_digest', 'destination_binding_digest',
51
+ 'consequence_mode', 'capability_template_digest', 'escrow_profile_digest',
52
+ 'units', 'unit', 'authorized_at',
53
+ ]);
54
+ const OUTCOME_PAYLOAD_KEYS = new Set([
55
+ 'evidence_id', 'tenant_id', 'instance_id', 'remedy_operation_id',
56
+ 'remedy_action_digest', 'destination_binding_digest', 'units', 'unit',
57
+ 'outcome', 'observed_at', 'reconciliation',
58
+ ]);
59
+ const ORIGINAL_OUTCOME_PAYLOAD_KEYS = new Set([
60
+ 'evidence_id', 'tenant_id', 'instance_id', 'original_operation_id',
61
+ 'original_action_digest', 'terminal_evidence_digest', 'outcome',
62
+ 'observed_at',
63
+ ]);
64
+
65
+ export interface RemedyProgramEvidenceSource {
66
+ get(input: Readonly<{
67
+ tenantId: string;
68
+ evidenceId: string;
69
+ evidenceDigest: string;
70
+ }>): unknown | Promise<unknown>;
71
+ }
72
+
73
+ export interface RemedyProgramPinnedAuthority {
74
+ authorityId: string;
75
+ trustedKeys: Record<string, string>;
76
+ }
77
+
78
+ export interface RemedyProgramOriginalEffectBinding {
79
+ agreementId: string;
80
+ caid: string;
81
+ bindingDigest: string;
82
+ profileDigest: string;
83
+ amendmentDigests: string[];
84
+ }
85
+
86
+ export interface RemedyProgramAdapterOptions {
87
+ tenantId: string;
88
+ environment: string;
89
+ audience: string;
90
+ evidenceSource: RemedyProgramEvidenceSource;
91
+ actionEscrow: {
92
+ trustedKeys: Record<string, { operator_id: string; public_key: string }>;
93
+ originalEffects: Record<string, RemedyProgramOriginalEffectBinding>;
94
+ };
95
+ revokerKeys: Record<string, { public_key: string; key_id?: string }>;
96
+ disputeAuthority: RemedyProgramPinnedAuthority;
97
+ remedyAuthority: RemedyProgramPinnedAuthority;
98
+ providerAuthority: RemedyProgramPinnedAuthority;
99
+ now?: () => number | string | Date;
100
+ }
101
+
102
+ function isRecord(value: unknown): value is DataRecord {
103
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
104
+ const prototype = Object.getPrototypeOf(value);
105
+ return prototype === Object.prototype || prototype === null;
106
+ }
107
+
108
+ function isDataRecord(value: unknown): value is DataRecord {
109
+ if (!isRecord(value)) return false;
110
+ return Reflect.ownKeys(value).every((key) => {
111
+ if (typeof key !== 'string') return false;
112
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
113
+ return descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
114
+ });
115
+ }
116
+
117
+ function exactKeys(value: unknown, keys: Set<string>): value is DataRecord {
118
+ return isDataRecord(value)
119
+ && Object.keys(value).length === keys.size
120
+ && Object.keys(value).every((key) => keys.has(key));
121
+ }
122
+
123
+ function validContext(value: unknown): value is string {
124
+ return typeof value === 'string' && value.length > 0
125
+ && Buffer.byteLength(value, 'utf8') <= 512
126
+ && !/[\u0000-\u001f\u007f]/.test(value);
127
+ }
128
+
129
+ function validId(value: unknown): value is string {
130
+ return typeof value === 'string' && ID.test(value);
131
+ }
132
+
133
+ function instant(value: unknown): number {
134
+ if (typeof value !== 'string') return NaN;
135
+ const parsed = Date.parse(value);
136
+ return Number.isFinite(parsed) ? parsed : NaN;
137
+ }
138
+
139
+ function canonicalCopy<T>(value: T): T {
140
+ return JSON.parse(canonicalize(value));
141
+ }
142
+
143
+ function deepFreeze<T>(value: T): Readonly<T> {
144
+ if (value === null || typeof value !== 'object' || Object.isFrozen(value)) return value;
145
+ Object.freeze(value);
146
+ for (const child of Object.values(value as DataRecord)) deepFreeze(child);
147
+ return value;
148
+ }
149
+
150
+ function canonicalDigest(value: unknown): string {
151
+ return `sha256:${crypto.createHash('sha256').update(canonicalize(value)).digest('hex')}`;
152
+ }
153
+
154
+ /** Digest an exact evidence artifact for use as the kernel's evidence reference. */
155
+ export function remedyProgramEvidenceDigest(value: unknown): string {
156
+ return canonicalDigest(value);
157
+ }
158
+
159
+ function signingBody(value: DataRecord): DataRecord {
160
+ return {
161
+ version: value.version,
162
+ kind: value.kind,
163
+ issuer: value.issuer,
164
+ payload: value.payload,
165
+ content_digest: value.content_digest,
166
+ };
167
+ }
168
+
169
+ /** Domain-separated canonical bytes for the closed signed evidence envelope. */
170
+ export function remedyProgramEvidenceSigningBytes(value: unknown): Buffer {
171
+ if (!isDataRecord(value)) throw new TypeError('remedy evidence signing body invalid');
172
+ const body = signingBody(value);
173
+ if (!exactKeys(body, SIGNING_BODY_KEYS)) {
174
+ throw new TypeError('remedy evidence signing body invalid');
175
+ }
176
+ return Buffer.from(REMEDY_PROGRAM_EVIDENCE_DOMAIN + canonicalize(body), 'utf8');
177
+ }
178
+
179
+ function strictBase64url(value: unknown, length?: number): Buffer | null {
180
+ if (typeof value !== 'string' || !BASE64URL.test(value) || value.length % 4 === 1) return null;
181
+ const bytes = Buffer.from(value, 'base64url');
182
+ return bytes.toString('base64url') === value
183
+ && (length === undefined || bytes.length === length) ? bytes : null;
184
+ }
185
+
186
+ function ed25519PublicKey(value: unknown): crypto.KeyObject | null {
187
+ try {
188
+ const bytes = strictBase64url(value);
189
+ if (!bytes) return null;
190
+ const key = crypto.createPublicKey({ key: bytes, format: 'der', type: 'spki' });
191
+ return key.asymmetricKeyType === 'ed25519' ? key : null;
192
+ } catch {
193
+ return null;
194
+ }
195
+ }
196
+
197
+ function validTrustedKeys(value: unknown): value is Record<string, string> {
198
+ return isDataRecord(value) && Object.keys(value).length > 0
199
+ && Object.entries(value).every(([keyId, key]) => validId(keyId) && ed25519PublicKey(key) !== null);
200
+ }
201
+
202
+ function validAuthority(value: unknown): value is RemedyProgramPinnedAuthority {
203
+ return exactKeys(value, AUTHORITY_KEYS)
204
+ && validContext(value.authorityId)
205
+ && validTrustedKeys(value.trustedKeys);
206
+ }
207
+
208
+ function validOriginal(value: unknown, stored = false): value is DataRecord {
209
+ return exactKeys(value, stored ? STORED_ORIGINAL_KEYS : ORIGINAL_KEYS)
210
+ && typeof value.caid === 'string' && CAID.test(value.caid)
211
+ && typeof value.action_digest === 'string' && DIGEST.test(value.action_digest)
212
+ && validId(value.operation_id)
213
+ && value.consequence_mode === 'action-escrow'
214
+ && typeof value.consequence_digest === 'string' && DIGEST.test(value.consequence_digest)
215
+ && typeof value.terminal_evidence_digest === 'string' && DIGEST.test(value.terminal_evidence_digest)
216
+ && ['executed', 'indeterminate'].includes(value.outcome)
217
+ && Number.isFinite(instant(value.occurred_at))
218
+ && (!stored || (typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)));
219
+ }
220
+
221
+ function snapshotData<T>(value: T): Readonly<T> {
222
+ return deepFreeze(canonicalCopy(value));
223
+ }
224
+
225
+ function failure(): Readonly<{ ok: false }> {
226
+ return Object.freeze({ ok: false });
227
+ }
228
+
229
+ function contextMatches(expected: unknown, options: Readonly<RemedyProgramAdapterOptions>): expected is DataRecord {
230
+ return isDataRecord(expected)
231
+ && expected.tenant_id === options.tenantId
232
+ && expected.environment === options.environment
233
+ && expected.audience === options.audience
234
+ && validId(expected.instance_id);
235
+ }
236
+
237
+ function payloadMatches(value: DataRecord, expected: DataRecord, fields: string[]): boolean {
238
+ return fields.every((field) => value[field] === expected[field]);
239
+ }
240
+
241
+ function verifiedSignedEvidence(
242
+ value: unknown,
243
+ kind: string,
244
+ authority: Readonly<RemedyProgramPinnedAuthority>,
245
+ ): DataRecord | null {
246
+ try {
247
+ const evidence = canonicalCopy(value) as unknown;
248
+ if (!exactKeys(evidence, SIGNED_EVIDENCE_KEYS)
249
+ || evidence.version !== REMEDY_PROGRAM_EVIDENCE_VERSION
250
+ || evidence.kind !== kind
251
+ || !exactKeys(evidence.issuer, ISSUER_KEYS)
252
+ || evidence.issuer.authority_id !== authority.authorityId
253
+ || !validId(evidence.issuer.key_id)
254
+ || !isDataRecord(evidence.payload)
255
+ || typeof evidence.content_digest !== 'string'
256
+ || !DIGEST.test(evidence.content_digest)
257
+ || !exactKeys(evidence.signature, SIGNATURE_KEYS)
258
+ || evidence.signature.algorithm !== 'Ed25519') return null;
259
+ const unsigned = {
260
+ version: evidence.version,
261
+ kind: evidence.kind,
262
+ issuer: evidence.issuer,
263
+ payload: evidence.payload,
264
+ };
265
+ if (canonicalDigest(unsigned) !== evidence.content_digest) return null;
266
+ const keyValue = authority.trustedKeys[evidence.issuer.key_id];
267
+ const key = ed25519PublicKey(keyValue);
268
+ const signature = strictBase64url(evidence.signature.value, 64);
269
+ if (!key || !signature
270
+ || !crypto.verify(null, remedyProgramEvidenceSigningBytes(evidence), key, signature)) return null;
271
+ return evidence;
272
+ } catch {
273
+ return null;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Build all required Remedy Program callbacks using only pinned configuration
279
+ * and concrete repository verifiers. There are intentionally no verifier
280
+ * override hooks.
281
+ */
282
+ export function createRemedyProgramAdapters(options: RemedyProgramAdapterOptions) {
283
+ if (!isDataRecord(options)
284
+ || !validContext(options.tenantId)
285
+ || !validContext(options.environment)
286
+ || !validContext(options.audience)
287
+ || !options.evidenceSource
288
+ || typeof options.evidenceSource.get !== 'function'
289
+ || !isDataRecord(options.actionEscrow)
290
+ || !isDataRecord(options.actionEscrow.trustedKeys)
291
+ || Object.keys(options.actionEscrow.trustedKeys).length === 0
292
+ || !isDataRecord(options.actionEscrow.originalEffects)
293
+ || Object.keys(options.actionEscrow.originalEffects).length === 0
294
+ || !isDataRecord(options.revokerKeys)
295
+ || !validAuthority(options.disputeAuthority)
296
+ || !validAuthority(options.remedyAuthority)
297
+ || !validAuthority(options.providerAuthority)
298
+ || (options.now !== undefined && typeof options.now !== 'function')) {
299
+ throw new TypeError('remedy program adapter configuration invalid');
300
+ }
301
+ for (const [keyId, pin] of Object.entries(options.actionEscrow.trustedKeys)) {
302
+ if (!validId(keyId) || !exactKeys(pin, new Set(['operator_id', 'public_key']))
303
+ || !validContext(pin.operator_id) || ed25519PublicKey(pin.public_key) === null) {
304
+ throw new TypeError('Action Escrow state key configuration invalid');
305
+ }
306
+ }
307
+ for (const [operationId, binding] of Object.entries(options.actionEscrow.originalEffects)) {
308
+ if (!validId(operationId) || !exactKeys(binding, ORIGINAL_BINDING_KEYS)
309
+ || !validContext(binding.agreementId)
310
+ || typeof binding.caid !== 'string' || !CAID.test(binding.caid)
311
+ || typeof binding.bindingDigest !== 'string' || !DIGEST.test(binding.bindingDigest)
312
+ || typeof binding.profileDigest !== 'string' || !DIGEST.test(binding.profileDigest)
313
+ || !Array.isArray(binding.amendmentDigests)
314
+ || binding.amendmentDigests.some((entry: unknown) => typeof entry !== 'string' || !DIGEST.test(entry))
315
+ || new Set(binding.amendmentDigests).size !== binding.amendmentDigests.length) {
316
+ throw new TypeError('Action Escrow original-effect binding invalid');
317
+ }
318
+ }
319
+
320
+ const pinned = snapshotData({
321
+ tenantId: options.tenantId,
322
+ environment: options.environment,
323
+ audience: options.audience,
324
+ actionEscrow: options.actionEscrow,
325
+ revokerKeys: options.revokerKeys,
326
+ disputeAuthority: options.disputeAuthority,
327
+ remedyAuthority: options.remedyAuthority,
328
+ providerAuthority: options.providerAuthority,
329
+ }) as Readonly<RemedyProgramAdapterOptions>;
330
+ const getEvidence = options.evidenceSource.get.bind(options.evidenceSource);
331
+ const now = options.now ?? Date.now;
332
+
333
+ const resolvedNow = (): number | string | Date | null => {
334
+ try {
335
+ const value = now();
336
+ if (value instanceof Date) return Number.isFinite(value.getTime()) ? new Date(value) : null;
337
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null;
338
+ return Number.isFinite(instant(value)) ? value : null;
339
+ } catch {
340
+ return null;
341
+ }
342
+ };
343
+
344
+ const resolveEvidence = async (
345
+ evidenceId: string,
346
+ evidenceDigest: string,
347
+ ): Promise<unknown | null> => {
348
+ try {
349
+ const value = await getEvidence(Object.freeze({
350
+ tenantId: pinned.tenantId,
351
+ evidenceId,
352
+ evidenceDigest,
353
+ }));
354
+ const copy = canonicalCopy(value);
355
+ return canonicalDigest(copy) === evidenceDigest ? deepFreeze(copy) : null;
356
+ } catch {
357
+ return null;
358
+ }
359
+ };
360
+
361
+ const resolveSigned = async (
362
+ evidenceId: string,
363
+ evidenceDigest: string,
364
+ kind: string,
365
+ authority: Readonly<RemedyProgramPinnedAuthority>,
366
+ ): Promise<DataRecord | null> => {
367
+ const value = await resolveEvidence(evidenceId, evidenceDigest);
368
+ return verifiedSignedEvidence(value, kind, authority);
369
+ };
370
+
371
+ async function verifyOriginalEffect(input: Readonly<DataRecord>) {
372
+ try {
373
+ if (!exactKeys(input, new Set(['original', 'evidence', 'expected']))
374
+ || !validOriginal(input.original)
375
+ || !contextMatches(input.expected, pinned)
376
+ || !exactKeys(input.evidence, new Set(['snapshot', 'statement']))) return failure();
377
+ const original = input.original;
378
+ const expected = input.expected;
379
+ const binding = pinned.actionEscrow.originalEffects[original.operation_id];
380
+ if (!binding || original.caid !== binding.caid
381
+ || original.consequence_digest !== binding.bindingDigest) return failure();
382
+ const snapshot = input.evidence.snapshot;
383
+ const statement = input.evidence.statement;
384
+ const stage = original.outcome === 'executed' ? 'released' : 'release_indeterminate';
385
+ if (!isDataRecord(snapshot) || !isDataRecord(statement)
386
+ || snapshot['@version'] !== 'EP-ACTION-ESCROW-STATE-v1'
387
+ || snapshot.state !== stage
388
+ || !Number.isSafeInteger(snapshot.revision) || snapshot.revision < 0
389
+ || snapshot.release_action_digest !== original.action_digest
390
+ || snapshot.document_action_binding_digest !== binding.bindingDigest
391
+ || snapshot.profile_digest !== binding.profileDigest
392
+ || !isDataRecord(snapshot.release)
393
+ || snapshot.release.operation_idempotency_key !== original.operation_id
394
+ || statement.statement_digest !== original.terminal_evidence_digest
395
+ || statement.payload?.occurred_at !== original.occurred_at) return failure();
396
+ const evaluation = resolvedNow();
397
+ if (evaluation === null) return failure();
398
+ const verified = verifyActionEscrowStateStatement(statement, {
399
+ trustedKeys: pinned.actionEscrow.trustedKeys,
400
+ stateRecord: snapshot,
401
+ expectedAgreementId: binding.agreementId,
402
+ expectedBindingDigest: binding.bindingDigest,
403
+ expectedActionDigest: original.action_digest,
404
+ expectedProfileDigest: binding.profileDigest,
405
+ expectedState: stage,
406
+ expectedRevision: snapshot.revision,
407
+ expectedAmendmentDigests: binding.amendmentDigests,
408
+ expectedPreviousStatementDigest: statement.payload.previous_statement_digest,
409
+ now: evaluation,
410
+ });
411
+ if (verified.valid !== true || verified.statement_digest !== original.terminal_evidence_digest) {
412
+ return failure();
413
+ }
414
+ return Object.freeze({
415
+ ok: true,
416
+ ...canonicalCopy(original),
417
+ evidence_digest: original.terminal_evidence_digest,
418
+ });
419
+ } catch {
420
+ return failure();
421
+ }
422
+ }
423
+
424
+ async function verifyRevocationEvidence(input: Readonly<DataRecord>) {
425
+ try {
426
+ if (!exactKeys(input, new Set(['evidence', 'expected']))
427
+ || !contextMatches(input.expected, pinned)
428
+ || !validOriginal(input.expected.original, true)
429
+ || !exactKeys(input.evidence, new Set(['id', 'digest']))
430
+ || !validId(input.evidence.id)
431
+ || typeof input.evidence.digest !== 'string' || !DIGEST.test(input.evidence.digest)) {
432
+ return failure();
433
+ }
434
+ const statement: any = await resolveEvidence(input.evidence.id, input.evidence.digest);
435
+ const evaluation = resolvedNow();
436
+ if (!isDataRecord(statement) || evaluation === null) return failure();
437
+ const target = {
438
+ target_type: 'commit' as const,
439
+ target_id: input.expected.original.operation_id,
440
+ action_hash: input.expected.original.action_digest,
441
+ };
442
+ const verified = verifyRevocation(target, statement, {
443
+ revokerKeys: pinned.revokerKeys,
444
+ now: evaluation,
445
+ });
446
+ if (verified.valid !== true) return failure();
447
+ return Object.freeze({
448
+ ok: true,
449
+ evidence_id: input.evidence.id,
450
+ evidence_digest: input.evidence.digest,
451
+ target_operation_id: target.target_id,
452
+ action_digest: target.action_hash,
453
+ authority_id: statement.revoker_id,
454
+ revoked_at: statement.revoked_at,
455
+ });
456
+ } catch {
457
+ return failure();
458
+ }
459
+ }
460
+
461
+ async function verifyDispute(input: Readonly<DataRecord>) {
462
+ try {
463
+ if (!exactKeys(input, new Set(['dispute', 'expected']))
464
+ || !contextMatches(input.expected, pinned)
465
+ || !validOriginal(input.expected.original, true)
466
+ || !isDataRecord(input.dispute)
467
+ || !validId(input.dispute.evidence_id)
468
+ || typeof input.dispute.evidence_digest !== 'string'
469
+ || !DIGEST.test(input.dispute.evidence_digest)) return failure();
470
+ const artifact = await resolveSigned(
471
+ input.dispute.evidence_id,
472
+ input.dispute.evidence_digest,
473
+ 'dispute',
474
+ pinned.disputeAuthority,
475
+ );
476
+ if (!artifact || !exactKeys(artifact.payload, DISPUTE_PAYLOAD_KEYS)) return failure();
477
+ const payload = artifact.payload;
478
+ if (payload.tenant_id !== pinned.tenantId
479
+ || payload.instance_id !== input.expected.instance_id
480
+ || payload.original_operation_id !== input.expected.original.operation_id
481
+ || payload.original_action_digest !== input.expected.original.action_digest
482
+ || !payloadMatches(payload, input.dispute, [
483
+ 'evidence_id', 'dispute_id', 'challenger_id', 'requested_units', 'opened_at',
484
+ ])) return failure();
485
+ return Object.freeze({
486
+ ok: true,
487
+ dispute_id: payload.dispute_id,
488
+ evidence_id: payload.evidence_id,
489
+ evidence_digest: input.dispute.evidence_digest,
490
+ challenger_id: payload.challenger_id,
491
+ requested_units: payload.requested_units,
492
+ opened_at: payload.opened_at,
493
+ original_operation_id: payload.original_operation_id,
494
+ original_action_digest: payload.original_action_digest,
495
+ });
496
+ } catch {
497
+ return failure();
498
+ }
499
+ }
500
+
501
+ async function verifyRemedyAuthorization(input: Readonly<DataRecord>) {
502
+ try {
503
+ if (!exactKeys(input, new Set(['authorization', 'expected']))
504
+ || !contextMatches(input.expected, pinned)
505
+ || !validOriginal(input.expected.original, true)
506
+ || !isDataRecord(input.expected.dispute)
507
+ || !isDataRecord(input.authorization)
508
+ || !validId(input.authorization.evidence_id)
509
+ || typeof input.authorization.evidence_digest !== 'string'
510
+ || !DIGEST.test(input.authorization.evidence_digest)) return failure();
511
+ const artifact = await resolveSigned(
512
+ input.authorization.evidence_id,
513
+ input.authorization.evidence_digest,
514
+ 'remedy_authorization',
515
+ pinned.remedyAuthority,
516
+ );
517
+ if (!artifact || !exactKeys(artifact.payload, AUTHORIZATION_PAYLOAD_KEYS)) return failure();
518
+ const payload = artifact.payload;
519
+ const authorizationFields = [
520
+ 'evidence_id', 'remedy_operation_id', 'remedy_caid', 'remedy_action_digest',
521
+ 'consequence_mode', 'capability_template_digest', 'escrow_profile_digest',
522
+ 'units', 'authorized_at',
523
+ ];
524
+ if (payload.tenant_id !== pinned.tenantId
525
+ || payload.instance_id !== input.expected.instance_id
526
+ || payload.dispute_id !== input.expected.dispute.dispute_id
527
+ || payload.original_operation_id !== input.expected.original.operation_id
528
+ || payload.original_action_digest !== input.expected.original.action_digest
529
+ || payload.destination_binding_digest !== input.expected.destination_binding_digest
530
+ || payload.unit !== input.expected.unit
531
+ || !payloadMatches(payload, input.authorization, authorizationFields)) return failure();
532
+ return Object.freeze({
533
+ ok: true,
534
+ evidence_id: payload.evidence_id,
535
+ evidence_digest: input.authorization.evidence_digest,
536
+ remedy_operation_id: payload.remedy_operation_id,
537
+ remedy_caid: payload.remedy_caid,
538
+ remedy_action_digest: payload.remedy_action_digest,
539
+ consequence_mode: payload.consequence_mode,
540
+ capability_template_digest: payload.capability_template_digest,
541
+ escrow_profile_digest: payload.escrow_profile_digest,
542
+ units: payload.units,
543
+ authorized_at: payload.authorized_at,
544
+ dispute_id: payload.dispute_id,
545
+ original_operation_id: payload.original_operation_id,
546
+ destination_binding_digest: payload.destination_binding_digest,
547
+ unit: payload.unit,
548
+ });
549
+ } catch {
550
+ return failure();
551
+ }
552
+ }
553
+
554
+ async function verifyRemedyOutcome(input: Readonly<DataRecord>) {
555
+ try {
556
+ if (!exactKeys(input, new Set(['evidence', 'outcome', 'expected', 'reconciliation']))
557
+ || !contextMatches(input.expected, pinned)
558
+ || !validOriginal(input.expected.original, true)
559
+ || typeof input.reconciliation !== 'boolean'
560
+ || !isDataRecord(input.evidence)
561
+ || !validId(input.evidence.evidence_id)
562
+ || typeof input.evidence.evidence_digest !== 'string'
563
+ || !DIGEST.test(input.evidence.evidence_digest)) return failure();
564
+ const artifact = await resolveSigned(
565
+ input.evidence.evidence_id,
566
+ input.evidence.evidence_digest,
567
+ 'provider_outcome',
568
+ pinned.providerAuthority,
569
+ );
570
+ if (!artifact || !exactKeys(artifact.payload, OUTCOME_PAYLOAD_KEYS)) return failure();
571
+ const payload = artifact.payload;
572
+ if (payload.tenant_id !== pinned.tenantId
573
+ || payload.instance_id !== input.expected.instance_id
574
+ || payload.reconciliation !== input.reconciliation
575
+ || payload.outcome !== input.outcome
576
+ || payload.evidence_id !== input.evidence.evidence_id
577
+ || payload.observed_at !== input.evidence.observed_at
578
+ || !payloadMatches(payload, input.expected, [
579
+ 'remedy_operation_id', 'remedy_action_digest', 'destination_binding_digest',
580
+ 'units', 'unit',
581
+ ])) return failure();
582
+ return Object.freeze({
583
+ ok: true,
584
+ evidence_id: payload.evidence_id,
585
+ evidence_digest: input.evidence.evidence_digest,
586
+ remedy_operation_id: payload.remedy_operation_id,
587
+ remedy_action_digest: payload.remedy_action_digest,
588
+ destination_binding_digest: payload.destination_binding_digest,
589
+ units: payload.units,
590
+ unit: payload.unit,
591
+ outcome: payload.outcome,
592
+ observed_at: payload.observed_at,
593
+ });
594
+ } catch {
595
+ return failure();
596
+ }
597
+ }
598
+
599
+ async function verifyOriginalReconciliation(input: Readonly<DataRecord>) {
600
+ try {
601
+ if (!exactKeys(input, new Set(['evidence', 'outcome', 'expected']))
602
+ || !contextMatches(input.expected, pinned)
603
+ || !validOriginal(input.expected.original, true)
604
+ || !isDataRecord(input.evidence)
605
+ || !validId(input.evidence.evidence_id)
606
+ || typeof input.evidence.evidence_digest !== 'string'
607
+ || !DIGEST.test(input.evidence.evidence_digest)) return failure();
608
+ const artifact = await resolveSigned(
609
+ input.evidence.evidence_id,
610
+ input.evidence.evidence_digest,
611
+ 'original_outcome',
612
+ pinned.providerAuthority,
613
+ );
614
+ if (!artifact || !exactKeys(artifact.payload, ORIGINAL_OUTCOME_PAYLOAD_KEYS)) return failure();
615
+ const payload = artifact.payload;
616
+ if (payload.tenant_id !== pinned.tenantId
617
+ || payload.instance_id !== input.expected.instance_id
618
+ || payload.evidence_id !== input.evidence.evidence_id
619
+ || payload.observed_at !== input.evidence.observed_at
620
+ || payload.outcome !== input.outcome
621
+ || payload.original_operation_id !== input.expected.original.operation_id
622
+ || payload.original_action_digest !== input.expected.original.action_digest
623
+ || payload.terminal_evidence_digest !== input.expected.original.terminal_evidence_digest) {
624
+ return failure();
625
+ }
626
+ return Object.freeze({
627
+ ok: true,
628
+ evidence_id: payload.evidence_id,
629
+ evidence_digest: input.evidence.evidence_digest,
630
+ original_operation_id: payload.original_operation_id,
631
+ original_action_digest: payload.original_action_digest,
632
+ terminal_evidence_digest: payload.terminal_evidence_digest,
633
+ outcome: payload.outcome,
634
+ observed_at: payload.observed_at,
635
+ });
636
+ } catch {
637
+ return failure();
638
+ }
639
+ }
640
+
641
+ return Object.freeze({
642
+ verifyOriginalEffect,
643
+ verifyRevocation: verifyRevocationEvidence,
644
+ verifyDispute,
645
+ verifyRemedyAuthorization,
646
+ verifyRemedyOutcome,
647
+ verifyOriginalReconciliation,
648
+ });
649
+ }
650
+
651
+ export default Object.freeze({
652
+ REMEDY_PROGRAM_EVIDENCE_VERSION,
653
+ REMEDY_PROGRAM_EVIDENCE_DOMAIN,
654
+ remedyProgramEvidenceDigest,
655
+ remedyProgramEvidenceSigningBytes,
656
+ createRemedyProgramAdapters,
657
+ });
@@ -452,7 +452,7 @@ function verifyTrustProgramRevocationInternal(input: TrustProgramRevocationVerif
452
452
  try {
453
453
  const result = verifyPortableRevocation(
454
454
  target,
455
- statement as object,
455
+ statement as Record<string, unknown>,
456
456
  { revokerKeys, now },
457
457
  );
458
458
  if (!isDataRecord(result) || typeof result.valid !== 'boolean'