@emilia-protocol/gate 0.13.0 → 0.15.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 (57) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +89 -0
  3. package/aeb-consumption-store.js +3 -0
  4. package/dist/aeb-consumption-store.d.ts +80 -0
  5. package/dist/aeb-consumption-store.d.ts.map +1 -0
  6. package/dist/aeb-consumption-store.js +430 -0
  7. package/dist/aeb-consumption-store.js.map +1 -0
  8. package/dist/capability-receipt.d.ts +8 -0
  9. package/dist/capability-receipt.d.ts.map +1 -1
  10. package/dist/capability-receipt.js +17 -0
  11. package/dist/capability-receipt.js.map +1 -1
  12. package/dist/index.d.ts +13 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +17 -4
  15. package/dist/index.js.map +1 -1
  16. package/dist/proposal-to-effect-postgres.d.ts +129 -0
  17. package/dist/proposal-to-effect-postgres.d.ts.map +1 -0
  18. package/dist/proposal-to-effect-postgres.js +1511 -0
  19. package/dist/proposal-to-effect-postgres.js.map +1 -0
  20. package/dist/proposal-to-effect-status.d.ts +55 -0
  21. package/dist/proposal-to-effect-status.d.ts.map +1 -0
  22. package/dist/proposal-to-effect-status.js +280 -0
  23. package/dist/proposal-to-effect-status.js.map +1 -0
  24. package/dist/proposal-to-effect.d.ts +294 -0
  25. package/dist/proposal-to-effect.d.ts.map +1 -0
  26. package/dist/proposal-to-effect.js +970 -0
  27. package/dist/proposal-to-effect.js.map +1 -0
  28. package/dist/remedy-case-set-postgres.d.ts +73 -0
  29. package/dist/remedy-case-set-postgres.d.ts.map +1 -0
  30. package/dist/remedy-case-set-postgres.js +846 -0
  31. package/dist/remedy-case-set-postgres.js.map +1 -0
  32. package/dist/remedy-case-set.d.ts +53 -0
  33. package/dist/remedy-case-set.d.ts.map +1 -0
  34. package/dist/remedy-case-set.js +571 -0
  35. package/dist/remedy-case-set.js.map +1 -0
  36. package/dist/remedy-program-adapters.d.ts +137 -0
  37. package/dist/remedy-program-adapters.d.ts.map +1 -0
  38. package/dist/remedy-program-adapters.js +590 -0
  39. package/dist/remedy-program-adapters.js.map +1 -0
  40. package/dist/trust-program-revocation.js.map +1 -1
  41. package/package.json +280 -63
  42. package/proposal-to-effect-postgres.js +3 -0
  43. package/proposal-to-effect-status.js +2 -0
  44. package/proposal-to-effect.js +4 -0
  45. package/remedy-case-set-postgres.js +3 -0
  46. package/remedy-case-set.js +3 -0
  47. package/remedy-program-adapters.js +3 -0
  48. package/src/aeb-consumption-store.ts +521 -0
  49. package/src/capability-receipt.ts +17 -0
  50. package/src/index.ts +57 -3
  51. package/src/proposal-to-effect-postgres.ts +1740 -0
  52. package/src/proposal-to-effect-status.ts +357 -0
  53. package/src/proposal-to-effect.ts +1276 -0
  54. package/src/remedy-case-set-postgres.ts +1008 -0
  55. package/src/remedy-case-set.ts +603 -0
  56. package/src/remedy-program-adapters.ts +657 -0
  57. package/src/trust-program-revocation.ts +1 -1
