@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,332 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Adapters that turn independently verified protocol artifacts into the exact
|
|
4
|
+
* result contract consumed by the Action Escrow state kernel.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
verifyDocumentActionBinding,
|
|
8
|
+
} from '@emilia-protocol/verify/document-action-binding';
|
|
9
|
+
import { canonicalize, hashCanonical } from './execution-binding.js';
|
|
10
|
+
|
|
11
|
+
export const ACTION_ESCROW_AGREEMENT_DIGEST_VERSION =
|
|
12
|
+
'EP-ACTION-ESCROW-AGREEMENT-DIGEST-v1';
|
|
13
|
+
|
|
14
|
+
const HASH = /^sha256:[0-9a-f]{64}$/;
|
|
15
|
+
const AMOUNT = /^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/;
|
|
16
|
+
const CURRENCY = /^[A-Z]{3}$/;
|
|
17
|
+
export const ACTION_ESCROW_REQUIRED_MATERIAL_TERM_IDS = Object.freeze([
|
|
18
|
+
'amendment_version',
|
|
19
|
+
'completion_requirements_digest',
|
|
20
|
+
'document_authorizes_payment',
|
|
21
|
+
'milestone_name',
|
|
22
|
+
'payee_id',
|
|
23
|
+
'release.amount',
|
|
24
|
+
'release.destination_id',
|
|
25
|
+
'release.milestone_id',
|
|
26
|
+
'release_requires_mutual_approval',
|
|
27
|
+
'retainage_amount',
|
|
28
|
+
]);
|
|
29
|
+
const RELEASE_TEMPLATE_KEYS = new Set([
|
|
30
|
+
'action_type',
|
|
31
|
+
'action_escrow_profile_digest',
|
|
32
|
+
'agreement_id',
|
|
33
|
+
'agreement_digest',
|
|
34
|
+
'milestone_id',
|
|
35
|
+
'amount',
|
|
36
|
+
'currency',
|
|
37
|
+
'destination_id',
|
|
38
|
+
'payee_id',
|
|
39
|
+
'custodian_provider',
|
|
40
|
+
'custodian_environment',
|
|
41
|
+
'custodian_transaction_id',
|
|
42
|
+
'custodian_milestone_id',
|
|
43
|
+
'document_sha256',
|
|
44
|
+
'material_terms_sha256',
|
|
45
|
+
'completion_evidence_sha256',
|
|
46
|
+
'amendment_version',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function isRecord(value) {
|
|
50
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
51
|
+
const prototype = Object.getPrototypeOf(value);
|
|
52
|
+
return prototype === Object.prototype || prototype === null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function canonicalCopy(value) {
|
|
56
|
+
return JSON.parse(canonicalize(value));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function deepFreeze(value) {
|
|
60
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
61
|
+
Object.freeze(value);
|
|
62
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validString(value, max = 256) {
|
|
67
|
+
return typeof value === 'string'
|
|
68
|
+
&& value.length > 0
|
|
69
|
+
&& value.length <= max
|
|
70
|
+
&& !/[\u0000-\u001f\u007f]/.test(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sortedParties(parties) {
|
|
74
|
+
if (!Array.isArray(parties)) return null;
|
|
75
|
+
try {
|
|
76
|
+
return canonicalCopy(parties).sort((left, right) => {
|
|
77
|
+
const leftKey = `${left.role}\u0000${left.party_id}`;
|
|
78
|
+
const rightKey = `${right.role}\u0000${right.party_id}`;
|
|
79
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
80
|
+
});
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function bytesCopy(value) {
|
|
87
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
88
|
+
return Uint8Array.from(value);
|
|
89
|
+
}
|
|
90
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value.slice(0));
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function refusal(reason) {
|
|
95
|
+
return Object.freeze({
|
|
96
|
+
valid: false,
|
|
97
|
+
reason,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function computeActionEscrowAgreementDigest(agreementId) {
|
|
102
|
+
if (!validString(agreementId)) return null;
|
|
103
|
+
try {
|
|
104
|
+
return `sha256:${hashCanonical({
|
|
105
|
+
'@version': ACTION_ESCROW_AGREEMENT_DIGEST_VERSION,
|
|
106
|
+
agreement_id: agreementId,
|
|
107
|
+
})}`;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function exactExpected(value) {
|
|
114
|
+
if (!isRecord(value)
|
|
115
|
+
|| !HASH.test(value.agreement_digest)
|
|
116
|
+
|| !HASH.test(value.document_action_binding_digest)
|
|
117
|
+
|| !HASH.test(value.release_action_digest)
|
|
118
|
+
|| !HASH.test(value.parties_digest)
|
|
119
|
+
|| !HASH.test(value.profile_digest)
|
|
120
|
+
|| !validString(value.milestone_id)
|
|
121
|
+
|| !Array.isArray(value.parties)) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const parties = sortedParties(value.parties);
|
|
125
|
+
if (!parties || parties.length < 2) return null;
|
|
126
|
+
return {
|
|
127
|
+
agreement_digest: value.agreement_digest,
|
|
128
|
+
document_action_binding_digest: value.document_action_binding_digest,
|
|
129
|
+
release_action_digest: value.release_action_digest,
|
|
130
|
+
milestone_id: value.milestone_id,
|
|
131
|
+
parties,
|
|
132
|
+
parties_digest: value.parties_digest,
|
|
133
|
+
profile_digest: value.profile_digest,
|
|
134
|
+
supersedes_document_action_binding_digest:
|
|
135
|
+
value.supersedes_document_action_binding_digest ?? null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function validateActionEscrowReleaseTemplate(template, {
|
|
140
|
+
profileDigest,
|
|
141
|
+
agreementId,
|
|
142
|
+
agreementDigest,
|
|
143
|
+
milestoneId,
|
|
144
|
+
documentDigest,
|
|
145
|
+
materialTerms,
|
|
146
|
+
} = {}) {
|
|
147
|
+
if (!isRecord(template)
|
|
148
|
+
|| Object.keys(template).length !== RELEASE_TEMPLATE_KEYS.size
|
|
149
|
+
|| Object.keys(template).some((key) => !RELEASE_TEMPLATE_KEYS.has(key))
|
|
150
|
+
|| template.action_type !== 'escrow.milestone.release'
|
|
151
|
+
|| template.action_escrow_profile_digest !== profileDigest
|
|
152
|
+
|| template.agreement_id !== agreementId
|
|
153
|
+
|| template.agreement_digest !== agreementDigest
|
|
154
|
+
|| template.milestone_id !== milestoneId
|
|
155
|
+
|| typeof template.amount !== 'string'
|
|
156
|
+
|| !AMOUNT.test(template.amount)
|
|
157
|
+
|| typeof template.currency !== 'string'
|
|
158
|
+
|| !CURRENCY.test(template.currency)
|
|
159
|
+
|| !validString(template.destination_id, 512)
|
|
160
|
+
|| !validString(template.payee_id, 512)
|
|
161
|
+
|| !validString(template.custodian_provider, 128)
|
|
162
|
+
|| !['sandbox', 'production'].includes(template.custodian_environment)
|
|
163
|
+
|| !validString(template.custodian_transaction_id, 256)
|
|
164
|
+
|| !validString(template.custodian_milestone_id, 256)
|
|
165
|
+
|| template.document_sha256 !== documentDigest
|
|
166
|
+
|| !HASH.test(template.material_terms_sha256)
|
|
167
|
+
|| (materialTerms !== undefined
|
|
168
|
+
&& template.material_terms_sha256 !== `sha256:${hashCanonical(materialTerms)}`)
|
|
169
|
+
|| !HASH.test(template.completion_evidence_sha256)
|
|
170
|
+
|| !Number.isSafeInteger(template.amendment_version)
|
|
171
|
+
|| template.amendment_version < 1) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const copy = canonicalCopy(template);
|
|
176
|
+
return Buffer.byteLength(canonicalize(copy), 'utf8') <= 64 * 1024 ? copy : null;
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function exactActionTemplate(binding, expected) {
|
|
183
|
+
return validateActionEscrowReleaseTemplate(binding?.release_action?.template, {
|
|
184
|
+
profileDigest: expected.profile_digest,
|
|
185
|
+
agreementId: binding?.agreement_id,
|
|
186
|
+
agreementDigest: expected.agreement_digest,
|
|
187
|
+
milestoneId: expected.milestone_id,
|
|
188
|
+
documentDigest: binding?.document?.digest,
|
|
189
|
+
materialTerms: binding?.material_terms,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function materialTermsMatchAction(binding, template) {
|
|
194
|
+
if (!Array.isArray(binding?.material_terms)) return false;
|
|
195
|
+
const terms = new Map(binding.material_terms.map((term) => [term.term_id, term]));
|
|
196
|
+
const amount = terms.get('release.amount');
|
|
197
|
+
const destination = terms.get('release.destination_id');
|
|
198
|
+
const milestone = terms.get('release.milestone_id');
|
|
199
|
+
const amendment = terms.get('amendment_version');
|
|
200
|
+
const completionRequirements = terms.get('completion_requirements_digest');
|
|
201
|
+
const documentAuthorizesPayment = terms.get('document_authorizes_payment');
|
|
202
|
+
const milestoneName = terms.get('milestone_name');
|
|
203
|
+
const payee = terms.get('payee_id');
|
|
204
|
+
const releaseRequiresMutualApproval = terms.get('release_requires_mutual_approval');
|
|
205
|
+
const retainage = terms.get('retainage_amount');
|
|
206
|
+
return amount?.type === 'amount'
|
|
207
|
+
&& amount.value === template.amount
|
|
208
|
+
&& amount.currency === template.currency
|
|
209
|
+
&& ['identifier', 'string'].includes(destination?.type)
|
|
210
|
+
&& destination.value === template.destination_id
|
|
211
|
+
&& ['identifier', 'string'].includes(milestone?.type)
|
|
212
|
+
&& milestone.value === template.milestone_id
|
|
213
|
+
&& amendment?.type === 'integer'
|
|
214
|
+
&& amendment.value === template.amendment_version
|
|
215
|
+
&& completionRequirements?.type === 'digest'
|
|
216
|
+
&& HASH.test(completionRequirements.value)
|
|
217
|
+
&& documentAuthorizesPayment?.type === 'boolean'
|
|
218
|
+
&& documentAuthorizesPayment.value === false
|
|
219
|
+
&& milestoneName?.type === 'string'
|
|
220
|
+
&& validString(milestoneName.value, 2048)
|
|
221
|
+
&& payee?.type === 'identifier'
|
|
222
|
+
&& payee.value === template.payee_id
|
|
223
|
+
&& releaseRequiresMutualApproval?.type === 'boolean'
|
|
224
|
+
&& releaseRequiresMutualApproval.value === true
|
|
225
|
+
&& retainage?.type === 'amount'
|
|
226
|
+
&& retainage.currency === template.currency;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Create a kernel verifier backed by the public DAB verifier.
|
|
231
|
+
*
|
|
232
|
+
* The document resolver is mandatory: mapping authenticity without checking
|
|
233
|
+
* the final bytes is insufficient for a release decision.
|
|
234
|
+
*/
|
|
235
|
+
export function createActionEscrowDocumentBindingVerifier({
|
|
236
|
+
issuerKeys,
|
|
237
|
+
resolveDocumentBytes,
|
|
238
|
+
allowedMediaTypes = ['application/pdf'],
|
|
239
|
+
allowedPartyRoles = ['client', 'contractor'],
|
|
240
|
+
now = Date.now,
|
|
241
|
+
} = {}) {
|
|
242
|
+
if (!isRecord(issuerKeys)
|
|
243
|
+
|| Object.keys(issuerKeys).length === 0
|
|
244
|
+
|| typeof resolveDocumentBytes !== 'function'
|
|
245
|
+
|| typeof now !== 'function') {
|
|
246
|
+
throw new TypeError('pinned DAB keys, document resolver, and clock are required');
|
|
247
|
+
}
|
|
248
|
+
const pinnedIssuerKeys = deepFreeze(canonicalCopy(issuerKeys));
|
|
249
|
+
const mediaTypes = deepFreeze(canonicalCopy(allowedMediaTypes));
|
|
250
|
+
const partyRoles = deepFreeze(canonicalCopy(allowedPartyRoles));
|
|
251
|
+
|
|
252
|
+
return async function verifyForKernel(binding, untrustedExpected) {
|
|
253
|
+
try {
|
|
254
|
+
const expected = exactExpected(untrustedExpected);
|
|
255
|
+
if (!expected) return refusal('invalid_expected_binding_context');
|
|
256
|
+
const evaluationTime = now();
|
|
257
|
+
const options = {
|
|
258
|
+
issuerKeys: pinnedIssuerKeys,
|
|
259
|
+
now: evaluationTime,
|
|
260
|
+
allowedMediaTypes: mediaTypes,
|
|
261
|
+
allowedPartyRoles: partyRoles,
|
|
262
|
+
allowedActionTypes: ['escrow.milestone.release'],
|
|
263
|
+
requiredMaterialTermIds: ACTION_ESCROW_REQUIRED_MATERIAL_TERM_IDS,
|
|
264
|
+
expectedRequiredParties: expected.parties,
|
|
265
|
+
expectedSupersedesDigest:
|
|
266
|
+
expected.supersedes_document_action_binding_digest,
|
|
267
|
+
};
|
|
268
|
+
const authenticated = verifyDocumentActionBinding(binding, options);
|
|
269
|
+
if (!authenticated.valid) return refusal(authenticated.reason);
|
|
270
|
+
if (authenticated.binding_digest !== expected.document_action_binding_digest
|
|
271
|
+
|| authenticated.action_digest !== expected.release_action_digest
|
|
272
|
+
|| computeActionEscrowAgreementDigest(authenticated.agreement_id)
|
|
273
|
+
!== expected.agreement_digest) {
|
|
274
|
+
return refusal('kernel_binding_context_mismatch');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const actionTemplate = exactActionTemplate(binding, expected);
|
|
278
|
+
if (!actionTemplate || !materialTermsMatchAction(binding, actionTemplate)) {
|
|
279
|
+
return refusal('material_action_mapping_mismatch');
|
|
280
|
+
}
|
|
281
|
+
const resolved = await resolveDocumentBytes(deepFreeze({
|
|
282
|
+
agreement_id: authenticated.agreement_id,
|
|
283
|
+
binding_id: authenticated.binding_id,
|
|
284
|
+
binding_digest: authenticated.binding_digest,
|
|
285
|
+
document_digest: authenticated.document_digest,
|
|
286
|
+
document_media_type: binding.document.media_type,
|
|
287
|
+
document_byte_length: binding.document.byte_length,
|
|
288
|
+
}));
|
|
289
|
+
const documentBytes = bytesCopy(resolved);
|
|
290
|
+
if (!documentBytes) return refusal('document_bytes_unavailable');
|
|
291
|
+
const verified = verifyDocumentActionBinding(binding, {
|
|
292
|
+
...options,
|
|
293
|
+
documentBytes,
|
|
294
|
+
documentMediaType: binding.document.media_type,
|
|
295
|
+
releaseActionTemplate: actionTemplate,
|
|
296
|
+
});
|
|
297
|
+
if (!verified.valid) return refusal(verified.reason);
|
|
298
|
+
|
|
299
|
+
return deepFreeze({
|
|
300
|
+
valid: true,
|
|
301
|
+
reason: 'valid',
|
|
302
|
+
verification_digest: verified.binding_digest,
|
|
303
|
+
document_digest: verified.document_digest,
|
|
304
|
+
agreement_id: verified.agreement_id,
|
|
305
|
+
binding_id: verified.binding_id,
|
|
306
|
+
release_action_template: actionTemplate,
|
|
307
|
+
agreement_digest: expected.agreement_digest,
|
|
308
|
+
document_action_binding_digest: verified.binding_digest,
|
|
309
|
+
milestone_id: expected.milestone_id,
|
|
310
|
+
release_action_digest: verified.action_digest,
|
|
311
|
+
parties_digest: expected.parties_digest,
|
|
312
|
+
profile_digest: expected.profile_digest,
|
|
313
|
+
...(verified.supersedes_digest === null
|
|
314
|
+
? {}
|
|
315
|
+
: {
|
|
316
|
+
supersedes_document_action_binding_digest:
|
|
317
|
+
verified.supersedes_digest,
|
|
318
|
+
}),
|
|
319
|
+
});
|
|
320
|
+
} catch {
|
|
321
|
+
return refusal('document_action_binding_verifier_failed');
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export default Object.freeze({
|
|
327
|
+
ACTION_ESCROW_AGREEMENT_DIGEST_VERSION,
|
|
328
|
+
ACTION_ESCROW_REQUIRED_MATERIAL_TERM_IDS,
|
|
329
|
+
computeActionEscrowAgreementDigest,
|
|
330
|
+
createActionEscrowDocumentBindingVerifier,
|
|
331
|
+
validateActionEscrowReleaseTemplate,
|
|
332
|
+
});
|