@sixb/pg 0.1.12 → 0.1.13

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 (38) hide show
  1. package/dist/auth-storage/access-tokens.d.ts.map +1 -1
  2. package/dist/auth-storage/device-authorizations.d.ts.map +1 -1
  3. package/dist/auth-storage/rows.d.ts +6 -3
  4. package/dist/auth-storage/rows.d.ts.map +1 -1
  5. package/dist/auth-storage/sessions.d.ts +8 -1
  6. package/dist/auth-storage/sessions.d.ts.map +1 -1
  7. package/dist/auth-storage/shared.d.ts.map +1 -1
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +366 -70
  11. package/dist/index.js.map +16 -15
  12. package/dist/migrations.d.ts.map +1 -1
  13. package/dist/objects/storage.d.ts.map +1 -1
  14. package/dist/ontology-storage/materializations.d.ts +0 -2
  15. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  16. package/dist/ontology-storage/shared.d.ts +2 -1
  17. package/dist/ontology-storage/shared.d.ts.map +1 -1
  18. package/dist/pg-file-upload-session-storage.d.ts +6 -0
  19. package/dist/pg-file-upload-session-storage.d.ts.map +1 -0
  20. package/dist/pg-workflow-intervention-storage.d.ts.map +1 -1
  21. package/package.json +3 -3
  22. package/src/auth-storage/access-tokens.ts +10 -14
  23. package/src/auth-storage/auth-storage.ts +4 -6
  24. package/src/auth-storage/device-authorizations.ts +3 -4
  25. package/src/auth-storage/rows.ts +17 -7
  26. package/src/auth-storage/sessions.ts +82 -4
  27. package/src/auth-storage/shared.ts +8 -2
  28. package/src/index.ts +5 -0
  29. package/src/migrations/046-workflow-intervention-principals.sql +15 -0
  30. package/src/migrations/047-ontology-commit-attribution.sql +50 -0
  31. package/src/migrations/048-file-upload-sessions.sql +34 -0
  32. package/src/migrations/049-native-sessions.sql +19 -0
  33. package/src/migrations.ts +12 -0
  34. package/src/objects/storage.ts +2 -0
  35. package/src/ontology-storage/materializations.ts +15 -58
  36. package/src/ontology-storage/shared.ts +7 -3
  37. package/src/pg-file-upload-session-storage.ts +143 -0
  38. package/src/pg-workflow-intervention-storage.ts +11 -15
package/dist/index.js CHANGED
@@ -1162,7 +1162,14 @@ function rowToSessionRecord(row) {
1162
1162
  revokedAt: row.revoked_at ? toDate(row.revoked_at) : undefined,
1163
1163
  lastSeenAt: row.last_seen_at ? toDate(row.last_seen_at) : undefined,
1164
1164
  userAgent: row.user_agent ?? undefined,
1165
- ipAddress: row.ip_address ?? undefined
1165
+ ipAddress: row.ip_address ?? undefined,
1166
+ bearer: row.refresh_token_hash !== null && row.client_name !== null && row.access_expires_at !== null ? {
1167
+ clientName: row.client_name,
1168
+ accessExpiresAt: toDate(row.access_expires_at),
1169
+ refreshTokenHash: row.refresh_token_hash,
1170
+ previousRefreshTokenHash: row.previous_refresh_token_hash ?? undefined,
1171
+ refreshedAt: row.refreshed_at ? toDate(row.refreshed_at) : undefined
1172
+ } : undefined
1166
1173
  };
1167
1174
  }
