@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
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@emilia-protocol/gate",
3
- "version": "0.9.5",
3
+ "version": "0.11.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/emiliaprotocol/emilia-protocol.git",
7
7
  "directory": "packages/gate"
8
8
  },
9
- "description": "EMILIA Gate \u2014 the Trusted Action Firewall. Deny-by-default enforcement for consequential machine actions: an action runs only with a valid, in-scope, sufficiently-assured, non-replayed EMILIA authorization receipt (proof a named human authorized this exact action). Composes require-receipt + verify; adds assurance-tier enforcement, one-time consumption (replay defense), a tamper-evident evidence log, default high-risk action packs, execution-field binding, reliance packets, an MCP drop-in, a GitHub system-of-record adapter, and EG-1 conformance. Framework-agnostic, fails closed. Reference implementation, experimental.",
9
+ "description": "EMILIA Gate \u2014 the Consequence Firewall. Deny-by-default enforcement for consequential machine actions: an action runs only with a valid, in-scope, sufficiently-assured, non-replayed EMILIA authorization receipt (proof a named human authorized this exact action). Composes require-receipt + verify; adds assurance-tier enforcement, one-time consumption (replay defense), a tamper-evident evidence log, default high-risk action packs, execution-field binding, reliance packets, an MCP drop-in, a GitHub system-of-record adapter, and EG-1 conformance. Framework-agnostic, fails closed. Reference implementation, experimental.",
10
10
  "license": "Apache-2.0",
11
11
  "type": "module",
12
12
  "main": "index.js",
@@ -31,6 +31,11 @@
31
31
  "./package.json": "./package.json",
32
32
  "./enterprise": "./enterprise.js",
33
33
  "./metering": "./metering.js",
34
+ "./network-witness": "./network-witness.js",
35
+ "./deployment-attestation": "./deployment-attestation.js",
36
+ "./coverage": "./coverage.js",
37
+ "./settlement": "./settlement.js",
38
+ "./control-plane": "./control-plane.js",
34
39
  "./siem": "./siem.js",
35
40
  "./roster": "./roster.js",
36
41
  "./reports/art14": "./reports/art14.js",
@@ -39,7 +44,16 @@
39
44
  "./breakglass": "./breakglass.js",
40
45
  "./challenge-store": "./challenge-store.js",
41
46
  "./store-postgres": "./store-postgres.js",
47
+ "./evidence-postgres": "./evidence-postgres.js",
48
+ "./witness-postgres": "./witness-postgres.js",
42
49
  "./evidence": "./evidence.js",
50
+ "./action-escrow": "./action-escrow.js",
51
+ "./action-escrow-evidence": "./action-escrow-evidence.js",
52
+ "./action-escrow-state": "./action-escrow-state.js",
53
+ "./action-escrow-postgres": "./action-escrow-postgres.js",
54
+ "./action-escrow-verifiers": "./action-escrow-verifiers.js",
55
+ "./action-escrow-custodian": "./action-escrow-custodian.js",
56
+ "./action-escrow-package": "./action-escrow-package.js",
43
57
  "./reports/auditor-workpaper": "./reports/auditor-workpaper.js",
44
58
  "./reports/reperform": "./reports/reperform.js",
45
59
  "./reports/external-verification": "./reports/external-verification.js",
@@ -52,9 +66,20 @@
52
66
  },
