@emilia-protocol/gate 0.14.0 → 0.15.1

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