1168
1175
  function rowToAccessTokenRecord(row) {
@@ -1171,8 +1178,7 @@ function rowToAccessTokenRecord(row) {
1171
1178
  projectId: row.project_id,
1172
1179
  name: row.name,
1173
1180
  kind: row.kind,
1174
- subjectType: row.subject_type,
1175
- subjectId: row.subject_id,
1181
+ subject: { type: row.subject_type, id: row.subject_id },
1176
1182
  tokenHash: row.token_hash,
1177
1183
  groupIds: parseOptionalStringArray(row.group_ids),
1178
1184
  createdByPrincipal: principalFromColumns2(row),
@@ -1247,8 +1253,6 @@ function rowToDeviceAuthorizationRecord(row) {
1247
1253
  deviceCodeHash: row.device_code_hash,
1248
1254
  userCode: row.user_code,
1249
1255
  clientName: row.client_name,
1250
- tokenName: row.token_name,
1251
- tokenExpiresAt: toDate(row.token_expires_at),
1252
1256
  status: row.status,
1253
1257
  approvedUserId: row.approved_user_id ?? undefined,
1254
1258
  approvedSessionId: row.approved_session_id ?? undefined,
@@ -1559,7 +1563,10 @@ async function createSession(sql, input) {
1559
1563
  expires_at,
1560
1564
  absolute_expires_at,
1561
1565
  user_agent,
1562
- ip_address
1566
+ ip_address,
1567
+ client_name,
1568
+ access_expires_at,
1569
+ refresh_token_hash
1563
1570
  ) VALUES (
1564
1571
  ${projectId},
1565
1572
  ${id},
@@ -1571,7 +1578,10 @@ async function createSession(sql, input) {
1571
1578
  ${input.expiresAt},
1572
1579
  ${input.absoluteExpiresAt ?? null},
1573
1580
  ${input.userAgent ?? null},
1574
- ${input.ipAddress ?? null}
1581
+ ${input.ipAddress ?? null},
1582
+ ${input.bearer ? assertNonEmpty(input.bearer.clientName, "Session client name") : null},
1583
+ ${input.bearer?.accessExpiresAt ?? null},
1584
+ ${input.bearer ? assertNonEmpty(input.bearer.refreshTokenHash, "Refresh token hash") : null}
1575
1585
  )
1576
1586
  RETURNING *
1577
1587
  `;
@@ -1763,10 +1773,10 @@ class PgAuthAccessTokenStore {
1763
1773
  const id = assertNonEmpty(input.id, "Access token id");
1764
1774
  const projectId = assertNonEmpty(input.projectId, "Project id");
1765
1775
  const name = assertNonEmpty(input.name, "Access token name");
1766
- const subjectId = assertNonEmpty(input.subjectId, "Access token subject id");
1776
+ const subjectId = assertNonEmpty(input.subject.id, "Access token subject id");
1767
1777
  const tokenHash = assertNonEmpty(input.tokenHash, "Access token hash");
1768
1778
  assertKindMatchesSubject(input);
1769
- await this.assertSubjectExists(projectId, input.subjectType, subjectId);
1779
+ await this.assertSubjectExists(projectId, input.subject.type, subjectId);
1770
1780
  const groupIds = input.groupIds === undefined ? undefined : normalizeGroupIds(input.groupIds);
1771
1781
  try {
1772
1782
  const [row] = await this.sql`
@@ -1790,7 +1800,7 @@ class PgAuthAccessTokenStore {
1790
1800
  ${id},
1791
1801
  ${name},
1792
1802
  ${input.kind},
1793
- ${input.subjectType},
1803
+ ${input.subject.type},
1794
1804
  ${subjectId},
1795
1805
  ${tokenHash},
1796
1806
  ${serializeOptionalStringArray(groupIds)},
@@ -1825,13 +1835,9 @@ class PgAuthAccessTokenStore {
1825
1835
  whereClauses.push(`kind = $${nextIndex++}`);
1826
1836
  params.push(input.kind);
1827
1837
  }
1828
- if (input.subjectType) {
1829
- whereClauses.push(`subject_type = $${nextIndex++}`);
1830
- params.push(input.subjectType);
1831
- }
1832
- if (input.subjectId) {
1833
- whereClauses.push(`subject_id = $${nextIndex++}`);
1834
- params.push(input.subjectId);
1838
+ if (input.subject) {
1839
+ whereClauses.push(`subject_type = $${nextIndex++}`, `subject_id = $${nextIndex++}`);
1840
+ params.push(input.subject.type, input.subject.id);
1835
1841
  }
1836
1842
  if (!input.includeRevoked) {
1837
1843
  whereClauses.push("revoked_at IS NULL");
@@ -1919,10 +1925,10 @@ class PgAuthAccessTokenStore {
1919
1925
  }
1920
1926
  }
1921
1927
  function assertKindMatchesSubject(input) {
1922
- if (input.kind === "personal" && input.subjectType === "user" || input.kind === "serviceAccount" && input.subjectType === "serviceAccount") {
1928
+ if (input.kind === "personal" && input.subject.type === "user" || input.kind === "serviceAccount" && input.subject.type === "serviceAccount") {
1923
1929
  return;
1924
1930
  }
1925
- throw new AuthStorageError2("invalid_input", `[Sixb] Access token kind '${input.kind}' cannot target subject type '${input.subjectType}'.`);
1931
+ throw new AuthStorageError2("invalid_input", `[Sixb] Access token kind '${input.kind}' cannot target subject type '${input.subject.type}'.`);
1926
1932
  }
1927
1933
 
1928
1934
  // src/auth-storage/device-authorizations.ts
@@ -1949,16 +1955,15 @@ class PgAuthDeviceAuthorizationStore {
1949
1955
  try {
1950
1956
  const [row] = await tx`
1951
1957
  INSERT INTO auth_device_authorizations (
1952
- project_id, id, device_code_hash, user_code, client_name, token_name,
1953
- token_expires_at, status, created_at, expires_at
1958
+ project_id, id, device_code_hash, user_code, client_name, status, created_at,
1959
+ expires_at
1954
1960
  ) VALUES (
1955
1961
  ${assertNonEmpty(input.projectId, "Project id")},
1956
1962
  ${assertNonEmpty(input.id, "Device authorization id")},
1957
1963
  ${assertNonEmpty(input.deviceCodeHash, "Device code hash")},
1958
1964
  ${assertNonEmpty(input.userCode, "User code")},
1959
1965
  ${assertNonEmpty(input.clientName, "Client name")},
1960
- ${assertNonEmpty(input.tokenName, "Token name")},
1961
- ${input.tokenExpiresAt}, 'pending', ${input.createdAt}, ${input.expiresAt}
1966
+ 'pending', ${input.createdAt}, ${input.expiresAt}
1962
1967
  ) RETURNING *
1963
1968
  `;
1964
1969
  return rowToDeviceAuthorizationRecord(row);
@@ -2700,11 +2705,53 @@ class PgAuthSessionStore {
2700
2705
  }
2701
2706
  async findValidByTokenHash(params) {
2702
2707
  const row = await getSessionRowById(this.sql, params);
2703
- if (!row || row.audience !== params.audience || row.token_hash !== params.tokenHash || row.revoked_at || new Date(row.expires_at) <= params.now || row.absolute_expires_at !== null && new Date(row.absolute_expires_at) <= params.now) {
2708
+ if (!row || row.refresh_token_hash !== null || row.audience !== params.audience || row.token_hash !== params.tokenHash || !isLiveSessionRow(row, params.now)) {
2709
+ return null;
2710
+ }
2711
+ return rowToSessionRecord(row);
2712
+ }
2713
+ async findValidByAccessTokenHash(params) {
2714
+ const row = await getSessionRowById(this.sql, params);
2715
+ if (!row || row.access_expires_at === null || row.token_hash !== params.tokenHash || new Date(row.access_expires_at) <= params.now || !isLiveSessionRow(row, params.now)) {
2704
2716
  return null;
2705
2717
  }
2706
2718
  return rowToSessionRecord(row);
2707
2719
  }
2720
+ async rotateRefreshToken(params) {
2721
+ return runPgTransaction(this.sql, async (tx) => {
2722
+ const row = await getSessionRowById(tx, params, { forUpdate: true });
2723
+ if (!row?.refresh_token_hash || !isLiveSessionRow(row, params.now)) {
2724
+ return { status: "invalid" };
2725
+ }
2726
+ const replacedRecently = row.previous_refresh_token_hash === params.refreshTokenHash && row.refreshed_at !== null && params.now.getTime() - new Date(row.refreshed_at).getTime() <= params.reuseGraceMs;
2727
+ if (row.refresh_token_hash !== params.refreshTokenHash && !replacedRecently) {
2728
+ if (row.previous_refresh_token_hash !== params.refreshTokenHash) {
2729
+ return { status: "invalid" };
2730
+ }
2731
+ await tx`
2732
+ UPDATE auth_sessions SET revoked_at = ${params.now}
2733
+ WHERE project_id = ${params.projectId} AND id = ${params.id}
2734
+ `;
2735
+ return { status: "reused" };
2736
+ }
2737
+ const [rotated] = await tx`
2738
+ UPDATE auth_sessions
2739
+ SET token_hash = ${params.next.tokenHash},
2740
+ access_expires_at = ${params.next.accessExpiresAt},
2741
+ previous_refresh_token_hash = refresh_token_hash,
2742
+ refresh_token_hash = ${params.next.refreshTokenHash},
2743
+ refreshed_at = ${params.now},
2744
+ last_seen_at = ${params.now},
2745
+ expires_at = GREATEST(
2746
+ expires_at,
2747
+ LEAST(${params.next.expiresAt}, COALESCE(absolute_expires_at, ${params.next.expiresAt}))
2748
+ )
2749
+ WHERE project_id = ${params.projectId} AND id = ${params.id}
2750
+ RETURNING *
2751
+ `;
2752
+ return rotated ? { status: "rotated", session: rowToSessionRecord(rotated) } : { status: "invalid" };
2753
+ });
2754
+ }
2708
2755
  async renewIfValid(params) {
2709
2756
  const [row] = await this.sql`
2710
2757
  UPDATE auth_sessions
@@ -2720,6 +2767,7 @@ class PgAuthSessionStore {
2720
2767
  AND id = ${params.id}
2721
2768
  AND audience = ${params.audience}
2722
2769
  AND token_hash = ${params.tokenHash}
2770
+ AND refresh_token_hash IS NULL
2723
2771
  AND revoked_at IS NULL
2724
2772
  AND expires_at > ${params.now}
2725
2773
  AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.now})
@@ -2767,6 +2815,9 @@ class PgAuthSessionStore {
2767
2815
  });
2768
2816
  }
2769
2817
  }
2818
+ function isLiveSessionRow(row, now) {
2819
+ return row.revoked_at === null && new Date(row.expires_at) > now && (row.absolute_expires_at === null || new Date(row.absolute_expires_at) > now);
2820
+ }
2770
2821
 
2771
2822
  // src/auth-storage/users.ts
2772
2823
  import { AuthStorageError as AuthStorageError10 } from "@sixb/core/storage";
@@ -2928,7 +2979,7 @@ class PgAuthStorage {
2928
2979
  throw new AuthStorageError11("missing_device_authorization", `[Sixb] Device authorization '${input.id}' not found for project '${input.projectId}'.`);
2929
2980
  }
2930
2981
  assertCompletableDeviceAuthorization(authorization, input);
2931
- const accessToken = await new PgAuthAccessTokenStore(tx).create(input.accessToken);
2982
+ const session = await createSession(tx, input.session);
2932
2983
  const [consumed] = await tx`
2933
2984
  UPDATE auth_device_authorizations
2934
2985
  SET status = 'consumed', consumed_at = ${input.completedAt}
@@ -2944,7 +2995,7 @@ class PgAuthStorage {
2944
2995
  status: "consumed",
2945
2996
  consumedAt: new Date(input.completedAt)
2946
2997
  },
2947
- accessToken
2998
+ session
2948
2999
  };
2949
3000
  });
2950
3001
  }
@@ -3406,7 +3457,7 @@ function assertCompletableDeviceAuthorization(authorization, input) {
3406
3457
  if (authorization.status === "denied") {
3407
3458
  throw new AuthStorageError11("device_authorization_denied", "[Sixb] Device authorization was denied.");
3408
3459
  }
3409
- if (authorization.status !== "approved" || authorization.expiresAt <= input.completedAt || !authorization.approvedUserId || !authorization.approvedSessionId || input.accessToken.projectId !== input.projectId || input.accessToken.subjectType !== "user" || input.accessToken.subjectId !== authorization.approvedUserId || input.accessToken.createdBySessionId !== authorization.approvedSessionId) {
3460
+ if (authorization.status !== "approved" || authorization.expiresAt <= input.completedAt || !authorization.approvedUserId || !authorization.approvedSessionId || input.session.projectId !== input.projectId || input.session.userId !== authorization.approvedUserId) {
3410
3461
  throw new AuthStorageError11("invalid_device_authorization", "[Sixb] Device authorization is invalid.");
3411
3462
  }
3412
3463
  }
@@ -7244,6 +7295,99 @@ var _045_agent_thread_sandbox_state_default = `ALTER TABLE agent_threads ADD COL
7244
7295
  CHECK (sandbox_state IS NULL OR jsonb_typeof(sandbox_state) = 'object');
7245
7296
  `;
7246
7297
 
7298
+ // src/migrations/046-workflow-intervention-principals.sql
7299
+ var _046_workflow_intervention_principals_default = `-- Intervention actors were stored as { principalType, principalId }. Rewrite them to the
7300
+ -- canonical Principal shape { type, id } used by every other stored principal.
7301
+ UPDATE workflow_interventions
7302
+ SET submitted_by = jsonb_build_object(
7303
+ 'type', submitted_by->>'principalType',
7304
+ 'id', submitted_by->>'principalId'
7305
+ )
7306
+ WHERE submitted_by->>'principalType' IS NOT NULL;
7307
+
7308
+ UPDATE workflow_interventions
7309
+ SET cancelled_by = jsonb_build_object(
7310
+ 'type', cancelled_by->>'principalType',
7311
+ 'id', cancelled_by->>'principalId'
7312
+ )
7313
+ WHERE cancelled_by->>'principalType' IS NOT NULL;
7314
+ `;
7315
+
7316
+ // src/migrations/047-ontology-commit-attribution.sql
7317
+ var _047_ontology_commit_attribution_default = `-- Commits and their events name who requested the write and what ran it, replacing the
7318
+ -- authority-only \`actor\`. Both are copied from the commit's execution, which stays canonical and
7319
+ -- is immutable, so the backfill is exact.
7320
+ ALTER TABLE ontology_commits
7321
+ ADD COLUMN requested_by JSONB,
7322
+ ADD COLUMN executor JSONB;
7323
+
7324
+ UPDATE ontology_commits c SET
7325
+ requested_by = CASE
7326
+ WHEN e.requested_by_user_id IS NOT NULL
7327
+ THEN jsonb_build_object('type', 'user', 'id', e.requested_by_user_id)
7328
+ WHEN e.requested_by_service_account_id IS NOT NULL
7329
+ THEN jsonb_build_object('type', 'serviceAccount', 'id', e.requested_by_service_account_id)
7330
+ END,
7331
+ executor = CASE e.executor_kind
7332
+ WHEN 'request' THEN jsonb_build_object('type', 'request', 'requestId', e.executor_id)
7333
+ WHEN 'agent' THEN jsonb_build_object('type', 'agent', 'runId', e.executor_id)
7334
+ WHEN 'kernel' THEN jsonb_build_object(
7335
+ 'type', 'kernel',
7336
+ 'operation',
7337
+ CASE e.authority_kernel_operation
7338
+ WHEN 'ontology.recover'
7339
+ THEN jsonb_build_object('type', 'ontology.recover', 'recoveryId', e.executor_id)
7340
+ ELSE jsonb_build_object('type', 'ontology.indexVectors', 'indexingId', e.executor_id)
7341
+ END
7342
+ )
7343
+ ELSE jsonb_build_object(
7344
+ 'type', 'primitive',
7345
+ 'kind', e.executor_kind,
7346
+ 'id', e.authority_primitive_id,
7347
+ 'runId', e.executor_id
7348
+ )
7349
+ END
7350
+ FROM executions e
7351
+ WHERE e.project_id = c.project_id AND e.id = c.execution_id;
7352
+
7353
+ ALTER TABLE ontology_commits ALTER COLUMN executor SET NOT NULL;
7354
+
7355
+ -- Events not yet published would otherwise reach consumers in the retired shape.
7356
+ UPDATE ontology_outbox o SET envelope =
7357
+ (o.envelope - 'actor')
7358
+ || jsonb_build_object('executor', c.executor)
7359
+ || CASE
7360
+ WHEN c.requested_by IS NULL THEN '{}'::jsonb
7361
+ ELSE jsonb_build_object('requestedBy', c.requested_by)
7362
+ END
7363
+ FROM ontology_commits c
7364
+ WHERE c.project_id = o.project_id AND c.id = o.commit_id;
7365
+
7366
+ ALTER TABLE ontology_commits DROP COLUMN actor;
7367
+ `;
7368
+
7369
+ // src/migrations/049-native-sessions.sql
7370
+ var _049_native_sessions_default = `-- Native clients, such as the CLI, hold their session as bearer tokens: a
7371
+ -- short-lived access token, whose hash is \`token_hash\`, and a rotating refresh token. Browser
7372
+ -- sessions leave these columns NULL; \`refresh_token_hash IS NULL\` is what marks a cookie session.
7373
+ ALTER TABLE auth_sessions
7374
+ ADD COLUMN client_name TEXT,
7375
+ ADD COLUMN access_expires_at TIMESTAMPTZ,
7376
+ ADD COLUMN refresh_token_hash TEXT,
7377
+ ADD COLUMN previous_refresh_token_hash TEXT,
7378
+ ADD COLUMN refreshed_at TIMESTAMPTZ,
7379
+ ADD CONSTRAINT auth_sessions_bearer_state CHECK (
7380
+ (refresh_token_hash IS NULL) = (client_name IS NULL)
7381
+ AND (refresh_token_hash IS NULL) = (access_expires_at IS NULL)
7382
+ AND (refresh_token_hash IS NOT NULL OR previous_refresh_token_hash IS NULL)
7383
+ );
7384
+
7385
+ -- The device flow now starts a session instead of issuing a personal access token.
7386
+ ALTER TABLE auth_device_authorizations
7387
+ DROP COLUMN token_name,
7388
+ DROP COLUMN token_expires_at;
7389
+ `;
7390
+
7247
7391
  // src/migrations/037-execution-requester-groups.sql
7248
7392
  var _037_execution_requester_groups_default = `ALTER TABLE executions ADD COLUMN requester_group_ids JSONB NOT NULL DEFAULT '[]'::jsonb;
7249
7393
 
@@ -7282,6 +7426,43 @@ ALTER TABLE connector_authorization_attempts
7282
7426
  ALTER COLUMN code_verifier DROP NOT NULL;
7283
7427
  `;
7284
7428
 
7429
+ // src/migrations/048-file-upload-sessions.sql
7430
+ var _048_file_upload_sessions_default = `-- Durable staged-upload sessions, so an upload survives restarts and spans API replicas.
7431
+ CREATE TABLE file_upload_sessions (
7432
+ id TEXT PRIMARY KEY CHECK (length(trim(id)) > 0),
7433
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
7434
+ owner_principal_type TEXT NOT NULL
7435
+ CHECK (owner_principal_type IN ('user', 'serviceAccount', 'system')),
7436
+ owner_principal_id TEXT NOT NULL CHECK (length(owner_principal_id) > 0),
7437
+ strategy TEXT NOT NULL CHECK (strategy IN ('server', 'direct-put', 'multipart')),
7438
+ status TEXT NOT NULL CHECK (status IN ('pending', 'completed', 'aborted')),
7439
+ file_name TEXT,
7440
+ media_type TEXT,
7441
+ logical_path TEXT,
7442
+ expected_size_bytes BIGINT CHECK (expected_size_bytes IS NULL OR expected_size_bytes >= 0),
7443
+ expected_digest TEXT,
7444
+ provider_upload JSONB CHECK (provider_upload IS NULL OR jsonb_typeof(provider_upload) = 'object'),
7445
+ signed_parts JSONB NOT NULL CHECK (jsonb_typeof(signed_parts) = 'array'),
7446
+ file_ref JSONB CHECK (file_ref IS NULL OR jsonb_typeof(file_ref) = 'object'),
7447
+ created_at TIMESTAMPTZ NOT NULL,
7448
+ expires_at TIMESTAMPTZ NOT NULL,
7449
+ completed_at TIMESTAMPTZ,
7450
+ aborted_at TIMESTAMPTZ,
7451
+ -- Derived by core (fileUploadSessionReapAt); NULL while an abandoned upload awaits its abort.
7452
+ reap_at TIMESTAMPTZ,
7453
+ CHECK ((status = 'completed') = (completed_at IS NOT NULL)),
7454
+ CHECK ((status = 'aborted') = (aborted_at IS NOT NULL))
7455
+ );
7456
+
7457
+ CREATE INDEX idx_file_upload_sessions_reap
7458
+ ON file_upload_sessions (reap_at)
7459
+ WHERE reap_at IS NOT NULL;
7460
+
7461
+ CREATE INDEX idx_file_upload_sessions_abandoned
7462
+ ON file_upload_sessions (project_id, expires_at)
7463
+ WHERE status = 'pending' AND provider_upload IS NOT NULL;
7464
+ `;
7465
+
7285
7466
  // src/migrations.ts
7286
7467
  var POSTGRES_STORAGE_ADAPTER_ID = "SixbPostgresStorage";
7287
7468
  function pgSql(id, sqlText) {
@@ -7501,7 +7682,11 @@ var postgresStorageMigrations = defineMigrations({
7501
7682
  pgSql("042-vector-profiles", _042_vector_profiles_default),
7502
7683
  pgSql("043-vector-indexing", _043_vector_indexing_default),
7503
7684
  pgSql("044-vector-batching", _044_vector_batching_default),
7504
- pgSql("045-agent-thread-sandbox-state", _045_agent_thread_sandbox_state_default)
7685
+ pgSql("045-agent-thread-sandbox-state", _045_agent_thread_sandbox_state_default),
7686
+ pgSql("046-workflow-intervention-principals", _046_workflow_intervention_principals_default),
7687
+ pgSql("047-ontology-commit-attribution", _047_ontology_commit_attribution_default),
7688
+ pgSql("048-file-upload-sessions", _048_file_upload_sessions_default),
7689
+ pgSql("049-native-sessions", _049_native_sessions_default)
7505
7690
  ]
7506
7691
  });
7507
7692
 
@@ -9625,7 +9810,8 @@ var PG_OBJECT_QUERY_CAPABILITIES = {
9625
9810
  double: { equality: true, ordering: true },
9626
9811
  decimal: { equality: true, ordering: true },
9627
9812
  date: { equality: true, ordering: true },
9628
- timestamp: { equality: true, ordering: true }
9813
+ timestamp: { equality: true, ordering: true },
9814
+ userRef: { equality: true }
9629
9815
  },
9630
9816
  limits: {
9631
9817
  totalCount: true,
@@ -9828,9 +10014,10 @@ function commitRecord(row) {
9828
10014
  requestHash: row.request_hash,
9829
10015
  executionId: row.execution_id,
9830
10016
  origin: structuredClone(row.origin),
9831
- ...row.actor === null ? {} : {
9832
- actor: structuredClone(row.actor)
10017
+ ...row.requested_by === null ? {} : {
10018
+ requestedBy: structuredClone(row.requested_by)
9833
10019
  },
10020
+ executor: structuredClone(row.executor),
9834
10021
  ontologyRevision: row.ontology_revision,
9835
10022
  ...row.projection_revision === null ? {} : { projectionRevision: row.projection_revision },
9836
10023
  ...row.ownership_hash === null ? {} : { ownershipHash: row.ownership_hash },
@@ -9979,6 +10166,9 @@ import {
9979
10166
  telemetryPointSortKey as telemetryPointSortKey2
9980
10167
  } from "@sixb/core/internal/materialization";
9981
10168
  import {
10169
+ assertExpectedLinkRevision,
10170
+ assertExpectedLinkScopeRevision,
10171
+ assertExpectedObjectRevision,
9982
10172
  assertMaterializationFinalizationCorrelation,
9983
10173
  assertMaterializationHeader,
9984
10174
  assertMaterializationLaneCompletion,
@@ -11915,17 +12105,15 @@ class PgOntologyMaterializationStorage {
11915
12105
  await this.assertSources(input.expected.sources, input.commit.projectId);
11916
12106
  const objectRevisions = await reader.effectiveObjectRevisions(input.expected.objects.map((expected) => expected.ref), true);
11917
12107
  for (const expected of input.expected.objects) {
11918
- this.assertObject(objectRevisions.get(objectRefKey3(expected.ref)) ?? null, expected);
12108
+ assertExpectedObjectRevision(objectRevisions.get(objectRefKey3(expected.ref)) ?? null, expected);
11919
12109
  }
11920
12110
  const linkRevisions = await reader.effectiveLinkLastCommits(input.expected.links.map((expected) => expected.ref), true);
11921
12111
  for (const expected of input.expected.links) {
11922
- this.assertLink(linkRevisions.get(linkRefKey3(expected.ref)), expected);
12112
+ assertExpectedLinkRevision(linkRevisions.get(linkRefKey3(expected.ref)), expected);
11923
12113
  }
11924
12114
  const linkScopeRevisions = await reader.linkScopeRevisions(input.expected.linkScopes);
11925
12115
  for (const [index, expected] of input.expected.linkScopes.entries()) {
11926
- if (linkScopeRevisions[index]?.fingerprint !== expected.fingerprint) {
11927
- throw new MaterializationConflictError6("effective-state", `Expected link scope changed for ${expected.source.objectTypeId}:${expected.source.primaryId}.${expected.linkId}.`);
11928
- }
12116
+ assertExpectedLinkScopeRevision(linkScopeRevisions[index]?.fingerprint, expected);
11929
12117
  }
11930
12118
  const points = await reader.exactPoints(input.expected.points, true);
11931
12119
  const pointRevisions = new Map(points.map((point) => [telemetryPointKey3(point.series, point.at), point.lastCommitId]));
@@ -12185,28 +12373,6 @@ class PgOntologyMaterializationStorage {
12185
12373
  throw new MaterializationConflictError6("projection-fence", `Source '${expected.source.projectionId}' changed.`);
12186
12374
  }
12187
12375
  }
12188
- assertObject(row, expected) {
12189
- if (!expected.exists) {
12190
- if (row) {
12191
- throw new MaterializationConflictError6("effective-state", `Expected object ${objectRefKey3(expected.ref)} to be absent.`);
12192
- }
12193
- return;
12194
- }
12195
- if (!row || row.version !== expected.version || row.lastCommitId !== expected.lastCommitId) {
12196
- throw new MaterializationConflictError6("effective-state", `Expected object ${objectRefKey3(expected.ref)} changed.`);
12197
- }
12198
- }
12199
- assertLink(lastCommitId, expected) {
12200
- if (!expected.exists) {
12201
- if (lastCommitId !== undefined) {
12202
- throw new MaterializationConflictError6("effective-state", `Expected link ${linkRefKey3(expected.ref)} to be absent.`);
12203
- }
12204
- return;
12205
- }
12206
- if (lastCommitId === undefined || lastCommitId !== expected.lastCommitId) {
12207
- throw new MaterializationConflictError6("effective-state", `Expected link ${linkRefKey3(expected.ref)} changed.`);
12208
- }
12209
- }
12210
12376
  async assertFinalization(session, input) {
12211
12377
  const { commit } = session.header;
12212
12378
  const { result } = input.finalization;
@@ -12442,18 +12608,19 @@ class PgOntologyMaterializationStorage {
12442
12608
  async insertCommit(header, input) {
12443
12609
  const { commit } = header;
12444
12610
  const origin = originColumns(commit.origin);
12445
- const actor = commit.actor === undefined ? null : jsonParameter2(this.sql, commit.actor);
12611
+ const requestedBy = commit.requestedBy === undefined ? null : jsonParameter2(this.sql, commit.requestedBy);
12446
12612
  const rows = await this.sql`
12447
12613
  INSERT INTO ontology_commits (
12448
12614
  project_id, id, idempotency_key, request_hash, execution_id,
12449
- origin_kind, origin_run_id, origin_batch_ordinal, origin, actor,
12615
+ origin_kind, origin_run_id, origin_batch_ordinal, origin, requested_by, executor,
12450
12616
  ontology_revision, projection_revision, ownership_hash,
12451
12617
  intent, result, committed_at
12452
12618
  ) VALUES (
12453
12619
  ${commit.projectId}, ${commit.id}, ${commit.idempotencyKey}, ${commit.requestHash},
12454
12620
  ${commit.executionId},
12455
12621
  ${origin.kind}, ${origin.runId}, ${origin.batchOrdinal},
12456
- ${jsonParameter2(this.sql, commit.origin)}, ${actor}, ${commit.ontologyRevision},
12622
+ ${jsonParameter2(this.sql, commit.origin)}, ${requestedBy},
12623
+ ${jsonParameter2(this.sql, commit.executor)}, ${commit.ontologyRevision},
12457
12624
  ${commit.projectionRevision ?? null}, ${commit.ownershipHash ?? null},
12458
12625
  ${jsonParameter2(this.sql, commit.intent)},
12459
12626
  ${jsonParameter2(this.sql, input.finalization.result)}, ${commit.committedAt}
@@ -15547,6 +15714,132 @@ function toStorageRow(row) {
15547
15714
  };
15548
15715
  }
15549
15716
 
15717
+ // src/pg-file-upload-session-storage.ts
15718
+ import {
15719
+ DurableFileUploadSessions,
15720
+ fileUploadSessionReapAt,
15721
+ parseFileUploadSessionRow
15722
+ } from "@sixb/core/internal/file-upload-session-storage-provider";
15723
+ class PgFileUploadSessionStorage extends DurableFileUploadSessions {
15724
+ constructor(sql) {
15725
+ super(new PgFileUploadSessionBackend(sql));
15726
+ }
15727
+ }
15728
+
15729
+ class PgFileUploadSessionBackend {
15730
+ sql;
15731
+ constructor(sql) {
15732
+ this.sql = sql;
15733
+ }
15734
+ read(run) {
15735
+ return run(new PgFileUploadSessionPersistence(this.sql, false));
15736
+ }
15737
+ transaction(run) {
15738
+ return runPgTransaction(this.sql, (tx) => run(new PgFileUploadSessionPersistence(tx, true)));
15739
+ }
15740
+ }
15741
+
15742
+ class PgFileUploadSessionPersistence {
15743
+ sql;
15744
+ locking;
15745
+ constructor(sql, locking) {
15746
+ this.sql = sql;
15747
+ this.locking = locking;
15748
+ }
15749
+ async now() {
15750
+ const [row] = await this.sql`SELECT clock_timestamp() AS now`;
15751
+ if (!row)
15752
+ throw new Error("[SixbPg] Database clock returned no row.");
15753
+ return new Date(row.now);
15754
+ }
15755
+ async insert(session) {
15756
+ const rows = await this.sql`
15757
+ INSERT INTO file_upload_sessions (
15758
+ id, project_id, owner_principal_type, owner_principal_id, strategy, status, file_name,
15759
+ media_type, logical_path, expected_size_bytes, expected_digest, provider_upload,
15760
+ signed_parts, file_ref, created_at, expires_at, completed_at, aborted_at, reap_at
15761
+ ) VALUES (
15762
+ ${session.id}, ${session.projectId}, ${session.principal.type}, ${session.principal.id},
15763
+ ${session.strategy}, ${session.status}, ${session.fileName ?? null}, ${session.mediaType ?? null},
15764
+ ${session.logicalPath ?? null}, ${session.expectedSizeBytes ?? null},
15765
+ ${session.expectedDigest ?? null}, ${jsonText(session.providerUpload)}::text::jsonb,
15766
+ ${JSON.stringify(session.signedParts)}::text::jsonb, ${jsonText(session.fileRef)}::text::jsonb,
15767
+ ${session.createdAt}, ${session.expiresAt}, ${session.completedAt ?? null},
15768
+ ${session.abortedAt ?? null}, ${fileUploadSessionReapAt(session)}
15769
+ )
15770
+ ON CONFLICT (id) DO NOTHING
15771
+ RETURNING id
15772
+ `;
15773
+ return rows.length === 1;
15774
+ }
15775
+ async get(uploadId) {
15776
+ const [row] = this.locking ? await this.sql`
15777
+ SELECT * FROM file_upload_sessions WHERE id = ${uploadId} FOR UPDATE
15778
+ ` : await this.sql`
15779
+ SELECT * FROM file_upload_sessions WHERE id = ${uploadId}
15780
+ `;
15781
+ return row ? sessionFromRow(row) : null;
15782
+ }
15783
+ async update(session) {
15784
+ await this.sql`
15785
+ UPDATE file_upload_sessions
15786
+ SET status = ${session.status},
15787
+ provider_upload = ${jsonText(session.providerUpload)}::text::jsonb,
15788
+ signed_parts = ${JSON.stringify(session.signedParts)}::text::jsonb,
15789
+ file_ref = ${jsonText(session.fileRef)}::text::jsonb,
15790
+ created_at = ${session.createdAt},
15791
+ expires_at = ${session.expiresAt},
15792
+ completed_at = ${session.completedAt ?? null},
15793
+ aborted_at = ${session.abortedAt ?? null},
15794
+ reap_at = ${fileUploadSessionReapAt(session)}
15795
+ WHERE id = ${session.id}
15796
+ `;
15797
+ }
15798
+ async listAbandoned(input) {
15799
+ const rows = await this.sql`
15800
+ SELECT * FROM file_upload_sessions
15801
+ WHERE project_id = ${input.projectId} AND status = 'pending'
15802
+ AND provider_upload IS NOT NULL AND expires_at <= ${input.now}
15803
+ ORDER BY expires_at, id COLLATE "C"
15804
+ LIMIT ${input.limit}
15805
+ `;
15806
+ return rows.map(sessionFromRow);
15807
+ }
15808
+ async deleteReapable(now) {
15809
+ const rows = await this.sql`
15810
+ DELETE FROM file_upload_sessions
15811
+ WHERE reap_at IS NOT NULL AND reap_at <= ${now}
15812
+ RETURNING id
15813
+ `;
15814
+ return rows.length;
15815
+ }
15816
+ }
15817
+ function sessionFromRow(row) {
15818
+ return parseFileUploadSessionRow({
15819
+ id: row.id,
15820
+ projectId: row.project_id,
15821
+ ownerPrincipalType: row.owner_principal_type,
15822
+ ownerPrincipalId: row.owner_principal_id,
15823
+ strategy: row.strategy,
15824
+ status: row.status,
15825
+ fileName: row.file_name,
15826
+ mediaType: row.media_type,
15827
+ logicalPath: row.logical_path,
15828
+ expectedSizeBytes: row.expected_size_bytes,
15829
+ expectedDigest: row.expected_digest,
15830
+ providerUpload: row.provider_upload,
15831
+ signedParts: row.signed_parts,
15832
+ fileRef: row.file_ref,
15833
+ createdAt: row.created_at,
15834
+ expiresAt: row.expires_at,
15835
+ completedAt: row.completed_at,
15836
+ abortedAt: row.aborted_at
15837
+ });
15838
+ }
15839
+ function jsonText(value) {
15840
+ return value === undefined ? null : JSON.stringify(value);
15841
+ }
15842
+
15550
15843
  // src/pg-pipeline-run-storage.ts
15551
15844
  import { parseSixbFailure as parseSixbFailure4, serializeSixbFailure as serializeSixbFailure3 } from "@sixb/core/internal/errors";
15552
15845
  import { assertPipelineRunExecution } from "@sixb/core/internal/pipeline-run-storage-provider";
@@ -17218,7 +17511,7 @@ class PgWorkflowInterventionStorage {
17218
17511
  SET
17219
17512
  status = ${"submitted"},
17220
17513
  submitted_at = ${input.submittedAt ?? new Date},
17221
- submitted_by = ${serializeActor(input.submittedBy)}::text::jsonb,
17514
+ submitted_by = ${serializePrincipal(input.submittedBy)}::text::jsonb,
17222
17515
  response = ${serializeRecord(input.response)}::text::jsonb
17223
17516
  WHERE project_id = ${input.projectId} AND id = ${input.id}
17224
17517
  RETURNING *
@@ -17234,7 +17527,7 @@ class PgWorkflowInterventionStorage {
17234
17527
  SET
17235
17528
  status = ${"cancelled"},
17236
17529
  cancelled_at = ${input.cancelledAt ?? new Date},
17237
- cancelled_by = ${serializeActor(input.cancelledBy)}::text::jsonb
17530
+ cancelled_by = ${serializePrincipal(input.cancelledBy)}::text::jsonb
17238
17531
  WHERE project_id = ${input.projectId} AND id = ${input.id}
17239
17532
  RETURNING *
17240
17533
  `;
@@ -17361,10 +17654,10 @@ function serializeRecord(value) {
17361
17654
  function parseRecord(value) {
17362
17655
  return typeof value === "string" ? JSON.parse(value) : value;
17363
17656
  }
17364
- function serializeActor(actor) {
17365
- return actor ? JSON.stringify(actor) : null;
17657
+ function serializePrincipal(principal) {
17658
+ return principal ? JSON.stringify(principal) : null;
17366
17659
  }
17367
- function parseActor(value) {
17660
+ function parsePrincipal(value) {
17368
17661
  return typeof value === "string" ? JSON.parse(value) : value ?? undefined;
17369
17662
  }
17370
17663
  function rowToWorkflowInterventionRecord(row) {
@@ -17384,10 +17677,10 @@ function rowToWorkflowInterventionRecord(row) {
17384
17677
  requestedAt: new Date(row.requested_at),
17385
17678
  expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
17386
17679
  submittedAt: row.submitted_at ? new Date(row.submitted_at) : undefined,
17387
- submittedBy: parseActor(row.submitted_by),
17680
+ submittedBy: parsePrincipal(row.submitted_by),
17388
17681
  response: row.response ? parseRecord(row.response) : undefined,
17389
17682
  cancelledAt: row.cancelled_at ? new Date(row.cancelled_at) : undefined,
17390
- cancelledBy: parseActor(row.cancelled_by),
17683
+ cancelledBy: parsePrincipal(row.cancelled_by),
17391
17684
  expiredAt: row.expired_at ? new Date(row.expired_at) : undefined
17392
17685
  };
17393
17686
  }
@@ -18455,6 +18748,7 @@ class PostgresStorage {
18455
18748
  connectorConnections;
18456
18749
  shareGrants;
18457
18750
  shareSessions;
18751
+ fileUploadSessions;
18458
18752
  migrators;
18459
18753
  sql;
18460
18754
  schemaName;
@@ -18508,6 +18802,7 @@ class PostgresStorage {
18508
18802
  this.connectorConnections = createOperationScopedFacade(stores.connectorConnections, scope);
18509
18803
  this.shareGrants = createOperationScopedFacade(stores.shareGrants, scope);
18510
18804
  this.shareSessions = createOperationScopedFacade(stores.shareSessions, scope);
18805
+ this.fileUploadSessions = createOperationScopedFacade(stores.fileUploadSessions, scope);
18511
18806
  registerPostgresStorageTestingAdapter(this, (durationMs) => stores.connectorConnections.advanceTimeForTesting(durationMs));
18512
18807
  }
18513
18808
  async ping() {
@@ -18602,7 +18897,8 @@ function createPostgresStores(sql, options) {
18602
18897
  rules: new PgRulesStorage(sql),
18603
18898
  connectorConnections: new PgConnectorConnectionStorage(sql),
18604
18899
  shareGrants,
18605
- shareSessions
18900
+ shareSessions,
18901
+ fileUploadSessions: new PgFileUploadSessionStorage(sql)
18606
18902
  };
18607
18903
  }
18608
18904
  function resolveTimeoutMillis(value, label) {
@@ -18619,4 +18915,4 @@ export {
18619
18915
  migratePostgresStorage
18620
18916
  };
18621
18917
 
18622
- //# debugId=C35F49117A3070E564756E2164756E21
18918
+ //# debugId=F4D8409142F8F3AF64756E2164756E21