@emilia-protocol/gate 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +20 -0
- package/action-escrow-custodian.js +395 -0
- package/action-escrow-evidence.js +859 -0
- package/action-escrow-package.js +1669 -0
- package/action-escrow-postgres.js +338 -0
- package/action-escrow-state.js +397 -0
- package/action-escrow-verifiers.js +332 -0
- package/action-escrow.js +2448 -0
- package/package.json +17 -3
- package/strict-json.js +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,39 @@
|
|
|
4
4
|
All notable changes to `@emilia-protocol/gate` are documented here.
|
|
5
5
|
This package follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## 0.11.0 (2026-07-17)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Action Escrow modules for exact document/action binding, evidence
|
|
12
|
+
verification, a signed lifecycle state machine, durable Postgres state and
|
|
13
|
+
journal storage, licensed-custodian adapters, portable assurance packages,
|
|
14
|
+
and fail-closed release enforcement.
|
|
15
|
+
- Public package exports for `action-escrow`, `action-escrow-state`,
|
|
16
|
+
`action-escrow-postgres`, `action-escrow-custodian`,
|
|
17
|
+
`action-escrow-package`, and `action-escrow-verifiers`.
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
|
|
21
|
+
- Release requires exact profile, party, final-document, material-term,
|
|
22
|
+
funding, milestone, action, and approval binding under relying-party-pinned
|
|
23
|
+
policy.
|
|
24
|
+
- Release approvals are fresh and action-specific, release is consumed once,
|
|
25
|
+
storage failure refuses, and an ambiguous provider effect enters
|
|
26
|
+
reconciliation instead of being retried.
|
|
27
|
+
- Release approvals now use the canonical `EP-RESOLUTION-v1` binding-moment
|
|
28
|
+
hash, relying-party-pinned option mapping, initiator, per-party nonce, and
|
|
29
|
+
evaluation time. The reference scenario uses real WebAuthn-shaped P-256
|
|
30
|
+
signatures rather than a resolution-like demo envelope.
|
|
31
|
+
- Runtime roles cannot also act as contract parties. Provider and effect
|
|
32
|
+
references are fenced to prevent substitution across actions or sessions.
|
|
33
|
+
|
|
34
|
+
### Distribution
|
|
35
|
+
|
|
36
|
+
- Gate now depends on `@emilia-protocol/verify` 3.11.0 and
|
|
37
|
+
`@emilia-protocol/require-receipt` 0.6.1. All shipped imports remain within
|
|
38
|
+
declared package boundaries.
|
|
39
|
+
|
|
7
40
|
## 0.10.0 (2026-07-16)
|
|
8
41
|
|
|
9
42
|
### Added
|
package/README.md
CHANGED
|
@@ -295,6 +295,26 @@ challenge. The Gate composes that and adds the lifecycle controls a firewall nee
|
|
|
295
295
|
can establish a declared surface as `gated`; a passive network witness alone is always
|
|
296
296
|
`witness_only`. Inventory completeness remains an explicit relying-party assumption.
|
|
297
297
|
|
|
298
|
+
## Action Escrow
|
|
299
|
+
|
|
300
|
+
Action Escrow is the Gate profile for a two-party agreement whose downstream
|
|
301
|
+
release must obey the exact final document. The customer application supplies
|
|
302
|
+
the signed agreement, material terms, party acceptances, funding evidence,
|
|
303
|
+
milestone evidence, and action-specific release approvals. Gate verifies and
|
|
304
|
+
binds those inputs, advances a signed lifecycle, and consumes the release once.
|
|
305
|
+
Each release approval is a standard `EP-RESOLUTION-v1` WebAuthn record over a
|
|
306
|
+
canonical binding moment. Gate independently pins the approval option,
|
|
307
|
+
initiator, per-party nonce, evaluation time, exact action digest, and the
|
|
308
|
+
document and milestone-evidence digests rendered to the approver.
|
|
309
|
+
|
|
310
|
+
The public modules are `action-escrow`, `action-escrow-state`,
|
|
311
|
+
`action-escrow-postgres`, `action-escrow-custodian`,
|
|
312
|
+
`action-escrow-package`, and `action-escrow-verifiers`. A licensed external
|
|
313
|
+
provider holds or moves funds; EMILIA does not take custody, inspect work,
|
|
314
|
+
adjudicate disputes, or make an agreement legally enforceable. An ambiguous
|
|
315
|
+
provider outcome enters reconciliation and is never retried as though nothing
|
|
316
|
+
happened.
|
|
317
|
+
|
|
298
318
|
## Production custody
|
|
299
319
|
|
|
300
320
|
The three things a serious buyer (CISO, auditor, insurer) asks after the demo:
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Typed bridge between the Action Escrow kernel and an authenticated external
|
|
4
|
+
* custodian adapter. Provider observations are signed by the deployment
|
|
5
|
+
* operator because TLS API responses are not portable offline evidence.
|
|
6
|
+
*/
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { canonicalize, hashCanonical } from './execution-binding.js';
|
|
9
|
+
import { validateActionEscrowReleaseTemplate } from './action-escrow-verifiers.js';
|
|
10
|
+
|
|
11
|
+
export const ACTION_ESCROW_CUSTODIAN_OBSERVATION_VERSION =
|
|
12
|
+
'EP-ACTION-ESCROW-CUSTODIAN-OBSERVATION-v1';
|
|
13
|
+
const DOMAIN = `${ACTION_ESCROW_CUSTODIAN_OBSERVATION_VERSION}\0`;
|
|
14
|
+
const HASH = /^sha256:[0-9a-f]{64}$/;
|
|
15
|
+
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
18
|
+
const prototype = Object.getPrototypeOf(value);
|
|
19
|
+
return prototype === Object.prototype || prototype === null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function canonicalCopy(value) {
|
|
23
|
+
return JSON.parse(canonicalize(value));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canonicalDigest(value) {
|
|
27
|
+
return `sha256:${hashCanonical(value)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validString(value, max = 512) {
|
|
31
|
+
return typeof value === 'string'
|
|
32
|
+
&& value.length > 0
|
|
33
|
+
&& value.length <= max
|
|
34
|
+
&& !/[\u0000-\u001f\u007f]/.test(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function strictInstant(value) {
|
|
38
|
+
if (typeof value !== 'string') return false;
|
|
39
|
+
const parsed = Date.parse(value);
|
|
40
|
+
return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function deepFreeze(value) {
|
|
44
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
45
|
+
Object.freeze(value);
|
|
46
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function signingBytes(payload) {
|
|
51
|
+
return Buffer.concat([
|
|
52
|
+
Buffer.from(DOMAIN, 'utf8'),
|
|
53
|
+
Buffer.from(canonicalize(payload), 'utf8'),
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function requestScope(request) {
|
|
58
|
+
const { request_digest: _digest, ...scope } = request;
|
|
59
|
+
// The digest identifies the reserved mutation. GET is only a reconciliation
|
|
60
|
+
// transport for that same POST and must not create a second request identity.
|
|
61
|
+
return { ...scope, method: 'POST' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeKernelRequest(value, adapter) {
|
|
65
|
+
try {
|
|
66
|
+
if (!isRecord(value)
|
|
67
|
+
|| (value.method !== 'POST' && value.method !== 'GET')
|
|
68
|
+
|| value.provider_id !== adapter.provider
|
|
69
|
+
|| value.profile?.provider_id !== adapter.provider
|
|
70
|
+
|| !HASH.test(value.agreement_digest)
|
|
71
|
+
|| !HASH.test(value.document_action_binding_digest)
|
|
72
|
+
|| !HASH.test(value.release_action_digest)
|
|
73
|
+
|| !HASH.test(value.parties_digest)
|
|
74
|
+
|| !HASH.test(value.profile_digest)
|
|
75
|
+
|| !HASH.test(value.document_digest)
|
|
76
|
+
|| !HASH.test(value.request_digest)
|
|
77
|
+
|| !validString(value.milestone_id, 256)
|
|
78
|
+
|| !validString(value.idempotency_key, 256)
|
|
79
|
+
|| !isRecord(value.release_action_template)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const template = validateActionEscrowReleaseTemplate(
|
|
83
|
+
value.release_action_template,
|
|
84
|
+
{
|
|
85
|
+
profileDigest: value.profile_digest,
|
|
86
|
+
agreementId: value.agreement_id,
|
|
87
|
+
agreementDigest: value.agreement_digest,
|
|
88
|
+
milestoneId: value.milestone_id,
|
|
89
|
+
documentDigest: value.document_digest,
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
if (!template
|
|
93
|
+
|| template.custodian_provider !== adapter.provider
|
|
94
|
+
|| template.custodian_environment !== adapter.environment
|
|
95
|
+
|| canonicalDigest({
|
|
96
|
+
'@version': 'EP-ACTION-ESCROW-PROVIDER-REQUEST-v1',
|
|
97
|
+
...requestScope(value),
|
|
98
|
+
}) !== value.request_digest) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return canonicalCopy(value);
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function transactionMatches(transaction, request) {
|
|
108
|
+
if (!isRecord(transaction)
|
|
109
|
+
|| transaction.transaction_id
|
|
110
|
+
!== request.release_action_template.custodian_transaction_id
|
|
111
|
+
|| transaction.currency !== request.release_action_template.currency
|
|
112
|
+
|| !Array.isArray(transaction.milestones)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const milestone = transaction.milestones.find(
|
|
116
|
+
(entry) => entry.provider_item_id
|
|
117
|
+
=== request.release_action_template.custodian_milestone_id,
|
|
118
|
+
);
|
|
119
|
+
if (!milestone || !Array.isArray(milestone.schedules) || milestone.schedules.length !== 1) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const schedule = milestone.schedules[0];
|
|
123
|
+
return schedule.amount === request.release_action_template.amount
|
|
124
|
+
&& schedule.beneficiary_customer === request.release_action_template.destination_id;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function providerResultMatches(result, request, adapter, operation) {
|
|
128
|
+
return isRecord(result)
|
|
129
|
+
&& result.provider === adapter.provider
|
|
130
|
+
&& result.environment === adapter.environment
|
|
131
|
+
&& result.operation === operation
|
|
132
|
+
&& result.transaction_id
|
|
133
|
+
=== request.release_action_template.custodian_transaction_id
|
|
134
|
+
&& (operation === 'reconcile_transaction'
|
|
135
|
+
|| (
|
|
136
|
+
result.milestone_id
|
|
137
|
+
=== request.release_action_template.custodian_milestone_id
|
|
138
|
+
&& result.effect_reference === request.idempotency_key
|
|
139
|
+
));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function transactionFromResult(result) {
|
|
143
|
+
return isRecord(result?.transaction) ? result.transaction : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function observationStatus(result) {
|
|
147
|
+
if (result.kind === 'released') return 'released';
|
|
148
|
+
if (result.kind !== 'provider_action_required') return null;
|
|
149
|
+
if (result.provider_phase === 'not_accepted') return 'not_released';
|
|
150
|
+
if (result.provider_phase === 'accepted_pending_disbursement') return 'pending';
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizePrivateKey(value) {
|
|
155
|
+
const key = value instanceof crypto.KeyObject ? value : crypto.createPrivateKey(value);
|
|
156
|
+
if (key.asymmetricKeyType !== 'ed25519') {
|
|
157
|
+
throw new TypeError('custodian observation signer must use Ed25519');
|
|
158
|
+
}
|
|
159
|
+
return key;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The bridge implements the kernel's release/getRelease contract. It never
|
|
164
|
+
* claims that EMILIA holds funds or that the external provider is licensed.
|
|
165
|
+
*/
|
|
166
|
+
export function createActionEscrowCustodianBridge({
|
|
167
|
+
adapter,
|
|
168
|
+
observationSigner,
|
|
169
|
+
now = () => new Date().toISOString(),
|
|
170
|
+
} = {}) {
|
|
171
|
+
if (!isRecord(adapter)
|
|
172
|
+
|| adapter.kind !== 'external_custodian'
|
|
173
|
+
|| !validString(adapter.provider, 128)
|
|
174
|
+
|| !['sandbox', 'production'].includes(adapter.environment)
|
|
175
|
+
|| typeof adapter.reconcileTransaction !== 'function'
|
|
176
|
+
|| typeof adapter.releaseMilestone !== 'function'
|
|
177
|
+
|| typeof adapter.requestMilestoneDisbursement !== 'function'
|
|
178
|
+
|| !isRecord(observationSigner)
|
|
179
|
+
|| !validString(observationSigner.key_id, 256)
|
|
180
|
+
|| typeof now !== 'function') {
|
|
181
|
+
throw new TypeError('external custodian adapter and observation signer are required');
|
|
182
|
+
}
|
|
183
|
+
const privateKey = normalizePrivateKey(observationSigner.privateKey);
|
|
184
|
+
|
|
185
|
+
async function preflight(request) {
|
|
186
|
+
const reconciled = await adapter.reconcileTransaction({
|
|
187
|
+
transactionId: request.release_action_template.custodian_transaction_id,
|
|
188
|
+
});
|
|
189
|
+
if (!providerResultMatches(
|
|
190
|
+
reconciled,
|
|
191
|
+
request,
|
|
192
|
+
adapter,
|
|
193
|
+
'reconcile_transaction',
|
|
194
|
+
)
|
|
195
|
+
|| reconciled.kind !== 'reconciled'
|
|
196
|
+
|| !transactionMatches(reconciled.transaction, request)) {
|
|
197
|
+
throw new Error('custodian transaction does not match the signed release action');
|
|
198
|
+
}
|
|
199
|
+
return reconciled.transaction;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function signObservation(request, status, result) {
|
|
203
|
+
const observedAt = now();
|
|
204
|
+
if (!strictInstant(observedAt)) throw new Error('invalid custodian observation clock');
|
|
205
|
+
const transaction = transactionFromResult(result);
|
|
206
|
+
const payload = {
|
|
207
|
+
'@version': ACTION_ESCROW_CUSTODIAN_OBSERVATION_VERSION,
|
|
208
|
+
provider_id: result.provider,
|
|
209
|
+
environment: result.environment,
|
|
210
|
+
statement_type: 'release',
|
|
211
|
+
status,
|
|
212
|
+
agreement_digest: request.agreement_digest,
|
|
213
|
+
document_action_binding_digest: request.document_action_binding_digest,
|
|
214
|
+
milestone_id: request.milestone_id,
|
|
215
|
+
release_action_digest: request.release_action_digest,
|
|
216
|
+
parties_digest: request.parties_digest,
|
|
217
|
+
profile_digest: request.profile_digest,
|
|
218
|
+
provider_idempotency_key: request.idempotency_key,
|
|
219
|
+
provider_request_digest: request.request_digest,
|
|
220
|
+
provider_effect_reference: result.effect_reference,
|
|
221
|
+
provider_transaction_id: result.transaction_id,
|
|
222
|
+
provider_milestone_id: result.milestone_id,
|
|
223
|
+
amount: request.release_action_template.amount,
|
|
224
|
+
currency: request.release_action_template.currency,
|
|
225
|
+
destination_id: request.release_action_template.destination_id,
|
|
226
|
+
provider_snapshot_digest: canonicalDigest(transaction),
|
|
227
|
+
observed_at: observedAt,
|
|
228
|
+
};
|
|
229
|
+
const signature = crypto.sign(null, signingBytes(payload), privateKey);
|
|
230
|
+
return deepFreeze({
|
|
231
|
+
payload,
|
|
232
|
+
signature: {
|
|
233
|
+
algorithm: 'Ed25519',
|
|
234
|
+
key_id: observationSigner.key_id,
|
|
235
|
+
value: signature.toString('base64url'),
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return Object.freeze({
|
|
241
|
+
provider: adapter.provider,
|
|
242
|
+
environment: adapter.environment,
|
|
243
|
+
async release(untrustedRequest) {
|
|
244
|
+
const request = normalizeKernelRequest(untrustedRequest, adapter);
|
|
245
|
+
if (!request || request.method !== 'POST') {
|
|
246
|
+
throw new Error('invalid kernel release request');
|
|
247
|
+
}
|
|
248
|
+
await preflight(request);
|
|
249
|
+
const result = await adapter.releaseMilestone({
|
|
250
|
+
effectReference: request.idempotency_key,
|
|
251
|
+
transactionId: request.release_action_template.custodian_transaction_id,
|
|
252
|
+
milestoneId: request.release_action_template.custodian_milestone_id,
|
|
253
|
+
});
|
|
254
|
+
if (!providerResultMatches(
|
|
255
|
+
result,
|
|
256
|
+
request,
|
|
257
|
+
adapter,
|
|
258
|
+
'release_milestone',
|
|
259
|
+
)
|
|
260
|
+
|| !['released', 'release_submitted', 'provider_action_required'].includes(result.kind)
|
|
261
|
+
|| (
|
|
262
|
+
['released', 'release_submitted'].includes(result.kind)
|
|
263
|
+
&& !transactionMatches(transactionFromResult(result), request)
|
|
264
|
+
)) {
|
|
265
|
+
throw new Error('custodian release outcome is not authoritative');
|
|
266
|
+
}
|
|
267
|
+
return { accepted: true };
|
|
268
|
+
},
|
|
269
|
+
async getRelease(untrustedRequest) {
|
|
270
|
+
const request = normalizeKernelRequest(untrustedRequest, adapter);
|
|
271
|
+
if (!request || request.method !== 'GET') {
|
|
272
|
+
throw new Error('invalid kernel reconciliation request');
|
|
273
|
+
}
|
|
274
|
+
await preflight(request);
|
|
275
|
+
const result = await adapter.requestMilestoneDisbursement({
|
|
276
|
+
effectReference: request.idempotency_key,
|
|
277
|
+
transactionId: request.release_action_template.custodian_transaction_id,
|
|
278
|
+
milestoneId: request.release_action_template.custodian_milestone_id,
|
|
279
|
+
});
|
|
280
|
+
const status = observationStatus(result);
|
|
281
|
+
const transaction = transactionFromResult(result);
|
|
282
|
+
if (!providerResultMatches(
|
|
283
|
+
result,
|
|
284
|
+
request,
|
|
285
|
+
adapter,
|
|
286
|
+
'request_milestone_disbursement',
|
|
287
|
+
)
|
|
288
|
+
|| !status
|
|
289
|
+
|| !transaction
|
|
290
|
+
|| !transactionMatches(transaction, request)) {
|
|
291
|
+
throw new Error('custodian release state is indeterminate');
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
authenticated: true,
|
|
295
|
+
statement: signObservation(request, status, result),
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function createActionEscrowCustodianStatementVerifier({
|
|
302
|
+
operatorKeys,
|
|
303
|
+
providerId,
|
|
304
|
+
environment,
|
|
305
|
+
} = {}) {
|
|
306
|
+
if (!isRecord(operatorKeys)
|
|
307
|
+
|| !validString(providerId, 128)
|
|
308
|
+
|| !['sandbox', 'production'].includes(environment)) {
|
|
309
|
+
throw new TypeError('pinned operator keys and provider identity are required');
|
|
310
|
+
}
|
|
311
|
+
const pins = canonicalCopy(operatorKeys);
|
|
312
|
+
return async function verifyStatement(statement, expected) {
|
|
313
|
+
try {
|
|
314
|
+
if (!isRecord(statement)
|
|
315
|
+
|| !isRecord(statement.payload)
|
|
316
|
+
|| !isRecord(statement.signature)
|
|
317
|
+
|| statement.signature.algorithm !== 'Ed25519'
|
|
318
|
+
|| !validString(statement.signature.key_id, 256)
|
|
319
|
+
|| !validString(statement.signature.value, 1024)) {
|
|
320
|
+
return { valid: false, reason: 'malformed_custodian_observation' };
|
|
321
|
+
}
|
|
322
|
+
const pin = pins[statement.signature.key_id];
|
|
323
|
+
if (!isRecord(pin) || typeof pin.public_key !== 'string') {
|
|
324
|
+
return { valid: false, reason: 'custodian_operator_key_not_pinned' };
|
|
325
|
+
}
|
|
326
|
+
const publicKey = crypto.createPublicKey({
|
|
327
|
+
key: Buffer.from(pin.public_key, 'base64url'),
|
|
328
|
+
type: 'spki',
|
|
329
|
+
format: 'der',
|
|
330
|
+
});
|
|
331
|
+
if (publicKey.asymmetricKeyType !== 'ed25519'
|
|
332
|
+
|| !crypto.verify(
|
|
333
|
+
null,
|
|
334
|
+
signingBytes(statement.payload),
|
|
335
|
+
publicKey,
|
|
336
|
+
Buffer.from(statement.signature.value, 'base64url'),
|
|
337
|
+
)) {
|
|
338
|
+
return { valid: false, reason: 'custodian_observation_signature_invalid' };
|
|
339
|
+
}
|
|
340
|
+
const payload = statement.payload;
|
|
341
|
+
const exact = payload['@version'] === ACTION_ESCROW_CUSTODIAN_OBSERVATION_VERSION
|
|
342
|
+
&& payload.provider_id === providerId
|
|
343
|
+
&& payload.environment === environment
|
|
344
|
+
&& payload.statement_type === expected.statement_type
|
|
345
|
+
&& payload.agreement_digest === expected.agreement_digest
|
|
346
|
+
&& payload.document_action_binding_digest
|
|
347
|
+
=== expected.document_action_binding_digest
|
|
348
|
+
&& payload.milestone_id === expected.milestone_id
|
|
349
|
+
&& payload.release_action_digest === expected.release_action_digest
|
|
350
|
+
&& payload.parties_digest === expected.parties_digest
|
|
351
|
+
&& payload.profile_digest === expected.profile_digest
|
|
352
|
+
&& payload.provider_idempotency_key === expected.provider_idempotency_key
|
|
353
|
+
&& payload.provider_request_digest === expected.provider_request_digest
|
|
354
|
+
&& payload.provider_effect_reference === expected.provider_idempotency_key
|
|
355
|
+
&& payload.provider_transaction_id === expected.provider_transaction_id
|
|
356
|
+
&& payload.provider_milestone_id === expected.provider_milestone_id
|
|
357
|
+
&& payload.amount === expected.amount
|
|
358
|
+
&& payload.currency === expected.currency
|
|
359
|
+
&& payload.destination_id === expected.destination_id
|
|
360
|
+
&& ['released', 'not_released', 'pending'].includes(payload.status)
|
|
361
|
+
&& HASH.test(payload.provider_snapshot_digest)
|
|
362
|
+
&& strictInstant(payload.observed_at);
|
|
363
|
+
if (!exact) return { valid: false, reason: 'custodian_observation_binding_mismatch' };
|
|
364
|
+
return {
|
|
365
|
+
valid: true,
|
|
366
|
+
authenticated: true,
|
|
367
|
+
statement_type: payload.statement_type,
|
|
368
|
+
status: payload.status,
|
|
369
|
+
statement_digest: canonicalDigest(statement),
|
|
370
|
+
provider_id: payload.provider_id,
|
|
371
|
+
agreement_digest: payload.agreement_digest,
|
|
372
|
+
document_action_binding_digest: payload.document_action_binding_digest,
|
|
373
|
+
milestone_id: payload.milestone_id,
|
|
374
|
+
release_action_digest: payload.release_action_digest,
|
|
375
|
+
parties_digest: payload.parties_digest,
|
|
376
|
+
profile_digest: payload.profile_digest,
|
|
377
|
+
provider_idempotency_key: payload.provider_idempotency_key,
|
|
378
|
+
provider_request_digest: payload.provider_request_digest,
|
|
379
|
+
provider_transaction_id: payload.provider_transaction_id,
|
|
380
|
+
provider_milestone_id: payload.provider_milestone_id,
|
|
381
|
+
amount: payload.amount,
|
|
382
|
+
currency: payload.currency,
|
|
383
|
+
destination_id: payload.destination_id,
|
|
384
|
+
};
|
|
385
|
+
} catch {
|
|
386
|
+
return { valid: false, reason: 'malformed_custodian_observation' };
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export default Object.freeze({
|
|
392
|
+
ACTION_ESCROW_CUSTODIAN_OBSERVATION_VERSION,
|
|
393
|
+
createActionEscrowCustodianBridge,
|
|
394
|
+
createActionEscrowCustodianStatementVerifier,
|
|
395
|
+
});
|