@@ -0,0 +1,1511 @@
1
+ // @ts-nocheck
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * PostgreSQL custody for Proposal-to-Effect consequence attempts.
5
+ *
6
+ * The application issues an opaque owner capability and sends only its keyed
7
+ * digest to PostgreSQL. The database owns state-transition atomicity, terminal
8
+ * immutability, and the exact attempt/provider-evidence join.
9
+ */
10
+ import crypto from 'node:crypto';
11
+ const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{2,255}$/;
12
+ const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
13
+ const OWNER_PATTERN = /^pto-owner:v1:[A-Za-z0-9_-]{43}$/;
14
+ const OWNER_DOMAIN = 'EMILIA-PROPOSAL-TO-EFFECT-POSTGRES-v1:OWNER\0';
15
+ const ATTEMPT_DOMAIN = 'EMILIA-PROPOSAL-TO-EFFECT-POSTGRES-v1:ATTEMPT\0';
16
+ const EVIDENCE_DOMAIN = 'EMILIA-PROPOSAL-TO-EFFECT-POSTGRES-v1:EVIDENCE\0';
17
+ const OWNER_BYTES = 32;
18
+ const DEFAULT_LEASE_SECONDS = 30;
19
+ const MAX_LEASE_SECONDS = 300;
20
+ const STATES = new Set([
21
+ 'RESERVED',
22
+ 'INVOKING',
23
+ 'INDETERMINATE',
24
+ 'COMMITTED',
25
+ 'RELEASED',
26
+ 'ESCALATED',
27
+ ]);
28
+ const TERMINAL_STATES = new Set([
29
+ 'COMMITTED',
30
+ 'RELEASED',
31
+ 'ESCALATED',
32
+ ]);
33
+ /**
34
+ * Install under a dedicated non-login owner. Runtime roles should receive only
35
+ * schema USAGE plus EXECUTE on the RPCs they need; no table privilege is
36
+ * required or intended. Keep recover_attempt limited to the server role that
37
+ * runs authorize_recovery.
38
+ */
39
+ export const PROPOSAL_TO_EFFECT_POSTGRES_DDL = String.raw `
40
+ CREATE SCHEMA IF NOT EXISTS proposal_to_effect_private;
41
+ REVOKE ALL ON SCHEMA proposal_to_effect_private FROM PUBLIC;
42
+
43
+ DO $roles$
44
+ BEGIN
45
+ IF NOT EXISTS (
46
+ SELECT 1 FROM pg_catalog.pg_roles
47
+ WHERE rolname = 'proposal_to_effect_executor'
48
+ ) THEN
49
+ CREATE ROLE proposal_to_effect_executor NOLOGIN
50
+ NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
51
+ END IF;
52
+ IF NOT EXISTS (
53
+ SELECT 1 FROM pg_catalog.pg_roles
54
+ WHERE rolname = 'proposal_to_effect_recovery'
55
+ ) THEN
56
+ CREATE ROLE proposal_to_effect_recovery NOLOGIN
57
+ NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
58
+ END IF;
59
+ END
60
+ $roles$;
61
+
62
+ CREATE TABLE IF NOT EXISTS proposal_to_effect_private.tenant_principals (
63
+ principal_name NAME NOT NULL,
64
+ tenant_id TEXT COLLATE "C" NOT NULL
65
+ CHECK (tenant_id ~ '^[A-Za-z0-9][A-Za-z0-9:_.@/-]{2,255}$'),
66
+ can_execute BOOLEAN NOT NULL DEFAULT FALSE,
67
+ can_recover BOOLEAN NOT NULL DEFAULT FALSE,
68
+ created_at TIMESTAMPTZ NOT NULL DEFAULT pg_catalog.clock_timestamp(),
69
+ PRIMARY KEY (principal_name, tenant_id),
70
+ CHECK (can_execute OR can_recover)
71
+ );
72
+
73
+ CREATE TABLE IF NOT EXISTS proposal_to_effect_private.consequence_attempts (
74
+ tenant_id TEXT COLLATE "C" NOT NULL
75
+ CHECK (tenant_id ~ '^[A-Za-z0-9][A-Za-z0-9:_.@/-]{2,255}$'),
76
+ provider_id TEXT COLLATE "C" NOT NULL
77
+ CHECK (provider_id ~ '^[A-Za-z0-9][A-Za-z0-9:_.@/-]{2,255}$'),
78
+ provider_account_id TEXT COLLATE "C" NOT NULL
79
+ CHECK (provider_account_id ~ '^[A-Za-z0-9][A-Za-z0-9:_.@/-]{2,255}$'),
80
+ environment TEXT COLLATE "C" NOT NULL
81
+ CHECK (environment ~ '^[A-Za-z0-9][A-Za-z0-9:_.@/-]{2,255}$'),
82
+ attempt_id TEXT COLLATE "C" NOT NULL
83
+ CHECK (attempt_id ~ '^[A-Za-z0-9][A-Za-z0-9:_.@/-]{2,255}$'),
84
+ operation_digest TEXT COLLATE "C" NOT NULL
85
+ CHECK (operation_digest ~ '^sha256:[a-f0-9]{64}$'),
86
+ request_digest TEXT COLLATE "C" NOT NULL
87
+ CHECK (request_digest ~ '^sha256:[a-f0-9]{64}$'),
88
+ action_digest TEXT COLLATE "C" NOT NULL
89
+ CHECK (action_digest ~ '^sha256:[a-f0-9]{64}$'),
90
+ config_digest TEXT COLLATE "C" NOT NULL
91
+ CHECK (config_digest ~ '^sha256:[a-f0-9]{64}$'),
92
+ attempt_digest TEXT COLLATE "C" NOT NULL
93
+ CHECK (attempt_digest ~ '^sha256:[a-f0-9]{64}$'),
94
+ owner_digest TEXT COLLATE "C" NOT NULL UNIQUE
95
+ CHECK (owner_digest ~ '^sha256:[a-f0-9]{64}$'),
96
+ owner_generation BIGINT NOT NULL DEFAULT 0 CHECK (owner_generation >= 0),
97
+ state TEXT COLLATE "C" NOT NULL DEFAULT 'RESERVED'
98
+ CHECK (state IN (
99
+ 'RESERVED', 'INVOKING', 'INDETERMINATE',
100
+ 'COMMITTED', 'RELEASED', 'ESCALATED'
101
+ )),
102
+ evidence_digest TEXT COLLATE "C"
103
+ CHECK (evidence_digest IS NULL OR evidence_digest ~ '^sha256:[a-f0-9]{64}$'),
104
+ evidence_binding_digest TEXT COLLATE "C"
105
+ CHECK (
106
+ evidence_binding_digest IS NULL
107
+ OR evidence_binding_digest ~ '^sha256:[a-f0-9]{64}$'
108
+ ),
109
+ last_heartbeat_at TIMESTAMPTZ NOT NULL,
110
+ lease_expires_at TIMESTAMPTZ NOT NULL,
111
+ created_at TIMESTAMPTZ NOT NULL,
112
+ updated_at TIMESTAMPTZ NOT NULL,
113
+ PRIMARY KEY (
114
+ tenant_id, provider_id, provider_account_id, environment, attempt_id
115
+ ),
116
+ UNIQUE (
117
+ tenant_id, provider_id, provider_account_id, environment, operation_digest
118
+ ),
119
+ UNIQUE (
120
+ tenant_id, provider_id, provider_account_id, environment, request_digest
121
+ ),
122
+ UNIQUE (
123
+ tenant_id, provider_id, provider_account_id, environment, attempt_digest
124
+ ),
125
+ UNIQUE (
126
+ tenant_id, provider_id, provider_account_id, environment,
127
+ attempt_id, attempt_digest
128
+ ),
129
+ CHECK (
130
+ (evidence_digest IS NULL AND evidence_binding_digest IS NULL)
131
+ OR
132
+ (evidence_digest IS NOT NULL AND evidence_binding_digest IS NOT NULL)
133
+ ),
134
+ CHECK (created_at <= updated_at),
135
+ CHECK (created_at <= last_heartbeat_at),
136
+ CHECK (last_heartbeat_at < lease_expires_at)
137
+ );
138
+
139
+ CREATE TABLE IF NOT EXISTS proposal_to_effect_private.provider_evidence (
140
+ tenant_id TEXT COLLATE "C" NOT NULL,
141
+ provider_id TEXT COLLATE "C" NOT NULL,
142
+ provider_account_id TEXT COLLATE "C" NOT NULL,
143
+ environment TEXT COLLATE "C" NOT NULL,
144
+ attempt_id TEXT COLLATE "C" NOT NULL,
145
+ attempt_digest TEXT COLLATE "C" NOT NULL
146
+ CHECK (attempt_digest ~ '^sha256:[a-f0-9]{64}$'),
147
+ operation_id TEXT COLLATE "C" NOT NULL
148
+ CHECK (operation_id ~ '^[A-Za-z0-9][A-Za-z0-9:_.@/-]{2,255}$'),
149
+ caid TEXT COLLATE "C" NOT NULL
150
+ CHECK (caid ~ '^[A-Za-z0-9][A-Za-z0-9:_.@/-]{2,255}$'),
151
+ action_digest TEXT COLLATE "C" NOT NULL
152
+ CHECK (action_digest ~ '^sha256:[a-f0-9]{64}$'),
153
+ evidence_id TEXT COLLATE "C" NOT NULL
154
+ CHECK (evidence_id ~ '^[A-Za-z0-9][A-Za-z0-9:_.@/-]{2,255}$'),
155
+ observed_at TIMESTAMPTZ NOT NULL,
156
+ outcome TEXT COLLATE "C" NOT NULL
157
+ CHECK (outcome IN ('COMMITTED', 'NOT_COMMITTED', 'ESCALATED')),
158
+ evidence_digest TEXT COLLATE "C" NOT NULL
159
+ CHECK (evidence_digest ~ '^sha256:[a-f0-9]{64}$'),
160
+ evidence_binding_digest TEXT COLLATE "C" NOT NULL
161
+ CHECK (evidence_binding_digest ~ '^sha256:[a-f0-9]{64}$'),
162
+ recorded_at TIMESTAMPTZ NOT NULL DEFAULT pg_catalog.clock_timestamp(),
163
+ PRIMARY KEY (
164
+ tenant_id, provider_id, provider_account_id, environment, evidence_digest
165
+ ),
166
+ UNIQUE (
167
+ tenant_id, provider_id, provider_account_id, environment, evidence_id
168
+ ),
169
+ UNIQUE (
170
+ tenant_id, provider_id, provider_account_id, environment,
171
+ evidence_binding_digest
172
+ ),
173
+ FOREIGN KEY (
174
+ tenant_id, provider_id, provider_account_id, environment,
175
+ attempt_id, attempt_digest
176
+ ) REFERENCES proposal_to_effect_private.consequence_attempts (
177
+ tenant_id, provider_id, provider_account_id, environment,
178
+ attempt_id, attempt_digest
179
+ ) ON DELETE RESTRICT
180
+ );
181
+
182
+ ALTER TABLE proposal_to_effect_private.tenant_principals
183
+ ENABLE ROW LEVEL SECURITY;
184
+ ALTER TABLE proposal_to_effect_private.tenant_principals
185
+ FORCE ROW LEVEL SECURITY;
186
+ ALTER TABLE proposal_to_effect_private.consequence_attempts
187
+ ENABLE ROW LEVEL SECURITY;
188
+ ALTER TABLE proposal_to_effect_private.consequence_attempts
189
+ FORCE ROW LEVEL SECURITY;
190
+ ALTER TABLE proposal_to_effect_private.provider_evidence
191
+ ENABLE ROW LEVEL SECURITY;
192
+ ALTER TABLE proposal_to_effect_private.provider_evidence
193
+ FORCE ROW LEVEL SECURITY;
194
+
195
+ DO $ddl$
196
+ BEGIN
197
+ IF NOT EXISTS (
198
+ SELECT 1
199
+ FROM pg_catalog.pg_policies
200
+ WHERE schemaname = 'proposal_to_effect_private'
201
+ AND tablename = 'tenant_principals'
202
+ AND policyname = 'proposal_to_effect_principals_owner_only'
203
+ ) THEN
204
+ CREATE POLICY proposal_to_effect_principals_owner_only
205
+ ON proposal_to_effect_private.tenant_principals
206
+ USING (
207
+ CURRENT_USER = (
208
+ SELECT tableowner
209
+ FROM pg_catalog.pg_tables
210
+ WHERE schemaname = 'proposal_to_effect_private'
211
+ AND tablename = 'tenant_principals'
212
+ )
213
+ )
214
+ WITH CHECK (
215
+ CURRENT_USER = (
216
+ SELECT tableowner
217
+ FROM pg_catalog.pg_tables
218
+ WHERE schemaname = 'proposal_to_effect_private'
219
+ AND tablename = 'tenant_principals'
220
+ )
221
+ );
222
+ END IF;
223
+ IF NOT EXISTS (
224
+ SELECT 1
225
+ FROM pg_catalog.pg_policies
226
+ WHERE schemaname = 'proposal_to_effect_private'
227
+ AND tablename = 'consequence_attempts'
228
+ AND policyname = 'proposal_to_effect_owner_only'
229
+ ) THEN
230
+ CREATE POLICY proposal_to_effect_owner_only
231
+ ON proposal_to_effect_private.consequence_attempts
232
+ USING (
233
+ CURRENT_USER = (
234
+ SELECT tableowner
235
+ FROM pg_catalog.pg_tables
236
+ WHERE schemaname = 'proposal_to_effect_private'
237
+ AND tablename = 'consequence_attempts'
238
+ )
239
+ )
240
+ WITH CHECK (
241
+ CURRENT_USER = (
242
+ SELECT tableowner
243
+ FROM pg_catalog.pg_tables
244
+ WHERE schemaname = 'proposal_to_effect_private'
245
+ AND tablename = 'consequence_attempts'
246
+ )
247
+ );
248
+ END IF;
249
+ IF NOT EXISTS (
250
+ SELECT 1
251
+ FROM pg_catalog.pg_policies
252
+ WHERE schemaname = 'proposal_to_effect_private'
253
+ AND tablename = 'provider_evidence'
254
+ AND policyname = 'proposal_to_effect_evidence_owner_only'
255
+ ) THEN
256
+ CREATE POLICY proposal_to_effect_evidence_owner_only
257
+ ON proposal_to_effect_private.provider_evidence
258
+ USING (
259
+ CURRENT_USER = (
260
+ SELECT tableowner
261
+ FROM pg_catalog.pg_tables
262
+ WHERE schemaname = 'proposal_to_effect_private'
263
+ AND tablename = 'provider_evidence'
264
+ )
265
+ )
266
+ WITH CHECK (
267
+ CURRENT_USER = (
268
+ SELECT tableowner
269
+ FROM pg_catalog.pg_tables
270
+ WHERE schemaname = 'proposal_to_effect_private'
271
+ AND tablename = 'provider_evidence'
272
+ )
273
+ );
274
+ END IF;
275
+ END
276
+ $ddl$;
277
+
278
+ REVOKE ALL ON TABLE proposal_to_effect_private.tenant_principals FROM PUBLIC;
279
+ REVOKE ALL ON TABLE proposal_to_effect_private.consequence_attempts FROM PUBLIC;
280
+ REVOKE ALL ON TABLE proposal_to_effect_private.provider_evidence FROM PUBLIC;
281
+
282
+ CREATE OR REPLACE FUNCTION proposal_to_effect_private.assert_tenant_principal(
283
+ p_tenant_id TEXT,
284
+ p_recovery BOOLEAN
285
+ )
286
+ RETURNS VOID
287
+ LANGUAGE plpgsql
288
+ VOLATILE
289
+ SECURITY DEFINER
290
+ SET search_path = ''
291
+ AS $fn$
292
+ DECLARE
293
+ v_role_ok BOOLEAN;
294
+ v_binding_ok BOOLEAN;
295
+ BEGIN
296
+ v_role_ok := CASE
297
+ WHEN p_recovery IS TRUE THEN pg_catalog.pg_has_role(
298
+ SESSION_USER, 'proposal_to_effect_recovery', 'MEMBER'
299
+ )
300
+ WHEN p_recovery IS FALSE THEN pg_catalog.pg_has_role(
301
+ SESSION_USER, 'proposal_to_effect_executor', 'MEMBER'
302
+ )
303
+ ELSE pg_catalog.pg_has_role(
304
+ SESSION_USER, 'proposal_to_effect_executor', 'MEMBER'
305
+ ) OR pg_catalog.pg_has_role(
306
+ SESSION_USER, 'proposal_to_effect_recovery', 'MEMBER'
307
+ )
308
+ END;
309
+ SELECT EXISTS (
310
+ SELECT 1
311
+ FROM proposal_to_effect_private.tenant_principals AS principals
312
+ WHERE principals.principal_name = SESSION_USER
313
+ AND principals.tenant_id = p_tenant_id
314
+ AND CASE
315
+ WHEN p_recovery IS TRUE THEN principals.can_recover
316
+ WHEN p_recovery IS FALSE THEN principals.can_execute
317
+ ELSE principals.can_execute OR principals.can_recover
318
+ END
319
+ ) INTO v_binding_ok;
320
+ IF v_role_ok IS NOT TRUE OR v_binding_ok IS NOT TRUE THEN
321
+ RAISE EXCEPTION 'PTE_TENANT_PRINCIPAL_REFUSED'
322
+ USING ERRCODE = '42501';
323
+ END IF;
324
+ END
325
+ $fn$;
326
+
327
+ CREATE OR REPLACE FUNCTION proposal_to_effect_private.guard_attempt_mutation()
328
+ RETURNS trigger
329
+ LANGUAGE plpgsql
330
+ SET search_path = ''
331
+ AS $fn$
332
+ BEGIN
333
+ IF TG_OP = 'DELETE' THEN
334
+ RAISE EXCEPTION 'PTE_ATTEMPT_DELETE_REFUSED';
335
+ END IF;
336
+ IF OLD.state IN ('COMMITTED', 'RELEASED', 'ESCALATED') THEN
337
+ RAISE EXCEPTION 'PTE_TERMINAL_ATTEMPT_IMMUTABLE';
338
+ END IF;
339
+ IF ROW(
340
+ NEW.tenant_id, NEW.provider_id, NEW.provider_account_id, NEW.environment,
341
+ NEW.attempt_id, NEW.operation_digest, NEW.request_digest,
342
+ NEW.action_digest, NEW.config_digest, NEW.attempt_digest, NEW.created_at
343
+ ) IS DISTINCT FROM ROW(
344
+ OLD.tenant_id, OLD.provider_id, OLD.provider_account_id, OLD.environment,
345
+ OLD.attempt_id, OLD.operation_digest, OLD.request_digest,
346
+ OLD.action_digest, OLD.config_digest, OLD.attempt_digest, OLD.created_at
347
+ ) THEN
348
+ RAISE EXCEPTION 'PTE_ATTEMPT_BINDING_IMMUTABLE';
349
+ END IF;
350
+ IF NEW.updated_at < OLD.updated_at
351
+ OR NEW.last_heartbeat_at < OLD.last_heartbeat_at
352
+ OR NEW.lease_expires_at < OLD.lease_expires_at
353
+ OR NEW.last_heartbeat_at >= NEW.lease_expires_at THEN
354
+ RAISE EXCEPTION 'PTE_LEASE_REWIND_REFUSED';
355
+ END IF;
356
+
357
+ IF NEW.owner_generation = OLD.owner_generation THEN
358
+ IF NEW.owner_digest IS DISTINCT FROM OLD.owner_digest OR NOT (
359
+ (OLD.state = NEW.state)
360
+ OR
361
+ (OLD.state = 'RESERVED' AND NEW.state = 'INVOKING')
362
+ OR (OLD.state = 'INVOKING' AND NEW.state = 'INDETERMINATE')
363
+ OR (
364
+ OLD.state = 'INDETERMINATE'
365
+ AND NEW.state IN ('COMMITTED', 'RELEASED', 'ESCALATED')
366
+ )
367
+ ) THEN
368
+ RAISE EXCEPTION 'PTE_ATTEMPT_TRANSITION_REFUSED';
369
+ END IF;
370
+ IF ROW(NEW.evidence_digest, NEW.evidence_binding_digest)
371
+ IS DISTINCT FROM ROW(OLD.evidence_digest, OLD.evidence_binding_digest)
372
+ AND NOT (
373
+ OLD.state = 'INDETERMINATE'
374
+ AND NEW.state IN ('COMMITTED', 'RELEASED', 'ESCALATED')
375
+ AND OLD.evidence_digest IS NULL
376
+ AND OLD.evidence_binding_digest IS NULL
377
+ AND NEW.evidence_digest IS NOT NULL
378
+ AND NEW.evidence_binding_digest IS NOT NULL
379
+ ) THEN
380
+ RAISE EXCEPTION 'PTE_ATTEMPT_EVIDENCE_REBIND_REFUSED';
381
+ END IF;
382
+ ELSIF NEW.owner_generation = OLD.owner_generation + 1 THEN
383
+ IF NEW.owner_digest IS NOT DISTINCT FROM OLD.owner_digest
384
+ OR ROW(NEW.evidence_digest, NEW.evidence_binding_digest)
385
+ IS DISTINCT FROM ROW(OLD.evidence_digest, OLD.evidence_binding_digest)
386
+ OR NOT (
387
+ (OLD.state = 'RESERVED' AND NEW.state = 'RESERVED')
388
+ OR (OLD.state = 'INVOKING' AND NEW.state = 'INDETERMINATE')
389
+ OR (OLD.state = 'INDETERMINATE' AND NEW.state = 'INDETERMINATE')
390
+ ) THEN
391
+ RAISE EXCEPTION 'PTE_ATTEMPT_RECOVERY_REFUSED';
392
+ END IF;
393
+ ELSE
394
+ RAISE EXCEPTION 'PTE_OWNER_GENERATION_REFUSED';
395
+ END IF;
396
+ RETURN NEW;
397
+ END
398
+ $fn$;
399
+
400
+ DROP TRIGGER IF EXISTS proposal_to_effect_attempt_guard
401
+ ON proposal_to_effect_private.consequence_attempts;
402
+ CREATE TRIGGER proposal_to_effect_attempt_guard
403
+ BEFORE UPDATE OR DELETE ON proposal_to_effect_private.consequence_attempts
404
+ FOR EACH ROW EXECUTE FUNCTION proposal_to_effect_private.guard_attempt_mutation();
405
+
406
+ CREATE OR REPLACE FUNCTION proposal_to_effect_private.guard_evidence_mutation()
407
+ RETURNS trigger
408
+ LANGUAGE plpgsql
409
+ SET search_path = ''
410
+ AS $fn$
411
+ BEGIN
412
+ RAISE EXCEPTION 'PTE_PROVIDER_EVIDENCE_IMMUTABLE';
413
+ END
414
+ $fn$;
415
+
416
+ DROP TRIGGER IF EXISTS proposal_to_effect_evidence_guard
417
+ ON proposal_to_effect_private.provider_evidence;
418
+ CREATE TRIGGER proposal_to_effect_evidence_guard
419
+ BEFORE UPDATE OR DELETE ON proposal_to_effect_private.provider_evidence
420
+ FOR EACH ROW EXECUTE FUNCTION proposal_to_effect_private.guard_evidence_mutation();
421
+
422
+ CREATE OR REPLACE FUNCTION proposal_to_effect_private.reserve_attempt(
423
+ p_tenant_id TEXT,
424
+ p_provider_id TEXT,
425
+ p_provider_account_id TEXT,
426
+ p_environment TEXT,
427
+ p_attempt_id TEXT,
428
+ p_operation_digest TEXT,
429
+ p_request_digest TEXT,
430
+ p_action_digest TEXT,
431
+ p_config_digest TEXT,
432
+ p_attempt_digest TEXT,
433
+ p_owner_digest TEXT,
434
+ p_lease_seconds INTEGER
435
+ )
436
+ RETURNS TABLE(applied BOOLEAN, reason TEXT)
437
+ LANGUAGE plpgsql
438
+ SECURITY DEFINER
439
+ SET search_path = ''
440
+ AS $fn$
441
+ DECLARE
442
+ v_count BIGINT;
443
+ v_now TIMESTAMPTZ;
444
+ BEGIN
445
+ PERFORM proposal_to_effect_private.assert_tenant_principal(p_tenant_id, FALSE);
446
+ IF p_lease_seconds < 1 OR p_lease_seconds > 300 THEN
447
+ RAISE EXCEPTION 'PTE_LEASE_DURATION_REFUSED'
448
+ USING ERRCODE = '22023';
449
+ END IF;
450
+ v_now := pg_catalog.clock_timestamp();
451
+ INSERT INTO proposal_to_effect_private.consequence_attempts (
452
+ tenant_id, provider_id, provider_account_id, environment, attempt_id,
453
+ operation_digest, request_digest, action_digest, config_digest,
454
+ attempt_digest, owner_digest, last_heartbeat_at, lease_expires_at,
455
+ created_at, updated_at
456
+ ) VALUES (
457
+ p_tenant_id, p_provider_id, p_provider_account_id, p_environment, p_attempt_id,
458
+ p_operation_digest, p_request_digest, p_action_digest, p_config_digest,
459
+ p_attempt_digest, p_owner_digest, v_now,
460
+ v_now + pg_catalog.make_interval(secs => p_lease_seconds),
461
+ v_now, v_now
462
+ )
463
+ ON CONFLICT DO NOTHING;
464
+ GET DIAGNOSTICS v_count = ROW_COUNT;
465
+ IF v_count = 1 THEN
466
+ RETURN QUERY SELECT TRUE, NULL::TEXT;
467
+ ELSIF EXISTS (
468
+ SELECT 1
469
+ FROM proposal_to_effect_private.consequence_attempts
470
+ WHERE tenant_id = p_tenant_id
471
+ AND provider_id = p_provider_id
472
+ AND provider_account_id = p_provider_account_id
473
+ AND environment = p_environment
474
+ AND attempt_id = p_attempt_id
475
+ AND operation_digest = p_operation_digest
476
+ AND request_digest = p_request_digest
477
+ AND action_digest = p_action_digest
478
+ AND config_digest = p_config_digest
479
+ AND attempt_digest = p_attempt_digest
480
+ AND owner_digest = p_owner_digest
481
+ AND owner_generation = 0
482
+ AND state = 'RESERVED'
483
+ ) THEN
484
+ -- idempotent_reservation: the original COMMIT may have succeeded.
485
+ RETURN QUERY SELECT TRUE, NULL::TEXT;
486
+ ELSIF EXISTS (
487
+ SELECT 1
488
+ FROM proposal_to_effect_private.consequence_attempts
489
+ WHERE tenant_id = p_tenant_id
490
+ AND provider_id = p_provider_id
491
+ AND provider_account_id = p_provider_account_id
492
+ AND environment = p_environment
493
+ AND attempt_id = p_attempt_id
494
+ ) THEN
495
+ RETURN QUERY SELECT FALSE, 'attempt_exists'::TEXT;
496
+ ELSE
497
+ RETURN QUERY SELECT FALSE, 'binding_conflict'::TEXT;
498
+ END IF;
499
+ END
500
+ $fn$;
501
+
502
+ CREATE OR REPLACE FUNCTION proposal_to_effect_private.transition_attempt(
503
+ p_tenant_id TEXT,
504
+ p_attempt_id TEXT,
505
+ p_owner_digest TEXT,
506
+ p_expected_state TEXT,
507
+ p_next_state TEXT,
508
+ p_lease_seconds INTEGER
509
+ )
510
+ RETURNS TABLE(applied BOOLEAN, reason TEXT)
511
+ LANGUAGE plpgsql
512
+ SECURITY DEFINER
513
+ SET search_path = ''
514
+ AS $fn$
515
+ DECLARE
516
+ v_count BIGINT;
517
+ v_now TIMESTAMPTZ;
518
+ BEGIN
519
+ PERFORM proposal_to_effect_private.assert_tenant_principal(p_tenant_id, FALSE);
520
+ IF p_lease_seconds < 1 OR p_lease_seconds > 300 THEN
521
+ RAISE EXCEPTION 'PTE_LEASE_DURATION_REFUSED'
522
+ USING ERRCODE = '22023';
523
+ END IF;
524
+ v_now := pg_catalog.clock_timestamp();
525
+ UPDATE proposal_to_effect_private.consequence_attempts
526
+ SET state = p_next_state,
527
+ last_heartbeat_at = v_now,
528
+ lease_expires_at = v_now + pg_catalog.make_interval(secs => p_lease_seconds),
529
+ updated_at = v_now
530
+ WHERE tenant_id = p_tenant_id
531
+ AND attempt_id = p_attempt_id
532
+ AND owner_digest = p_owner_digest
533
+ AND state = p_expected_state
534
+ AND (
535
+ (p_expected_state = 'RESERVED' AND p_next_state = 'INVOKING')
536
+ OR (p_expected_state = 'INVOKING' AND p_next_state = 'INDETERMINATE')
537
+ OR (
538
+ p_expected_state = 'INDETERMINATE'
539
+ AND p_next_state IN ('COMMITTED', 'RELEASED', 'ESCALATED')
540
+ )
541
+ );
542
+ GET DIAGNOSTICS v_count = ROW_COUNT;
543
+ IF v_count = 1 THEN
544
+ RETURN QUERY SELECT TRUE, NULL::TEXT;
545
+ ELSIF EXISTS (
546
+ SELECT 1
547
+ FROM proposal_to_effect_private.consequence_attempts
548
+ WHERE tenant_id = p_tenant_id
549
+ AND attempt_id = p_attempt_id
550
+ AND owner_digest = p_owner_digest
551
+ AND state = p_next_state
552
+ ) THEN
553
+ -- idempotent_transition: retry after an ambiguous COMMIT.
554
+ RETURN QUERY SELECT TRUE, NULL::TEXT;
555
+ ELSE
556
+ RETURN QUERY SELECT FALSE, NULL::TEXT;
557
+ END IF;
558
+ END
559
+ $fn$;
560
+
561
+ CREATE OR REPLACE FUNCTION proposal_to_effect_private.heartbeat_attempt(
562
+ p_tenant_id TEXT,
563
+ p_attempt_id TEXT,
564
+ p_owner_digest TEXT,
565
+ p_lease_seconds INTEGER
566
+ )
567
+ RETURNS TABLE(applied BOOLEAN, reason TEXT)
568
+ LANGUAGE plpgsql
569
+ SECURITY DEFINER
570
+ SET search_path = ''
571
+ AS $fn$
572
+ DECLARE
573
+ v_count BIGINT;
574
+ v_now TIMESTAMPTZ;
575
+ BEGIN
576
+ PERFORM proposal_to_effect_private.assert_tenant_principal(p_tenant_id, FALSE);
577
+ IF p_lease_seconds < 1 OR p_lease_seconds > 300 THEN
578
+ RAISE EXCEPTION 'PTE_LEASE_DURATION_REFUSED'
579
+ USING ERRCODE = '22023';
580
+ END IF;
581
+ v_now := pg_catalog.clock_timestamp();
582
+ UPDATE proposal_to_effect_private.consequence_attempts
583
+ SET last_heartbeat_at = v_now,
584
+ lease_expires_at = v_now + pg_catalog.make_interval(secs => p_lease_seconds),
585
+ updated_at = v_now
586
+ WHERE tenant_id = p_tenant_id
587
+ AND attempt_id = p_attempt_id
588
+ AND owner_digest = p_owner_digest
589
+ AND state IN ('RESERVED', 'INVOKING', 'INDETERMINATE');
590
+ GET DIAGNOSTICS v_count = ROW_COUNT;
591
+ RETURN QUERY SELECT v_count = 1, NULL::TEXT;
592
+ END
593
+ $fn$;
594
+
595
+ CREATE OR REPLACE FUNCTION proposal_to_effect_private.reconcile_attempt(
596
+ p_tenant_id TEXT,
597
+ p_provider_id TEXT,
598
+ p_provider_account_id TEXT,
599
+ p_environment TEXT,
600
+ p_attempt_id TEXT,
601
+ p_owner_digest TEXT,
602
+ p_operation_digest TEXT,
603
+ p_request_digest TEXT,
604
+ p_action_digest TEXT,
605
+ p_config_digest TEXT,
606
+ p_attempt_digest TEXT,
607
+ p_operation_id TEXT,
608
+ p_caid TEXT,
609
+ p_next_state TEXT,
610
+ p_evidence_id TEXT,
611
+ p_observed_at TIMESTAMPTZ,
612
+ p_outcome TEXT,
613
+ p_evidence_digest TEXT,
614
+ p_evidence_binding_digest TEXT
615
+ )
616
+ RETURNS TABLE(applied BOOLEAN, reason TEXT)
617
+ LANGUAGE plpgsql
618
+ SECURITY DEFINER
619
+ SET search_path = ''
620
+ AS $fn$
621
+ DECLARE
622
+ v_attempt_digest TEXT;
623
+ BEGIN
624
+ PERFORM proposal_to_effect_private.assert_tenant_principal(p_tenant_id, FALSE);
625
+ IF NOT (
626
+ (p_outcome = 'COMMITTED' AND p_next_state = 'COMMITTED')
627
+ OR (p_outcome = 'NOT_COMMITTED' AND p_next_state = 'RELEASED')
628
+ OR (p_outcome = 'ESCALATED' AND p_next_state = 'ESCALATED')
629
+ ) THEN
630
+ RETURN QUERY SELECT FALSE, NULL::TEXT;
631
+ RETURN;
632
+ END IF;
633
+ BEGIN
634
+ UPDATE proposal_to_effect_private.consequence_attempts
635
+ SET state = p_next_state,
636
+ evidence_digest = p_evidence_digest,
637
+ evidence_binding_digest = p_evidence_binding_digest,
638
+ updated_at = pg_catalog.clock_timestamp()
639
+ WHERE tenant_id = p_tenant_id
640
+ AND provider_id = p_provider_id
641
+ AND provider_account_id = p_provider_account_id
642
+ AND environment = p_environment
643
+ AND attempt_id = p_attempt_id
644
+ AND owner_digest = p_owner_digest
645
+ AND operation_digest = p_operation_digest
646
+ AND request_digest = p_request_digest
647
+ AND action_digest = p_action_digest
648
+ AND config_digest = p_config_digest
649
+ AND attempt_digest = p_attempt_digest
650
+ AND state = 'INDETERMINATE'
651
+ RETURNING attempt_digest INTO v_attempt_digest;
652
+ IF v_attempt_digest IS NOT NULL THEN
653
+ INSERT INTO proposal_to_effect_private.provider_evidence (
654
+ tenant_id, provider_id, provider_account_id, environment,
655
+ attempt_id, attempt_digest, operation_id, caid, action_digest,
656
+ evidence_id, observed_at, outcome,
657
+ evidence_digest, evidence_binding_digest
658
+ ) VALUES (
659
+ p_tenant_id, p_provider_id, p_provider_account_id, p_environment,
660
+ p_attempt_id, p_attempt_digest, p_operation_id, p_caid, p_action_digest,
661
+ p_evidence_id, p_observed_at, p_outcome,
662
+ p_evidence_digest, p_evidence_binding_digest
663
+ );
664
+ RETURN QUERY SELECT TRUE, NULL::TEXT;
665
+ RETURN;
666
+ END IF;
667
+ EXCEPTION
668
+ WHEN unique_violation OR foreign_key_violation OR check_violation THEN
669
+ v_attempt_digest := NULL;
670
+ END;
671
+ IF EXISTS (
672
+ SELECT 1
673
+ FROM proposal_to_effect_private.consequence_attempts AS attempts
674
+ JOIN proposal_to_effect_private.provider_evidence AS evidence
675
+ USING (
676
+ tenant_id, provider_id, provider_account_id, environment,
677
+ attempt_id, attempt_digest
678
+ )
679
+ WHERE attempts.tenant_id = p_tenant_id
680
+ AND attempts.provider_id = p_provider_id
681
+ AND attempts.provider_account_id = p_provider_account_id
682
+ AND attempts.environment = p_environment
683
+ AND attempts.attempt_id = p_attempt_id
684
+ AND attempts.owner_digest = p_owner_digest
685
+ AND attempts.operation_digest = p_operation_digest
686
+ AND attempts.request_digest = p_request_digest
687
+ AND attempts.action_digest = p_action_digest
688
+ AND attempts.config_digest = p_config_digest
689
+ AND attempts.attempt_digest = p_attempt_digest
690
+ AND attempts.state = p_next_state
691
+ AND attempts.evidence_digest = p_evidence_digest
692
+ AND attempts.evidence_binding_digest = p_evidence_binding_digest
693
+ AND evidence.operation_id = p_operation_id
694
+ AND evidence.caid = p_caid
695
+ AND evidence.action_digest = p_action_digest
696
+ AND evidence.evidence_id = p_evidence_id
697
+ AND evidence.observed_at = p_observed_at
698
+ AND evidence.outcome = p_outcome
699
+ AND evidence.evidence_digest = p_evidence_digest
700
+ AND evidence.evidence_binding_digest = p_evidence_binding_digest
701
+ ) THEN
702
+ -- idempotent_reconciliation: retry after an ambiguous COMMIT.
703
+ RETURN QUERY SELECT TRUE, NULL::TEXT;
704
+ ELSE
705
+ RETURN QUERY SELECT FALSE, NULL::TEXT;
706
+ END IF;
707
+ END
708
+ $fn$;
709
+
710
+ CREATE OR REPLACE FUNCTION proposal_to_effect_private.read_attempt(
711
+ p_tenant_id TEXT,
712
+ p_provider_id TEXT,
713
+ p_provider_account_id TEXT,
714
+ p_environment TEXT,
715
+ p_attempt_id TEXT,
716
+ p_request_digest TEXT
717
+ )
718
+ RETURNS TABLE(
719
+ tenant_id TEXT,
720
+ provider_id TEXT,
721
+ provider_account_id TEXT,
722
+ environment TEXT,
723
+ attempt_id TEXT,
724
+ operation_digest TEXT,
725
+ request_digest TEXT,
726
+ action_digest TEXT,
727
+ config_digest TEXT,
728
+ attempt_digest TEXT,
729
+ state TEXT,
730
+ evidence_digest TEXT,
731
+ owner_generation BIGINT,
732
+ last_heartbeat_at TEXT,
733
+ lease_expires_at TEXT,
734
+ lease_stale BOOLEAN
735
+ )
736
+ LANGUAGE plpgsql
737
+ VOLATILE
738
+ SECURITY DEFINER
739
+ SET search_path = ''
740
+ AS $fn$
741
+ BEGIN
742
+ PERFORM proposal_to_effect_private.assert_tenant_principal(p_tenant_id, NULL);
743
+ RETURN QUERY SELECT
744
+ attempts.tenant_id,
745
+ attempts.provider_id,
746
+ attempts.provider_account_id,
747
+ attempts.environment,
748
+ attempts.attempt_id,
749
+ attempts.operation_digest,
750
+ attempts.request_digest,
751
+ attempts.action_digest,
752
+ attempts.config_digest,
753
+ attempts.attempt_digest,
754
+ attempts.state,
755
+ attempts.evidence_digest,
756
+ attempts.owner_generation,
757
+ pg_catalog.to_char(
758
+ attempts.last_heartbeat_at AT TIME ZONE 'UTC',
759
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
760
+ ),
761
+ pg_catalog.to_char(
762
+ attempts.lease_expires_at AT TIME ZONE 'UTC',
763
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
764
+ ),
765
+ attempts.lease_expires_at <= pg_catalog.clock_timestamp()
766
+ FROM proposal_to_effect_private.consequence_attempts AS attempts
767
+ WHERE attempts.tenant_id = p_tenant_id
768
+ AND attempts.provider_id = p_provider_id
769
+ AND attempts.provider_account_id = p_provider_account_id
770
+ AND attempts.environment = p_environment
771
+ AND attempts.attempt_id = p_attempt_id
772
+ AND attempts.request_digest = p_request_digest;
773
+ END
774
+ $fn$;
775
+
776
+ CREATE OR REPLACE FUNCTION proposal_to_effect_private.recover_attempt(
777
+ p_tenant_id TEXT,
778
+ p_provider_id TEXT,
779
+ p_provider_account_id TEXT,
780
+ p_environment TEXT,
781
+ p_attempt_id TEXT,
782
+ p_request_digest TEXT,
783
+ p_attempt_digest TEXT,
784
+ p_owner_generation BIGINT,
785
+ p_expected_state TEXT,
786
+ p_expected_lease_expires_at TIMESTAMPTZ,
787
+ p_next_owner_digest TEXT,
788
+ p_lease_seconds INTEGER
789
+ )
790
+ RETURNS TABLE(applied BOOLEAN, reason TEXT)
791
+ LANGUAGE plpgsql
792
+ SECURITY DEFINER
793
+ SET search_path = ''
794
+ AS $fn$
795
+ DECLARE
796
+ v_count BIGINT;
797
+ v_now TIMESTAMPTZ;
798
+ BEGIN
799
+ PERFORM proposal_to_effect_private.assert_tenant_principal(p_tenant_id, TRUE);
800
+ IF p_lease_seconds < 1 OR p_lease_seconds > 300 THEN
801
+ RAISE EXCEPTION 'PTE_LEASE_DURATION_REFUSED'
802
+ USING ERRCODE = '22023';
803
+ END IF;
804
+ v_now := pg_catalog.clock_timestamp();
805
+ UPDATE proposal_to_effect_private.consequence_attempts
806
+ SET owner_digest = p_next_owner_digest,
807
+ owner_generation = owner_generation + 1,
808
+ state = CASE
809
+ WHEN state = 'RESERVED' THEN 'RESERVED'
810
+ ELSE 'INDETERMINATE'
811
+ END,
812
+ last_heartbeat_at = v_now,
813
+ lease_expires_at = v_now + pg_catalog.make_interval(secs => p_lease_seconds),
814
+ updated_at = v_now
815
+ WHERE tenant_id = p_tenant_id
816
+ AND provider_id = p_provider_id
817
+ AND provider_account_id = p_provider_account_id
818
+ AND environment = p_environment
819
+ AND attempt_id = p_attempt_id
820
+ AND request_digest = p_request_digest
821
+ AND attempt_digest = p_attempt_digest
822
+ AND owner_generation = p_owner_generation
823
+ AND state = p_expected_state
824
+ AND state IN ('RESERVED', 'INVOKING', 'INDETERMINATE')
825
+ AND lease_expires_at = p_expected_lease_expires_at
826
+ AND lease_expires_at <= pg_catalog.clock_timestamp();
827
+ GET DIAGNOSTICS v_count = ROW_COUNT;
828
+ IF v_count = 1 THEN
829
+ RETURN QUERY SELECT TRUE, NULL::TEXT;
830
+ ELSIF EXISTS (
831
+ SELECT 1
832
+ FROM proposal_to_effect_private.consequence_attempts
833
+ WHERE tenant_id = p_tenant_id
834
+ AND provider_id = p_provider_id
835
+ AND provider_account_id = p_provider_account_id
836
+ AND environment = p_environment
837
+ AND attempt_id = p_attempt_id
838
+ AND request_digest = p_request_digest
839
+ AND attempt_digest = p_attempt_digest
840
+ AND owner_generation = p_owner_generation + 1
841
+ AND owner_digest = p_next_owner_digest
842
+ AND state = CASE
843
+ WHEN p_expected_state = 'RESERVED' THEN 'RESERVED'
844
+ ELSE 'INDETERMINATE'
845
+ END
846
+ ) THEN
847
+ -- idempotent_recovery: retry after an ambiguous COMMIT.
848
+ RETURN QUERY SELECT TRUE, NULL::TEXT;
849
+ ELSIF EXISTS (
850
+ SELECT 1
851
+ FROM proposal_to_effect_private.consequence_attempts
852
+ WHERE tenant_id = p_tenant_id
853
+ AND provider_id = p_provider_id
854
+ AND provider_account_id = p_provider_account_id
855
+ AND environment = p_environment
856
+ AND attempt_id = p_attempt_id
857
+ AND request_digest = p_request_digest
858
+ AND attempt_digest = p_attempt_digest
859
+ AND owner_generation = p_owner_generation
860
+ AND state = p_expected_state
861
+ AND lease_expires_at = p_expected_lease_expires_at
862
+ AND lease_expires_at > pg_catalog.clock_timestamp()
863
+ ) THEN
864
+ RETURN QUERY SELECT FALSE, 'attempt_not_stale'::TEXT;
865
+ ELSE
866
+ RETURN QUERY SELECT FALSE, 'recovery_conflict'::TEXT;
867
+ END IF;
868
+ END
869
+ $fn$;
870
+
871
+ REVOKE ALL ON ALL FUNCTIONS IN SCHEMA proposal_to_effect_private FROM PUBLIC;
872
+
873
+ GRANT USAGE ON SCHEMA proposal_to_effect_private
874
+ TO proposal_to_effect_executor, proposal_to_effect_recovery;
875
+ GRANT EXECUTE ON FUNCTION proposal_to_effect_private.reserve_attempt(
876
+ TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, INTEGER
877
+ ) TO proposal_to_effect_executor;
878
+ GRANT EXECUTE ON FUNCTION proposal_to_effect_private.transition_attempt(
879
+ TEXT, TEXT, TEXT, TEXT, TEXT, INTEGER
880
+ ) TO proposal_to_effect_executor;
881
+ GRANT EXECUTE ON FUNCTION proposal_to_effect_private.heartbeat_attempt(
882
+ TEXT, TEXT, TEXT, INTEGER
883
+ ) TO proposal_to_effect_executor;
884
+ GRANT EXECUTE ON FUNCTION proposal_to_effect_private.reconcile_attempt(
885
+ TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT,
886
+ TEXT, TEXT, TEXT, TEXT, TIMESTAMPTZ, TEXT, TEXT, TEXT
887
+ ) TO proposal_to_effect_executor;
888
+ GRANT EXECUTE ON FUNCTION proposal_to_effect_private.read_attempt(
889
+ TEXT, TEXT, TEXT, TEXT, TEXT, TEXT
890
+ ) TO proposal_to_effect_executor, proposal_to_effect_recovery;
891
+ GRANT EXECUTE ON FUNCTION proposal_to_effect_private.recover_attempt(
892
+ TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, BIGINT, TEXT, TIMESTAMPTZ,
893
+ TEXT, INTEGER
894
+ ) TO proposal_to_effect_recovery;
895
+ `;
896
+ export const PROPOSAL_TO_EFFECT_POSTGRES_SQL = Object.freeze({
897
+ reserve: `SELECT * FROM proposal_to_effect_private.reserve_attempt(
898
+ $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
899
+ )`,
900
+ transition: `SELECT * FROM proposal_to_effect_private.transition_attempt(
901
+ $1, $2, $3, $4, $5, $6
902
+ )`,
903
+ heartbeat: `SELECT * FROM proposal_to_effect_private.heartbeat_attempt(
904
+ $1, $2, $3, $4
905
+ )`,
906
+ reconcile: `SELECT * FROM proposal_to_effect_private.reconcile_attempt(
907
+ $1, $2, $3, $4, $5, $6, $7, $8, $9,
908
+ $10, $11, $12, $13, $14, $15, $16, $17, $18, $19
909
+ )`,
910
+ read: `SELECT * FROM proposal_to_effect_private.read_attempt(
911
+ $1, $2, $3, $4, $5, $6
912
+ )`,
913
+ recover: `SELECT * FROM proposal_to_effect_private.recover_attempt(
914
+ $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
915
+ )`,
916
+ });
917
+ function isRecord(value) {
918
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
919
+ }
920
+ function hasExactKeys(value, expected) {
921
+ const actual = Reflect.ownKeys(value);
922
+ if (actual.some((key) => typeof key !== 'string') || actual.length !== expected.length) {
923
+ return false;
924
+ }
925
+ const names = new Set(actual);
926
+ return expected.every((key) => names.has(key));
927
+ }
928
+ function assertIdentifier(value, name) {
929
+ if (typeof value !== 'string' || !IDENTIFIER_PATTERN.test(value)) {
930
+ throw new TypeError(`${name} is invalid`);
931
+ }
932
+ }
933
+ function assertDigest(value, name) {
934
+ if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
935
+ throw new TypeError(`${name} is invalid`);
936
+ }
937
+ }
938
+ function assertInstant(value, name) {
939
+ if (typeof value !== 'string'
940
+ || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)
941
+ || !Number.isFinite(Date.parse(value))
942
+ || new Date(Date.parse(value)).toISOString() !== value) {
943
+ throw new TypeError(`${name} is invalid`);
944
+ }
945
+ }
946
+ function assertOwner(value) {
947
+ if (typeof value !== 'string' || !OWNER_PATTERN.test(value)) {
948
+ throw new TypeError('consequence attempt owner is invalid');
949
+ }
950
+ }
951
+ function assertBinding(value) {
952
+ if (!isRecord(value) || !hasExactKeys(value, [
953
+ 'tenant_id',
954
+ 'provider_id',
955
+ 'provider_account_id',
956
+ 'environment',
957
+ 'attempt_id',
958
+ 'request_digest',
959
+ ])) {
960
+ throw new TypeError('consequence attempt binding is invalid');
961
+ }
962
+ assertIdentifier(value.tenant_id, 'tenant_id');
963
+ assertIdentifier(value.provider_id, 'provider_id');
964
+ assertIdentifier(value.provider_account_id, 'provider_account_id');
965
+ assertIdentifier(value.environment, 'environment');
966
+ assertIdentifier(value.attempt_id, 'attempt_id');
967
+ assertDigest(value.request_digest, 'request_digest');
968
+ }
969
+ function assertAttemptReference(value) {
970
+ if (!isRecord(value) || !hasExactKeys(value, [
971
+ 'tenant_id',
972
+ 'provider_id',
973
+ 'provider_account_id',
974
+ 'environment',
975
+ 'attempt_id',
976
+ 'request_digest',
977
+ ])) {
978
+ throw new TypeError('consequence attempt reference is invalid');
979
+ }
980
+ assertBinding(value);
981
+ }
982
+ function assertResolvedDigests(value) {
983
+ if (!isRecord(value) || !hasExactKeys(value, [
984
+ 'operation_digest',
985
+ 'action_digest',
986
+ 'config_digest',
987
+ ])) {
988
+ throw new TypeError('resolved consequence attempt digests are invalid');
989
+ }
990
+ assertDigest(value.operation_digest, 'operation_digest');
991
+ assertDigest(value.action_digest, 'action_digest');
992
+ assertDigest(value.config_digest, 'config_digest');
993
+ }
994
+ function cloneBinding(binding) {
995
+ return {
996
+ tenant_id: binding.tenant_id,
997
+ provider_id: binding.provider_id,
998
+ provider_account_id: binding.provider_account_id,
999
+ environment: binding.environment,
1000
+ attempt_id: binding.attempt_id,
1001
+ request_digest: binding.request_digest,
1002
+ };
1003
+ }
1004
+ function sha256(domain, value) {
1005
+ return `sha256:${crypto.createHash('sha256')
1006
+ .update(domain)
1007
+ .update(JSON.stringify(value))
1008
+ .digest('hex')}`;
1009
+ }
1010
+ export function proposalToEffectAttemptDigest(binding, digests) {
1011
+ assertBinding(binding);
1012
+ assertResolvedDigests(digests);
1013
+ return sha256(ATTEMPT_DOMAIN, {
1014
+ tenant_id: binding.tenant_id,
1015
+ provider_id: binding.provider_id,
1016
+ provider_account_id: binding.provider_account_id,
1017
+ environment: binding.environment,
1018
+ attempt_id: binding.attempt_id,
1019
+ operation_digest: digests.operation_digest,
1020
+ request_digest: binding.request_digest,
1021
+ action_digest: digests.action_digest,
1022
+ config_digest: digests.config_digest,
1023
+ });
1024
+ }
1025
+ function evidenceBindingDigest(attemptDigest, evidence) {
1026
+ return sha256(EVIDENCE_DOMAIN, {
1027
+ attempt_digest: attemptDigest,
1028
+ operation_id: evidence.operation_id,
1029
+ caid: evidence.caid,
1030
+ action_digest: evidence.action_digest,
1031
+ evidence_id: evidence.evidence_id,
1032
+ observed_at: evidence.observed_at,
1033
+ outcome: evidence.outcome,
1034
+ evidence_digest: evidence.evidence_digest,
1035
+ });
1036
+ }
1037
+ function parseOwnerGeneration(value) {
1038
+ const parsed = typeof value === 'number'
1039
+ ? value
1040
+ : typeof value === 'string' && /^(?:0|[1-9][0-9]*)$/.test(value)
1041
+ ? Number(value)
1042
+ : NaN;
1043
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
1044
+ throw new Error('malformed Postgres result: owner_generation');
1045
+ }
1046
+ return parsed;
1047
+ }
1048
+ function assertResultEnvelope(result, operation) {
1049
+ if (!result || !Array.isArray(result.rows)
1050
+ || (result.rowCount !== null
1051
+ && (!Number.isSafeInteger(result.rowCount) || result.rowCount < 0))) {
1052
+ throw new Error(`malformed Postgres result: ${operation}`);
1053
+ }
1054
+ }
1055
+ function parseApplied(result, operation) {
1056
+ assertResultEnvelope(result, operation);
1057
+ if (result.rowCount !== 1 || result.rows.length !== 1 || !isRecord(result.rows[0])
1058
+ || !hasExactKeys(result.rows[0], ['applied', 'reason'])
1059
+ || typeof result.rows[0].applied !== 'boolean'
1060
+ || (result.rows[0].reason !== null && typeof result.rows[0].reason !== 'string')
1061
+ || (result.rows[0].applied && result.rows[0].reason !== null)) {
1062
+ throw new Error(`malformed Postgres result: ${operation}`);
1063
+ }
1064
+ return {
1065
+ applied: result.rows[0].applied,
1066
+ reason: result.rows[0].reason,
1067
+ };
1068
+ }
1069
+ function parseSnapshot(result, input) {
1070
+ assertResultEnvelope(result, 'read');
1071
+ if (result.rowCount === 0 && result.rows.length === 0)
1072
+ return null;
1073
+ const row = result.rows[0];
1074
+ if (result.rowCount !== 1 || result.rows.length !== 1 || !isRecord(row)
1075
+ || !hasExactKeys(row, [
1076
+ 'tenant_id',
1077
+ 'provider_id',
1078
+ 'provider_account_id',
1079
+ 'environment',
1080
+ 'attempt_id',
1081
+ 'operation_digest',
1082
+ 'request_digest',
1083
+ 'action_digest',
1084
+ 'config_digest',
1085
+ 'attempt_digest',
1086
+ 'state',
1087
+ 'evidence_digest',
1088
+ 'owner_generation',
1089
+ 'last_heartbeat_at',
1090
+ 'lease_expires_at',
1091
+ 'lease_stale',
1092
+ ])) {
1093
+ throw new Error('malformed Postgres result: read');
1094
+ }
1095
+ assertIdentifier(row.tenant_id, 'read tenant_id');
1096
+ assertIdentifier(row.provider_id, 'read provider_id');
1097
+ assertIdentifier(row.provider_account_id, 'read provider_account_id');
1098
+ assertIdentifier(row.environment, 'read environment');
1099
+ assertIdentifier(row.attempt_id, 'read attempt_id');
1100
+ assertDigest(row.operation_digest, 'read operation_digest');
1101
+ assertDigest(row.request_digest, 'read request_digest');
1102
+ assertDigest(row.action_digest, 'read action_digest');
1103
+ assertDigest(row.config_digest, 'read config_digest');
1104
+ assertDigest(row.attempt_digest, 'read attempt_digest');
1105
+ if (typeof row.state !== 'string' || !STATES.has(row.state)) {
1106
+ throw new Error('malformed Postgres result: read state');
1107
+ }
1108
+ if (row.evidence_digest !== null) {
1109
+ assertDigest(row.evidence_digest, 'read evidence_digest');
1110
+ }
1111
+ assertInstant(row.last_heartbeat_at, 'read last_heartbeat_at');
1112
+ assertInstant(row.lease_expires_at, 'read lease_expires_at');
1113
+ if (typeof row.lease_stale !== 'boolean'
1114
+ || Date.parse(row.last_heartbeat_at) >= Date.parse(row.lease_expires_at)) {
1115
+ throw new Error('malformed Postgres result: read lease');
1116
+ }
1117
+ if (row.tenant_id !== input.tenant_id
1118
+ || row.provider_id !== input.provider_id
1119
+ || row.provider_account_id !== input.provider_account_id
1120
+ || row.environment !== input.environment
1121
+ || row.attempt_id !== input.attempt_id
1122
+ || row.request_digest !== input.request_digest) {
1123
+ throw new Error('malformed Postgres result: read binding');
1124
+ }
1125
+ return {
1126
+ tenant_id: row.tenant_id,
1127
+ provider_id: row.provider_id,
1128
+ provider_account_id: row.provider_account_id,
1129
+ environment: row.environment,
1130
+ attempt_id: row.attempt_id,
1131
+ operation_digest: row.operation_digest,
1132
+ request_digest: row.request_digest,
1133
+ action_digest: row.action_digest,
1134
+ config_digest: row.config_digest,
1135
+ attempt_digest: row.attempt_digest,
1136
+ state: row.state,
1137
+ evidence_digest: row.evidence_digest,
1138
+ owner_generation: parseOwnerGeneration(row.owner_generation),
1139
+ last_heartbeat_at: row.last_heartbeat_at,
1140
+ lease_expires_at: row.lease_expires_at,
1141
+ lease_stale: row.lease_stale,
1142
+ };
1143
+ }
1144
+ function transitionAllowed(expected, next) {
1145
+ return (expected === 'RESERVED' && next === 'INVOKING')
1146
+ || (expected === 'INVOKING' && next === 'INDETERMINATE')
1147
+ || (expected === 'INDETERMINATE'
1148
+ && (next === 'COMMITTED' || next === 'RELEASED' || next === 'ESCALATED'));
1149
+ }
1150
+ function terminalStateFor(outcome) {
1151
+ return outcome === 'COMMITTED'
1152
+ ? 'COMMITTED'
1153
+ : outcome === 'NOT_COMMITTED'
1154
+ ? 'RELEASED'
1155
+ : 'ESCALATED';
1156
+ }
1157
+ class AmbiguousPostgresCommitError extends Error {
1158
+ constructor(operation, cause) {
1159
+ super(`proposal-to-effect ${operation} COMMIT acknowledgement is ambiguous`, {
1160
+ cause,
1161
+ });
1162
+ this.name = 'AmbiguousPostgresCommitError';
1163
+ }
1164
+ }
1165
+ /**
1166
+ * Build the owner-fenced store consumed by createProposalToEffect(), plus
1167
+ * operational read/recovery methods for restart-safe saga repair.
1168
+ */
1169
+ export function createProposalToEffectPostgresStore(options) {
1170
+ if (!isRecord(options)
1171
+ || !options.pool
1172
+ || typeof options.pool.connect !== 'function'
1173
+ || !options.recovery_pool
1174
+ || typeof options.recovery_pool.connect !== 'function'
1175
+ || options.recovery_pool === options.pool
1176
+ || !(options.owner_hmac_sha256_key instanceof Uint8Array)
1177
+ || options.owner_hmac_sha256_key.byteLength < 32
1178
+ || typeof options.resolve_binding_digests !== 'function'
1179
+ || typeof options.authorize_recovery !== 'function'
1180
+ || (options.random_bytes !== undefined && typeof options.random_bytes !== 'function')
1181
+ || (options.lease_seconds !== undefined
1182
+ && (!Number.isSafeInteger(options.lease_seconds)
1183
+ || options.lease_seconds < 1
1184
+ || options.lease_seconds > MAX_LEASE_SECONDS))) {
1185
+ throw new TypeError('createProposalToEffectPostgresStore configuration is invalid');
1186
+ }
1187
+ const pool = options.pool;
1188
+ const recoveryPool = options.recovery_pool;
1189
+ const ownerKey = Buffer.from(options.owner_hmac_sha256_key);
1190
+ const randomBytes = options.random_bytes ?? ((size) => crypto.randomBytes(size));
1191
+ const leaseSeconds = options.lease_seconds ?? DEFAULT_LEASE_SECONDS;
1192
+ async function transaction(selectedPool, readOnly, operation, work) {
1193
+ const client = await selectedPool.connect();
1194
+ if (!client || typeof client.query !== 'function' || typeof client.release !== 'function') {
1195
+ throw new TypeError('proposal-to-effect pg pool returned an invalid client');
1196
+ }
1197
+ let began = false;
1198
+ let discardError;
1199
+ try {
1200
+ await client.query(readOnly
1201
+ ? 'BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'
1202
+ : 'BEGIN ISOLATION LEVEL READ COMMITTED READ WRITE');
1203
+ began = true;
1204
+ const value = await work(client);
1205
+ try {
1206
+ await client.query('COMMIT');
1207
+ began = false;
1208
+ }
1209
+ catch (error) {
1210
+ began = false;
1211
+ discardError = error instanceof Error
1212
+ ? error
1213
+ : new Error('unknown PostgreSQL COMMIT failure');
1214
+ throw new AmbiguousPostgresCommitError(operation, error);
1215
+ }
1216
+ return value;
1217
+ }
1218
+ catch (error) {
1219
+ if (began) {
1220
+ try {
1221
+ await client.query('ROLLBACK');
1222
+ }
1223
+ catch (rollbackError) {
1224
+ throw new AggregateError([error, rollbackError], 'proposal-to-effect transaction and rollback both failed');
1225
+ }
1226
+ }
1227
+ throw error;
1228
+ }
1229
+ finally {
1230
+ client.release(discardError);
1231
+ }
1232
+ }
1233
+ async function retryAmbiguousCommit(selectedPool, readOnly, operation, work) {
1234
+ try {
1235
+ return await transaction(selectedPool, readOnly, operation, work);
1236
+ }
1237
+ catch (error) {
1238
+ if (!(error instanceof AmbiguousPostgresCommitError))
1239
+ throw error;
1240
+ return transaction(selectedPool, readOnly, operation, work);
1241
+ }
1242
+ }
1243
+ function issueOwner() {
1244
+ const bytes = randomBytes(OWNER_BYTES);
1245
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength !== OWNER_BYTES) {
1246
+ throw new TypeError('proposal-to-effect owner entropy source is invalid');
1247
+ }
1248
+ return (`pto-owner:v1:${Buffer.from(bytes).toString('base64url')}`);
1249
+ }
1250
+ function ownerDigest(owner) {
1251
+ return `sha256:${crypto.createHmac('sha256', ownerKey)
1252
+ .update(OWNER_DOMAIN)
1253
+ .update(owner)
1254
+ .digest('hex')}`;
1255
+ }
1256
+ async function resolveDigests(attemptBinding) {
1257
+ const resolved = await options.resolve_binding_digests(Object.freeze(cloneBinding(attemptBinding)));
1258
+ assertResolvedDigests(resolved);
1259
+ return {
1260
+ operation_digest: resolved.operation_digest,
1261
+ action_digest: resolved.action_digest,
1262
+ config_digest: resolved.config_digest,
1263
+ };
1264
+ }
1265
+ async function readInternal(selectedPool, input) {
1266
+ return retryAmbiguousCommit(selectedPool, true, 'read', async (client) => parseSnapshot(await client.query(PROPOSAL_TO_EFFECT_POSTGRES_SQL.read, [
1267
+ input.tenant_id,
1268
+ input.provider_id,
1269
+ input.provider_account_id,
1270
+ input.environment,
1271
+ input.attempt_id,
1272
+ input.request_digest,
1273
+ ]), input));
1274
+ }
1275
+ const store = {
1276
+ durable: true,
1277
+ ownershipFenced: true,
1278
+ compareAndSwap: true,
1279
+ atomicEvidenceBinding: true,
1280
+ async reserve(attemptBinding) {
1281
+ assertBinding(attemptBinding);
1282
+ const digests = await resolveDigests(attemptBinding);
1283
+ const attemptDigest = proposalToEffectAttemptDigest(attemptBinding, digests);
1284
+ const owner = issueOwner();
1285
+ const result = await retryAmbiguousCommit(pool, false, 'reserve', async (client) => parseApplied(await client.query(PROPOSAL_TO_EFFECT_POSTGRES_SQL.reserve, [
1286
+ attemptBinding.tenant_id,
1287
+ attemptBinding.provider_id,
1288
+ attemptBinding.provider_account_id,
1289
+ attemptBinding.environment,
1290
+ attemptBinding.attempt_id,
1291
+ digests.operation_digest,
1292
+ attemptBinding.request_digest,
1293
+ digests.action_digest,
1294
+ digests.config_digest,
1295
+ attemptDigest,
1296
+ ownerDigest(owner),
1297
+ leaseSeconds,
1298
+ ]), 'reserve'));
1299
+ if (!result.applied) {
1300
+ return {
1301
+ reserved: false,
1302
+ reason: result.reason === 'attempt_exists' || result.reason === 'binding_conflict'
1303
+ ? result.reason
1304
+ : 'attempt_conflict',
1305
+ };
1306
+ }
1307
+ return { reserved: true, owner };
1308
+ },
1309
+ async transition(input) {
1310
+ if (!isRecord(input) || !hasExactKeys(input, [
1311
+ 'tenant_id',
1312
+ 'attempt_id',
1313
+ 'owner',
1314
+ 'expected_state',
1315
+ 'next_state',
1316
+ ])) {
1317
+ throw new TypeError('consequence attempt transition is invalid');
1318
+ }
1319
+ assertIdentifier(input.tenant_id, 'transition tenant_id');
1320
+ assertIdentifier(input.attempt_id, 'transition attempt_id');
1321
+ assertOwner(input.owner);
1322
+ if (typeof input.expected_state !== 'string'
1323
+ || typeof input.next_state !== 'string'
1324
+ || !transitionAllowed(input.expected_state, input.next_state)) {
1325
+ throw new TypeError('consequence attempt state transition is invalid');
1326
+ }
1327
+ const result = await retryAmbiguousCommit(pool, false, 'transition', async (client) => parseApplied(await client.query(PROPOSAL_TO_EFFECT_POSTGRES_SQL.transition, [
1328
+ input.tenant_id,
1329
+ input.attempt_id,
1330
+ ownerDigest(input.owner),
1331
+ input.expected_state,
1332
+ input.next_state,
1333
+ leaseSeconds,
1334
+ ]), 'transition'));
1335
+ return result.applied;
1336
+ },
1337
+ async heartbeat(input) {
1338
+ if (!isRecord(input) || !hasExactKeys(input, [
1339
+ 'tenant_id',
1340
+ 'attempt_id',
1341
+ 'owner',
1342
+ ])) {
1343
+ throw new TypeError('consequence attempt heartbeat is invalid');
1344
+ }
1345
+ assertIdentifier(input.tenant_id, 'heartbeat tenant_id');
1346
+ assertIdentifier(input.attempt_id, 'heartbeat attempt_id');
1347
+ assertOwner(input.owner);
1348
+ const result = await retryAmbiguousCommit(pool, false, 'heartbeat', async (client) => parseApplied(await client.query(PROPOSAL_TO_EFFECT_POSTGRES_SQL.heartbeat, [
1349
+ input.tenant_id,
1350
+ input.attempt_id,
1351
+ ownerDigest(input.owner),
1352
+ leaseSeconds,
1353
+ ]), 'heartbeat'));
1354
+ return result.applied;
1355
+ },
1356
+ async reconcile(input) {
1357
+ if (!isRecord(input) || !hasExactKeys(input, [
1358
+ 'tenant_id',
1359
+ 'attempt_id',
1360
+ 'owner',
1361
+ 'expected_state',
1362
+ 'next_state',
1363
+ 'evidence',
1364
+ ])) {
1365
+ throw new TypeError('consequence attempt reconciliation is invalid');
1366
+ }
1367
+ assertIdentifier(input.tenant_id, 'reconcile tenant_id');
1368
+ assertIdentifier(input.attempt_id, 'reconcile attempt_id');
1369
+ assertOwner(input.owner);
1370
+ if (input.expected_state !== 'INDETERMINATE'
1371
+ || !isRecord(input.evidence)
1372
+ || !hasExactKeys(input.evidence, [
1373
+ 'tenant_id',
1374
+ 'provider_id',
1375
+ 'provider_account_id',
1376
+ 'environment',
1377
+ 'attempt_id',
1378
+ 'request_digest',
1379
+ 'operation_id',
1380
+ 'caid',
1381
+ 'action_digest',
1382
+ 'evidence_id',
1383
+ 'observed_at',
1384
+ 'outcome',
1385
+ 'evidence_digest',
1386
+ ])) {
1387
+ throw new TypeError('consequence attempt evidence binding is invalid');
1388
+ }
1389
+ assertBinding({
1390
+ tenant_id: input.evidence.tenant_id,
1391
+ provider_id: input.evidence.provider_id,
1392
+ provider_account_id: input.evidence.provider_account_id,
1393
+ environment: input.evidence.environment,
1394
+ attempt_id: input.evidence.attempt_id,
1395
+ request_digest: input.evidence.request_digest,
1396
+ });
1397
+ assertIdentifier(input.evidence.operation_id, 'evidence operation_id');
1398
+ assertIdentifier(input.evidence.caid, 'evidence caid');
1399
+ assertDigest(input.evidence.action_digest, 'evidence action_digest');
1400
+ assertIdentifier(input.evidence.evidence_id, 'evidence_id');
1401
+ assertDigest(input.evidence.evidence_digest, 'evidence_digest');
1402
+ if (typeof input.evidence.observed_at !== 'string'
1403
+ || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(input.evidence.observed_at)
1404
+ || !Number.isFinite(Date.parse(input.evidence.observed_at))
1405
+ || !['COMMITTED', 'NOT_COMMITTED', 'ESCALATED'].includes(input.evidence.outcome)
1406
+ || input.tenant_id !== input.evidence.tenant_id
1407
+ || input.attempt_id !== input.evidence.attempt_id
1408
+ || input.next_state !== terminalStateFor(input.evidence.outcome)) {
1409
+ throw new TypeError('consequence attempt evidence binding is invalid');
1410
+ }
1411
+ const attemptBinding = {
1412
+ tenant_id: input.evidence.tenant_id,
1413
+ provider_id: input.evidence.provider_id,
1414
+ provider_account_id: input.evidence.provider_account_id,
1415
+ environment: input.evidence.environment,
1416
+ attempt_id: input.evidence.attempt_id,
1417
+ request_digest: input.evidence.request_digest,
1418
+ };
1419
+ const digests = await resolveDigests(attemptBinding);
1420
+ if (input.evidence.action_digest !== digests.action_digest) {
1421
+ throw new TypeError('consequence attempt evidence binding is invalid');
1422
+ }
1423
+ const attemptDigest = proposalToEffectAttemptDigest(attemptBinding, digests);
1424
+ const providerEvidence = input.evidence;
1425
+ const result = await retryAmbiguousCommit(pool, false, 'reconcile', async (client) => parseApplied(await client.query(PROPOSAL_TO_EFFECT_POSTGRES_SQL.reconcile, [
1426
+ attemptBinding.tenant_id,
1427
+ attemptBinding.provider_id,
1428
+ attemptBinding.provider_account_id,
1429
+ attemptBinding.environment,
1430
+ attemptBinding.attempt_id,
1431
+ ownerDigest(input.owner),
1432
+ digests.operation_digest,
1433
+ attemptBinding.request_digest,
1434
+ digests.action_digest,
1435
+ digests.config_digest,
1436
+ attemptDigest,
1437
+ providerEvidence.operation_id,
1438
+ providerEvidence.caid,
1439
+ input.next_state,
1440
+ providerEvidence.evidence_id,
1441
+ providerEvidence.observed_at,
1442
+ providerEvidence.outcome,
1443
+ providerEvidence.evidence_digest,
1444
+ evidenceBindingDigest(attemptDigest, providerEvidence),
1445
+ ]), 'reconcile'));
1446
+ return result.applied;
1447
+ },
1448
+ async read(input) {
1449
+ assertAttemptReference(input);
1450
+ const snapshot = await readInternal(pool, input);
1451
+ if (!snapshot)
1452
+ return null;
1453
+ const { owner_generation: _ownerGeneration, ...operational } = snapshot;
1454
+ return operational;
1455
+ },
1456
+ async recover(input) {
1457
+ assertAttemptReference(input);
1458
+ const snapshot = await readInternal(recoveryPool, input);
1459
+ if (!snapshot)
1460
+ return { recovered: false, reason: 'attempt_not_found' };
1461
+ if (TERMINAL_STATES.has(snapshot.state)) {
1462
+ return { recovered: false, reason: 'terminal_state_immutable' };
1463
+ }
1464
+ if (!snapshot.lease_stale) {
1465
+ return { recovered: false, reason: 'attempt_not_stale' };
1466
+ }
1467
+ const authorized = await options.authorize_recovery(Object.freeze({
1468
+ ...snapshot,
1469
+ }));
1470
+ if (authorized !== true) {
1471
+ return { recovered: false, reason: 'recovery_not_authorized' };
1472
+ }
1473
+ const owner = issueOwner();
1474
+ const recovered = await retryAmbiguousCommit(recoveryPool, false, 'recover', async (client) => parseApplied(await client.query(PROPOSAL_TO_EFFECT_POSTGRES_SQL.recover, [
1475
+ input.tenant_id,
1476
+ input.provider_id,
1477
+ input.provider_account_id,
1478
+ input.environment,
1479
+ input.attempt_id,
1480
+ input.request_digest,
1481
+ snapshot.attempt_digest,
1482
+ snapshot.owner_generation,
1483
+ snapshot.state,
1484
+ snapshot.lease_expires_at,
1485
+ ownerDigest(owner),
1486
+ leaseSeconds,
1487
+ ]), 'recover'));
1488
+ if (!recovered.applied) {
1489
+ return {
1490
+ recovered: false,
1491
+ reason: recovered.reason === 'attempt_not_stale'
1492
+ ? 'attempt_not_stale'
1493
+ : 'recovery_conflict',
1494
+ };
1495
+ }
1496
+ return {
1497
+ recovered: true,
1498
+ owner,
1499
+ state: snapshot.state === 'RESERVED' ? 'RESERVED' : 'INDETERMINATE',
1500
+ };
1501
+ },
1502
+ };
1503
+ return Object.freeze(store);
1504
+ }
1505
+ export default {
1506
+ PROPOSAL_TO_EFFECT_POSTGRES_DDL,
1507
+ PROPOSAL_TO_EFFECT_POSTGRES_SQL,
1508
+ proposalToEffectAttemptDigest,
1509
+ createProposalToEffectPostgresStore,
1510
+ };
1511
+ //# sourceMappingURL=proposal-to-effect-postgres.js.map