@emilia-protocol/gate 0.9.4 → 0.10.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 (74) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +190 -0
  3. package/README.md +75 -16
  4. package/action-control-manifest.js +26 -0
  5. package/adapters/_kit.js +47 -5
  6. package/adapters/aws.js +15 -4
  7. package/adapters/github-demo.mjs +25 -22
  8. package/adapters/github.js +1 -1
  9. package/adapters/jira.js +1 -0
  10. package/adapters/linear.js +1 -0
  11. package/adapters/salesforce.js +1 -0
  12. package/adapters/stripe.js +1 -1
  13. package/adapters/supabase.js +26 -4
  14. package/adapters/vercel.js +33 -4
  15. package/aec-execution.js +16 -4
  16. package/breakglass.js +285 -51
  17. package/control-plane.js +341 -0
  18. package/coverage.js +722 -0
  19. package/custody-demo.mjs +13 -3
  20. package/demo.mjs +9 -3
  21. package/deploy/helm/README.md +16 -8
  22. package/deploy/helm/emilia-gate/Chart.yaml +3 -1
  23. package/deploy/helm/emilia-gate/templates/NOTES.txt +3 -2
  24. package/deploy/helm/emilia-gate/templates/deployment.yaml +55 -1
  25. package/deploy/helm/emilia-gate/values.yaml +18 -2
  26. package/deploy/helm/emilia-gate-service/Chart.yaml +11 -0
  27. package/deploy/helm/emilia-gate-service/README.md +81 -0
  28. package/deploy/helm/emilia-gate-service/templates/NOTES.txt +12 -0
  29. package/deploy/helm/emilia-gate-service/templates/_helpers.tpl +83 -0
  30. package/deploy/helm/emilia-gate-service/templates/deployment.yaml +153 -0
  31. package/deploy/helm/emilia-gate-service/templates/migration-job.yaml +73 -0
  32. package/deploy/helm/emilia-gate-service/templates/networkpolicy.yaml +113 -0
  33. package/deploy/helm/emilia-gate-service/templates/pdb.yaml +14 -0
  34. package/deploy/helm/emilia-gate-service/templates/service.yaml +20 -0
  35. package/deploy/helm/emilia-gate-service/templates/serviceaccount.yaml +8 -0
  36. package/deploy/helm/emilia-gate-service/tests/fixtures/001_gate.sql +26 -0
  37. package/deploy/helm/emilia-gate-service/tests/fixtures/002_runtime_access.sql +32 -0
  38. package/deploy/helm/emilia-gate-service/tests/fixtures/gate.config.mjs +29 -0
  39. package/deploy/helm/emilia-gate-service/tests/render-check.sh +145 -0
  40. package/deploy/helm/emilia-gate-service/values.schema.json +145 -0
  41. package/deploy/helm/emilia-gate-service/values.yaml +166 -0
  42. package/deploy/sql/001-runtime.sql +707 -0
  43. package/deploy/terraform/README.md +24 -14
  44. package/deploy/terraform/main.tf +59 -0
  45. package/deploy/terraform/service/README.md +81 -0
  46. package/deploy/terraform/service/main.tf +718 -0
  47. package/deploy/terraform/service/outputs.tf +19 -0
  48. package/deploy/terraform/service/tests/validate.sh +24 -0
  49. package/deploy/terraform/service/tests/validation.tftest.hcl +168 -0
  50. package/deploy/terraform/service/variables.tf +459 -0
  51. package/deploy/terraform/service/versions.tf +10 -0
  52. package/deploy/terraform/variables.tf +95 -2
  53. package/deployment-attestation.js +248 -0
  54. package/eg1-conformance.js +46 -12
  55. package/enterprise.js +6 -2
  56. package/ep-assure.mjs +3 -2
  57. package/evidence-postgres.js +357 -0
  58. package/evidence.js +10 -3
  59. package/execution-binding.js +141 -26
  60. package/index.js +556 -115
  61. package/key-registry.js +51 -19
  62. package/mcp.js +4 -2
  63. package/network-witness.js +500 -0
  64. package/package.json +25 -5
  65. package/reliance-kernel.js +5 -6
  66. package/reliance-packet.js +43 -10
  67. package/reports/assurance-package.js +45 -19
  68. package/reports/reperform.js +78 -18
  69. package/reports/underwriter.js +1 -1
  70. package/settlement.js +300 -0
  71. package/store-postgres.js +14 -0
  72. package/store.js +26 -5
  73. package/strict-json.js +86 -0
  74. package/witness-postgres.js +97 -0
