@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/adapters/_kit.js CHANGED
@@ -6,15 +6,29 @@
6
6
  * gate.run(), and FAIL CLOSED — if the gate refuses, the real client call is
7
7
  * never made and we throw EMILIA_RECEIPT_REQUIRED.
8
8
  *
9
- * An op spec is: { selector, observed(params) -> {fields...}, perform(client, params) -> result }.
9
+ * An op spec is: { selector, observed(params) -> {fields...},
10
+ * actuator?(params, observed) -> {fields...}, perform(client, actuator) -> result }.
10
11
  * `observed` must return the same material fields the action pack binds, drawn
11
12
  * from the call params (the system-of-record facts), so a receipt for resource A
12
13
  * cannot authorize a mutation of resource B.
13
14
  */
14
- import { hashCanonical } from '../execution-binding.js';
15
+ import { canonicalize, hashCanonical } from '../execution-binding.js';
15
16
 
16
17
  export { hashCanonical };
17
18
 
19
+ function deepFreeze(value) {
20
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
21
+ for (const child of Object.values(value)) deepFreeze(child);
22
+ Object.freeze(value);
23
+ }
24
+ return value;
25
+ }
26
+
27
+ /** Canonical plain-data snapshot used at the actuator boundary. */
28
+ export function canonicalActuatorObject(value) {
29
+ return deepFreeze(JSON.parse(canonicalize(value)));
30
+ }
31
+
18
32
  /** Build an EP-ACTION-RISK-MANIFEST-v0.1 from a frozen action pack (deep-copied). */
19
33
  export function manifestFromPack(pack, extraActions = []) {
20
34
  return {
@@ -26,6 +40,9 @@ export function manifestFromPack(pack, extraActions = []) {
26
40
  execution_binding: a.execution_binding
27
41
  ? { ...a.execution_binding, required_fields: [...a.execution_binding.required_fields] }
28
42
  : undefined,
43
+ ...(a.business_authorization
44
+ ? { business_authorization: canonicalActuatorObject(a.business_authorization) }
45
+ : {}),
29
46
  })),
30
47
  ...extraActions,
31
48
  ],
