@opengeni/db 0.13.0 → 0.13.4

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
@@ -76,7 +76,7 @@ import {
76
76
  workspaceVariableSetVariables,
77
77
  workspaceVariableSets,
78
78
  workspaces
79
- } from "./chunk-N4WTE6SB.js";
79
+ } from "./chunk-IQOXTFKA.js";
80
80
  import {
81
81
  migrate,
82
82
  runMigrations
@@ -5194,11 +5194,15 @@ var REFRESH_WINDOW_MS = 6e4;
5194
5194
  var CONNECTION_REFRESH_TIMEOUT_MS = 1e4;
5195
5195
  function buildConnectionTokenResolver(db, settings, deps = defaultDeps2) {
5196
5196
  const load = async (input) => {
5197
+ const subjectOwned = input.connectionRef.subjectScope === "subject";
5198
+ if (subjectOwned && !input.subjectId) {
5199
+ return null;
5200
+ }
5197
5201
  const request = {
5198
5202
  workspaceId: input.workspaceId,
5199
5203
  providerDomain: input.connectionRef.providerDomain,
5200
- // I1 deliberately accepts workspace-shared connections only at runtime.
5201
- allowSubjectOwned: false
5204
+ allowSubjectOwned: subjectOwned,
5205
+ ...subjectOwned ? { subjectId: input.subjectId } : {}
5202
5206
  };
5203
5207
  if (input.connectionRef.connectionId !== void 0) {
5204
5208
  request.connectionId = input.connectionRef.connectionId;
@@ -5206,10 +5210,12 @@ function buildConnectionTokenResolver(db, settings, deps = defaultDeps2) {
5206
5210
  if (input.connectionRef.kind !== void 0) {
5207
5211
  request.kind = input.connectionRef.kind;
5208
5212
  }
5209
- if (input.subjectId !== void 0) {
5210
- request.subjectId = input.subjectId;
5213
+ const credential = await deps.loadCredential(db, settings, request);
5214
+ if (!credential) return null;
5215
+ if (subjectOwned) {
5216
+ return credential.subjectId === input.subjectId ? credential : null;
5211
5217
  }
5212
- return deps.loadCredential(db, settings, request);
5218
+ return credential.subjectId === null ? credential : null;
5213
5219
  };
5214
5220
  const snapshot = async (cred, ref, destinationUrl) => {
5215
5221
  if (cred.status !== "active") {
@@ -5244,7 +5250,7 @@ function buildConnectionTokenResolver(db, settings, deps = defaultDeps2) {
5244
5250
  ...ref.selectedResources ? { selectedResources: copySelectedResources(ref.selectedResources) } : {}
5245
5251
  };
5246
5252
  }
5247
- await deps.recordUsed(db, cred.workspaceId, cred.id);
5253
+ await deps.recordUsed(db, cred.workspaceId, cred.id, cred.subjectId);
5248
5254
  return {
5249
5255
  status: "ok",
5250
5256
  headers,
@@ -5264,7 +5270,8 @@ function buildConnectionTokenResolver(db, settings, deps = defaultDeps2) {
5264
5270
  workspaceId: cred.workspaceId,
5265
5271
  credentialEncrypted: deps.encrypt(key, JSON.stringify(refreshed.credential)),
5266
5272
  expiresAt: refreshed.expiresAt,
5267
- lastRefreshAt: deps.now()
5273
+ lastRefreshAt: deps.now(),
5274
+ subjectId: cred.subjectId
5268
5275
  };
5269
5276
  if (refreshed.grantedScopes !== void 0) {
5270
5277
  refreshRecord.grantedScopes = refreshed.grantedScopes;
@@ -5273,7 +5280,8 @@ function buildConnectionTokenResolver(db, settings, deps = defaultDeps2) {
5273
5280
  if (persisted) {
5274
5281
  const current = await load({
5275
5282
  workspaceId: cred.workspaceId,
5276
- connectionRef: { ...ref, connectionId: cred.id }
5283
+ connectionRef: { ...ref, connectionId: cred.id },
5284
+ ...cred.subjectId ? { subjectId: cred.subjectId } : {}
5277
5285
  });
5278
5286
  if (current) {
5279
5287
  return current;
@@ -5281,7 +5289,8 @@ function buildConnectionTokenResolver(db, settings, deps = defaultDeps2) {
5281
5289
  }
5282
5290
  const winner = await load({
5283
5291
  workspaceId: cred.workspaceId,
5284
- connectionRef: { ...ref, connectionId: cred.id }
5292
+ connectionRef: { ...ref, connectionId: cred.id },
5293
+ ...cred.subjectId ? { subjectId: cred.subjectId } : {}
5285
5294
  });
5286
5295
  if (winner?.status === "active") {
5287
5296
  return winner;
@@ -5289,7 +5298,7 @@ function buildConnectionTokenResolver(db, settings, deps = defaultDeps2) {
5289
5298
  throw new Error("connection credential changed during token refresh");
5290
5299
  };
5291
5300
  const refreshSingleFlight = (cred, ref) => {
5292
- const key = `${cred.id}:${cred.version}`;
5301
+ const key = `${cred.subjectId ?? "workspace"}:${cred.id}:${cred.version}`;
5293
5302
  const existing = inflight2.get(key);
5294
5303
  if (existing) {
5295
5304
  return existing;
@@ -5334,7 +5343,8 @@ function buildConnectionTokenResolver(db, settings, deps = defaultDeps2) {
5334
5343
  error instanceof Error ? error.message : String(error),
5335
5344
  {
5336
5345
  id: cred.id,
5337
- version: cred.version
5346
+ version: cred.version,
5347
+ subjectId: cred.subjectId
5338
5348
  }
5339
5349
  ).catch(() => void 0);
5340
5350
  }
@@ -6151,6 +6161,17 @@ async function bootstrapWorkspace(db, input) {
6151
6161
  updatedAt: /* @__PURE__ */ new Date()
6152
6162
  }).where(eq7(workspaceMemberships.id, membership.id));
6153
6163
  }
6164
+ await setRlsContext(tx, {
6165
+ accountId: workspace.accountId,
6166
+ workspaceId: null
6167
+ });
6168
+ const memberships = await tx.select({
6169
+ membership: workspaceMemberships,
6170
+ workspace: workspaces
6171
+ }).from(workspaceMemberships).innerJoin(
6172
+ workspaces,
6173
+ eq7(workspaceMemberships.workspaceId, workspaces.id)
6174
+ ).where(eq7(workspaceMemberships.subjectId, input.subjectId)).orderBy(desc2(workspaces.createdAt));
6154
6175
  return {
6155
6176
  mode: input.accountExternalSource === "opengeni:local" ? "local" : "configured",
6156
6177
  subjectId: input.subjectId,
@@ -6164,15 +6185,13 @@ async function bootstrapWorkspace(db, input) {
6164
6185
  permissions: input.accountPermissions ?? allAccountPermissions
6165
6186
  }
6166
6187
  ],
6167
- workspaceGrants: [
6168
- {
6169
- workspaceId: workspace.id,
6170
- accountId: account.id,
6171
- subjectId: input.subjectId,
6172
- ...input.subjectLabel ? { subjectLabel: input.subjectLabel } : {},
6173
- permissions: workspacePermissions
6174
- }
6175
- ],
6188
+ workspaceGrants: memberships.map((row) => ({
6189
+ workspaceId: row.workspace.id,
6190
+ accountId: row.workspace.accountId,
6191
+ subjectId: input.subjectId,
6192
+ ...input.subjectLabel ? { subjectLabel: input.subjectLabel } : {},
6193
+ permissions: row.membership.permissions
6194
+ })),
6176
6195
  defaultAccountId: account.id,
6177
6196
  defaultWorkspaceId: workspace.id
6178
6197
  };
@@ -7615,7 +7634,7 @@ async function upsertRegistryCapabilityCatalogItem(db, input) {
7615
7634
  tags: input.tags ?? ["mcp", "integration", input.tier],
7616
7635
  homepageUrl: input.homepageUrl ?? `https://${input.providerDomain}`,
7617
7636
  endpointUrl: input.mcpUrl,
7618
- installUrl: input.homepageUrl ?? `https://${input.providerDomain}`,
7637
+ installUrl: input.installUrl ?? input.homepageUrl ?? `https://${input.providerDomain}`,
7619
7638
  authModel: input.authKind === "none" ? null : "credential_ref",
7620
7639
  providerDomain: input.providerDomain,
7621
7640
  surfaceType: "mcp",
@@ -8032,36 +8051,48 @@ var connectionMetadataColumns = {
8032
8051
  function connectionSubjectVisibility(subjectId) {
8033
8052
  return subjectId ? or(isNull2(connections.subjectId), eq7(connections.subjectId, subjectId)) : isNull2(connections.subjectId);
8034
8053
  }
8054
+ function connectionExactSubject(subjectId) {
8055
+ return subjectId ? eq7(connections.subjectId, subjectId) : isNull2(connections.subjectId);
8056
+ }
8057
+ async function withConnectionSubjectRls(db, workspaceId, subjectId, fn) {
8058
+ return subjectId ? await withWorkspaceSubjectRls(db, workspaceId, subjectId, fn) : await withWorkspaceRls(db, workspaceId, fn);
8059
+ }
8060
+ async function createConnectionInScope(db, input) {
8061
+ const [row] = await db.insert(connections).values({
8062
+ accountId: input.accountId,
8063
+ workspaceId: input.workspaceId,
8064
+ subjectId: input.subjectId ?? null,
8065
+ providerDomain: input.providerDomain,
8066
+ kind: input.kind,
8067
+ status: input.status ?? "active",
8068
+ credentialEncrypted: input.credentialEncrypted,
8069
+ grantedScopes: input.grantedScopes ?? [],
8070
+ expiresAt: input.expiresAt ?? null,
8071
+ verifiedInstallAt: input.verifiedInstallAt ?? null,
8072
+ verifiedInstallVersion: input.verifiedInstallVersion ?? null,
8073
+ metadata: input.metadata ?? {},
8074
+ createdBySubjectId: input.createdBySubjectId ?? null,
8075
+ updatedBySubjectId: input.updatedBySubjectId ?? input.createdBySubjectId ?? null
8076
+ }).returning(connectionMetadataColumns);
8077
+ if (!row) {
8078
+ throw new Error("Failed to create connection");
8079
+ }
8080
+ return mapConnectionMetadata(row);
8081
+ }
8035
8082
  async function createConnection(db, input) {
8036
8083
  return await withRlsContext(
8037
8084
  db,
8038
8085
  { accountId: input.accountId, workspaceId: input.workspaceId },
8039
8086
  async (scopedDb) => {
8040
- const [row] = await scopedDb.insert(connections).values({
8041
- accountId: input.accountId,
8042
- workspaceId: input.workspaceId,
8043
- subjectId: input.subjectId ?? null,
8044
- providerDomain: input.providerDomain,
8045
- kind: input.kind,
8046
- status: input.status ?? "active",
8047
- credentialEncrypted: input.credentialEncrypted,
8048
- grantedScopes: input.grantedScopes ?? [],
8049
- expiresAt: input.expiresAt ?? null,
8050
- verifiedInstallAt: input.verifiedInstallAt ?? null,
8051
- verifiedInstallVersion: input.verifiedInstallVersion ?? null,
8052
- metadata: input.metadata ?? {},
8053
- createdBySubjectId: input.createdBySubjectId ?? null,
8054
- updatedBySubjectId: input.updatedBySubjectId ?? input.createdBySubjectId ?? null
8055
- }).returning(connectionMetadataColumns);
8056
- if (!row) {
8057
- throw new Error("Failed to create connection");
8087
+ if (input.subjectId) {
8088
+ await setSubjectRlsContext(scopedDb, input.subjectId);
8058
8089
  }
8059
- return mapConnectionMetadata(row);
8090
+ return await createConnectionInScope(scopedDb, input);
8060
8091
  }
8061
8092
  );
8062
8093
  }
8063
8094
  async function listConnectionsMetadata(db, workspaceId, subjectId) {
8064
- return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
8095
+ return await withConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
8065
8096
  const rows = await scopedDb.select(connectionMetadataColumns).from(connections).where(
8066
8097
  and7(
8067
8098
  eq7(connections.workspaceId, workspaceId),
@@ -8072,7 +8103,7 @@ async function listConnectionsMetadata(db, workspaceId, subjectId) {
8072
8103
  });
8073
8104
  }
8074
8105
  async function getConnectionMetadata(db, workspaceId, connectionId, subjectId) {
8075
- return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
8106
+ return await withConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
8076
8107
  const [row] = await scopedDb.select(connectionMetadataColumns).from(connections).where(
8077
8108
  and7(
8078
8109
  eq7(connections.workspaceId, workspaceId),
@@ -8083,66 +8114,217 @@ async function getConnectionMetadata(db, workspaceId, connectionId, subjectId) {
8083
8114
  return row ? mapConnectionMetadata(row) : null;
8084
8115
  });
8085
8116
  }
8117
+ async function updateConnectionInScope(db, input) {
8118
+ const set = {
8119
+ updatedAt: /* @__PURE__ */ new Date(),
8120
+ ...input.providerDomain !== void 0 ? { providerDomain: input.providerDomain } : {},
8121
+ ...input.subjectId !== void 0 ? { subjectId: input.subjectId } : {},
8122
+ ...input.kind !== void 0 ? { kind: input.kind } : {},
8123
+ ...input.status !== void 0 ? { status: input.status } : {},
8124
+ ...input.credentialEncrypted !== void 0 ? {
8125
+ credentialEncrypted: input.credentialEncrypted,
8126
+ version: sql4`${connections.version} + 1`,
8127
+ lastError: null
8128
+ } : {},
8129
+ ...input.grantedScopes !== void 0 ? { grantedScopes: input.grantedScopes } : {},
8130
+ ...input.expiresAt !== void 0 ? { expiresAt: input.expiresAt } : {},
8131
+ ...input.verifiedInstallAt !== void 0 ? { verifiedInstallAt: input.verifiedInstallAt } : {},
8132
+ ...input.verifiedInstallVersion !== void 0 ? { verifiedInstallVersion: input.verifiedInstallVersion } : {},
8133
+ ...input.metadata !== void 0 ? { metadata: input.metadata } : {},
8134
+ ...input.updatedBySubjectId !== void 0 ? { updatedBySubjectId: input.updatedBySubjectId } : {}
8135
+ };
8136
+ const [row] = await db.update(connections).set(set).where(
8137
+ and7(
8138
+ eq7(connections.workspaceId, input.workspaceId),
8139
+ eq7(connections.id, input.connectionId),
8140
+ connectionSubjectVisibility(input.visibleToSubjectId),
8141
+ ...input.expectedVersion !== void 0 ? [eq7(connections.version, input.expectedVersion)] : []
8142
+ )
8143
+ ).returning(connectionMetadataColumns);
8144
+ return row ? mapConnectionMetadata(row) : null;
8145
+ }
8086
8146
  async function updateConnection(db, input) {
8087
- return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
8088
- const set = {
8089
- updatedAt: /* @__PURE__ */ new Date(),
8090
- ...input.providerDomain !== void 0 ? { providerDomain: input.providerDomain } : {},
8091
- ...input.subjectId !== void 0 ? { subjectId: input.subjectId } : {},
8092
- ...input.kind !== void 0 ? { kind: input.kind } : {},
8093
- ...input.status !== void 0 ? { status: input.status } : {},
8094
- ...input.credentialEncrypted !== void 0 ? {
8095
- credentialEncrypted: input.credentialEncrypted,
8096
- version: sql4`${connections.version} + 1`,
8097
- lastError: null
8098
- } : {},
8099
- ...input.grantedScopes !== void 0 ? { grantedScopes: input.grantedScopes } : {},
8100
- ...input.expiresAt !== void 0 ? { expiresAt: input.expiresAt } : {},
8101
- ...input.verifiedInstallAt !== void 0 ? { verifiedInstallAt: input.verifiedInstallAt } : {},
8102
- ...input.verifiedInstallVersion !== void 0 ? { verifiedInstallVersion: input.verifiedInstallVersion } : {},
8103
- ...input.metadata !== void 0 ? { metadata: input.metadata } : {},
8104
- ...input.updatedBySubjectId !== void 0 ? { updatedBySubjectId: input.updatedBySubjectId } : {}
8105
- };
8106
- const [row] = await scopedDb.update(connections).set(set).where(
8107
- and7(
8108
- eq7(connections.workspaceId, input.workspaceId),
8109
- eq7(connections.id, input.connectionId),
8110
- connectionSubjectVisibility(input.visibleToSubjectId),
8111
- ...input.expectedVersion !== void 0 ? [eq7(connections.version, input.expectedVersion)] : []
8112
- )
8113
- ).returning(connectionMetadataColumns);
8114
- return row ? mapConnectionMetadata(row) : null;
8115
- });
8147
+ return await withConnectionSubjectRls(
8148
+ db,
8149
+ input.workspaceId,
8150
+ input.visibleToSubjectId,
8151
+ async (scopedDb) => await updateConnectionInScope(scopedDb, input)
8152
+ );
8153
+ }
8154
+ async function revokeConnectionInScope(db, workspaceId, connectionId, updatedBySubjectId, expectedVersion) {
8155
+ const [row] = await db.update(connections).set({
8156
+ status: "revoked",
8157
+ // The version bump invalidates any in-flight refresh's (id, version) CAS,
8158
+ // so a racing refresh cannot commit and flip the row back to active.
8159
+ version: sql4`${connections.version} + 1`,
8160
+ // Status-only revocation does not replace the verified credential or bot
8161
+ // identity. Carry the marker to the same new CAS version so the dedicated
8162
+ // reinstall path can still recognize (but not use) the inactive row.
8163
+ verifiedInstallVersion: sql4`case
8164
+ when ${connections.verifiedInstallAt} is null then null
8165
+ else ${connections.version} + 1
8166
+ end`,
8167
+ updatedBySubjectId: updatedBySubjectId ?? null,
8168
+ updatedAt: /* @__PURE__ */ new Date()
8169
+ }).where(
8170
+ and7(
8171
+ eq7(connections.workspaceId, workspaceId),
8172
+ eq7(connections.id, connectionId),
8173
+ // Same visibility rule as get/update: shared rows plus the caller's own
8174
+ // subject rows. Cross-subject revocation (admin janitorial) arrives with
8175
+ // the subject-connections UX in I5, deliberately not before.
8176
+ connectionSubjectVisibility(updatedBySubjectId),
8177
+ ...expectedVersion !== void 0 ? [eq7(connections.version, expectedVersion)] : []
8178
+ )
8179
+ ).returning(connectionMetadataColumns);
8180
+ return row ? mapConnectionMetadata(row) : null;
8116
8181
  }
8117
8182
  async function revokeConnection(db, workspaceId, connectionId, updatedBySubjectId) {
8118
- return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
8119
- const [row] = await scopedDb.update(connections).set({
8120
- status: "revoked",
8121
- // The version bump invalidates any in-flight refresh's (id, version) CAS,
8122
- // so a racing refresh cannot commit and flip the row back to active.
8123
- version: sql4`${connections.version} + 1`,
8124
- // Status-only revocation does not replace the verified credential or bot
8125
- // identity. Carry the marker to the same new CAS version so the dedicated
8126
- // reinstall path can still recognize (but not use) the inactive row.
8127
- verifiedInstallVersion: sql4`case
8128
- when ${connections.verifiedInstallAt} is null then null
8129
- else ${connections.version} + 1
8130
- end`,
8131
- updatedBySubjectId: updatedBySubjectId ?? null,
8132
- updatedAt: /* @__PURE__ */ new Date()
8133
- }).where(
8134
- and7(
8135
- eq7(connections.workspaceId, workspaceId),
8136
- eq7(connections.id, connectionId),
8137
- // Same visibility rule as get/update: shared rows plus the caller's own
8138
- // subject rows. Cross-subject revocation (admin janitorial) arrives with
8139
- // the subject-connections UX in I5, deliberately not before.
8140
- connectionSubjectVisibility(updatedBySubjectId)
8141
- )
8142
- ).returning(connectionMetadataColumns);
8143
- return row ? mapConnectionMetadata(row) : null;
8183
+ return await withConnectionSubjectRls(
8184
+ db,
8185
+ workspaceId,
8186
+ updatedBySubjectId,
8187
+ async (scopedDb) => await revokeConnectionInScope(scopedDb, workspaceId, connectionId, updatedBySubjectId)
8188
+ );
8189
+ }
8190
+ var SlackBotLifecycleSuccessAuditError = class extends Error {
8191
+ constructor() {
8192
+ super("OpenGeni Slack bot lifecycle success audit failed");
8193
+ this.name = "SlackBotLifecycleSuccessAuditError";
8194
+ }
8195
+ };
8196
+ async function insertSlackBotLifecycleSuccessAuditInScope(db, input) {
8197
+ try {
8198
+ await db.insert(auditEvents).values({
8199
+ accountId: input.accountId,
8200
+ workspaceId: input.workspaceId,
8201
+ subjectId: input.subjectId,
8202
+ action: input.action,
8203
+ targetType: "connection",
8204
+ targetId: input.connectionId,
8205
+ metadata: {
8206
+ credentialRole: input.credentialRole,
8207
+ credentialLabel: input.credentialLabel,
8208
+ connectionId: input.connectionId,
8209
+ slackTeamId: input.slackTeamId,
8210
+ outcome: "succeeded"
8211
+ }
8212
+ });
8213
+ } catch {
8214
+ throw new SlackBotLifecycleSuccessAuditError();
8215
+ }
8216
+ }
8217
+ async function assertWorkspaceAccountPairInScope(db, accountId, workspaceId) {
8218
+ const [workspace] = await db.select({ id: workspaces.id }).from(workspaces).where(and7(eq7(workspaces.id, workspaceId), eq7(workspaces.accountId, accountId))).limit(1);
8219
+ if (!workspace) {
8220
+ throw new Error("Workspace does not belong to the expected account");
8221
+ }
8222
+ }
8223
+ async function withSlackBotLifecycleRls(db, input, fn) {
8224
+ return await withRlsContext(
8225
+ db,
8226
+ { accountId: input.accountId, workspaceId: input.workspaceId },
8227
+ async (scopedDb) => {
8228
+ await setSubjectRlsContext(scopedDb, input.subjectId);
8229
+ await assertWorkspaceAccountPairInScope(scopedDb, input.accountId, input.workspaceId);
8230
+ return await fn(scopedDb);
8231
+ }
8232
+ );
8233
+ }
8234
+ async function createConnectionWithSlackBotSuccessAudit(db, input) {
8235
+ if (input.connection.accountId !== input.accountId || input.connection.workspaceId !== input.workspaceId) {
8236
+ throw new Error("Slack bot connection and lifecycle audit tenant must match");
8237
+ }
8238
+ return await withSlackBotLifecycleRls(db, input, async (scopedDb) => {
8239
+ const connection = await createConnectionInScope(scopedDb, input.connection);
8240
+ await insertSlackBotLifecycleSuccessAuditInScope(scopedDb, {
8241
+ ...input,
8242
+ action: "slack_bot.connected",
8243
+ connectionId: connection.id
8244
+ });
8245
+ return connection;
8144
8246
  });
8145
8247
  }
8248
+ async function updateConnectionWithSlackBotSuccessAudit(db, input) {
8249
+ if (input.connection.workspaceId !== input.workspaceId) {
8250
+ throw new Error("Slack bot connection and lifecycle audit workspace must match");
8251
+ }
8252
+ return await withSlackBotLifecycleRls(db, input, async (scopedDb) => {
8253
+ const connection = await updateConnectionInScope(scopedDb, input.connection);
8254
+ if (!connection) return null;
8255
+ if (connection.accountId !== input.accountId) {
8256
+ throw new Error("Slack bot connection and lifecycle audit account must match");
8257
+ }
8258
+ await insertSlackBotLifecycleSuccessAuditInScope(scopedDb, {
8259
+ ...input,
8260
+ action: "slack_bot.reinstalled",
8261
+ connectionId: connection.id
8262
+ });
8263
+ return connection;
8264
+ });
8265
+ }
8266
+ async function revokeConnectionWithSlackBotSuccessAudit(db, input) {
8267
+ return await withSlackBotLifecycleRls(db, input, async (scopedDb) => {
8268
+ const connection = await revokeConnectionInScope(
8269
+ scopedDb,
8270
+ input.workspaceId,
8271
+ input.connectionId,
8272
+ input.subjectId,
8273
+ input.expectedVersion
8274
+ );
8275
+ if (!connection) return null;
8276
+ if (connection.accountId !== input.accountId) {
8277
+ throw new Error("Slack bot connection and lifecycle audit account must match");
8278
+ }
8279
+ await insertSlackBotLifecycleSuccessAuditInScope(scopedDb, {
8280
+ ...input,
8281
+ action: "slack_bot.disconnected",
8282
+ connectionId: connection.id
8283
+ });
8284
+ return connection;
8285
+ });
8286
+ }
8287
+ async function recordSlackBotInstallCallbackFailure(db, input) {
8288
+ if (!/^[a-f0-9]{64}$/.test(input.callbackDigest)) {
8289
+ throw new Error("Slack callback digest must be a lowercase SHA-256 value");
8290
+ }
8291
+ return await withRlsContext(
8292
+ db,
8293
+ { accountId: input.accountId, workspaceId: input.workspaceId },
8294
+ async (scopedDb) => {
8295
+ await setSubjectRlsContext(scopedDb, input.subjectId);
8296
+ await assertWorkspaceAccountPairInScope(scopedDb, input.accountId, input.workspaceId);
8297
+ await scopedDb.execute(
8298
+ sql4`select pg_advisory_xact_lock(hashtextextended(${`slack-callback-failure:${input.workspaceId}:${input.callbackDigest}`}, 0))`
8299
+ );
8300
+ const [existing] = await scopedDb.select({ id: auditEvents.id }).from(auditEvents).where(
8301
+ and7(
8302
+ eq7(auditEvents.accountId, input.accountId),
8303
+ eq7(auditEvents.workspaceId, input.workspaceId),
8304
+ eq7(auditEvents.action, "slack_bot.install.callback.failed"),
8305
+ eq7(auditEvents.targetType, "slack_oauth_callback"),
8306
+ eq7(auditEvents.targetId, input.callbackDigest)
8307
+ )
8308
+ ).limit(1);
8309
+ if (existing) return false;
8310
+ await scopedDb.insert(auditEvents).values({
8311
+ accountId: input.accountId,
8312
+ workspaceId: input.workspaceId,
8313
+ subjectId: input.subjectId,
8314
+ action: "slack_bot.install.callback.failed",
8315
+ targetType: "slack_oauth_callback",
8316
+ targetId: input.callbackDigest,
8317
+ metadata: {
8318
+ outcome: "failed",
8319
+ installMode: input.installMode,
8320
+ stage: input.stage,
8321
+ reason: input.reason
8322
+ }
8323
+ });
8324
+ return true;
8325
+ }
8326
+ );
8327
+ }
8146
8328
  async function claimSlackBotPostOperation(db, input) {
8147
8329
  const claimLeaseMs = Math.max(1, Math.min(Math.trunc(input.claimLeaseMs), 12e4));
8148
8330
  return await withRlsContext(
@@ -8302,13 +8484,16 @@ async function getSlackBotPostOperation(db, workspaceId, connectionId, operation
8302
8484
  });
8303
8485
  }
8304
8486
  async function loadConnectionCredentialForBroker(db, settings, input) {
8487
+ if (input.allowSubjectOwned && !input.subjectId) {
8488
+ return null;
8489
+ }
8305
8490
  const key = environmentsEncryptionKeyBytes3(settings);
8306
8491
  if (!key) {
8307
8492
  throw new Error(
8308
8493
  "connection credential present but OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured"
8309
8494
  );
8310
8495
  }
8311
- const subjectPredicate = input.allowSubjectOwned ? connectionSubjectVisibility(input.subjectId) : isNull2(connections.subjectId);
8496
+ const subjectPredicate = input.allowSubjectOwned ? connectionExactSubject(input.subjectId) : isNull2(connections.subjectId);
8312
8497
  const conditions = [
8313
8498
  eq7(connections.workspaceId, input.workspaceId),
8314
8499
  subjectPredicate
@@ -8321,70 +8506,81 @@ async function loadConnectionCredentialForBroker(db, settings, input) {
8321
8506
  conditions.push(eq7(connections.kind, input.kind));
8322
8507
  }
8323
8508
  }
8324
- return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
8325
- const [row] = await scopedDb.select().from(connections).where(and7(...conditions)).orderBy(
8326
- desc2(sql4`(${connections.status} = 'active')`),
8327
- desc2(connections.updatedAt)
8328
- ).limit(1);
8329
- if (!row) {
8330
- return null;
8331
- }
8332
- let credential;
8333
- try {
8334
- credential = JSON.parse(decryptEnvironmentValue(key, row.credentialEncrypted));
8335
- } catch (error) {
8336
- throw new Error(
8337
- `connection credential could not be decrypted for ${row.id}: ${error instanceof Error ? error.message : String(error)}`,
8338
- { cause: error }
8339
- );
8340
- }
8341
- if (!credential || typeof credential !== "object" || Array.isArray(credential)) {
8342
- throw new Error(`connection credential bundle for ${row.id} is not a JSON object`);
8509
+ return await withConnectionSubjectRls(
8510
+ db,
8511
+ input.workspaceId,
8512
+ input.allowSubjectOwned ? input.subjectId : null,
8513
+ async (scopedDb) => {
8514
+ const [row] = await scopedDb.select().from(connections).where(and7(...conditions)).orderBy(
8515
+ desc2(sql4`(${connections.status} = 'active')`),
8516
+ desc2(connections.updatedAt)
8517
+ ).limit(1);
8518
+ if (!row) {
8519
+ return null;
8520
+ }
8521
+ let credential;
8522
+ try {
8523
+ credential = JSON.parse(decryptEnvironmentValue(key, row.credentialEncrypted));
8524
+ } catch (error) {
8525
+ throw new Error(
8526
+ `connection credential could not be decrypted for ${row.id}: ${error instanceof Error ? error.message : String(error)}`,
8527
+ { cause: error }
8528
+ );
8529
+ }
8530
+ if (!credential || typeof credential !== "object" || Array.isArray(credential)) {
8531
+ throw new Error(`connection credential bundle for ${row.id} is not a JSON object`);
8532
+ }
8533
+ return {
8534
+ id: row.id,
8535
+ accountId: row.accountId,
8536
+ workspaceId: row.workspaceId,
8537
+ subjectId: row.subjectId,
8538
+ providerDomain: row.providerDomain,
8539
+ kind: row.kind,
8540
+ status: row.status,
8541
+ credential,
8542
+ grantedScopes: row.grantedScopes,
8543
+ expiresAt: row.expiresAt,
8544
+ lastRefreshAt: row.lastRefreshAt,
8545
+ version: row.version,
8546
+ metadata: row.metadata
8547
+ };
8343
8548
  }
8344
- return {
8345
- id: row.id,
8346
- accountId: row.accountId,
8347
- workspaceId: row.workspaceId,
8348
- subjectId: row.subjectId,
8349
- providerDomain: row.providerDomain,
8350
- kind: row.kind,
8351
- status: row.status,
8352
- credential,
8353
- grantedScopes: row.grantedScopes,
8354
- expiresAt: row.expiresAt,
8355
- lastRefreshAt: row.lastRefreshAt,
8356
- version: row.version,
8357
- metadata: row.metadata
8358
- };
8359
- });
8549
+ );
8360
8550
  }
8361
8551
  async function recordConnectionTokenRefresh(db, input) {
8362
- return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
8363
- const set = {
8364
- credentialEncrypted: input.credentialEncrypted,
8365
- expiresAt: input.expiresAt,
8366
- lastRefreshAt: input.lastRefreshAt,
8367
- status: "active",
8368
- lastError: null,
8369
- version: sql4`${connections.version} + 1`,
8370
- updatedAt: /* @__PURE__ */ new Date(),
8371
- ...input.grantedScopes !== void 0 ? { grantedScopes: input.grantedScopes } : {}
8372
- };
8373
- const updated = await scopedDb.update(connections).set(set).where(
8374
- and7(
8375
- eq7(connections.id, input.id),
8376
- eq7(connections.workspaceId, input.workspaceId),
8377
- eq7(connections.version, input.version),
8378
- // A refresh may only ever renew a live credential; revoked/errored rows
8379
- // stay dead even if a status change somewhere forgot to bump version.
8380
- eq7(connections.status, "active")
8381
- )
8382
- ).returning({ id: connections.id });
8383
- return updated.length > 0;
8384
- });
8552
+ return await withConnectionSubjectRls(
8553
+ db,
8554
+ input.workspaceId,
8555
+ input.subjectId,
8556
+ async (scopedDb) => {
8557
+ const set = {
8558
+ credentialEncrypted: input.credentialEncrypted,
8559
+ expiresAt: input.expiresAt,
8560
+ lastRefreshAt: input.lastRefreshAt,
8561
+ status: "active",
8562
+ lastError: null,
8563
+ version: sql4`${connections.version} + 1`,
8564
+ updatedAt: /* @__PURE__ */ new Date(),
8565
+ ...input.grantedScopes !== void 0 ? { grantedScopes: input.grantedScopes } : {}
8566
+ };
8567
+ const updated = await scopedDb.update(connections).set(set).where(
8568
+ and7(
8569
+ eq7(connections.id, input.id),
8570
+ eq7(connections.workspaceId, input.workspaceId),
8571
+ eq7(connections.version, input.version),
8572
+ connectionExactSubject(input.subjectId),
8573
+ // A refresh may only ever renew a live credential; revoked/errored rows
8574
+ // stay dead even if a status change somewhere forgot to bump version.
8575
+ eq7(connections.status, "active")
8576
+ )
8577
+ ).returning({ id: connections.id });
8578
+ return updated.length > 0;
8579
+ }
8580
+ );
8385
8581
  }
8386
8582
  async function setConnectionStatus(db, workspaceId, status, lastError, guard) {
8387
- return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
8583
+ return await withConnectionSubjectRls(db, workspaceId, guard.subjectId, async (scopedDb) => {
8388
8584
  const updated = await scopedDb.update(connections).set({
8389
8585
  status,
8390
8586
  lastError,
@@ -8398,21 +8594,23 @@ async function setConnectionStatus(db, workspaceId, status, lastError, guard) {
8398
8594
  and7(
8399
8595
  eq7(connections.workspaceId, workspaceId),
8400
8596
  eq7(connections.id, guard.id),
8401
- eq7(connections.version, guard.version)
8597
+ eq7(connections.version, guard.version),
8598
+ connectionExactSubject(guard.subjectId)
8402
8599
  )
8403
8600
  ).returning({ id: connections.id });
8404
8601
  return updated.length > 0;
8405
8602
  });
8406
8603
  }
8407
- async function recordConnectionUsed(db, workspaceId, connectionId) {
8408
- await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
8604
+ async function recordConnectionUsed(db, workspaceId, connectionId, subjectId) {
8605
+ await withConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
8409
8606
  await scopedDb.update(connections).set({
8410
8607
  lastUsedAt: /* @__PURE__ */ new Date(),
8411
8608
  updatedAt: /* @__PURE__ */ new Date()
8412
8609
  }).where(
8413
8610
  and7(
8414
8611
  eq7(connections.workspaceId, workspaceId),
8415
- eq7(connections.id, connectionId)
8612
+ eq7(connections.id, connectionId),
8613
+ connectionExactSubject(subjectId)
8416
8614
  )
8417
8615
  );
8418
8616
  });
@@ -13536,6 +13734,7 @@ async function createSessionInTransaction(tx, input, id) {
13536
13734
  initialMessage: input.initialMessage,
13537
13735
  initialTurnInstructions: input.initialTurnInstructions ?? null,
13538
13736
  resources: input.resources,
13737
+ skills: input.skills ?? [],
13539
13738
  tools: input.tools ?? [],
13540
13739
  toolPolicy: input.toolPolicy ?? null,
13541
13740
  metadata: input.metadata,
@@ -14852,25 +15051,34 @@ async function getSessionLineage(db, workspaceId, sessionId) {
14852
15051
  if (rootRows.length === 0) {
14853
15052
  return null;
14854
15053
  }
14855
- const ancestorRows = await scopedDb.execute(sql4`
14856
- with recursive ancestors(id, parent_session_id, depth, path) as (
14857
- select ${sessions.id}, ${sessions.parentSessionId}, 0, array[${sessions.id}]
15054
+ const ancestorLineageRows = await scopedDb.execute(sql4`
15055
+ with recursive ancestors(id, parent_session_id, depth, path, cycle) as (
15056
+ select ${sessions.id}, ${sessions.parentSessionId}, 0, array[${sessions.id}], false
14858
15057
  from ${sessions}
14859
15058
  where ${sessions.workspaceId} = ${workspaceId}
14860
15059
  and ${sessions.id} = ${sessionId}
14861
15060
  union all
14862
- select parent.id, parent.parent_session_id, ancestors.depth + 1, ancestors.path || parent.id
15061
+ select
15062
+ parent.id,
15063
+ parent.parent_session_id,
15064
+ ancestors.depth + 1,
15065
+ ancestors.path || parent.id,
15066
+ parent.id = any(ancestors.path)
14863
15067
  from ${sessions} parent
14864
15068
  join ancestors on ancestors.parent_session_id = parent.id
14865
15069
  where parent.workspace_id = ${workspaceId}
14866
- and ancestors.depth < 10
14867
- and not parent.id = any(ancestors.path)
15070
+ and not ancestors.cycle
15071
+ and ancestors.depth < 64
14868
15072
  )
14869
- select id, parent_session_id as "parentSessionId", depth, path
15073
+ select id, parent_session_id as "parentSessionId", depth, path, cycle
14870
15074
  from ancestors
14871
- where depth > 0
14872
15075
  order by depth desc
14873
15076
  `);
15077
+ const frontier = ancestorLineageRows[0];
15078
+ if (!frontier || frontier.cycle || frontier.parentSessionId !== null || Number(frontier.depth) >= 64) {
15079
+ throw new Error(`session lineage for ${sessionId} has no valid workspace root`);
15080
+ }
15081
+ const ancestorRows = ancestorLineageRows.filter((row) => row.depth > 0);
14874
15082
  const childRows = await scopedDb.execute(sql4`
14875
15083
  with recursive descendants(id, parent_session_id, depth, path) as (
14876
15084
  select child.id, child.parent_session_id, 1, array[${sessionId}, child.id]
@@ -14885,7 +15093,7 @@ async function getSessionLineage(db, workspaceId, sessionId) {
14885
15093
  and descendants.depth < 5
14886
15094
  and not child.id = any(descendants.path)
14887
15095
  )
14888
- select id, parent_session_id as "parentSessionId", depth, path
15096
+ select id, parent_session_id as "parentSessionId", depth, path, false as cycle
14889
15097
  from descendants
14890
15098
  order by path
14891
15099
  limit ${descendantLimit + 1}
@@ -22694,6 +22902,12 @@ async function claimSessionWorkForAttempt(db, workspaceId, input) {
22694
22902
  and7(
22695
22903
  eq7(sessionAttemptInterruptions.workspaceId, workspaceId),
22696
22904
  eq7(sessionAttemptInterruptions.sessionId, sessionId),
22905
+ inArray3(sessionAttemptInterruptions.state, [
22906
+ "pending",
22907
+ "delivered",
22908
+ "acknowledged",
22909
+ "settled"
22910
+ ]),
22697
22911
  isNull2(sessionTurnAttempts.quiescedAt)
22698
22912
  )
22699
22913
  ).orderBy(desc2(sessionAttemptInterruptions.requestedAt)).limit(1);
@@ -22905,11 +23119,15 @@ async function claimSessionWorkForAttempt(db, workspaceId, input) {
22905
23119
  desc2(sessionSystemUpdates.createdAt),
22906
23120
  desc2(sessionSystemUpdates.id)
22907
23121
  ).limit(1).for("update");
22908
- const rows = pendingAgentSteer ? [] : await rawRows(
23122
+ const rows = await rawRows(
22909
23123
  tx,
22910
23124
  sql4`select id, trigger_event_id, metadata from session_turns
22911
23125
  where workspace_id = ${workspaceId} and session_id = ${sessionId}
22912
23126
  and status = 'queued' and source in ('user', 'api')
23127
+ and (
23128
+ ${Boolean(pendingAgentSteer)} = false
23129
+ or metadata->>'delivery' = 'steer'
23130
+ )
22913
23131
  order by position asc, created_at asc, id asc
22914
23132
  limit 1`
22915
23133
  );
@@ -25415,7 +25633,7 @@ async function getSessionQueueSnapshot(db, workspaceId, sessionId) {
25415
25633
  return {
25416
25634
  version: session.queueVersion,
25417
25635
  effectiveControl: serializeEffectiveSessionControl(effectiveControl),
25418
- stoppingPreviousAttempt: latestInterruption !== null && latestInterruption.quiescedAt === null,
25636
+ stoppingPreviousAttempt: latestInterruption !== null && latestInterruption.interruptionState !== "rejected_stale" && latestInterruption.quiescedAt === null,
25419
25637
  items: rows.map(mapSessionTurn)
25420
25638
  };
25421
25639
  });
@@ -26571,6 +26789,7 @@ function mapSession(row, effectiveControl, mcpServers = [], pin = mapSessionPin(
26571
26789
  titleSource: row.titleSource ?? null,
26572
26790
  instructions: row.instructions ?? null,
26573
26791
  resources: row.resources,
26792
+ skills: row.skills ?? [],
26574
26793
  tools: row.tools,
26575
26794
  toolPolicy: row.toolPolicy ?? {
26576
26795
  mode: "legacy",
@@ -27314,6 +27533,7 @@ export {
27314
27533
  SessionPinVersionConflictError,
27315
27534
  SessionSpawnDeniedDbError,
27316
27535
  SessionToolPolicyVersionConflictError,
27536
+ SlackBotLifecycleSuccessAuditError,
27317
27537
  WORKSPACE_MEMORY_BLOCK_EMPTY,
27318
27538
  WORKSPACE_MEMORY_BLOCK_HEADER_POPULATED,
27319
27539
  WORKSPACE_MEMORY_BLOCK_TOKEN_BUDGET,
@@ -27417,6 +27637,7 @@ export {
27417
27637
  countWorkspacesForAccount,
27418
27638
  createApiKey,
27419
27639
  createConnection,
27640
+ createConnectionWithSlackBotSuccessAudit,
27420
27641
  createDb,
27421
27642
  createDeviceEnrollmentRequest,
27422
27643
  createEnrollment,
@@ -27727,6 +27948,7 @@ export {
27727
27948
  recordRetainedProcessReconciliationProof,
27728
27949
  recordSessionActiveCodexCredential,
27729
27950
  recordSkippedContextCompaction,
27951
+ recordSlackBotInstallCallbackFailure,
27730
27952
  recordStreamAcknowledgment,
27731
27953
  recordStripeWebhookEvent,
27732
27954
  recordUsageEvent,
@@ -27765,6 +27987,7 @@ export {
27765
27987
  retireHostExportConsumer,
27766
27988
  revokeApiKey,
27767
27989
  revokeConnection,
27990
+ revokeConnectionWithSlackBotSuccessAudit,
27768
27991
  revokeEnrollment,
27769
27992
  revokeViewer,
27770
27993
  rewindHostExportConsumer,
@@ -27833,6 +28056,7 @@ export {
27833
28056
  updateCodexAllocatorEligibility,
27834
28057
  updateCodexRotationSettings,
27835
28058
  updateConnection,
28059
+ updateConnectionWithSlackBotSuccessAudit,
27836
28060
  updateImportBatchCounts,
27837
28061
  updateKnowledgeMemory,
27838
28062
  updatePackInstallationStatus,