@sixb/pg 0.1.18 → 0.1.20

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.
package/dist/index.js CHANGED
@@ -1244,7 +1244,6 @@ function rowToSessionRecord(row) {
1244
1244
  projectId: row.project_id,
1245
1245
  userId: row.user_id,
1246
1246
  strategyId: row.strategy_id,
1247
- audience: row.audience,
1248
1247
  tokenHash: row.token_hash,
1249
1248
  createdAt: toDate(row.created_at),
1250
1249
  expiresAt: toDate(row.expires_at),
@@ -1311,7 +1310,6 @@ function rowToMagicLinkRecord(row) {
1311
1310
  id: row.id,
1312
1311
  projectId: row.project_id,
1313
1312
  strategyId: row.strategy_id,
1314
- audience: row.audience,
1315
1313
  email: row.email,
1316
1314
  tokenHash: row.token_hash,
1317
1315
  returnTo: row.return_to ?? undefined,
@@ -1326,7 +1324,6 @@ function rowToOidcAuthorizationAttemptRecord(row) {
1326
1324
  id: row.id,
1327
1325
  projectId: row.project_id,
1328
1326
  strategyId: row.strategy_id,
1329
- audience: row.audience,
1330
1327
  stateHash: row.state_hash,
1331
1328
  nonceHash: row.nonce_hash,
1332
1329
  codeVerifier: row.code_verifier,
@@ -1622,7 +1619,6 @@ async function consumeOidcAttempt(sql, params) {
1622
1619
  }
1623
1620
  async function validateCompleteSessionInput(sql, projectId, session) {
1624
1621
  const sessionId = assertNonEmpty(session.id, "Session id");
1625
- assertNonEmpty(session.audience, "Session audience");
1626
1622
  assertNonEmpty(session.tokenHash, "Session token hash");
1627
1623
  await assertSessionIdAvailable(sql, projectId, sessionId);
1628
1624
  }
@@ -1636,7 +1632,6 @@ async function createSession(sql, input) {
1636
1632
  const projectId = assertNonEmpty(input.projectId, "Project id");
1637
1633
  const userId = assertNonEmpty(input.userId, "User id");
1638
1634
  const strategyId = assertNonEmpty(input.strategyId, "Strategy id");
1639
- const audience = assertNonEmpty(input.audience, "Session audience");
1640
1635
  const tokenHash = assertNonEmpty(input.tokenHash, "Session token hash");
1641
1636
  await requireUserById(sql, { projectId, id: userId });
1642
1637
  await assertSessionIdAvailable(sql, projectId, id);
@@ -1647,7 +1642,6 @@ async function createSession(sql, input) {
1647
1642
  id,
1648
1643
  user_id,
1649
1644
  strategy_id,
1650
- audience,
1651
1645
  token_hash,
1652
1646
  created_at,
1653
1647
  expires_at,
@@ -1662,7 +1656,6 @@ async function createSession(sql, input) {
1662
1656
  ${id},
1663
1657
  ${userId},
1664
1658
  ${strategyId},
1665
- ${audience},
1666
1659
  ${tokenHash},
1667
1660
  ${input.createdAt},
1668
1661
  ${input.expiresAt},
@@ -1681,50 +1674,26 @@ async function createSession(sql, input) {
1681
1674
  }
1682
1675
  }
1683
1676
  async function revokeActiveSessionsForUser(sql, params) {
1684
- const rows = params.audience === undefined ? await sql`
1685
- SELECT *
1686
- FROM auth_sessions
1687
- WHERE project_id = ${params.projectId}
1688
- AND user_id = ${params.userId}
1689
- AND revoked_at IS NULL
1690
- AND expires_at > ${params.revokedAt}
1691
- AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
1692
- ORDER BY created_at ASC, id ASC
1693
- FOR UPDATE
1694
- ` : await sql`
1677
+ const rows = await sql`
1695
1678
  SELECT *
1696
1679
  FROM auth_sessions
1697
1680
  WHERE project_id = ${params.projectId}
1698
1681
  AND user_id = ${params.userId}
1699
- AND audience = ${params.audience}
1700
1682
  AND revoked_at IS NULL
1701
1683
  AND expires_at > ${params.revokedAt}
1702
1684
  AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
1703
1685
  ORDER BY created_at ASC, id ASC
1704
1686
  FOR UPDATE
1705
1687
  `;
1706
- if (params.audience === undefined) {
1707
- await sql`
1708
- UPDATE auth_sessions
1709
- SET revoked_at = ${params.revokedAt}
1710
- WHERE project_id = ${params.projectId}
1711
- AND user_id = ${params.userId}
1712
- AND revoked_at IS NULL
1713
- AND expires_at > ${params.revokedAt}
1714
- AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
1715
- `;
1716
- } else {
1717
- await sql`
1688
+ await sql`
1718
1689
  UPDATE auth_sessions
1719
1690
  SET revoked_at = ${params.revokedAt}
1720
1691
  WHERE project_id = ${params.projectId}
1721
1692
  AND user_id = ${params.userId}
1722
- AND audience = ${params.audience}
1723
1693
  AND revoked_at IS NULL
1724
1694
  AND expires_at > ${params.revokedAt}
1725
1695
  AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
1726
1696
  `;
1727
- }
1728
1697
  return rows.map((row) => rowToSessionRecord({
1729
1698
  ...row,
1730
1699
  revoked_at: params.revokedAt
@@ -2384,7 +2353,6 @@ class PgAuthInvitationStore {
2384
2353
  }
2385
2354
 
2386
2355
  // src/auth-storage/magic-links.ts
2387
- import { resolveAuthSessionAudience } from "@sixb/core";
2388
2356
  import { AuthStorageError as AuthStorageError5 } from "@sixb/core/storage";
2389
2357
  class PgAuthMagicLinkStore {
2390
2358
  sql;
@@ -2396,7 +2364,6 @@ class PgAuthMagicLinkStore {
2396
2364
  const id = assertNonEmpty(input.id, "Magic link id");
2397
2365
  const projectId = assertNonEmpty(input.projectId, "Project id");
2398
2366
  const strategyId = assertNonEmpty(input.strategyId, "Strategy id");
2399
- const audience = resolveAuthSessionAudience(input.audience);
2400
2367
  const email = normalizeEmail(input.email);
2401
2368
  const tokenHash = assertNonEmpty(input.tokenHash, "Magic link token hash");
2402
2369
  await lockAdvisoryKeys(tx, [authLockKey("magic-links", projectId, email)]);
@@ -2414,7 +2381,6 @@ class PgAuthMagicLinkStore {
2414
2381
  project_id,
2415
2382
  id,
2416
2383
  strategy_id,
2417
- audience,
2418
2384
  email,
2419
2385
  token_hash,
2420
2386
  return_to,
@@ -2424,7 +2390,6 @@ class PgAuthMagicLinkStore {
2424
2390
  ${projectId},
2425
2391
  ${id},
2426
2392
  ${strategyId},
2427
- ${audience},
2428
2393
  ${email},
2429
2394
  ${tokenHash},
2430
2395
  ${input.returnTo ?? null},
@@ -2472,7 +2437,6 @@ class PgAuthMagicLinkStore {
2472
2437
  }
2473
2438
 
2474
2439
  // src/auth-storage/oidc-attempts.ts
2475
- import { resolveAuthSessionAudience as resolveAuthSessionAudience2 } from "@sixb/core";
2476
2440
  import { AuthStorageError as AuthStorageError6 } from "@sixb/core/storage";
2477
2441
  class PgAuthOidcAuthorizationAttemptStore {
2478
2442
  sql;
@@ -2483,7 +2447,6 @@ class PgAuthOidcAuthorizationAttemptStore {
2483
2447
  const id = assertNonEmpty(input.id, "OIDC authorization attempt id");
2484
2448
  const projectId = assertNonEmpty(input.projectId, "Project id");
2485
2449
  const strategyId = assertNonEmpty(input.strategyId, "Strategy id");
2486
- const audience = resolveAuthSessionAudience2(input.audience);
2487
2450
  const stateHash = assertNonEmpty(input.stateHash, "OIDC state hash");
2488
2451
  const nonceHash = assertNonEmpty(input.nonceHash, "OIDC nonce hash");
2489
2452
  const codeVerifier = assertNonEmpty(input.codeVerifier, "OIDC code verifier");
@@ -2496,7 +2459,6 @@ class PgAuthOidcAuthorizationAttemptStore {
2496
2459
  project_id,
2497
2460
  id,
2498
2461
  strategy_id,
2499
- audience,
2500
2462
  state_hash,
2501
2463
  nonce_hash,
2502
2464
  code_verifier,
@@ -2507,7 +2469,6 @@ class PgAuthOidcAuthorizationAttemptStore {
2507
2469
  ${projectId},
2508
2470
  ${id},
2509
2471
  ${strategyId},
2510
- ${audience},
2511
2472
  ${stateHash},
2512
2473
  ${nonceHash},
2513
2474
  ${codeVerifier},
@@ -2765,21 +2726,6 @@ class PgAuthSessionStore {
2765
2726
  async getById(params) {
2766
2727
  return getSessionById(this.sql, params);
2767
2728
  }
2768
- async getActiveByUserId(params) {
2769
- const [row] = await this.sql`
2770
- SELECT *
2771
- FROM auth_sessions
2772
- WHERE project_id = ${params.projectId}
2773
- AND user_id = ${params.userId}
2774
- AND audience = ${params.audience}
2775
- AND revoked_at IS NULL
2776
- AND expires_at > ${params.now}
2777
- AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.now})
2778
- ORDER BY created_at DESC, id DESC
2779
- LIMIT 1
2780
- `;
2781
- return row ? rowToSessionRecord(row) : null;
2782
- }
2783
2729
  async listActiveByUserId(params) {
2784
2730
  const rows = await this.sql`
2785
2731
  SELECT *
@@ -2795,7 +2741,7 @@ class PgAuthSessionStore {
2795
2741
  }
2796
2742
  async findValidByTokenHash(params) {
2797
2743
  const row = await getSessionRowById(this.sql, params);
2798
- if (!row || row.refresh_token_hash !== null || row.audience !== params.audience || row.token_hash !== params.tokenHash || !isLiveSessionRow(row, params.now)) {
2744
+ if (!row || row.refresh_token_hash !== null || row.token_hash !== params.tokenHash || !isLiveSessionRow(row, params.now)) {
2799
2745
  return null;
2800
2746
  }
2801
2747
  return rowToSessionRecord(row);
@@ -2855,7 +2801,6 @@ class PgAuthSessionStore {
2855
2801
  last_seen_at = ${params.now}
2856
2802
  WHERE project_id = ${params.projectId}
2857
2803
  AND id = ${params.id}
2858
- AND audience = ${params.audience}
2859
2804
  AND token_hash = ${params.tokenHash}
2860
2805
  AND refresh_token_hash IS NULL
2861
2806
  AND revoked_at IS NULL
@@ -2883,11 +2828,7 @@ class PgAuthSessionStore {
2883
2828
  }
2884
2829
  async revokeActiveForUser(params) {
2885
2830
  return runPgTransaction(this.sql, async (tx) => {
2886
- const locks = [authLockKey("sessions", params.projectId, params.userId)];
2887
- if (params.audience) {
2888
- locks.push(authLockKey("sessions", params.projectId, params.userId, params.audience));
2889
- }
2890
- await lockAdvisoryKeys(tx, locks);
2831
+ await lockAdvisoryKeys(tx, [authLockKey("sessions", params.projectId, params.userId)]);
2891
2832
  return revokeActiveSessionsForUser(tx, params);
2892
2833
  });
2893
2834
  }
@@ -3146,7 +3087,6 @@ class PgAuthStorage {
3146
3087
  };
3147
3088
  }
3148
3089
  await validateCompleteSessionInput(tx, projectId, input.session);
3149
- assertSignInSessionAudience(projectId, input.session.audience, magicLink.audience);
3150
3090
  let newUserId;
3151
3091
  if (shouldCreateUser) {
3152
3092
  newUserId = assertNonEmpty(input.newUserId, "User id");
@@ -3251,7 +3191,6 @@ class PgAuthStorage {
3251
3191
  };
3252
3192
  }
3253
3193
  await validateCompleteSessionInput(tx, projectId, input.session);
3254
- assertSignInSessionAudience(projectId, input.session.audience, attempt.audience);
3255
3194
  let newUserId;
3256
3195
  if (shouldCreateUser) {
3257
3196
  newUserId = assertNonEmpty(input.newUserId, "User id");
@@ -3459,16 +3398,12 @@ class PgAuthStorage {
3459
3398
  return this.applyGroups(sql, { ...input, groupIds, source: "directory" });
3460
3399
  }
3461
3400
  async createSignInSession(sql, input) {
3462
- await lockAdvisoryKeys(sql, [
3463
- authLockKey("sessions", input.projectId, input.userId),
3464
- authLockKey("sessions", input.projectId, input.userId, input.session.audience)
3465
- ]);
3401
+ await lockAdvisoryKeys(sql, [authLockKey("sessions", input.projectId, input.userId)]);
3466
3402
  return createSession(sql, {
3467
3403
  id: input.session.id,
3468
3404
  projectId: input.projectId,
3469
3405
  userId: input.userId,
3470
3406
  strategyId: input.strategyId,
3471
- audience: input.session.audience,
3472
3407
  tokenHash: input.session.tokenHash,
3473
3408
  createdAt: input.session.createdAt,
3474
3409
  expiresAt: input.session.expiresAt,
@@ -3514,12 +3449,6 @@ class PgAuthStorage {
3514
3449
  return rowToIdentityRecord(row);
3515
3450
  }
3516
3451
  }
3517
- function assertSignInSessionAudience(projectId, sessionAudience, storedAudience) {
3518
- if (sessionAudience === storedAudience) {
3519
- return;
3520
- }
3521
- throw new AuthStorageError11("invalid_input", `[Sixb] Sign-in session audience '${sessionAudience}' does not match stored auth audience '${storedAudience}' for project '${projectId}'.`);
3522
- }
3523
3452
  function assertCompletableDeviceAuthorization(authorization, input) {
3524
3453
  if (authorization.deviceCodeHash !== input.deviceCodeHash) {
3525
3454
  throw new AuthStorageError11("invalid_device_authorization", "[Sixb] Invalid device authorization.");
@@ -8015,6 +7944,181 @@ CREATE INDEX idx_ontology_sources_drained
8015
7944
  WHERE status = 'superseded' AND root_count = 0;
8016
7945
  `;
8017
7946
 
7947
+ // src/migrations/061-terminal-action-runs.sql
7948
+ var _061_terminal_action_runs_default = `-- Actions run in the request that asks for them and record their run once, when it ends. No run
7949
+ -- is queued, running, or cancelled any more, and every run has started and finished.
7950
+
7951
+ -- A run left running after its edits committed, as a crash between the commit and the end of the
7952
+ -- run left it, succeeded: its edits and their outbox facts are in the commit. A run that stopped
7953
+ -- during its effects keeps that phase, and records effects that never ended.
7954
+ UPDATE action_runs AS run
7955
+ SET
7956
+ status = 'succeeded',
7957
+ error = NULL,
7958
+ finished_at = committed.committed_at,
7959
+ phase = CASE WHEN run.phase = 'effects' THEN 'effects' ELSE 'commit' END,
7960
+ effects_status = CASE
7961
+ WHEN run.phase = 'effects' AND run.effects_status IS NULL THEN 'failed'
7962
+ ELSE run.effects_status
7963
+ END,
7964
+ effects_completed_at = CASE
7965
+ WHEN run.phase = 'effects' AND run.effects_status IS NULL THEN now()
7966
+ ELSE run.effects_completed_at
7967
+ END,
7968
+ effects_error = CASE
7969
+ WHEN run.phase = 'effects' AND run.effects_status IS NULL THEN jsonb_build_object(
7970
+ 'code', 'internal.unexpected',
7971
+ 'message', 'Abandoned: Actions now run synchronously; these effects never finished.',
7972
+ 'retryable', false,
7973
+ 'at', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
7974
+ 'details', jsonb_build_object('actionId', run.action_id, 'runId', run.id, 'phase', 'effects')
7975
+ )
7976
+ ELSE run.effects_error
7977
+ END
7978
+ FROM ontology_commits AS committed
7979
+ WHERE run.status IN ('queued', 'running')
7980
+ AND committed.origin_kind = 'action'
7981
+ AND committed.project_id = run.project_id
7982
+ AND committed.origin_run_id = run.id;
7983
+
7984
+ -- Any other run left queued or running never finished, and nothing will resume it.
7985
+ UPDATE action_runs
7986
+ SET
7987
+ status = 'failed',
7988
+ finished_at = now(),
7989
+ error = jsonb_build_object(
7990
+ 'code', 'internal.unexpected',
7991
+ 'message', 'Abandoned: Actions now run synchronously; this run never finished.',
7992
+ 'retryable', false,
7993
+ 'at', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
7994
+ 'details', jsonb_build_object('actionId', action_id, 'runId', id)
7995
+ )
7996
+ WHERE status IN ('queued', 'running');
7997
+
7998
+ -- A cancelled run failed: it keeps the failure it recorded.
7999
+ UPDATE action_runs SET status = 'failed' WHERE status = 'cancelled';
8000
+
8001
+ -- Every failed run carries a failure. One recorded before failures were kept explains itself.
8002
+ UPDATE action_runs
8003
+ SET error = jsonb_build_object(
8004
+ 'code', 'internal.unexpected',
8005
+ 'message', 'This run failed before Sixb recorded why.',
8006
+ 'retryable', false,
8007
+ 'at', to_char(COALESCE(finished_at, now()) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
8008
+ 'details', jsonb_build_object('actionId', action_id, 'runId', id)
8009
+ )
8010
+ WHERE status = 'failed' AND error IS NULL;
8011
+
8012
+ -- The phases \`request\`, \`enqueue\`, and \`cancelled\` are gone: a run that stopped in one of them
8013
+ -- never got past validation. A run starts when it was queued at the latest.
8014
+ UPDATE action_runs
8015
+ SET
8016
+ phase = CASE
8017
+ WHEN phase IN ('validation', 'writeback', 'edits', 'commit', 'effects') THEN phase
8018
+ ELSE 'validation'
8019
+ END,
8020
+ started_at = COALESCE(started_at, queued_at),
8021
+ finished_at = COALESCE(finished_at, now()),
8022
+ writeback_completed_at = CASE
8023
+ WHEN writeback_status IS NULL THEN NULL
8024
+ ELSE COALESCE(writeback_completed_at, finished_at, now())
8025
+ END,
8026
+ effects_completed_at = CASE
8027
+ WHEN effects_status IS NULL THEN NULL
8028
+ ELSE COALESCE(effects_completed_at, finished_at, now())
8029
+ END
8030
+ WHERE phase IS NULL
8031
+ OR phase NOT IN ('validation', 'writeback', 'edits', 'commit', 'effects')
8032
+ OR started_at IS NULL
8033
+ OR finished_at IS NULL
8034
+ OR (writeback_status IS NOT NULL AND writeback_completed_at IS NULL)
8035
+ OR (effects_status IS NOT NULL AND effects_completed_at IS NULL);
8036
+
8037
+ -- A failure names the phase its run ended in, and only Action codes: an enqueue failure, which no
8038
+ -- run can meet any more, reads as the unexpected error it now is.
8039
+ UPDATE action_runs
8040
+ SET error = jsonb_set(
8041
+ CASE
8042
+ WHEN error->>'code' = 'queue.enqueue_failed'
8043
+ THEN error || '{"code": "internal.unexpected", "retryable": false}'::jsonb
8044
+ ELSE error
8045
+ END,
8046
+ '{details,phase}',
8047
+ to_jsonb(phase)
8048
+ )
8049
+ WHERE error IS NOT NULL
8050
+ AND (
8051
+ error->'details'->>'phase' IS DISTINCT FROM phase
8052
+ OR error->>'code' = 'queue.enqueue_failed'
8053
+ );
8054
+
8055
+ -- The status and phase checks were unnamed. Drop exactly the two that admit the removed values.
8056
+ DO $$
8057
+ DECLARE lifecycle_constraint RECORD;
8058
+ BEGIN
8059
+ FOR lifecycle_constraint IN
8060
+ SELECT conname
8061
+ FROM pg_constraint
8062
+ WHERE conrelid = 'action_runs'::regclass
8063
+ AND contype = 'c'
8064
+ AND (
8065
+ pg_get_constraintdef(oid) LIKE '%''queued''%'
8066
+ OR pg_get_constraintdef(oid) LIKE '%''enqueue''%'
8067
+ )
8068
+ LOOP
8069
+ EXECUTE format('ALTER TABLE action_runs DROP CONSTRAINT %I', lifecycle_constraint.conname);
8070
+ END LOOP;
8071
+ END $$;
8072
+
8073
+ -- The listing indexes ordered runs by \`COALESCE(started_at, queued_at)\`.
8074
+ DROP INDEX idx_action_runs_project_started;
8075
+ DROP INDEX idx_action_runs_project_action_started;
8076
+ DROP INDEX idx_action_runs_project_object_started;
8077
+ DROP INDEX idx_action_runs_project_status_started;
8078
+
8079
+ ALTER TABLE action_runs
8080
+ DROP COLUMN queued_at,
8081
+ ALTER COLUMN phase SET NOT NULL,
8082
+ ALTER COLUMN started_at SET NOT NULL,
8083
+ ALTER COLUMN finished_at SET NOT NULL,
8084
+ ADD CONSTRAINT action_runs_status_check CHECK (status IN ('succeeded', 'failed')),
8085
+ ADD CONSTRAINT action_runs_phase_check CHECK (
8086
+ phase IN ('validation', 'writeback', 'edits', 'commit', 'effects')
8087
+ ),
8088
+ ADD CONSTRAINT action_runs_failure_check CHECK ((status = 'failed') = (error IS NOT NULL));
8089
+
8090
+ CREATE INDEX idx_action_runs_project_started
8091
+ ON action_runs (project_id, started_at DESC);
8092
+ CREATE INDEX idx_action_runs_project_action_started
8093
+ ON action_runs (project_id, action_id, started_at DESC);
8094
+ CREATE INDEX idx_action_runs_project_object_started
8095
+ ON action_runs (project_id, object_type_id, primary_id, started_at DESC);
8096
+ CREATE INDEX idx_action_runs_project_status_started
8097
+ ON action_runs (project_id, status, started_at DESC);
8098
+ `;
8099
+
8100
+ // src/migrations/062-drop-auth-audience.sql
8101
+ var _062_drop_auth_audience_default = `-- A project has one browser app, so sessions, magic links and sign-in attempts no longer name the
8102
+ -- one they belong to. Atlas's browser sessions end here: their cookie is no longer read. Native
8103
+ -- sessions carried Atlas's name only because the column needed a value, so they stay.
8104
+ UPDATE auth_sessions
8105
+ SET revoked_at = now()
8106
+ WHERE audience = 'atlas'
8107
+ AND refresh_token_hash IS NULL
8108
+ AND revoked_at IS NULL;
8109
+
8110
+ -- Dropping the column drops the two indexes that name it.
8111
+ ALTER TABLE auth_sessions DROP COLUMN audience;
8112
+ CREATE INDEX idx_auth_sessions_user_active
8113
+ ON auth_sessions (project_id, user_id, expires_at DESC, created_at DESC, id DESC)
8114
+ WHERE revoked_at IS NULL;
8115
+ CREATE INDEX idx_auth_sessions_user_created
8116
+ ON auth_sessions (project_id, user_id, created_at DESC, id DESC);
8117
+
8118
+ ALTER TABLE auth_magic_links DROP COLUMN audience;
8119
+ ALTER TABLE auth_oidc_authorization_attempts DROP COLUMN audience;
8120
+ `;
8121
+
8018
8122
  // src/migrations.ts
8019
8123
  var POSTGRES_STORAGE_ADAPTER_ID = "SixbPostgresStorage";
8020
8124
  function pgSql(id, sqlText) {
@@ -8241,7 +8345,9 @@ var postgresStorageMigrations = defineMigrations({
8241
8345
  pgSql("057-audio-transcription", _057_audio_transcription_default),
8242
8346
  pgSql("058-reranking-model-kind", _058_reranking_model_kind_default),
8243
8347
  pgSql("059-file-download-grants", _059_file_download_grants_default),
8244
- pgSql("060-drained-source-versions", _060_drained_source_versions_default)
8348
+ pgSql("060-drained-source-versions", _060_drained_source_versions_default),
8349
+ pgSql("061-terminal-action-runs", _061_terminal_action_runs_default),
8350
+ pgSql("062-drop-auth-audience", _062_drop_auth_audience_default)
8245
8351
  ]
8246
8352
  });
8247
8353
 
@@ -15681,12 +15787,8 @@ import {
15681
15787
  serializeActionRunFailure
15682
15788
  } from "@sixb/core/internal/action-run-storage";
15683
15789
  import { assertActionRunExecution } from "@sixb/core/internal/action-run-storage-provider";
15684
- import {
15685
- actionRunPhaseRecordsEqual,
15686
- canRequeueActionRunAfterEnqueueFailure,
15687
- finishActionRunPhase
15688
- } from "@sixb/core/internal/storage";
15689
- import { ActionRunError, isTerminalActionRun } from "@sixb/core/storage";
15790
+ import { normalizeRecordActionRunInput, resolveActionRunEffects } from "@sixb/core/internal/storage";
15791
+ import { ActionRunError } from "@sixb/core/storage";
15690
15792
  class PgActionRunStorage {
15691
15793
  sql;
15692
15794
  executions;
@@ -15694,227 +15796,83 @@ class PgActionRunStorage {
15694
15796
  this.sql = sql;
15695
15797
  this.executions = executions;
15696
15798
  }
15697
- async lockForMaterialization(input) {
15698
- const [row] = await this.sql`
15699
- SELECT *
15700
- FROM action_runs
15701
- WHERE project_id = ${input.projectId} AND id = ${input.runId}
15702
- FOR UPDATE
15703
- `;
15704
- if (!row) {
15705
- throw new ActionRunError(`[SixbPg] Action run '${input.runId}' not found for project '${input.projectId}'.`);
15706
- }
15707
- if (row.action_id !== input.actionId) {
15708
- throw new ActionRunError(`[SixbPg] Action run '${input.runId}' does not belong to action '${input.actionId}'.`);
15709
- }
15710
- if (row.status !== "running") {
15711
- throw new ActionRunError(`[SixbPg] Action run '${input.runId}' cannot materialize from status '${row.status}'.`);
15712
- }
15713
- return rowToActionRunRecord(row);
15714
- }
15715
- async queue(input) {
15799
+ async record(input) {
15800
+ const record = normalizeRecordActionRunInput(input);
15716
15801
  await assertActionRunExecution({
15717
15802
  executions: this.executions,
15718
- projectId: input.projectId,
15719
- executionId: input.executionId,
15720
- runId: input.id,
15721
- actionId: input.actionId
15722
- });
15723
- try {
15724
- const [row] = await this.sql`
15725
- INSERT INTO action_runs (
15726
- project_id,
15727
- id,
15728
- execution_id,
15729
- action_id,
15730
- subject_kind,
15731
- object_type_id,
15732
- primary_id,
15733
- status,
15734
- phase,
15735
- queued_at,
15736
- started_at,
15737
- finished_at,
15738
- params,
15739
- idempotency_key,
15740
- writeback_status,
15741
- writeback_completed_at,
15742
- writeback_result,
15743
- writeback_error,
15744
- effects_status,
15745
- effects_completed_at,
15746
- effects_error,
15747
- error
15748
- ) VALUES (
15749
- ${input.projectId},
15750
- ${input.id},
15751
- ${input.executionId},
15752
- ${input.actionId},
15753
- ${input.subject.kind},
15754
- ${input.subject.kind === "object" ? input.subject.objectTypeId : null},
15755
- ${input.subject.kind === "object" ? input.subject.primaryId : null},
15756
- ${"queued"},
15757
- ${"request"},
15758
- ${input.queuedAt ?? new Date},
15759
- ${null},
15760
- ${null},
15761
- ${JSON.stringify(input.params)}::text::jsonb,
15762
- ${input.idempotencyKey},
15763
- ${null},
15764
- ${null},
15765
- ${null},
15766
- ${null},
15767
- ${null},
15768
- ${null},
15769
- ${null},
15770
- ${null}
15771
- )
15772
- RETURNING *
15773
- `;
15774
- return rowToActionRunRecord(row);
15775
- } catch (error) {
15776
- if (isUniqueViolation(error)) {
15777
- return this.requeueAfterEnqueueFailure(input);
15778
- }
15779
- throw error;
15780
- }
15781
- }
15782
- async requeueAfterEnqueueFailure(input) {
15783
- return runPgTransaction(this.sql, async (tx) => {
15784
- const [existing] = await tx`
15785
- SELECT * FROM action_runs
15786
- WHERE project_id = ${input.projectId} AND id = ${input.id}
15787
- FOR UPDATE
15788
- `;
15789
- if (!existing || !canRequeueActionRunAfterEnqueueFailure(rowToActionRunRecord(existing), input)) {
15790
- throw new ActionRunError(`[SixbPg] Action run '${input.id}' already exists for project '${input.projectId}'.`);
15791
- }
15792
- const [updated] = await tx`
15793
- UPDATE action_runs
15794
- SET
15795
- status = ${"queued"},
15796
- phase = ${"request"},
15797
- queued_at = ${input.queuedAt ?? new Date},
15798
- started_at = ${null},
15799
- finished_at = ${null},
15800
- writeback_status = ${null},
15801
- writeback_completed_at = ${null},
15802
- writeback_result = ${null},
15803
- writeback_error = ${null},
15804
- effects_status = ${null},
15805
- effects_completed_at = ${null},
15806
- effects_error = ${null},
15807
- error = ${null}
15808
- WHERE project_id = ${input.projectId} AND id = ${input.id}
15809
- RETURNING *
15810
- `;
15811
- return rowToActionRunRecord(updated);
15803
+ projectId: record.projectId,
15804
+ executionId: record.executionId,
15805
+ runId: record.id,
15806
+ actionId: record.actionId
15812
15807
  });
15813
- }
15814
- async start(input) {
15815
- const [updated] = await this.sql`
15816
- UPDATE action_runs
15817
- SET
15818
- status = ${"running"},
15819
- phase = ${input.phase ?? "validation"},
15820
- started_at = ${input.startedAt ?? new Date},
15821
- error = ${null}
15822
- WHERE project_id = ${input.projectId}
15823
- AND id = ${input.id}
15824
- AND status = ${"queued"}
15808
+ const [row] = await this.sql`
15809
+ INSERT INTO action_runs (
15810
+ project_id,
15811
+ id,
15812
+ execution_id,
15813
+ action_id,
15814
+ subject_kind,
15815
+ object_type_id,
15816
+ primary_id,
15817
+ status,
15818
+ phase,
15819
+ started_at,
15820
+ finished_at,
15821
+ params,
15822
+ idempotency_key,
15823
+ writeback_status,
15824
+ writeback_completed_at,
15825
+ writeback_result,
15826
+ writeback_error,
15827
+ error
15828
+ ) VALUES (
15829
+ ${record.projectId},
15830
+ ${record.id},
15831
+ ${record.executionId},
15832
+ ${record.actionId},
15833
+ ${record.subject.kind},
15834
+ ${record.subject.kind === "object" ? record.subject.objectTypeId : null},
15835
+ ${record.subject.kind === "object" ? record.subject.primaryId : null},
15836
+ ${record.status},
15837
+ ${record.phase},
15838
+ ${record.startedAt},
15839
+ ${record.finishedAt},
15840
+ ${JSON.stringify(record.params)}::text::jsonb,
15841
+ ${record.idempotencyKey},
15842
+ ${record.writeback?.status ?? null},
15843
+ ${record.writeback?.completedAt ?? null},
15844
+ ${record.writeback?.status === "succeeded" ? JSON.stringify(record.writeback.result) : null}::text::jsonb,
15845
+ ${record.writeback?.status === "failed" ? serializeActionRunFailure(record.writeback.error, "writeback") : null}::text::jsonb,
15846
+ ${record.status === "failed" ? serializeActionRunFailure(record.error) : null}::text::jsonb
15847
+ )
15848
+ ON CONFLICT DO NOTHING
15825
15849
  RETURNING *
15826
15850
  `;
15827
- if (updated) {
15828
- return rowToActionRunRecord(updated);
15829
- }
15830
- const existing = await this.getById({ projectId: input.projectId, id: input.id });
15831
- if (!existing) {
15832
- throw new ActionRunError(`[SixbPg] Action run '${input.id}' not found for project '${input.projectId}'.`);
15833
- }
15834
- throw new ActionRunError(`[SixbPg] Action run '${input.id}' cannot start from status '${existing.status}'.`);
15835
- }
15836
- async enterPhase(input) {
15837
- return runPgTransaction(this.sql, async (tx) => {
15838
- await this.requireRunningRun(tx, input.projectId, input.id, "transition phase");
15839
- const [updated] = await tx`
15840
- UPDATE action_runs
15841
- SET phase = ${input.phase}
15842
- WHERE project_id = ${input.projectId} AND id = ${input.id}
15843
- RETURNING *
15844
- `;
15845
- return rowToActionRunRecord(updated);
15846
- });
15847
- }
15848
- async recordWriteback(input) {
15849
- return runPgTransaction(this.sql, async (tx) => {
15850
- const existing = await this.requireRunningRun(tx, input.projectId, input.id, "record writeback");
15851
- const nextWriteback = toWritebackRecord(input, new Date(input.completedAt ?? new Date));
15852
- const currentWriteback = toActionRunWritebackRecord(existing);
15853
- if (currentWriteback) {
15854
- if (actionRunPhaseRecordsEqual(currentWriteback, nextWriteback)) {
15855
- return rowToActionRunRecord(existing);
15856
- }
15857
- throw new ActionRunError(`[SixbPg] Action run '${input.id}' already has a different writeback record.`);
15858
- }
15859
- const [updated] = await tx`
15860
- UPDATE action_runs
15861
- SET
15862
- phase = ${"writeback"},
15863
- writeback_status = ${input.status},
15864
- writeback_completed_at = ${nextWriteback.completedAt},
15865
- writeback_result = ${input.status === "succeeded" ? JSON.stringify(input.result) : null}::text::jsonb,
15866
- writeback_error = ${input.status === "failed" ? serializeActionRunFailure(input.error, "writeback") : null}::text::jsonb
15867
- WHERE project_id = ${input.projectId} AND id = ${input.id}
15868
- RETURNING *
15869
- `;
15870
- return rowToActionRunRecord(updated);
15871
- });
15851
+ if (row)
15852
+ return rowToActionRunRecord(row);
15853
+ const [conflict] = await this.sql`
15854
+ SELECT id FROM action_runs
15855
+ WHERE project_id = ${record.projectId} AND id = ${record.id}
15856
+ `;
15857
+ throw new ActionRunError(conflict ? `[SixbPg] Action run '${record.id}' is already recorded for project '${record.projectId}'.` : `[SixbPg] Execution '${record.executionId}' already belongs to another Action run.`);
15872
15858
  }
15873
15859
  async recordEffects(input) {
15874
- return runPgTransaction(this.sql, async (tx) => {
15875
- const existing = await this.requireRunningRun(tx, input.projectId, input.id, "record effects");
15876
- const nextEffects = toEffectsRecord(input, new Date(input.completedAt ?? new Date));
15877
- const currentEffects = toActionRunEffectsRecord(existing);
15878
- if (currentEffects) {
15879
- if (actionRunPhaseRecordsEqual(currentEffects, nextEffects)) {
15880
- return rowToActionRunRecord(existing);
15881
- }
15882
- throw new ActionRunError(`[SixbPg] Action run '${input.id}' already has a different effects record.`);
15883
- }
15884
- const [updated] = await tx`
15885
- UPDATE action_runs
15886
- SET
15887
- phase = ${"effects"},
15888
- effects_status = ${input.status},
15889
- effects_completed_at = ${nextEffects.completedAt},
15890
- effects_error = ${input.status === "failed" ? serializeActionRunFailure(input.error, "effects") : null}::text::jsonb
15891
- WHERE project_id = ${input.projectId} AND id = ${input.id}
15892
- RETURNING *
15893
- `;
15894
- return rowToActionRunRecord(updated);
15895
- });
15896
- }
15897
- async finish(input) {
15898
15860
  return runPgTransaction(this.sql, async (tx) => {
15899
15861
  const [existing] = await tx`
15900
15862
  SELECT * FROM action_runs
15901
15863
  WHERE project_id = ${input.projectId} AND id = ${input.id}
15902
15864
  FOR UPDATE
15903
15865
  `;
15904
- if (!existing) {
15905
- throw new ActionRunError(`[SixbPg] Action run '${input.id}' not found for project '${input.projectId}'.`);
15906
- }
15907
- if (isTerminalActionRun({ status: existing.status })) {
15908
- throw new ActionRunError(`[SixbPg] Action run '${input.id}' cannot finish from terminal status '${existing.status}'.`);
15909
- }
15910
- const phase = finishActionRunPhase(input, existing.phase);
15866
+ const { run, effects } = resolveActionRunEffects(existing ? rowToActionRunRecord(existing) : null, input);
15867
+ if (!effects)
15868
+ return run;
15911
15869
  const [updated] = await tx`
15912
15870
  UPDATE action_runs
15913
15871
  SET
15914
- status = ${input.status},
15915
- phase = ${phase},
15916
- finished_at = ${input.finishedAt ?? new Date},
15917
- error = ${input.status === "succeeded" ? null : serializeActionRunFailure(input.error)}::text::jsonb
15872
+ phase = ${"effects"},
15873
+ effects_status = ${effects.status},
15874
+ effects_completed_at = ${effects.completedAt},
15875
+ effects_error = ${effects.status === "failed" ? serializeActionRunFailure(effects.error, "effects") : null}::text::jsonb
15918
15876
  WHERE project_id = ${input.projectId} AND id = ${input.id}
15919
15877
  RETURNING *
15920
15878
  `;
@@ -15987,11 +15945,11 @@ class PgActionRunStorage {
15987
15945
  params.push(...input.statuses);
15988
15946
  }
15989
15947
  if (input.startedAfter) {
15990
- whereClauses.push(`COALESCE(started_at, queued_at) >= $${index++}`);
15948
+ whereClauses.push(`started_at >= $${index++}`);
15991
15949
  params.push(input.startedAfter);
15992
15950
  }
15993
15951
  if (input.startedBefore) {
15994
- whereClauses.push(`COALESCE(started_at, queued_at) <= $${index++}`);
15952
+ whereClauses.push(`started_at <= $${index++}`);
15995
15953
  params.push(input.startedBefore);
15996
15954
  }
15997
15955
  const where = `WHERE ${whereClauses.join(" AND ")}`;
@@ -16002,7 +15960,7 @@ class PgActionRunStorage {
16002
15960
  let query = `
16003
15961
  SELECT * FROM action_runs
16004
15962
  ${where}
16005
- ORDER BY COALESCE(started_at, queued_at) ${order}, id ${order}
15963
+ ORDER BY started_at ${order}, id ${order}
16006
15964
  `;
16007
15965
  if (input.limit !== undefined) {
16008
15966
  query += ` LIMIT $${index++} OFFSET $${index++}`;
@@ -16020,32 +15978,12 @@ class PgActionRunStorage {
16020
15978
  total
16021
15979
  };
16022
15980
  }
16023
- async requireRunningRun(runner, projectId, id, operation) {
16024
- const [existing] = await runner`
16025
- SELECT * FROM action_runs
16026
- WHERE project_id = ${projectId} AND id = ${id}
16027
- FOR UPDATE
16028
- `;
16029
- if (!existing) {
16030
- throw new ActionRunError(`[SixbPg] Action run '${id}' not found for project '${projectId}'.`);
16031
- }
16032
- if (existing.status !== "running") {
16033
- throw new ActionRunError(`[SixbPg] Action run '${id}' cannot ${operation} from status '${existing.status}'.`);
16034
- }
16035
- return existing;
16036
- }
16037
- }
16038
- function toActionRunFailure(row) {
16039
- return row.error === null ? undefined : parseActionRunFailure(row.error);
16040
- }
16041
- function parsePhaseFailure(value, expectedPhase) {
16042
- return parseActionRunFailure(value, expectedPhase);
16043
15981
  }
16044
15982
  function toActionRunWritebackRecord(row) {
16045
15983
  if (!row.writeback_status) {
16046
15984
  return;
16047
15985
  }
16048
- const completedAt = row.writeback_completed_at ? new Date(row.writeback_completed_at) : new Date(row.queued_at);
15986
+ const completedAt = new Date(row.writeback_completed_at ?? row.finished_at);
16049
15987
  if (row.writeback_status === "succeeded") {
16050
15988
  return {
16051
15989
  status: "succeeded",
@@ -16056,14 +15994,14 @@ function toActionRunWritebackRecord(row) {
16056
15994
  return {
16057
15995
  status: "failed",
16058
15996
  completedAt,
16059
- error: parsePhaseFailure(row.writeback_error, "writeback")
15997
+ error: parseActionRunFailure(row.writeback_error, "writeback")
16060
15998
  };
16061
15999
  }
16062
16000
  function toActionRunEffectsRecord(row) {
16063
16001
  if (!row.effects_status) {
16064
16002
  return;
16065
16003
  }
16066
- const completedAt = row.effects_completed_at ? new Date(row.effects_completed_at) : new Date(row.queued_at);
16004
+ const completedAt = new Date(row.effects_completed_at ?? row.finished_at);
16067
16005
  if (row.effects_status === "succeeded") {
16068
16006
  return {
16069
16007
  status: "succeeded",
@@ -16073,54 +16011,29 @@ function toActionRunEffectsRecord(row) {
16073
16011
  return {
16074
16012
  status: "failed",
16075
16013
  completedAt,
16076
- error: parsePhaseFailure(row.effects_error, "effects")
16077
- };
16078
- }
16079
- function toWritebackRecord(input, completedAt) {
16080
- if (input.status === "succeeded") {
16081
- return {
16082
- status: "succeeded",
16083
- completedAt,
16084
- result: input.result
16085
- };
16086
- }
16087
- return {
16088
- status: "failed",
16089
- completedAt,
16090
- error: input.error
16091
- };
16092
- }
16093
- function toEffectsRecord(input, completedAt) {
16094
- if (input.status === "succeeded") {
16095
- return {
16096
- status: "succeeded",
16097
- completedAt
16098
- };
16099
- }
16100
- return {
16101
- status: "failed",
16102
- completedAt,
16103
- error: input.error
16014
+ error: parseActionRunFailure(row.effects_error, "effects")
16104
16015
  };
16105
16016
  }
16106
16017
  function rowToActionRunRecord(row) {
16107
- return {
16018
+ const writeback = toActionRunWritebackRecord(row);
16019
+ const effects = toActionRunEffectsRecord(row);
16020
+ const fields = {
16108
16021
  id: row.id,
16109
16022
  projectId: row.project_id,
16110
16023
  executionId: row.execution_id,
16111
16024
  actionId: row.action_id,
16112
16025
  subject: rowToActionSubject(row),
16113
- status: row.status,
16114
- phase: row.phase ?? undefined,
16115
- queuedAt: new Date(row.queued_at),
16116
- startedAt: row.started_at ? new Date(row.started_at) : undefined,
16117
- finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
16026
+ phase: row.phase,
16027
+ startedAt: new Date(row.started_at),
16028
+ finishedAt: new Date(row.finished_at),
16118
16029
  params: row.params,
16119
16030
  idempotencyKey: row.idempotency_key,
16120
- writeback: toActionRunWritebackRecord(row),
16121
- effects: toActionRunEffectsRecord(row),
16122
- error: toActionRunFailure(row)
16031
+ ...writeback === undefined ? {} : { writeback },
16032
+ ...effects === undefined ? {} : { effects }
16123
16033
  };
16034
+ if (row.status === "succeeded")
16035
+ return { ...fields, status: "succeeded" };
16036
+ return { ...fields, status: "failed", error: parseActionRunFailure(row.error) };
16124
16037
  }
16125
16038
  function rowToActionSubject(row) {
16126
16039
  if (row.subject_kind === "none") {
@@ -21065,4 +20978,4 @@ export {
21065
20978
  migratePostgresStorage
21066
20979
  };
21067
20980
 
21068
- //# debugId=51821498ED16AC6B64756E2164756E21
20981
+ //# debugId=971E14D4D5198C8B64756E2164756E21