@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/LICENSE +190 -0
  3. package/README.md +95 -16
  4. package/action-control-manifest.js +26 -0
  5. package/action-escrow-custodian.js +395 -0
  6. package/action-escrow-evidence.js +859 -0
  7. package/action-escrow-package.js +1669 -0
  8. package/action-escrow-postgres.js +338 -0
  9. package/action-escrow-state.js +397 -0
  10. package/action-escrow-verifiers.js +332 -0
  11. package/action-escrow.js +2448 -0
  12. package/adapters/_kit.js +47 -5
  13. package/adapters/aws.js +15 -4
  14. package/adapters/github-demo.mjs +25 -22
  15. package/adapters/github.js +1 -1
  16. package/adapters/jira.js +1 -0
  17. package/adapters/linear.js +1 -0
  18. package/adapters/salesforce.js +1 -0
  19. package/adapters/stripe.js +1 -1
  20. package/adapters/supabase.js +26 -4
  21. package/adapters/vercel.js +33 -4
  22. package/aec-execution.js +1 -3
  23. package/breakglass.js +285 -51
  24. package/control-plane.js +341 -0
  25. package/coverage.js +722 -0
  26. package/custody-demo.mjs +13 -3
  27. package/demo.mjs +9 -3
  28. package/deploy/helm/README.md +16 -8
  29. package/deploy/helm/emilia-gate/Chart.yaml +3 -1
  30. package/deploy/helm/emilia-gate/templates/NOTES.txt +3 -2
  31. package/deploy/helm/emilia-gate/templates/deployment.yaml +55 -1
  32. package/deploy/helm/emilia-gate/values.yaml +18 -2
  33. package/deploy/helm/emilia-gate-service/Chart.yaml +11 -0
  34. package/deploy/helm/emilia-gate-service/README.md +81 -0
  35. package/deploy/helm/emilia-gate-service/templates/NOTES.txt +12 -0
  36. package/deploy/helm/emilia-gate-service/templates/_helpers.tpl +83 -0
  37. package/deploy/helm/emilia-gate-service/templates/deployment.yaml +153 -0
  38. package/deploy/helm/emilia-gate-service/templates/migration-job.yaml +73 -0
  39. package/deploy/helm/emilia-gate-service/templates/networkpolicy.yaml +113 -0
  40. package/deploy/helm/emilia-gate-service/templates/pdb.yaml +14 -0
  41. package/deploy/helm/emilia-gate-service/templates/service.yaml +20 -0
  42. package/deploy/helm/emilia-gate-service/templates/serviceaccount.yaml +8 -0
  43. package/deploy/helm/emilia-gate-service/tests/fixtures/001_gate.sql +26 -0
  44. package/deploy/helm/emilia-gate-service/tests/fixtures/002_runtime_access.sql +32 -0
  45. package/deploy/helm/emilia-gate-service/tests/fixtures/gate.config.mjs +29 -0
  46. package/deploy/helm/emilia-gate-service/tests/render-check.sh +145 -0
  47. package/deploy/helm/emilia-gate-service/values.schema.json +145 -0
  48. package/deploy/helm/emilia-gate-service/values.yaml +166 -0
  49. package/deploy/sql/001-runtime.sql +707 -0
  50. package/deploy/terraform/README.md +24 -14
  51. package/deploy/terraform/main.tf +59 -0
  52. package/deploy/terraform/service/README.md +81 -0
  53. package/deploy/terraform/service/main.tf +718 -0
  54. package/deploy/terraform/service/outputs.tf +19 -0
  55. package/deploy/terraform/service/tests/validate.sh +24 -0
  56. package/deploy/terraform/service/tests/validation.tftest.hcl +168 -0
  57. package/deploy/terraform/service/variables.tf +459 -0
  58. package/deploy/terraform/service/versions.tf +10 -0
  59. package/deploy/terraform/variables.tf +95 -2
  60. package/deployment-attestation.js +248 -0
  61. package/eg1-conformance.js +46 -12
  62. package/enterprise.js +6 -2
  63. package/ep-assure.mjs +3 -2
  64. package/evidence-postgres.js +357 -0
  65. package/evidence.js +10 -3
  66. package/execution-binding.js +141 -26
  67. package/index.js +556 -115
  68. package/key-registry.js +51 -19
  69. package/mcp.js +4 -2
  70. package/network-witness.js +500 -0
  71. package/package.json +39 -5
  72. package/reliance-kernel.js +5 -6
  73. package/reliance-packet.js +43 -10
  74. package/reports/assurance-package.js +45 -19
  75. package/reports/reperform.js +78 -18
  76. package/reports/underwriter.js +1 -1
  77. package/settlement.js +300 -0
  78. package/store-postgres.js +14 -0
  79. package/store.js +26 -5
  80. package/strict-json.js +103 -0
  81. 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
+ });