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