53
67
  "files": [
54
68
  "index.js",
69
+ "strict-json.js",
55
70
  "store.js",
56
71
  "challenge-store.js",
72
+ "store-postgres.js",
73
+ "evidence-postgres.js",
74
+ "witness-postgres.js",
57
75
  "evidence.js",
76
+ "action-escrow.js",
77
+ "action-escrow-evidence.js",
78
+ "action-escrow-state.js",
79
+ "action-escrow-postgres.js",
80
+ "action-escrow-verifiers.js",
81
+ "action-escrow-custodian.js",
82
+ "action-escrow-package.js",
58
83
  "action-packs.js",
59
84
  "action-control-manifest.js",
60
85
  "execution-binding.js",
@@ -86,16 +111,22 @@
86
111
  "adapters/salesforce.js",
87
112
  "demo.mjs",
88
113
  "custody-demo.mjs",
114
+ "LICENSE",
115
+ "CHANGELOG.md",
89
116
  "README.md",
90
117
  "enterprise.js",
91
118
  "metering.js",
119
+ "network-witness.js",
120
+ "deployment-attestation.js",
121
+ "coverage.js",
122
+ "settlement.js",
123
+ "control-plane.js",
92
124
  "siem.js",
93
125
  "roster.js",
94
126
  "reports/art14.js",
95
127
  "reports/underwriter.js",
96
128
  "metrics.js",
97
129
  "breakglass.js",
98
- "store-postgres.js",
99
130
  "deploy/helm/emilia-gate/Chart.yaml",
100
131
  "deploy/helm/emilia-gate/values.yaml",
101
132
  "deploy/helm/emilia-gate/templates/deployment.yaml",
@@ -104,11 +135,14 @@
104
135
  "deploy/helm/emilia-gate/templates/servicemonitor.yaml",
105
136
  "deploy/helm/emilia-gate/templates/NOTES.txt",
106
137
  "deploy/helm/README.md",
138
+ "deploy/helm/emilia-gate-service",
107
139
  "deploy/terraform/main.tf",
108
140
  "deploy/terraform/variables.tf",
109
141
  "deploy/terraform/outputs.tf",
110
142
  "deploy/terraform/versions.tf",
111
143
  "deploy/terraform/README.md",
144
+ "deploy/terraform/service",
145
+ "deploy/sql/001-runtime.sql",
112
146
  "reports/auditor-workpaper.js",
113
147
  "reports/reperform.js",
114
148
  "reports/external-verification.js"
@@ -120,8 +154,8 @@
120
154
  "cf1": "node cf1.mjs"
121
155
  },
122
156
  "dependencies": {
123
- "@emilia-protocol/require-receipt": "^0.5.0",
124
- "@emilia-protocol/verify": "^3.3.0"
157
+ "@emilia-protocol/require-receipt": "0.6.1",
158
+ "@emilia-protocol/verify": "3.11.0"
125
159
  },
126
160
  "keywords": [
127
161
  "emilia",
@@ -15,15 +15,14 @@
15
15
  * of its own — it enforces the one the pure offline verifier computed.
16
16
  */
17
17
  import { createEvidenceLog } from './evidence.js';
18
+ import {
19
+ evaluateReliance,
20
+ RELIANCE_PROFILE_VERSION,
21
+ RELIANCE_VERDICTS,
22
+ } from '@emilia-protocol/verify/reliance';
18
23
 
19
24
  const RECEIPT_REQUIRED_STATUS = 428;
20
25
 
21
- // Same cross-package resolution the Gate uses: prefer the published verifier,
22
- // fall back to the in-repo source so the monorepo builds without a node_modules
23
- // link. evaluateReliance is pure/offline; no DB, no network.
24
- const { evaluateReliance, RELIANCE_VERDICTS, RELIANCE_PROFILE_VERSION } = await import('@emilia-protocol/verify/reliance')
25
- .catch(() => import('../verify/reliance.js'));
26
-
27
26
  export { RELIANCE_VERDICTS, RELIANCE_PROFILE_VERSION };
28
27
 
29
28
  /** Build the 428 refusal for a non-`rely` verdict. */
@@ -51,17 +51,41 @@ function normalizeAdmissibility(adm) {
51
51
  };
52
52
  }
53
53
 
