@emilia-protocol/gate 0.14.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.
- package/CHANGELOG.md +32 -1
- package/README.md +32 -1
- package/aeb-consumption-store.js +3 -0
- package/dist/aeb-consumption-store.d.ts +80 -0
- package/dist/aeb-consumption-store.d.ts.map +1 -0
- package/dist/aeb-consumption-store.js +430 -0
- package/dist/aeb-consumption-store.js.map +1 -0
- package/dist/capability-receipt.d.ts +8 -0
- package/dist/capability-receipt.d.ts.map +1 -1
- package/dist/capability-receipt.js +17 -0
- package/dist/capability-receipt.js.map +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/dist/proposal-to-effect-postgres.d.ts +129 -0
- package/dist/proposal-to-effect-postgres.d.ts.map +1 -0
- package/dist/proposal-to-effect-postgres.js +1511 -0
- package/dist/proposal-to-effect-postgres.js.map +1 -0
- package/dist/proposal-to-effect-status.d.ts +55 -0
- package/dist/proposal-to-effect-status.d.ts.map +1 -0
- package/dist/proposal-to-effect-status.js +280 -0
- package/dist/proposal-to-effect-status.js.map +1 -0
- package/dist/proposal-to-effect.d.ts +151 -3
- package/dist/proposal-to-effect.d.ts.map +1 -1
- package/dist/proposal-to-effect.js +589 -46
- package/dist/proposal-to-effect.js.map +1 -1
- package/dist/remedy-case-set-postgres.d.ts +73 -0
- package/dist/remedy-case-set-postgres.d.ts.map +1 -0
- package/dist/remedy-case-set-postgres.js +846 -0
- package/dist/remedy-case-set-postgres.js.map +1 -0
- package/dist/remedy-case-set.d.ts +53 -0
- package/dist/remedy-case-set.d.ts.map +1 -0
- package/dist/remedy-case-set.js +571 -0
- package/dist/remedy-case-set.js.map +1 -0
- package/dist/remedy-program-adapters.d.ts +137 -0
- package/dist/remedy-program-adapters.d.ts.map +1 -0
- package/dist/remedy-program-adapters.js +590 -0
- package/dist/remedy-program-adapters.js.map +1 -0
- package/dist/trust-program-revocation.js.map +1 -1
- package/package.json +278 -63
- package/proposal-to-effect-postgres.js +3 -0
- package/proposal-to-effect-status.js +2 -0
- package/remedy-case-set-postgres.js +3 -0
- package/remedy-case-set.js +3 -0
- package/remedy-program-adapters.js +3 -0
- package/src/aeb-consumption-store.ts +521 -0
- package/src/capability-receipt.ts +17 -0
- package/src/index.ts +52 -3
- package/src/proposal-to-effect-postgres.ts +1740 -0
- package/src/proposal-to-effect-status.ts +357 -0
- package/src/proposal-to-effect.ts +755 -50
- package/src/remedy-case-set-postgres.ts +1008 -0
- package/src/remedy-case-set.ts +603 -0
- package/src/remedy-program-adapters.ts +657 -0
- 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
|
|
455
|
+
statement as Record<string, unknown>,
|
|
456
456
|
{ revokerKeys, now },
|
|
457
457
|
);
|
|
458
458
|
if (!isDataRecord(result) || typeof result.valid !== 'boolean'
|