@@ -44,8 +61,33 @@ export function createAdapter({ system, ops }) {
44
61
  if (!gate || typeof gate.run !== 'function') throw new Error(`${system} adapter requires an EMILIA Gate (with .run)`);
45
62
  const spec = ops[op];
46
63
  if (!spec) throw new Error(`${system} adapter: unknown op "${op}" (expected one of: ${OPS.join(', ')})`);
47
- const observedAction = spec.observed(params);
48
- const out = await gate.run({ selector: spec.selector, receipt, observedAction }, () => spec.perform(client, params));
64
+ if (!receipt) {
65
+ const refused = await gate.run(
66
+ { selector: spec.selector, receipt: null, observedAction: null },
67
+ () => { throw new Error('unreachable_adapter_effect'); },
68
+ );
69
+ const e = new Error(`EMILIA Gate refused ${system}:${op} — ${refused.authorization.reason}`);
70
+ e.code = 'EMILIA_RECEIPT_REQUIRED';
71
+ e.status = refused.status;
72
+ e.gate = refused.authorization;
73
+ e.challenge = refused.body;
74
+ throw e;
75
+ }
76
+ // Snapshot before any asynchronous verification. Caller-owned params may be
77
+ // mutated while gate.run() awaits evidence/storage; neither the observation
78
+ // nor the eventual provider call may follow that mutable reference.
79
+ const input = canonicalActuatorObject(params);
80
+ const observedAction = canonicalActuatorObject(spec.observed(input));
81
+ // Default to the verified fields only. Operations that need a preimage for a
82
+ // bound digest (SQL, RLS, secret values, queries) must opt in with an explicit
83
+ // actuator() constructor. The unrestricted caller object is never passed.
84
+ const actuator = canonicalActuatorObject(
85
+ typeof spec.actuator === 'function' ? spec.actuator(input, observedAction) : observedAction,
86
+ );
87
+ const out = await gate.run(
88
+ { selector: spec.selector, receipt, observedAction },
89
+ () => spec.perform(client, actuator),
90
+ );
49
91
  if (!out.ok) {
50
92
  const e = new Error(`EMILIA Gate refused ${system}:${op} — ${out.authorization.reason}`);
51
93
  e.code = 'EMILIA_RECEIPT_REQUIRED';
@@ -60,4 +102,4 @@ export function createAdapter({ system, ops }) {
60
102
  return { ops, OPS, guard };
61
103
  }
62
104
 
63
- export default { createAdapter, manifestFromPack, hashCanonical };
105
+ export default { createAdapter, manifestFromPack, hashCanonical, canonicalActuatorObject };
package/adapters/aws.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * import { createGate } from '@emilia-protocol/gate';
14
14
  * import { createAwsManifest, guardAwsMutation } from '@emilia-protocol/gate/adapters/aws';
15
15
  *
16
- * const gate = createGate({ manifest: createAwsManifest(), trustedKeys: [ISSUER] });
16
+ * const gate = createGate({ manifest: createAwsManifest(), trustedKeys: [ISSUER], store: sharedConsumptionStore });
17
17
  * // client: { iam: { attachUserPolicy, createAccessKey, deleteUser }, ec2: { authorizeSecurityGroupIngress } }
18
18
  * await guardAwsMutation(gate, client, {
19
19
  * op: 'iam.attach_policy', params: { user: 'svc-bot', policy_arn: 'arn:aws:iam::aws:policy/AdministratorAccess' }, receipt,
@@ -48,7 +48,7 @@ export const AWS_ACTION_PACK = Object.freeze([
48
48
  risk: 'critical', receipt_required: true, assurance_class: 'quorum',
49
49
  match: { protocol: 'aws', tool: 'authorize_security_group_ingress' },
50
50
  why: 'Opens the network. Bind group/CIDR/port so 0.0.0.0/0:22 cannot slip through.',
51
- execution_binding: { required_fields: ['action_type', 'group_id', 'cidr', 'from_port'] },
51
+ execution_binding: { required_fields: ['action_type', 'group_id', 'cidr', 'protocol', 'from_port', 'to_port'] },
52
52
  }),
53
53
  ]);
54
54
 
@@ -70,9 +70,20 @@ const OPS = {
70
70
  },
71
71
  'ec2.authorize_ingress': {
72
72
  selector: { protocol: 'aws', tool: 'authorize_security_group_ingress' },
73
- observed: (p) => ({ action_type: 'aws.ec2.authorize_ingress', group_id: p.group_id, cidr: p.cidr, from_port: p.from_port }),
73
+ observed: (p) => ({
74
+ action_type: 'aws.ec2.authorize_ingress',
75
+ group_id: p.group_id,
76
+ cidr: p.cidr,
77
+ protocol: p.protocol ?? 'tcp',
78
+ from_port: p.from_port,
79
+ to_port: p.to_port ?? p.from_port,
80
+ }),
74
81
  perform: (client, p) => client.ec2.authorizeSecurityGroupIngress({
75
- GroupId: p.group_id, CidrIp: p.cidr, FromPort: p.from_port, ToPort: p.to_port ?? p.from_port, IpProtocol: p.protocol ?? 'tcp',
82
+ GroupId: p.group_id,
83
+ CidrIp: p.cidr,
84
+ FromPort: p.from_port,
85
+ ToPort: p.to_port,
86
+ IpProtocol: p.protocol,
76
87
  }),
77
88
  },
78
89
  };
@@ -8,38 +8,41 @@
8
8
  * with no credentials; swap in `new Octokit({ auth })` for the real thing.
9
9
  * @license Apache-2.0
10
10
  */
11
- import crypto from 'node:crypto';
12
- import { createGate } from '../index.js';
11
+ import { createGate, createEg1Harness } from '../index.js';
13
12
  import { createGithubManifest, guardGithubMutation } from './github.js';
14
13
 
15
- const canon = (v) => (v == null ? JSON.stringify(v)
16
- : Array.isArray(v) ? `[${v.map(canon).join(',')}]`
17
- : typeof v === 'object' ? `{${Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canon(v[k])).join(',')}}`
18
- : JSON.stringify(v));
19
- const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
20
- const ISSUER = publicKey.export({ type: 'spki', format: 'der' }).toString('base64url');
21
- let n = 0;
22
- const mint = (extra = {}) => {
23
- const payload = {
24
- receipt_id: `gh_${++n}`, subject: 'agent:repo-bot', issuer: 'ep:org:demo', created_at: new Date().toISOString(),
25
- claim: { action_type: 'github.repo.delete', owner: 'acme', repo: 'prod', outcome: 'allow_with_signoff', approver: 'ep:approver:cto', ...extra },
26
- };
27
- return { '@version': 'EP-RECEIPT-v1', payload, signature: { algorithm: 'Ed25519', value: crypto.sign(null, Buffer.from(canon(payload), 'utf8'), privateKey).toString('base64url') } };
28
- };
14
+ const action = { action_type: 'github.repo.delete', owner: 'acme', repo: 'prod' };
15
+ const harness = createEg1Harness({ action, idPrefix: 'gh' });
29
16
 
17
+ const executed = [];
30
18
  const octokit = {
31
- repos: { delete: async (p) => { console.log(` !! GitHub API: repos.delete(${p.owner}/${p.repo}) EXECUTED`); return { status: 204 }; } },
19
+ repos: {
20
+ delete: async (p) => {
21
+ executed.push(`${p.owner}/${p.repo}`);
22
+ return { status: 204 };
23
+ },
24
+ },
32
25
  };
33
- const gate = createGate({ manifest: createGithubManifest(), trustedKeys: [ISSUER] });
26
+ const gate = createGate({
27
+ manifest: createGithubManifest(),
28
+ trustedKeys: [harness.publicKey],
29
+ approverKeys: harness.approverKeys,
30
+ rpId: harness.rpId,
31
+ allowedOrigins: harness.allowedOrigins,
32
+ allowEphemeralStore: true,
33
+ });
34
34
  const G = (s) => `\x1b[32m${s}\x1b[0m`; const R = (s) => `\x1b[31m${s}\x1b[0m`;
35
35
  const line = (s) => console.log(s);
36
36
 
37
37
  async function attempt(label, params, receipt) {
38
+ const before = executed.length;
38
39
  try {
39
40
  const { reliance } = await guardGithubMutation(gate, octokit, { op: 'repo.delete', params, receipt });
40
- line(` ${label} -> ${G('ALLOWED')} reliance=${reliance.verdict.toUpperCase()}`);
41
+ const mutation = executed.length === before + 1 ? ` GitHub API=${executed.at(-1)}` : ' GitHub API=NOT CALLED';
42
+ line(` ${label} -> ${G('ALLOWED')} reliance=${reliance.verdict.toUpperCase()}${mutation}`);
41
43
  } catch (e) {
42
- line(` ${label} -> ${R(`REFUSED ${e.status || ''}`)} (${e.gate?.reason || e.message})`);
44
+ const mutation = executed.length === before ? ' GitHub API=NOT CALLED' : ' GitHub API=CALLED';
45
+ line(` ${label} -> ${R(`REFUSED ${e.status || ''}`)} (${e.gate?.reason || e.message})${mutation}`);
43
46
  }
44
47
  }
45
48
 
@@ -47,10 +50,10 @@ line('='.repeat(66));
47
50
  line(' EMILIA Gate × GitHub — agent tries to delete the production repo');
48
51
  line('='.repeat(66));
49
52
  await attempt('1. delete acme/prod, no receipt ', { owner: 'acme', repo: 'prod' }, null);
50
- const approval = mint();
53
+ const approval = harness.mint({ outcome: 'allow_with_signoff' });
51
54
  await attempt('2. delete acme/prod, valid human signoff ', { owner: 'acme', repo: 'prod' }, approval);
52
55
  await attempt('3. same receipt replayed ', { owner: 'acme', repo: 'prod' }, approval);
53
- const reAppro = mint();
56
+ const reAppro = harness.mint({ outcome: 'allow_with_signoff' });
54
57
  await attempt('4. approved acme/prod, but targets staging ', { owner: 'acme', repo: 'staging' }, reAppro);
55
58
  line(' ' + '-'.repeat(62));
56
59
  line(' No receipt, no mutation. The GitHub API is only reached on an allowed line.');
@@ -12,7 +12,7 @@
12
12
  * import { createGate } from '@emilia-protocol/gate';
13
13
  * import { createGithubManifest, guardGithubMutation } from '@emilia-protocol/gate/adapters/github';
14
14
  *
15
- * const gate = createGate({ manifest: createGithubManifest(), trustedKeys: [ISSUER] });
15
+ * const gate = createGate({ manifest: createGithubManifest(), trustedKeys: [ISSUER], store: sharedConsumptionStore });
16
16
  * const octokit = new Octokit({ auth });
17
17
  * await guardGithubMutation(gate, octokit, {
18
18
  * op: 'repo.delete', params: { owner, repo }, receipt, // throws if no valid receipt
package/adapters/jira.js CHANGED
@@ -34,6 +34,7 @@ const OPS = {
34
34
  'issue.bulk_delete': {
35
35
  selector: { protocol: 'jira', tool: 'bulk_delete_issues' },
36
36
  observed: (p) => ({ action_type: 'jira.issue.bulk_delete', project: p.project, jql_hash: hashCanonical(String(p.jql || '').trim()) }),
37
+ actuator: (p, observed) => ({ ...observed, jql: p.jql }),
37
38
  perform: (c, p) => c.bulkDeleteIssues({ project: p.project, jql: p.jql }),
38
39
  },
39
40
  'project.delete': {
@@ -39,6 +39,7 @@ const OPS = {
39
39
  'issue.bulk_delete': {
40
40
  selector: { protocol: 'linear', tool: 'bulk_delete_issues' },
41
41
  observed: (p) => ({ action_type: 'linear.issue.bulk_delete', team: p.team, query_hash: hashCanonical(String(p.query || '').trim()) }),
42
+ actuator: (p, observed) => ({ ...observed, query: p.query }),
42
43
  perform: (c, p) => c.bulkDeleteIssues({ team: p.team, query: p.query }),
43
44
  },
44
45
  'team.delete': {
@@ -34,6 +34,7 @@ const OPS = {
34
34
  'records.bulk_delete': {
35
35
  selector: { protocol: 'salesforce', tool: 'bulk_delete' },
36
36
  observed: (p) => ({ action_type: 'salesforce.records.bulk_delete', object: p.object, soql_hash: hashCanonical(String(p.soql || '').trim()) }),
37
+ actuator: (p, observed) => ({ ...observed, soql: p.soql }),
37
38
  perform: (c, p) => c.bulkDelete({ object: p.object, soql: p.soql }),
38
39
  },
39
40
  'data.export': {
@@ -12,7 +12,7 @@
12
12
  * import { createGate } from '@emilia-protocol/gate';
13
13
  * import { createStripeManifest, guardStripeMutation } from '@emilia-protocol/gate/adapters/stripe';
14
14
  *
15
- * const gate = createGate({ manifest: createStripeManifest(), trustedKeys: [ISSUER] });
15
+ * const gate = createGate({ manifest: createStripeManifest(), trustedKeys: [ISSUER], store: sharedConsumptionStore });
16
16
  * await guardStripeMutation(gate, new Stripe(key), {
17
17
  * op: 'payout.create', params: { amount: 40000, currency: 'usd', destination: 'acct_x' }, receipt,
18
18
  * });
@@ -12,7 +12,7 @@
12
12
  * import { createGate } from '@emilia-protocol/gate';
13
13
  * import { createSupabaseManifest, guardSupabaseMutation, isDestructiveSql } from '@emilia-protocol/gate/adapters/supabase';
14
14
  *
15
- * const gate = createGate({ manifest: createSupabaseManifest(), trustedKeys: [ISSUER] });
15
+ * const gate = createGate({ manifest: createSupabaseManifest(), trustedKeys: [ISSUER], store: sharedConsumptionStore });
16
16
  * // client: anything with .query(sql) (node-postgres / a Supabase RPC wrapper).
17
17
  * await guardSupabaseMutation(gate, client, {
18
18
  * op: 'sql.destructive', params: { sql: 'DELETE FROM payments WHERE id=1', table: 'payments' }, receipt,
@@ -20,6 +20,8 @@
20
20
  */
21
21
  import { createAdapter, manifestFromPack, hashCanonical } from './_kit.js';
22
22
 
23
+ export const RLS_DEFINITION_BINDING_VERSION = 'EP-SUPABASE-RLS-DEFINITION-v1';
24
+
23
25
  const DESTRUCTIVE = /\b(delete|drop|truncate|alter\s+table)\b/i;
24
26
  const UPDATE_NO_WHERE = /\bupdate\b(?:(?!\bwhere\b).)*$/is;
25
27
 
@@ -34,6 +36,14 @@ export function statementHash(sql) {
34
36
  return hashCanonical(String(sql || '').replace(/\s+/g, ' ').trim().toLowerCase());
35
37
  }
36
38
 
39
+ /** Digest the exact canonical RLS definition without placing it in evidence. */
40
+ export function rlsDefinitionDigest(definition) {
41
+ return hashCanonical({
42
+ version: RLS_DEFINITION_BINDING_VERSION,
43
+ definition,
44
+ });
45
+ }
46
+
37
47
  export const SUPABASE_ACTION_PACK = Object.freeze([
38
48
  Object.freeze({
39
49
  id: 'supabase.sql.destructive', label: 'Destructive SQL', action_type: 'supabase.sql.destructive',
@@ -54,7 +64,11 @@ export const SUPABASE_ACTION_PACK = Object.freeze([
54
64
  risk: 'critical', receipt_required: true, assurance_class: 'quorum',
55
65
  match: { protocol: 'supabase', tool: 'alter_policy' },
56
66
  why: 'Changes who can read/write rows. Row-Level-Security changes deserve the two-person rule.',
57
- execution_binding: { required_fields: ['action_type', 'table', 'policy'] },
67
+ execution_binding: {
68
+ required_fields: [
69
+ 'action_type', 'table', 'policy', 'rls_definition_digest', 'rls_definition_version',
70
+ ],
71
+ },
58
72
  }),
59
73
  ]);
60
74
 
@@ -62,6 +76,7 @@ const OPS = {
62
76
  'sql.destructive': {
63
77
  selector: { protocol: 'supabase', tool: 'execute_sql' },
64
78
  observed: (p) => ({ action_type: 'supabase.sql.destructive', statement_hash: statementHash(p.sql) }),
79
+ actuator: (p, observed) => ({ ...observed, sql: p.sql }),
65
80
  perform: (client, p) => client.query(p.sql),
66
81
  },
67
82
  'data.export': {
@@ -71,7 +86,14 @@ const OPS = {
71
86
  },
72
87
  'rls.change': {
73
88
  selector: { protocol: 'supabase', tool: 'alter_policy' },
74
- observed: (p) => ({ action_type: 'supabase.rls.change', table: p.table, policy: p.policy }),
89
+ observed: (p) => ({
90
+ action_type: 'supabase.rls.change',
91
+ table: p.table,
92
+ policy: p.policy,
93
+ rls_definition_digest: rlsDefinitionDigest(p.definition),
94
+ rls_definition_version: RLS_DEFINITION_BINDING_VERSION,
95
+ }),
96
+ actuator: (p, observed) => ({ ...observed, definition: p.definition }),
75
97
  perform: (client, p) => client.alterPolicy(p.table, p.policy, p.definition),
76
98
  },
77
99
  };
@@ -96,5 +118,5 @@ export function guardSupabaseMutation(gate, client, args) {
96
118
 
97
119
  export default {
98
120
  SUPABASE_ACTION_PACK, SUPABASE_OPS, createSupabaseManifest, guardSupabaseMutation,
99
- isDestructiveSql, statementHash,
121
+ isDestructiveSql, statementHash, rlsDefinitionDigest, RLS_DEFINITION_BINDING_VERSION,
100
122
  };
@@ -5,7 +5,17 @@
5
5
  * never reach Vercel without a receipt bound to THIS project. Client injected
6
6
  * (the Vercel REST client or a thin wrapper).
7
7
  */
8
- import { createAdapter, manifestFromPack } from './_kit.js';
8
+ import { createAdapter, manifestFromPack, hashCanonical } from './_kit.js';
9
+
10
+ export const SECRET_VALUE_BINDING_VERSION = 'EP-VERCEL-SECRET-VALUE-v1';
11
+
12
+ /** Digest an exact secret value for receipt binding; callers must never log it. */
13
+ export function secretValueDigest(value) {
14
+ return hashCanonical({
15
+ version: SECRET_VALUE_BINDING_VERSION,
16
+ value,
17
+ });
18
+ }
9
19
 
10
20
  export const VERCEL_ACTION_PACK = Object.freeze([
11
21
  Object.freeze({
@@ -27,7 +37,11 @@ export const VERCEL_ACTION_PACK = Object.freeze([
27
37
  risk: 'high', receipt_required: true, assurance_class: 'class_a',
28
38
  match: { protocol: 'vercel', tool: 'upsert_env' },
29
39
  why: 'Changes production secrets/config. Bind project+key+target.',
30
- execution_binding: { required_fields: ['action_type', 'project', 'key', 'target'] },
40
+ execution_binding: {
41
+ required_fields: [
42
+ 'action_type', 'project', 'key', 'target', 'secret_value_digest', 'secret_value_version',
43
+ ],
44
+ },
31
45
  }),
32
46
  ]);
33
47
 
@@ -44,7 +58,15 @@ const OPS = {
44
58
  },
45
59
  'env.set': {
46
60
  selector: { protocol: 'vercel', tool: 'upsert_env' },
47
- observed: (p) => ({ action_type: 'vercel.env.set', project: p.project, key: p.key, target: p.target }),
61
+ observed: (p) => ({
62
+ action_type: 'vercel.env.set',
63
+ project: p.project,
64
+ key: p.key,
65
+ target: p.target,
66
+ secret_value_digest: secretValueDigest(p.value),
67
+ secret_value_version: SECRET_VALUE_BINDING_VERSION,
68
+ }),
69
+ actuator: (p, observed) => ({ ...observed, value: p.value }),
48
70
  perform: (c, p) => c.upsertEnv({ project: p.project, key: p.key, value: p.value, target: p.target }),
49
71
  },
50
72
  };
@@ -53,4 +75,11 @@ const adapter = createAdapter({ system: 'vercel', ops: OPS });
53
75
  export const VERCEL_OPS = adapter.OPS;
54
76
  export function createVercelManifest(extra = []) { return manifestFromPack(VERCEL_ACTION_PACK, extra); }
55
77
  export function guardVercelMutation(gate, client, args) { return adapter.guard(gate, client, args); }
56
- export default { VERCEL_ACTION_PACK, VERCEL_OPS, createVercelManifest, guardVercelMutation };
78
+ export default {
79
+ VERCEL_ACTION_PACK,
80
+ VERCEL_OPS,
81
+ SECRET_VALUE_BINDING_VERSION,
82
+ secretValueDigest,
83
+ createVercelManifest,
84
+ guardVercelMutation,
85
+ };
package/aec-execution.js CHANGED
@@ -9,9 +9,7 @@
9
9
  */
10
10
  import { createEvidenceLog, verifyEvidenceRecord } from './evidence.js';
11
11
  import { MemoryConsumptionStore } from './store.js';
12
-
13
- const { verifyAuthorizationChain } = await import('@emilia-protocol/verify/evidence-chain')
14
- .catch(() => import('../verify/evidence-chain.js'));
12
+ import { verifyAuthorizationChain } from '@emilia-protocol/verify/evidence-chain';
15
13
 
16
14
  const HUMAN_FLOORS = new Set(['class_a', 'quorum', 'class_a_or_quorum']);
17
15
  const HEX_256 = /^[0-9a-f]{64}$/;