@@ -0,0 +1,707 @@
1
+ -- SPDX-License-Identifier: Apache-2.0
2
+ -- EMILIA Gate Postgres evidence runtime (EP-GATE-PG-EVIDENCE-v1).
3
+ --
4
+ -- Run this migration as a non-runtime owner with CREATE EXTENSION, CREATEROLE,
5
+ -- and CREATE privileges. Grant emilia_gate_evidence_runtime to each login used
6
+ -- by Gate, then bind each login to exact evidence and binary witness stream
7
+ -- scopes with grant_runtime_scope() and grant_network_witness_scope(). Runtime
8
+ -- reads are RLS-filtered and each write function authorizes session_user.
9
+
10
+ BEGIN;
11
+
12
+ CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
13
+
14
+ DO $role$
15
+ BEGIN
16
+ IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = 'emilia_gate_evidence_runtime') THEN
17
+ CREATE ROLE emilia_gate_evidence_runtime
18
+ NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION;
19
+ END IF;
20
+ END
21
+ $role$;
22
+
23
+ DO $role_safety$
24
+ BEGIN
25
+ IF EXISTS (
26
+ SELECT 1
27
+ FROM pg_catalog.pg_roles
28
+ WHERE rolname = 'emilia_gate_evidence_runtime'
29
+ AND (
30
+ rolcanlogin OR rolsuper OR rolcreatedb OR rolcreaterole
31
+ OR rolreplication OR rolbypassrls
32
+ )
33
+ ) THEN
34
+ RAISE EXCEPTION 'emilia_gate_evidence_runtime has unsafe privileged attributes'
35
+ USING ERRCODE = '42501';
36
+ END IF;
37
+ END
38
+ $role_safety$;
39
+
40
+ ALTER ROLE emilia_gate_evidence_runtime NOLOGIN NOINHERIT;
41
+
42
+ CREATE SCHEMA IF NOT EXISTS emilia_gate_evidence;
43
+ REVOKE ALL ON SCHEMA emilia_gate_evidence FROM PUBLIC;
44
+ REVOKE CREATE ON SCHEMA emilia_gate_evidence FROM emilia_gate_evidence_runtime;
45
+ GRANT USAGE ON SCHEMA emilia_gate_evidence TO emilia_gate_evidence_runtime;
46
+
47
+ CREATE TABLE IF NOT EXISTS emilia_gate_evidence.heads (
48
+ tenant_id TEXT NOT NULL CHECK (char_length(tenant_id) BETWEEN 1 AND 256),
49
+ gate_id TEXT NOT NULL CHECK (char_length(gate_id) BETWEEN 1 AND 256),
50
+ stream_id TEXT NOT NULL CHECK (char_length(stream_id) BETWEEN 1 AND 256),
51
+ head_seq BIGINT NOT NULL DEFAULT -1,
52
+ head_hash TEXT,
53
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
54
+ PRIMARY KEY (tenant_id, gate_id, stream_id),
55
+ CONSTRAINT evidence_head_shape CHECK (
56
+ (head_seq = -1 AND head_hash IS NULL)
57
+ OR (head_seq BETWEEN 0 AND 9007199254740991 AND head_hash ~ '^[0-9a-f]{64}$')
58
+ )
59
+ );
60
+
61
+ CREATE TABLE IF NOT EXISTS emilia_gate_evidence.records (
62
+ tenant_id TEXT NOT NULL CHECK (char_length(tenant_id) BETWEEN 1 AND 256),
63
+ gate_id TEXT NOT NULL CHECK (char_length(gate_id) BETWEEN 1 AND 256),
64
+ stream_id TEXT NOT NULL CHECK (char_length(stream_id) BETWEEN 1 AND 256),
65
+ seq BIGINT NOT NULL CHECK (seq BETWEEN 0 AND 9007199254740991),
66
+ record_id TEXT NOT NULL CHECK (char_length(record_id) BETWEEN 16 AND 256),
67
+ prev_hash TEXT NOT NULL CHECK (prev_hash = 'genesis' OR prev_hash ~ '^[0-9a-f]{64}$'),
68
+ hash TEXT NOT NULL CHECK (hash ~ '^[0-9a-f]{64}$'),
69
+ record JSONB NOT NULL CHECK (jsonb_typeof(record) = 'object'),
70
+ recorded_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
71
+ PRIMARY KEY (tenant_id, gate_id, stream_id, seq),
72
+ UNIQUE (tenant_id, gate_id, stream_id, record_id),
73
+ UNIQUE (tenant_id, gate_id, stream_id, hash),
74
+ FOREIGN KEY (tenant_id, gate_id, stream_id)
75
+ REFERENCES emilia_gate_evidence.heads (tenant_id, gate_id, stream_id)
76
+ ON DELETE RESTRICT,
77
+ CONSTRAINT evidence_record_seq_match CHECK (
78
+ jsonb_typeof(record -> 'seq') = 'number'
79
+ AND (record ->> 'seq')::numeric = seq
80
+ ),
81
+ CONSTRAINT evidence_record_id_match CHECK (record ->> 'record_id' = record_id),
82
+ CONSTRAINT evidence_record_prev_match CHECK (record ->> 'prev_hash' = prev_hash),
83
+ CONSTRAINT evidence_record_hash_match CHECK (record ->> 'hash' = hash)
84
+ );
85
+
86
+ CREATE TABLE IF NOT EXISTS emilia_gate_evidence.runtime_scope_grants (
87
+ login_role NAME NOT NULL,
88
+ tenant_id TEXT NOT NULL CHECK (char_length(tenant_id) BETWEEN 1 AND 256),
89
+ gate_id TEXT NOT NULL CHECK (char_length(gate_id) BETWEEN 1 AND 256),
90
+ stream_id TEXT NOT NULL CHECK (char_length(stream_id) BETWEEN 1 AND 256),
91
+ granted_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
92
+ PRIMARY KEY (login_role, tenant_id, gate_id, stream_id)
93
+ );
94
+
95
+ CREATE TABLE IF NOT EXISTS emilia_gate_evidence.network_witness_scope_grants (
96
+ login_role NAME NOT NULL,
97
+ tenant_id TEXT NOT NULL CHECK (char_length(tenant_id) BETWEEN 1 AND 256),
98
+ gate_id TEXT NOT NULL CHECK (char_length(gate_id) BETWEEN 1 AND 256),
99
+ stream_key BYTEA NOT NULL CHECK (octet_length(stream_key) BETWEEN 1 AND 1024),
100
+ granted_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
101
+ PRIMARY KEY (login_role, tenant_id, gate_id, stream_key)
102
+ );
103
+
104
+ CREATE TABLE IF NOT EXISTS emilia_gate_evidence.network_witness_checkpoints (
105
+ tenant_id TEXT NOT NULL CHECK (char_length(tenant_id) BETWEEN 1 AND 256),
106
+ gate_id TEXT NOT NULL CHECK (char_length(gate_id) BETWEEN 1 AND 256),
107
+ stream_key BYTEA NOT NULL CHECK (octet_length(stream_key) BETWEEN 1 AND 1024),
108
+ sequence BIGINT NOT NULL CHECK (sequence BETWEEN 0 AND 9007199254740991),
109
+ statement_digest TEXT NOT NULL CHECK (statement_digest ~ '^sha256:[0-9a-f]{64}$'),
110
+ equivocated BOOLEAN NOT NULL DEFAULT FALSE,
111
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
112
+ PRIMARY KEY (tenant_id, gate_id, stream_key)
113
+ );
114
+
115
+ ALTER TABLE emilia_gate_evidence.network_witness_checkpoints
116
+ ADD COLUMN IF NOT EXISTS equivocated BOOLEAN NOT NULL DEFAULT FALSE;
117
+
118
+ -- Re-running the migration cannot leave the runtime role as an object owner;
119
+ -- ownership bypasses ordinary table ACLs in Postgres.
120
+ ALTER SCHEMA emilia_gate_evidence OWNER TO CURRENT_USER;
121
+ ALTER TABLE emilia_gate_evidence.heads OWNER TO CURRENT_USER;
122
+ ALTER TABLE emilia_gate_evidence.records OWNER TO CURRENT_USER;
123
+ ALTER TABLE emilia_gate_evidence.runtime_scope_grants OWNER TO CURRENT_USER;
124
+ ALTER TABLE emilia_gate_evidence.network_witness_scope_grants OWNER TO CURRENT_USER;
125
+ ALTER TABLE emilia_gate_evidence.network_witness_checkpoints OWNER TO CURRENT_USER;
126
+
127
+ CREATE OR REPLACE FUNCTION emilia_gate_evidence.reject_record_mutation()
128
+ RETURNS trigger
129
+ LANGUAGE plpgsql
130
+ SET search_path = pg_catalog, pg_temp
131
+ AS $function$
132
+ BEGIN
133
+ RAISE EXCEPTION 'EMILIA evidence records are append-only'
134
+ USING ERRCODE = '55000';
135
+ END
136
+ $function$;
137
+
138
+ ALTER FUNCTION emilia_gate_evidence.reject_record_mutation() OWNER TO CURRENT_USER;
139
+
140
+ DROP TRIGGER IF EXISTS evidence_records_are_immutable ON emilia_gate_evidence.records;
141
+ CREATE TRIGGER evidence_records_are_immutable
142
+ BEFORE UPDATE OR DELETE OR TRUNCATE ON emilia_gate_evidence.records
143
+ FOR EACH STATEMENT EXECUTE FUNCTION emilia_gate_evidence.reject_record_mutation();
144
+
145
+ CREATE OR REPLACE FUNCTION emilia_gate_evidence.runtime_scope_authorized(
146
+ p_tenant_id TEXT,
147
+ p_gate_id TEXT,
148
+ p_stream_id TEXT
149
+ )
150
+ RETURNS BOOLEAN
151
+ LANGUAGE sql
152
+ STABLE
153
+ SECURITY DEFINER
154
+ SET search_path = pg_catalog, pg_temp
155
+ AS $function$
156
+ SELECT EXISTS (
157
+ SELECT 1
158
+ FROM emilia_gate_evidence.runtime_scope_grants AS g
159
+ JOIN pg_catalog.pg_roles AS r ON r.rolname = g.login_role
160
+ WHERE g.login_role = session_user
161
+ AND g.tenant_id = p_tenant_id
162
+ AND g.gate_id = p_gate_id
163
+ AND g.stream_id = p_stream_id
164
+ AND r.rolcanlogin
165
+ AND NOT r.rolsuper
166
+ AND NOT r.rolbypassrls
167
+ AND pg_catalog.pg_has_role(
168
+ session_user,
169
+ 'emilia_gate_evidence_runtime',
170
+ 'MEMBER'
171
+ )
172
+ )
173
+ $function$;
174
+
175
+ CREATE OR REPLACE FUNCTION emilia_gate_evidence.grant_runtime_scope(
176
+ p_login_role NAME,
177
+ p_tenant_id TEXT,
178
+ p_gate_id TEXT,
179
+ p_stream_id TEXT
180
+ )
181
+ RETURNS VOID
182
+ LANGUAGE plpgsql
183
+ SECURITY DEFINER
184
+ SET search_path = pg_catalog, pg_temp
185
+ AS $function$
186
+ DECLARE
187
+ v_role pg_catalog.pg_roles%ROWTYPE;
188
+ BEGIN
189
+ IF p_tenant_id IS NULL OR char_length(p_tenant_id) NOT BETWEEN 1 AND 256
190
+ OR p_gate_id IS NULL OR char_length(p_gate_id) NOT BETWEEN 1 AND 256
191
+ OR p_stream_id IS NULL OR char_length(p_stream_id) NOT BETWEEN 1 AND 256 THEN
192
+ RAISE EXCEPTION 'invalid EMILIA evidence scope' USING ERRCODE = '22023';
193
+ END IF;
194
+
195
+ SELECT * INTO v_role
196
+ FROM pg_catalog.pg_roles
197
+ WHERE rolname = p_login_role;
198
+ IF NOT FOUND OR NOT v_role.rolcanlogin THEN
199
+ RAISE EXCEPTION 'EMILIA evidence scope requires an existing login role'
200
+ USING ERRCODE = '22023';
201
+ END IF;
202
+ IF v_role.rolsuper OR v_role.rolbypassrls THEN
203
+ RAISE EXCEPTION 'EMILIA runtime login must not bypass row security'
204
+ USING ERRCODE = '42501';
205
+ END IF;
206
+ IF NOT pg_catalog.pg_has_role(
207
+ p_login_role,
208
+ 'emilia_gate_evidence_runtime',
209
+ 'MEMBER'
210
+ ) THEN
211
+ RAISE EXCEPTION 'EMILIA runtime login must be a member of emilia_gate_evidence_runtime'
212
+ USING ERRCODE = '42501';
213
+ END IF;
214
+
215
+ INSERT INTO emilia_gate_evidence.runtime_scope_grants (
216
+ login_role, tenant_id, gate_id, stream_id
217
+ ) VALUES (
218
+ p_login_role, p_tenant_id, p_gate_id, p_stream_id
219
+ ) ON CONFLICT DO NOTHING;
220
+ END
221
+ $function$;
222
+
223
+ CREATE OR REPLACE FUNCTION emilia_gate_evidence.revoke_runtime_scope(
224
+ p_login_role NAME,
225
+ p_tenant_id TEXT,
226
+ p_gate_id TEXT,
227
+ p_stream_id TEXT
228
+ )
229
+ RETURNS BOOLEAN
230
+ LANGUAGE sql
231
+ SECURITY DEFINER
232
+ SET search_path = pg_catalog, pg_temp
233
+ AS $function$
234
+ WITH deleted AS (
235
+ DELETE FROM emilia_gate_evidence.runtime_scope_grants
236
+ WHERE login_role = p_login_role
237
+ AND tenant_id = p_tenant_id
238
+ AND gate_id = p_gate_id
239
+ AND stream_id = p_stream_id
240
+ RETURNING 1
241
+ )
242
+ SELECT EXISTS (SELECT 1 FROM deleted)
243
+ $function$;
244
+
245
+ CREATE OR REPLACE FUNCTION emilia_gate_evidence.network_witness_scope_authorized(
246
+ p_tenant_id TEXT,
247
+ p_gate_id TEXT,
248
+ p_stream_key BYTEA
249
+ )
250
+ RETURNS BOOLEAN
251
+ LANGUAGE sql
252
+ STABLE
253
+ SECURITY DEFINER
254
+ SET search_path = pg_catalog, pg_temp
255
+ AS $function$
256
+ SELECT EXISTS (
257
+ SELECT 1
258
+ FROM emilia_gate_evidence.network_witness_scope_grants AS g
259
+ JOIN pg_catalog.pg_roles AS r ON r.rolname = g.login_role
260
+ WHERE g.login_role = session_user
261
+ AND g.tenant_id = p_tenant_id
262
+ AND g.gate_id = p_gate_id
263
+ AND g.stream_key = p_stream_key
264
+ AND r.rolcanlogin
265
+ AND NOT r.rolsuper
266
+ AND NOT r.rolbypassrls
267
+ AND pg_catalog.pg_has_role(
268
+ session_user,
269
+ 'emilia_gate_evidence_runtime',
270
+ 'MEMBER'
271
+ )
272
+ )
273
+ $function$;
274
+
275
+ CREATE OR REPLACE FUNCTION emilia_gate_evidence.grant_network_witness_scope(
276
+ p_login_role NAME,
277
+ p_tenant_id TEXT,
278
+ p_gate_id TEXT,
279
+ p_stream_key BYTEA
280
+ )
281
+ RETURNS VOID
282
+ LANGUAGE plpgsql
283
+ SECURITY DEFINER
284
+ SET search_path = pg_catalog, pg_temp
285
+ AS $function$
286
+ DECLARE
287
+ v_role pg_catalog.pg_roles%ROWTYPE;
288
+ BEGIN
289
+ IF p_tenant_id IS NULL OR char_length(p_tenant_id) NOT BETWEEN 1 AND 256
290
+ OR p_gate_id IS NULL OR char_length(p_gate_id) NOT BETWEEN 1 AND 256
291
+ OR p_stream_key IS NULL OR octet_length(p_stream_key) NOT BETWEEN 1 AND 1024 THEN
292
+ RAISE EXCEPTION 'invalid EMILIA network-witness scope' USING ERRCODE = '22023';
293
+ END IF;
294
+
295
+ SELECT * INTO v_role
296
+ FROM pg_catalog.pg_roles
297
+ WHERE rolname = p_login_role;
298
+ IF NOT FOUND OR NOT v_role.rolcanlogin THEN
299
+ RAISE EXCEPTION 'EMILIA network-witness scope requires an existing login role'
300
+ USING ERRCODE = '22023';
301
+ END IF;
302
+ IF v_role.rolsuper OR v_role.rolbypassrls THEN
303
+ RAISE EXCEPTION 'EMILIA runtime login must not bypass row security'
304
+ USING ERRCODE = '42501';
305
+ END IF;
306
+ IF NOT pg_catalog.pg_has_role(
307
+ p_login_role,
308
+ 'emilia_gate_evidence_runtime',
309
+ 'MEMBER'
310
+ ) THEN
311
+ RAISE EXCEPTION 'EMILIA runtime login must be a member of emilia_gate_evidence_runtime'
312
+ USING ERRCODE = '42501';
313
+ END IF;
314
+
315
+ INSERT INTO emilia_gate_evidence.network_witness_scope_grants (
316
+ login_role, tenant_id, gate_id, stream_key
317
+ ) VALUES (
318
+ p_login_role, p_tenant_id, p_gate_id, p_stream_key
319
+ ) ON CONFLICT DO NOTHING;
320
+ END
321
+ $function$;
322
+
323
+ CREATE OR REPLACE FUNCTION emilia_gate_evidence.revoke_network_witness_scope(
324
+ p_login_role NAME,
325
+ p_tenant_id TEXT,
326
+ p_gate_id TEXT,
327
+ p_stream_key BYTEA
328
+ )
329
+ RETURNS BOOLEAN
330
+ LANGUAGE sql
331
+ SECURITY DEFINER
332
+ SET search_path = pg_catalog, pg_temp
333
+ AS $function$
334
+ WITH deleted AS (
335
+ DELETE FROM emilia_gate_evidence.network_witness_scope_grants
336
+ WHERE login_role = p_login_role
337
+ AND tenant_id = p_tenant_id
338
+ AND gate_id = p_gate_id
339
+ AND stream_key = p_stream_key
340
+ RETURNING 1
341
+ )
342
+ SELECT EXISTS (SELECT 1 FROM deleted)
343
+ $function$;
344
+
345
+ CREATE OR REPLACE FUNCTION emilia_gate_evidence.advance_network_witness_checkpoint(
346
+ p_tenant_id TEXT,
347
+ p_gate_id TEXT,
348
+ p_stream_key BYTEA,
349
+ p_sequence BIGINT,
350
+ p_statement_digest TEXT
351
+ )
352
+ RETURNS TABLE (accepted BOOLEAN, reason TEXT)
353
+ LANGUAGE plpgsql
354
+ SECURITY DEFINER
355
+ SET search_path = pg_catalog, pg_temp
356
+ AS $function$
357
+ DECLARE
358
+ v_current_digest TEXT;
359
+ v_current_sequence BIGINT;
360
+ v_equivocated BOOLEAN;
361
+ v_inserted INTEGER;
362
+ BEGIN
363
+ IF p_tenant_id IS NULL OR char_length(p_tenant_id) NOT BETWEEN 1 AND 256
364
+ OR p_gate_id IS NULL OR char_length(p_gate_id) NOT BETWEEN 1 AND 256
365
+ OR p_stream_key IS NULL OR octet_length(p_stream_key) NOT BETWEEN 1 AND 1024 THEN
366
+ RAISE EXCEPTION 'invalid EMILIA network-witness scope' USING ERRCODE = '22023';
367
+ END IF;
368
+ IF NOT emilia_gate_evidence.network_witness_scope_authorized(
369
+ p_tenant_id,
370
+ p_gate_id,
371
+ p_stream_key
372
+ ) THEN
373
+ RAISE EXCEPTION 'EMILIA network-witness scope is not authorized for session login %', session_user
374
+ USING ERRCODE = '42501';
375
+ END IF;
376
+ IF p_sequence IS NULL OR p_sequence < 0 OR p_sequence > 9007199254740991 THEN
377
+ RAISE EXCEPTION 'invalid EMILIA network-witness sequence' USING ERRCODE = '22003';
378
+ END IF;
379
+ IF p_statement_digest IS NULL
380
+ OR p_statement_digest !~ '^sha256:[0-9a-f]{64}$' THEN
381
+ RAISE EXCEPTION 'invalid EMILIA network-witness statement digest'
382
+ USING ERRCODE = '22023';
383
+ END IF;
384
+
385
+ INSERT INTO emilia_gate_evidence.network_witness_checkpoints (
386
+ tenant_id, gate_id, stream_key, sequence, statement_digest
387
+ ) VALUES (
388
+ p_tenant_id, p_gate_id, p_stream_key, p_sequence, p_statement_digest
389
+ ) ON CONFLICT (tenant_id, gate_id, stream_key) DO NOTHING;
390
+ GET DIAGNOSTICS v_inserted = ROW_COUNT;
391
+ IF v_inserted = 1 THEN
392
+ RETURN QUERY SELECT TRUE, NULL::TEXT;
393
+ RETURN;
394
+ END IF;
395
+
396
+ SELECT checkpoint.sequence, checkpoint.statement_digest, checkpoint.equivocated
397
+ INTO STRICT v_current_sequence, v_current_digest, v_equivocated
398
+ FROM emilia_gate_evidence.network_witness_checkpoints AS checkpoint
399
+ WHERE checkpoint.tenant_id = p_tenant_id
400
+ AND checkpoint.gate_id = p_gate_id
401
+ AND checkpoint.stream_key = p_stream_key
402
+ FOR UPDATE;
403
+
404
+ IF v_equivocated THEN
405
+ RETURN QUERY SELECT FALSE, 'sequence_equivocation'::TEXT;
406
+ RETURN;
407
+ END IF;
408
+
409
+ IF p_sequence < v_current_sequence THEN
410
+ RETURN QUERY SELECT FALSE, 'sequence_rollback'::TEXT;
411
+ RETURN;
412
+ END IF;
413
+ IF p_sequence = v_current_sequence THEN
414
+ IF p_statement_digest = v_current_digest THEN
415
+ RETURN QUERY SELECT FALSE, 'statement_replay'::TEXT;
416
+ ELSE
417
+ UPDATE emilia_gate_evidence.network_witness_checkpoints
418
+ SET equivocated = TRUE,
419
+ updated_at = clock_timestamp()
420
+ WHERE tenant_id = p_tenant_id
421
+ AND gate_id = p_gate_id
422
+ AND stream_key = p_stream_key
423
+ AND sequence = v_current_sequence
424
+ AND statement_digest = v_current_digest
425
+ AND equivocated = FALSE;
426
+ IF NOT FOUND THEN
427
+ RAISE EXCEPTION 'EMILIA network-witness equivocation fence was lost'
428
+ USING ERRCODE = '40001';
429
+ END IF;
430
+ RETURN QUERY SELECT FALSE, 'sequence_equivocation'::TEXT;
431
+ END IF;
432
+ RETURN;
433
+ END IF;
434
+
435
+ UPDATE emilia_gate_evidence.network_witness_checkpoints
436
+ SET sequence = p_sequence,
437
+ statement_digest = p_statement_digest,
438
+ updated_at = clock_timestamp()
439
+ WHERE tenant_id = p_tenant_id
440
+ AND gate_id = p_gate_id
441
+ AND stream_key = p_stream_key
442
+ AND sequence = v_current_sequence
443
+ AND statement_digest = v_current_digest
444
+ AND equivocated = FALSE;
445
+ IF NOT FOUND THEN
446
+ RAISE EXCEPTION 'EMILIA network-witness checkpoint fence was lost'
447
+ USING ERRCODE = '40001';
448
+ END IF;
449
+
450
+ RETURN QUERY SELECT TRUE, NULL::TEXT;
451
+ END
452
+ $function$;
453
+
454
+ ALTER FUNCTION emilia_gate_evidence.runtime_scope_authorized(TEXT, TEXT, TEXT)
455
+ OWNER TO CURRENT_USER;
456
+ ALTER FUNCTION emilia_gate_evidence.grant_runtime_scope(NAME, TEXT, TEXT, TEXT)
457
+ OWNER TO CURRENT_USER;
458
+ ALTER FUNCTION emilia_gate_evidence.revoke_runtime_scope(NAME, TEXT, TEXT, TEXT)
459
+ OWNER TO CURRENT_USER;
460
+ ALTER FUNCTION emilia_gate_evidence.network_witness_scope_authorized(TEXT, TEXT, BYTEA)
461
+ OWNER TO CURRENT_USER;
462
+ ALTER FUNCTION emilia_gate_evidence.grant_network_witness_scope(NAME, TEXT, TEXT, BYTEA)
463
+ OWNER TO CURRENT_USER;
464
+ ALTER FUNCTION emilia_gate_evidence.revoke_network_witness_scope(NAME, TEXT, TEXT, BYTEA)
465
+ OWNER TO CURRENT_USER;
466
+ ALTER FUNCTION emilia_gate_evidence.advance_network_witness_checkpoint(TEXT, TEXT, BYTEA, BIGINT, TEXT)
467
+ OWNER TO CURRENT_USER;
468
+
469
+ CREATE OR REPLACE FUNCTION emilia_gate_evidence.append_record(
470
+ p_tenant_id TEXT,
471
+ p_gate_id TEXT,
472
+ p_stream_id TEXT,
473
+ p_expected_head_hash TEXT,
474
+ p_record JSONB,
475
+ p_canonical_body TEXT
476
+ )
477
+ RETURNS BOOLEAN
478
+ LANGUAGE plpgsql
479
+ SECURITY DEFINER
480
+ SET search_path = pg_catalog, pg_temp
481
+ AS $function$
482
+ DECLARE
483
+ v_body JSONB;
484
+ v_head_hash TEXT;
485
+ v_head_seq BIGINT;
486
+ v_hash TEXT;
487
+ v_prev_hash TEXT;
488
+ v_record_id TEXT;
489
+ v_seq BIGINT;
490
+ v_updated INTEGER;
491
+ BEGIN
492
+ IF p_tenant_id IS NULL OR char_length(p_tenant_id) NOT BETWEEN 1 AND 256
493
+ OR p_gate_id IS NULL OR char_length(p_gate_id) NOT BETWEEN 1 AND 256
494
+ OR p_stream_id IS NULL OR char_length(p_stream_id) NOT BETWEEN 1 AND 256 THEN
495
+ RAISE EXCEPTION 'invalid EMILIA evidence scope' USING ERRCODE = '22023';
496
+ END IF;
497
+ IF NOT emilia_gate_evidence.runtime_scope_authorized(
498
+ p_tenant_id,
499
+ p_gate_id,
500
+ p_stream_id
501
+ ) THEN
502
+ RAISE EXCEPTION 'EMILIA evidence scope is not authorized for session login %', session_user
503
+ USING ERRCODE = '42501';
504
+ END IF;
505
+ IF p_expected_head_hash IS NOT NULL AND p_expected_head_hash !~ '^[0-9a-f]{64}$' THEN
506
+ RAISE EXCEPTION 'invalid expected EMILIA evidence head' USING ERRCODE = '22023';
507
+ END IF;
508
+ IF p_record IS NULL OR jsonb_typeof(p_record) <> 'object'
509
+ OR p_canonical_body IS NULL
510
+ OR octet_length(p_canonical_body) > 8388608
511
+ OR jsonb_typeof(p_record -> 'seq') <> 'number'
512
+ OR coalesce(p_record ->> 'seq', '') !~ '^(0|[1-9][0-9]*)$'
513
+ OR char_length(coalesce(p_record ->> 'record_id', '')) NOT BETWEEN 16 AND 256
514
+ OR coalesce(p_record ->> 'prev_hash', '') !~ '^(genesis|[0-9a-f]{64})$'
515
+ OR coalesce(p_record ->> 'hash', '') !~ '^[0-9a-f]{64}$' THEN
516
+ RAISE EXCEPTION 'invalid EMILIA evidence record' USING ERRCODE = '22023';
517
+ END IF;
518
+
519
+ BEGIN
520
+ v_body := p_canonical_body::jsonb;
521
+ v_seq := (p_record ->> 'seq')::bigint;
522
+ EXCEPTION WHEN OTHERS THEN
523
+ RAISE EXCEPTION 'invalid EMILIA canonical evidence body' USING ERRCODE = '22023';
524
+ END;
525
+ IF v_seq < 0 OR v_seq > 9007199254740991 THEN
526
+ RAISE EXCEPTION 'EMILIA evidence sequence is outside the safe-integer range'
527
+ USING ERRCODE = '22003';
528
+ END IF;
529
+
530
+ v_record_id := p_record ->> 'record_id';
531
+ v_prev_hash := p_record ->> 'prev_hash';
532
+ v_hash := p_record ->> 'hash';
533
+ IF v_body IS DISTINCT FROM (p_record - 'hash')
534
+ OR encode(public.digest(convert_to(p_canonical_body, 'UTF8'), 'sha256'), 'hex')
535
+ IS DISTINCT FROM v_hash THEN
536
+ RAISE EXCEPTION 'EMILIA evidence canonical hash mismatch' USING ERRCODE = '23514';
537
+ END IF;
538
+
539
+ INSERT INTO emilia_gate_evidence.heads (tenant_id, gate_id, stream_id)
540
+ VALUES (p_tenant_id, p_gate_id, p_stream_id)
541
+ ON CONFLICT (tenant_id, gate_id, stream_id) DO NOTHING;
542
+
543
+ SELECT h.head_seq, h.head_hash
544
+ INTO STRICT v_head_seq, v_head_hash
545
+ FROM emilia_gate_evidence.heads h
546
+ WHERE h.tenant_id = p_tenant_id
547
+ AND h.gate_id = p_gate_id
548
+ AND h.stream_id = p_stream_id
549
+ FOR UPDATE;
550
+
551
+ -- A false return means ordinary optimistic contention. Every malformed or
552
+ -- indeterminate condition raises, so callers cannot mistake it for a retry.
553
+ IF p_expected_head_hash IS DISTINCT FROM v_head_hash THEN
554
+ RETURN FALSE;
555
+ END IF;
556
+ IF v_seq <> v_head_seq + 1
557
+ OR v_prev_hash <> coalesce(v_head_hash, 'genesis') THEN
558
+ RAISE EXCEPTION 'EMILIA evidence record does not extend the locked head'
559
+ USING ERRCODE = '23514';
560
+ END IF;
561
+
562
+ INSERT INTO emilia_gate_evidence.records (
563
+ tenant_id, gate_id, stream_id, seq, record_id, prev_hash, hash, record
564
+ ) VALUES (
565
+ p_tenant_id, p_gate_id, p_stream_id, v_seq, v_record_id, v_prev_hash, v_hash, p_record
566
+ );
567
+
568
+ UPDATE emilia_gate_evidence.heads
569
+ SET head_seq = v_seq, head_hash = v_hash, updated_at = clock_timestamp()
570
+ WHERE tenant_id = p_tenant_id
571
+ AND gate_id = p_gate_id
572
+ AND stream_id = p_stream_id
573
+ AND head_seq = v_head_seq
574
+ AND head_hash IS NOT DISTINCT FROM v_head_hash;
575
+ GET DIAGNOSTICS v_updated = ROW_COUNT;
576
+ IF v_updated <> 1 THEN
577
+ RAISE EXCEPTION 'EMILIA evidence head fence was lost' USING ERRCODE = '40001';
578
+ END IF;
579
+
580
+ RETURN TRUE;
581
+ END
582
+ $function$;
583
+
584
+ ALTER FUNCTION emilia_gate_evidence.append_record(TEXT, TEXT, TEXT, TEXT, JSONB, TEXT)
585
+ OWNER TO CURRENT_USER;
586
+
587
+ ALTER TABLE emilia_gate_evidence.heads ENABLE ROW LEVEL SECURITY;
588
+ ALTER TABLE emilia_gate_evidence.heads FORCE ROW LEVEL SECURITY;
589
+ ALTER TABLE emilia_gate_evidence.records ENABLE ROW LEVEL SECURITY;
590
+ ALTER TABLE emilia_gate_evidence.records FORCE ROW LEVEL SECURITY;
591
+ ALTER TABLE emilia_gate_evidence.network_witness_checkpoints ENABLE ROW LEVEL SECURITY;
592
+ ALTER TABLE emilia_gate_evidence.network_witness_checkpoints FORCE ROW LEVEL SECURITY;
593
+
594
+ DROP POLICY IF EXISTS evidence_heads_runtime_read ON emilia_gate_evidence.heads;
595
+ CREATE POLICY evidence_heads_runtime_read
596
+ ON emilia_gate_evidence.heads
597
+ FOR SELECT
598
+ TO emilia_gate_evidence_runtime
599
+ USING (emilia_gate_evidence.runtime_scope_authorized(tenant_id, gate_id, stream_id));
600
+
601
+ DROP POLICY IF EXISTS evidence_records_runtime_read ON emilia_gate_evidence.records;
602
+ CREATE POLICY evidence_records_runtime_read
603
+ ON emilia_gate_evidence.records
604
+ FOR SELECT
605
+ TO emilia_gate_evidence_runtime
606
+ USING (emilia_gate_evidence.runtime_scope_authorized(tenant_id, gate_id, stream_id));
607
+
608
+ DROP POLICY IF EXISTS network_witness_runtime_read
609
+ ON emilia_gate_evidence.network_witness_checkpoints;
610
+ CREATE POLICY network_witness_runtime_read
611
+ ON emilia_gate_evidence.network_witness_checkpoints
612
+ FOR SELECT
613
+ TO emilia_gate_evidence_runtime
614
+ USING (emilia_gate_evidence.network_witness_scope_authorized(tenant_id, gate_id, stream_key));
615
+
616
+ -- FORCE RLS applies to the table owner too. Give only the current migration
617
+ -- owner an all-rows policy so SECURITY DEFINER append can perform its fenced
618
+ -- write after validating session_user. Re-runs replace stale owner policies.
619
+ DROP POLICY IF EXISTS evidence_heads_owner_all ON emilia_gate_evidence.heads;
620
+ DROP POLICY IF EXISTS evidence_records_owner_all ON emilia_gate_evidence.records;
621
+ DROP POLICY IF EXISTS network_witness_owner_all
622
+ ON emilia_gate_evidence.network_witness_checkpoints;
623
+ DO $policies$
624
+ BEGIN
625
+ EXECUTE format(
626
+ 'CREATE POLICY evidence_heads_owner_all ON emilia_gate_evidence.heads FOR ALL TO %I USING (true) WITH CHECK (true)',
627
+ current_user
628
+ );
629
+ EXECUTE format(
630
+ 'CREATE POLICY evidence_records_owner_all ON emilia_gate_evidence.records FOR ALL TO %I USING (true) WITH CHECK (true)',
631
+ current_user
632
+ );
633
+ EXECUTE format(
634
+ 'CREATE POLICY network_witness_owner_all ON emilia_gate_evidence.network_witness_checkpoints FOR ALL TO %I USING (true) WITH CHECK (true)',
635
+ current_user
636
+ );
637
+ END
638
+ $policies$;
639
+
640
+ REVOKE ALL ON TABLE
641
+ emilia_gate_evidence.records,
642
+ emilia_gate_evidence.heads,
643
+ emilia_gate_evidence.runtime_scope_grants,
644
+ emilia_gate_evidence.network_witness_scope_grants,
645
+ emilia_gate_evidence.network_witness_checkpoints
646
+ FROM PUBLIC;
647
+ REVOKE INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER
648
+ ON emilia_gate_evidence.records, emilia_gate_evidence.heads
649
+ FROM emilia_gate_evidence_runtime;
650
+ REVOKE ALL ON TABLE emilia_gate_evidence.runtime_scope_grants
651
+ FROM emilia_gate_evidence_runtime;
652
+ REVOKE ALL ON TABLE emilia_gate_evidence.network_witness_scope_grants
653
+ FROM emilia_gate_evidence_runtime;
654
+ REVOKE INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER
655
+ ON emilia_gate_evidence.network_witness_checkpoints
656
+ FROM emilia_gate_evidence_runtime;
657
+ GRANT SELECT ON emilia_gate_evidence.records, emilia_gate_evidence.heads
658
+ TO emilia_gate_evidence_runtime;
659
+ GRANT SELECT ON emilia_gate_evidence.network_witness_checkpoints
660
+ TO emilia_gate_evidence_runtime;
661
+
662
+ REVOKE ALL ON FUNCTION emilia_gate_evidence.reject_record_mutation() FROM PUBLIC;
663
+ REVOKE ALL ON FUNCTION emilia_gate_evidence.reject_record_mutation()
664
+ FROM emilia_gate_evidence_runtime;
665
+ REVOKE ALL ON FUNCTION emilia_gate_evidence.runtime_scope_authorized(TEXT, TEXT, TEXT)
666
+ FROM PUBLIC, emilia_gate_evidence_runtime;
667
+ GRANT EXECUTE ON FUNCTION emilia_gate_evidence.runtime_scope_authorized(TEXT, TEXT, TEXT)
668
+ TO emilia_gate_evidence_runtime;
669
+ REVOKE ALL ON FUNCTION emilia_gate_evidence.grant_runtime_scope(NAME, TEXT, TEXT, TEXT)
670
+ FROM PUBLIC, emilia_gate_evidence_runtime;
671
+ REVOKE ALL ON FUNCTION emilia_gate_evidence.revoke_runtime_scope(NAME, TEXT, TEXT, TEXT)
672
+ FROM PUBLIC, emilia_gate_evidence_runtime;
673
+ REVOKE ALL ON FUNCTION emilia_gate_evidence.network_witness_scope_authorized(TEXT, TEXT, BYTEA)
674
+ FROM PUBLIC, emilia_gate_evidence_runtime;
675
+ GRANT EXECUTE ON FUNCTION emilia_gate_evidence.network_witness_scope_authorized(TEXT, TEXT, BYTEA)
676
+ TO emilia_gate_evidence_runtime;
677
+ REVOKE ALL ON FUNCTION emilia_gate_evidence.grant_network_witness_scope(NAME, TEXT, TEXT, BYTEA)
678
+ FROM PUBLIC, emilia_gate_evidence_runtime;
679
+ REVOKE ALL ON FUNCTION emilia_gate_evidence.revoke_network_witness_scope(NAME, TEXT, TEXT, BYTEA)
680
+ FROM PUBLIC, emilia_gate_evidence_runtime;
681
+ REVOKE ALL ON FUNCTION emilia_gate_evidence.advance_network_witness_checkpoint(TEXT, TEXT, BYTEA, BIGINT, TEXT)
682
+ FROM PUBLIC, emilia_gate_evidence_runtime;
683
+ GRANT EXECUTE ON FUNCTION emilia_gate_evidence.advance_network_witness_checkpoint(TEXT, TEXT, BYTEA, BIGINT, TEXT)
684
+ TO emilia_gate_evidence_runtime;
685
+ REVOKE ALL ON FUNCTION emilia_gate_evidence.append_record(TEXT, TEXT, TEXT, TEXT, JSONB, TEXT)
686
+ FROM PUBLIC, emilia_gate_evidence_runtime;
687
+ GRANT EXECUTE ON FUNCTION emilia_gate_evidence.append_record(TEXT, TEXT, TEXT, TEXT, JSONB, TEXT)
688
+ TO emilia_gate_evidence_runtime;
689
+
690
+ COMMENT ON FUNCTION emilia_gate_evidence.append_record(TEXT, TEXT, TEXT, TEXT, JSONB, TEXT)
691
+ IS 'Authorize session_user, then atomically compare-and-append canonical EMILIA evidence under a scoped head row lock.';
692
+ COMMENT ON FUNCTION emilia_gate_evidence.grant_runtime_scope(NAME, TEXT, TEXT, TEXT)
693
+ IS 'Bind one non-bypass runtime login to one exact tenant/gate/stream evidence scope.';
694
+ COMMENT ON FUNCTION emilia_gate_evidence.grant_network_witness_scope(NAME, TEXT, TEXT, BYTEA)
695
+ IS 'Bind one non-bypass runtime login to one exact tenant/gate/binary witness stream scope.';
696
+ COMMENT ON TABLE emilia_gate_evidence.runtime_scope_grants
697
+ IS 'Owner-managed bindings from runtime login roles to exact textual evidence scopes.';
698
+ COMMENT ON TABLE emilia_gate_evidence.network_witness_scope_grants
699
+ IS 'Owner-managed bindings from runtime login roles to exact binary network-witness stream keys.';
700
+ COMMENT ON FUNCTION emilia_gate_evidence.advance_network_witness_checkpoint(TEXT, TEXT, BYTEA, BIGINT, TEXT)
701
+ IS 'Atomically advance a scoped witness checkpoint or distinguish replay, rollback, and same-sequence equivocation.';
702
+ COMMENT ON TABLE emilia_gate_evidence.network_witness_checkpoints
703
+ IS 'Latest tenant/gate/stream network-witness sequence and statement digest; a checkpoint, not complete observation history.';
704
+ COMMENT ON TABLE emilia_gate_evidence.records
705
+ IS 'Immutable tenant/gate/stream-scoped EMILIA evidence records.';
706
+
707
+ COMMIT;