@emilia-protocol/gate 0.9.5 → 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 +103 -0
- package/LICENSE +190 -0
- package/README.md +95 -16
- package/action-control-manifest.js +26 -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/adapters/_kit.js +47 -5
- package/adapters/aws.js +15 -4
- package/adapters/github-demo.mjs +25 -22
- package/adapters/github.js +1 -1
- package/adapters/jira.js +1 -0
- package/adapters/linear.js +1 -0
- package/adapters/salesforce.js +1 -0
- package/adapters/stripe.js +1 -1
- package/adapters/supabase.js +26 -4
- package/adapters/vercel.js +33 -4
- package/aec-execution.js +1 -3
- package/breakglass.js +285 -51
- package/control-plane.js +341 -0
- package/coverage.js +722 -0
- package/custody-demo.mjs +13 -3
- package/demo.mjs +9 -3
- package/deploy/helm/README.md +16 -8
- package/deploy/helm/emilia-gate/Chart.yaml +3 -1
- package/deploy/helm/emilia-gate/templates/NOTES.txt +3 -2
- package/deploy/helm/emilia-gate/templates/deployment.yaml +55 -1
- package/deploy/helm/emilia-gate/values.yaml +18 -2
- package/deploy/helm/emilia-gate-service/Chart.yaml +11 -0
- package/deploy/helm/emilia-gate-service/README.md +81 -0
- package/deploy/helm/emilia-gate-service/templates/NOTES.txt +12 -0
- package/deploy/helm/emilia-gate-service/templates/_helpers.tpl +83 -0
- package/deploy/helm/emilia-gate-service/templates/deployment.yaml +153 -0
- package/deploy/helm/emilia-gate-service/templates/migration-job.yaml +73 -0
- package/deploy/helm/emilia-gate-service/templates/networkpolicy.yaml +113 -0
- package/deploy/helm/emilia-gate-service/templates/pdb.yaml +14 -0
- package/deploy/helm/emilia-gate-service/templates/service.yaml +20 -0
- package/deploy/helm/emilia-gate-service/templates/serviceaccount.yaml +8 -0
- package/deploy/helm/emilia-gate-service/tests/fixtures/001_gate.sql +26 -0
- package/deploy/helm/emilia-gate-service/tests/fixtures/002_runtime_access.sql +32 -0
- package/deploy/helm/emilia-gate-service/tests/fixtures/gate.config.mjs +29 -0
- package/deploy/helm/emilia-gate-service/tests/render-check.sh +145 -0
- package/deploy/helm/emilia-gate-service/values.schema.json +145 -0
- package/deploy/helm/emilia-gate-service/values.yaml +166 -0
- package/deploy/sql/001-runtime.sql +707 -0
- package/deploy/terraform/README.md +24 -14
- package/deploy/terraform/main.tf +59 -0
- package/deploy/terraform/service/README.md +81 -0
- package/deploy/terraform/service/main.tf +718 -0
- package/deploy/terraform/service/outputs.tf +19 -0
- package/deploy/terraform/service/tests/validate.sh +24 -0
- package/deploy/terraform/service/tests/validation.tftest.hcl +168 -0
- package/deploy/terraform/service/variables.tf +459 -0
- package/deploy/terraform/service/versions.tf +10 -0
- package/deploy/terraform/variables.tf +95 -2
- package/deployment-attestation.js +248 -0
- package/eg1-conformance.js +46 -12
- package/enterprise.js +6 -2
- package/ep-assure.mjs +3 -2
- package/evidence-postgres.js +357 -0
- package/evidence.js +10 -3
- package/execution-binding.js +141 -26
- package/index.js +556 -115
- package/key-registry.js +51 -19
- package/mcp.js +4 -2
- package/network-witness.js +500 -0
- package/package.json +39 -5
- package/reliance-kernel.js +5 -6
- package/reliance-packet.js +43 -10
- package/reports/assurance-package.js +45 -19
- package/reports/reperform.js +78 -18
- package/reports/underwriter.js +1 -1
- package/settlement.js +300 -0
- package/store-postgres.js +14 -0
- package/store.js +26 -5
- package/strict-json.js +103 -0
- package/witness-postgres.js +97 -0
|
@@ -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
|
+
});
|