54
- function evidenceStatus(evidence) {
55
- if (!evidence) return { ok: null, length: null, head: null };
56
- if (typeof evidence.verify === 'function') return evidence.verify();
57
- return evidence;
54
+ async function evidenceStatus(evidence) {
55
+ if (!evidence) {
56
+ return { ok: false, length: null, head: null, reason: 'evidence_verification_unavailable' };
57
+ }
58
+ try {
59
+ const status = typeof evidence.verify === 'function'
60
+ ? await evidence.verify()
61
+ : await evidence;
62
+ if (!status || typeof status !== 'object' || Array.isArray(status)) {
63
+ return { ok: false, length: null, head: null, reason: 'evidence_verification_malformed' };
64
+ }
65
+ if (status.ok !== true) {
66
+ return {
67
+ ok: false,
68
+ length: Number.isSafeInteger(status.length) ? status.length : null,
69
+ head: typeof status.head === 'string' ? status.head : null,
70
+ reason: status.reason || 'evidence_verification_rejected',
71
+ };
72
+ }
73
+ return {
74
+ ...status,
75
+ ok: true,
76
+ length: Number.isSafeInteger(status.length) ? status.length : null,
77
+ head: typeof status.head === 'string' ? status.head : null,
78
+ };
79
+ } catch {
80
+ return { ok: false, length: null, head: null, reason: 'evidence_verification_failed' };
81
+ }
58
82
  }
59
83
 
60
84
  function check(id, ok, detail = null) {
61
85
  return { id, ok, ...(detail ? { detail } : {}) };
62
86
  }
63
87
 
64
- export function buildReliancePacket({
88
+ export async function buildReliancePacket({
65
89
  decision,
66
90
  execution = null,
67
91
  evidence = null,
@@ -70,12 +94,17 @@ export function buildReliancePacket({
70
94
  admissibility = null,
71
95
  verifier = '@emilia-protocol/gate',
72
96
  } = {}) {
73
- const evidenceCheck = evidenceStatus(evidence);
97
+ const evidenceCheck = await evidenceStatus(evidence);
74
98
  const decisionHash = decision?.evidence?.hash || decision?.hash || null;
75
- const executionBound = !execution || (execution.kind === 'execution' && execution.authorizes_decision === decisionHash);
99
+ const executionBound = Boolean(
100
+ execution
101
+ && execution.kind === 'execution'
102
+ && decisionHash
103
+ && execution.authorizes_decision === decisionHash,
104
+ );
76
105
  const bindingCheck = binding || decision?.evidence?.execution_binding || decision?.execution_binding || null;
77
106
  const allowed = decision?.allow === true;
78
- const evidenceOk = evidenceCheck.ok !== false;
107
+ const evidenceOk = evidenceCheck.ok === true;
79
108
  const bindingOk = bindingCheck ? bindingCheck.ok === true : true;
80
109
 
81
110
  // Admissibility block (relying-party evaluator output, computed OFFLINE against
@@ -96,6 +125,10 @@ export function buildReliancePacket({
96
125
  action: decision?.action || null,
97
126
  receipt_id: decision?.evidence?.receipt_id || decision?.receipt_id || null,
98
127
  subject: decision?.evidence?.subject || null,
128
+ policy_id: decision?.evidence?.evaluated_policy_id || null,
129
+ policy_hash: decision?.evidence?.evaluated_policy_hash || null,
130
+ tenant_id: decision?.evidence?.evaluated_tenant_id || null,
131
+ approvers: decision?.evidence?.evaluated_approvers || [],
99
132
  required_tier: decision?.evidence?.required_tier || decision?.required_tier || null,
100
133
  observed_tier: decision?.evidence?.have_tier || decision?.have_tier || null,
101
134
  decision_hash: decisionHash,
@@ -117,8 +150,8 @@ export function buildReliancePacket({
117
150
  check('assurance_sufficient', allowed || decision?.reason !== 'assurance_too_low', decision?.reason === 'assurance_too_low' ? 'receipt tier below action requirement' : null),
118
151
  check('receipt_one_time_consumed', allowed || decision?.reason === 'replay_refused' ? decision?.reason !== 'replay_refused' : null),
119
152
  check('execution_fields_bound', bindingCheck ? bindingCheck.ok === true : null, bindingCheck ? { missing_observed_fields: bindingCheck.missing_observed_fields || [], mismatched_fields: bindingCheck.mismatched_fields || [] } : 'no material execution-field binding required by this action'),
120
- check('execution_attests_decision', execution ? executionBound : null, execution ? null : 'no execution record supplied'),
121
- check('evidence_log_intact', evidenceCheck.ok === undefined ? null : evidenceCheck.ok, evidenceCheck.reason || null),
153
+ check('execution_attests_decision', executionBound, execution ? null : 'no execution record supplied'),
154
+ check('evidence_log_intact', evidenceCheck.ok === true, evidenceCheck.reason || null),
122
155
  // Admissibility verdict against the relying party's PINNED profile. null when
123
156
  // no profile/verdict was supplied (this reliance did not gate on admissibility);
124
157
  // true only for a recognized 'admissible' verdict; false otherwise (fail closed).
@@ -26,15 +26,11 @@
26
26
  * SUPPORTS a re-performance procedure; it never issues an opinion.
27
27
  */
28
28
  import { hashCanonical } from '../execution-binding.js';
29
+ import { evaluateReliance, RELIANCE_VERDICTS } from '@emilia-protocol/verify/reliance';
29
30
 
30
31
  export const ASSURANCE_PACKAGE_VERSION = 'EP-ASSURANCE-PACKAGE-v1';
31
32
  export const ASSURANCE_REPERFORMANCE_VERSION = 'EP-ASSURANCE-REPERFORMANCE-v1';
32
33
 
33
- // Cross-package resolution, same pattern the Gate uses: the pure offline reliance
34
- // verifier. No DB, no network.
35
- const { evaluateReliance, RELIANCE_VERDICTS } = await import('@emilia-protocol/verify/reliance')
36
- .catch(() => import('../../verify/reliance.js'));
37
-
38
34
  export { RELIANCE_VERDICTS };
39
35
 
40
36
  /**
@@ -70,6 +66,33 @@ function toIso(now) {
70
66
  return new Date(ms == null ? 0 : ms).toISOString();
71
67
  }
72
68
 
69
+ function portableJsonCopy(value, active = new Set()) {
70
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
71
+ if (typeof value === 'number') {
72
+ if (!Number.isFinite(value) || Object.is(value, -0)) {
73
+ throw new TypeError('assurance-package: value is not canonical JSON');
74
+ }
75
+ return value;
76
+ }
77
+ if (typeof value !== 'object') {
78
+ throw new TypeError('assurance-package: value is not JSON');
79
+ }
80
+ if (active.has(value)) throw new TypeError('assurance-package: cyclic value');
81
+ active.add(value);
82
+ try {
83
+ if (Array.isArray(value)) return value.map((entry) => portableJsonCopy(entry, active));
84
+ const prototype = Object.getPrototypeOf(value);
85
+ if (prototype !== Object.prototype && prototype !== null) {
86
+ throw new TypeError('assurance-package: value must use plain JSON objects');
87
+ }
88
+ return Object.fromEntries(
89
+ Object.entries(value).map(([key, entry]) => [key, portableJsonCopy(entry, active)]),
90
+ );
91
+ } finally {
92
+ active.delete(value);
93
+ }
94
+ }
95
+
73
96
  /**
74
97
  * Bundle N automated reliance decisions + the evidence each relied on into one
75
98
  * portable, content-addressed assurance package. Does NOT re-perform (that is the
@@ -87,18 +110,19 @@ function toIso(now) {
87
110
  export function buildAssurancePackage(decisions = [], { profile, organization = null, now = 0 } = {}) {
88
111
  if (!Array.isArray(decisions)) throw new Error('assurance-package: decisions must be an array');
89
112
  const items = decisions.map((d, i) => {
90
- const decision_id = d?.decision_id ?? `decision-${i}`;
113
+ const source = portableJsonCopy(d ?? {});
114
+ const decision_id = source.decision_id ?? `decision-${i}`;
91
115
  return {
92
116
  decision_id,
93
- action: d?.action ?? null,
94
- policy_hash: d?.action?.policy_hash ?? null,
95
- stated_verdict: typeof d?.stated_verdict === 'string' ? d.stated_verdict : null,
117
+ action: source.action ?? null,
118
+ policy_hash: source.action?.policy_hash ?? null,
119
+ stated_verdict: typeof source.stated_verdict === 'string' ? source.stated_verdict : null,
96
120
  evidence: {
97
- receipt: d?.receipt ?? null,
98
- quorum: d?.quorum ?? null,
99
- authority_proof: d?.authority_proof ?? null,
100
- revocation_state: d?.revocation_state ?? null,
101
- consumption: d?.consumption ?? null,
121
+ receipt: source.receipt ?? null,
122
+ quorum: source.quorum ?? null,
123
+ authority_proof: source.authority_proof ?? null,
124
+ revocation_state: source.revocation_state ?? null,
125
+ consumption: source.consumption ?? null,
102
126
  },
103
127
  };
104
128
  });
@@ -106,11 +130,12 @@ export function buildAssurancePackage(decisions = [], { profile, organization =
106
130
  const exceptions = items.filter((it) => it.stated_verdict && it.stated_verdict !== 'rely')
107
131
  .map((it) => ({ decision_id: it.decision_id, stated_verdict: it.stated_verdict, control_id: controlForVerdict(it.stated_verdict) }));
108
132
 
133
+ const profileCopy = profile == null ? null : portableJsonCopy(profile);
109
134
  const body = {
110
135
  '@version': ASSURANCE_PACKAGE_VERSION,
111
- organization,
112
- reliance_profile: profile ?? null,
113
- profile_hash: profile ? hashCanonical(profile) : null,
136
+ organization: organization == null ? null : portableJsonCopy(organization),
137
+ reliance_profile: profileCopy,
138
+ profile_hash: profileCopy ? hashCanonical(profileCopy) : null,
114
139
  control_catalog: RELIANCE_CONTROL_CATALOG,
115
140
  decisions: items,
116
141
  exception_history: exceptions,
@@ -140,15 +165,16 @@ export function buildAssurancePackage(decisions = [], { profile, organization =
140
165
  * @param {object} [opts.approverKeys] auditor-pinned approver keys (out of band)
141
166
  * @param {string} [opts.logPublicKey] auditor-pinned transparency-log key
142
167
  * @param {string} [opts.rpId]
168
+ * @param {string[]} [opts.allowedOrigins]
143
169
  * @param {object} [opts.revokerKeys]
144
170
  * @param {(key:object)=>boolean} [opts.isConsumed] auditor-owned consumption lookup
145
171
  * @param {number|string|Date} [opts.now] reliance-evaluation clock (pin for determinism)
146
172
  * @returns {object} EP-ASSURANCE-REPERFORMANCE-v1
147
173
  */
148
- export function reperformAssurancePackage(pkg, { approverKeys = {}, logPublicKey = null, rpId = null, revokerKeys = {}, isConsumed, now = 0 } = {}) {
174
+ export function reperformAssurancePackage(pkg, { approverKeys = {}, logPublicKey = null, rpId = null, allowedOrigins = [], revokerKeys = {}, isConsumed, now = 0 } = {}) {
149
175
  if (!pkg || pkg['@version'] !== ASSURANCE_PACKAGE_VERSION) throw new Error('assurance-reperform: not an EP-ASSURANCE-PACKAGE-v1');
150
176
  const profile = pkg.reliance_profile;
151
- const evalOpts = { approverKeys, logPublicKey, rpId, revokerKeys, ...(typeof isConsumed === 'function' ? { isConsumed } : {}) };
177
+ const evalOpts = { approverKeys, logPublicKey, rpId, allowedOrigins, revokerKeys, ...(typeof isConsumed === 'function' ? { isConsumed } : {}) };
152
178
  const relianceNow = typeof now === 'function' ? now() : now;
153
179
 
154
180
  const results = (Array.isArray(pkg.decisions) ? pkg.decisions : []).map((it) => {
@@ -37,15 +37,10 @@
37
37
  * Deterministic: pure over (entries, issuerKeys); time enters only through the
38
38
  * injectable `now` (generated_at). No sampling, no randomness.
39
39
  */
40
+ import crypto from 'node:crypto';
40
41
  import { createEvidenceLog } from '../evidence.js';
41
-
42
- // The protocol verifiers being re-driven. Same resolution pattern as
43
- // packages/gate/index.js: prefer the published package, fall back to the
44
- // in-repo source so the monorepo works without a node_modules link.
45
- const { verifyEmiliaReceipt } = await import('@emilia-protocol/require-receipt')
46
- .catch(() => import('../../require-receipt/index.js'));
47
- const { verifyWebAuthnSignoff, verifyQuorum } = await import('@emilia-protocol/verify')
48
- .catch(() => import('../../verify/index.js'));
42
+ import { validatePinnedQuorumPolicy, verifyEmiliaReceipt } from '@emilia-protocol/require-receipt';
43
+ import { verifyQuorum, verifyWebAuthnSignoff } from '@emilia-protocol/verify';
49
44
 
50
45
  export const REPERFORMANCE_VERSION = 'EP-GATE-REPERFORMANCE-v1';
51
46
 
@@ -107,7 +102,28 @@ function collectMaterial(e) {
107
102
  * what IS re-performed is the cryptography (signature, issuer trust, action
108
103
  * binding, ceremony checks).
109
104
  */
110
- function reverifyMaterial(m, { issuerKeys, action, rpId }) {
105
+ function spkiFingerprint(value) {
106
+ try {
107
+ const key = crypto.createPublicKey({ key: Buffer.from(value, 'base64url'), format: 'der', type: 'spki' });
108
+ return crypto.createHash('sha256').update(key.export({ type: 'spki', format: 'der' })).digest('hex');
109
+ } catch { return null; }
110
+ }
111
+
112
+ function pinnedApproverEntry(approverKeys, approver, carriedKey = null) {
113
+ if (!approverKeys || typeof approverKeys !== 'object' || Array.isArray(approverKeys)) return null;
114
+ const candidates = Object.values(approverKeys).filter((entry) => entry && typeof entry === 'object'
115
+ && entry.approver_id === approver && typeof entry.public_key === 'string');
116
+ if (carriedKey) {
117
+ const fingerprint = spkiFingerprint(carriedKey);
118
+ const matches = candidates.filter((entry) => fingerprint && spkiFingerprint(entry.public_key) === fingerprint);
119
+ return matches.length === 1 ? matches[0] : null;
120
+ }
121
+ return candidates.length === 1 ? candidates[0] : null;
122
+ }
123
+
124
+ function reverifyMaterial(m, {
125
+ issuerKeys, action, rpId, allowedOrigins, approverKeys, quorumPolicy, quorumPolicies,
126
+ }) {
111
127
  if (m.type === 'receipt') {
112
128
  const r = verifyEmiliaReceipt(m.doc, {
113
129
  trustedKeys: issuerKeys, allowInlineKey: false, action, maxAgeSec: 0,
@@ -115,14 +131,33 @@ function reverifyMaterial(m, { issuerKeys, action, rpId }) {
115
131
  return r.ok ? null : `receipt:${r.reason}`;
116
132
  }
117
133
  if (m.type === 'signoff') {
118
- // No pinned/carried approver key -> nothing to verify AGAINST. Named
119
- // failure, never a silent pass.
120
- if (!m.key || typeof m.key !== 'string') return 'signoff:no_approver_key_carried';
121
- const r = verifyWebAuthnSignoff(m.doc, m.key, rpId ? { rpId } : {});
134
+ if (typeof rpId !== 'string' || !rpId || !Array.isArray(allowedOrigins) || allowedOrigins.length === 0) {
135
+ return 'signoff:relying_party_scope_unpinned';
136
+ }
137
+ const approver = m.doc?.context?.approver;
138
+ const entry = pinnedApproverEntry(approverKeys, approver, m.key);
139
+ if (!entry) return 'signoff:approver_key_unpinned_or_ambiguous';
140
+ const r = verifyWebAuthnSignoff(m.doc, entry.public_key, { rpId, allowedOrigins });
122
141
  return r.valid ? null : `signoff:${r.error ? `error:${r.error}` : `checks_failed:${failingChecks(r.checks)}`}`;
123
142
  }
124
143
  if (m.type === 'quorum') {
125
- const r = verifyQuorum(m.doc, rpId ? { rpId } : {});
144
+ if (typeof rpId !== 'string' || !rpId || !Array.isArray(allowedOrigins) || allowedOrigins.length === 0) {
145
+ return 'quorum:relying_party_scope_unpinned';
146
+ }
147
+ const pinnedPolicy = (quorumPolicies && typeof quorumPolicies === 'object' ? quorumPolicies[action] : null)
148
+ || quorumPolicy;
149
+ const policy = validatePinnedQuorumPolicy(pinnedPolicy);
150
+ if (!policy.ok) return `quorum:${policy.reason}`;
151
+ const members = Array.isArray(m.doc?.members) ? m.doc.members.map((member) => {
152
+ const entry = pinnedApproverEntry(
153
+ approverKeys,
154
+ member?.signoff?.context?.approver,
155
+ member?.approver_public_key,
156
+ );
157
+ return entry ? { ...member, approver_public_key: entry.public_key } : null;
158
+ }) : [];
159
+ if (members.length === 0 || members.some((member) => !member)) return 'quorum:approver_key_unpinned_or_ambiguous';
160
+ const r = verifyQuorum({ ...m.doc, policy: policy.policy, members }, { rpId, allowedOrigins });
126
161
  return r.valid ? null : `quorum:checks_failed:${failingChecks(r.checks)}`;
127
162
  }
128
163
  /* c8 ignore next */
@@ -154,12 +189,24 @@ function reverifyMaterial(m, { issuerKeys, action, rpId }) {
154
189
  * @param {string[]} [o.issuerKeys=[]] pinned base64url SPKI issuer keys, sourced
155
190
  * by the AUDITOR out of band — never from the entries themselves
156
191
  * @param {number|function} [o.now=Date.now] clock for generated_at (pin for determinism)
157
- * @param {string} [o.rpId] bind carried WebAuthn assertions to this relying-party id
192
+ * @param {object} [o.approverKeys={}] auditor-pinned identity-bound approver keys
193
+ * @param {string} [o.rpId] bind carried WebAuthn assertions to this relying-party id
194
+ * @param {string[]} [o.allowedOrigins=[]] exact accepted WebAuthn origins
195
+ * @param {object} [o.quorumPolicy] auditor-pinned global organizational quorum rule
196
+ * @param {object} [o.quorumPolicies] action_type -> auditor-pinned quorum rule
158
197
  * @returns {Promise<object>} EP-GATE-REPERFORMANCE-v1 document:
159
198
  * { chain: {ok, entries, head}, receipts: {reverified, failed, not_reverifiable},
160
199
  * counts: {allows, denies, replays_blocked, by_action_type}, ... }
161
200
  */
162
- export async function reperformEvidence(entries = [], { issuerKeys = [], now = Date.now, rpId = null } = {}) {
201
+ export async function reperformEvidence(entries = [], {
202
+ issuerKeys = [],
203
+ approverKeys = {},
204
+ now = Date.now,
205
+ rpId = null,
206
+ allowedOrigins = [],
207
+ quorumPolicy = null,
208
+ quorumPolicies = {},
209
+ } = {}) {
163
210
  if (!Array.isArray(entries)) {
164
211
  throw new Error('reperform: entries must be an array (evidence.all() or a full export)');
165
212
  }
@@ -227,7 +274,13 @@ export async function reperformEvidence(entries = [], { issuerKeys = [], now = D
227
274
  let allOk = true;
228
275
  for (const m of material) {
229
276
  const reason = reverifyMaterial(m, {
230
- issuerKeys, action: typeof e.action === 'string' && e.action ? e.action : null, rpId,
277
+ issuerKeys,
278
+ approverKeys,
279
+ action: typeof e.action === 'string' && e.action ? e.action : null,
280
+ rpId,
281
+ allowedOrigins,
282
+ quorumPolicy,
283
+ quorumPolicies,
231
284
  });
232
285
  if (reason) { allOk = false; receipts.failed.push({ hash, reason }); }
233
286
  }
@@ -255,7 +308,8 @@ export async function reperformEvidence(entries = [], { issuerKeys = [], now = D
255
308
  reperforms:
256
309
  'Independent recomputation from the supplied evidence entries: the hash chain is rebuilt '
257
310
  + 'and checked with the evidence log\'s own verifier, carried receipt/signoff/quorum material '
258
- + 'is cryptographically re-verified against auditor-pinned issuer keys, and decision counts '
311
+ + 'is cryptographically re-verified against auditor-pinned issuer and identity-bound approver keys, '
312
+ + 'WebAuthn scope, and organizational quorum policy; decision counts '
259
313
  + 'are recomputed from scratch without reference to any report builder.',
260
314
  does_not_establish: [
261
315
  'Completeness of the log: a chain rewritten in full, or entries withheld before they were supplied, is not detectable from the entries alone. Anchor the head hash externally to close this.',
@@ -268,6 +322,12 @@ export async function reperformEvidence(entries = [], { issuerKeys = [], now = D
268
322
  input: {
269
323
  entries_supplied: entries.length,
270
324
  issuer_keys_pinned: issuerKeys.length,
325
+ approver_keys_pinned: approverKeys && typeof approverKeys === 'object'
326
+ ? Object.keys(approverKeys).length : 0,
327
+ relying_party_scope_pinned: typeof rpId === 'string' && rpId.length > 0
328
+ && Array.isArray(allowedOrigins) && allowedOrigins.length > 0,
329
+ quorum_policies_pinned: (quorumPolicy ? 1 : 0)
330
+ + (quorumPolicies && typeof quorumPolicies === 'object' ? Object.keys(quorumPolicies).length : 0),
271
331
  expects: 'the complete evidence log from genesis (evidence.all() or a full export)',
272
332
  },
273
333
  chain,
@@ -168,7 +168,7 @@ export function buildUnderwriterAttestation(entries = [], {
168
168
  status: 'Not an insurance document. This attestation carries no coverage effect until adopted by the carrier.',
169
169
  },
170
170
  control_in_force: {
171
- control: 'EMILIA Gate — Trusted Action Firewall',
171
+ control: 'EMILIA Gate — Consequence Firewall',
172
172
  mode: 'deny_by_default',
173
173
  statement: 'Guarded actions execute only with a valid, in-scope, sufficiently-assured, fresh, one-time authorization receipt; absent or insufficient proof is refused, and every decision is appended to a hash-chained evidence log.',
174
174
  guarded_decisions: guarded.length,