@opengeni/db 4.3.3-canary.2 → 4.4.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ import { RotateSessionMcpCredentialsReceipt } from "@opengeni/contracts";
2
+ import { type Database } from "./database.js";
3
+ export declare class SessionMcpCredentialRotationError extends Error {
4
+ readonly code: "invalid_request" | "not_found" | "not_quiescent" | "version_conflict" | "destination_conflict" | "brokered_server" | "operation_reuse" | "receipt_key_unavailable" | "authority_revoked";
5
+ constructor(code: "invalid_request" | "not_found" | "not_quiescent" | "version_conflict" | "destination_conflict" | "brokered_server" | "operation_reuse" | "receipt_key_unavailable" | "authority_revoked");
6
+ }
7
+ export type AtomicSessionMcpCredentialRotationInput = {
8
+ accountId: string;
9
+ workspaceId: string;
10
+ sessionId: string;
11
+ subjectId: string;
12
+ actorType: "human" | "service";
13
+ operationKey: string;
14
+ requestDigest: string;
15
+ digestKeyTag: string;
16
+ updates: Array<{
17
+ id: string;
18
+ expectedCredentialVersion: number;
19
+ expectedServerUrl: string;
20
+ headersEncrypted: Record<string, string>;
21
+ }>;
22
+ /** Trusted request authorizer, mandatory even for a committed receipt replay.
23
+ * Revalidate the original authenticated subject and permissions on this tx. */
24
+ authorize: (tx: Database) => Promise<void>;
25
+ };
26
+ /** Existing command receipts own durable idempotency; this is not a new queue
27
+ * command. No session/event/history/attempt/control/wake rows are mutated. */
28
+ export declare function rotateSessionMcpCredentialsAtomically(db: Database, input: AtomicSessionMcpCredentialRotationInput): Promise<RotateSessionMcpCredentialsReceipt>;
@@ -0,0 +1,185 @@
1
+ import {
2
+ lockSessionEventWriteRows
3
+ } from "./chunk-EYF6MH6C.js";
4
+ import "./chunk-QLA3UQNX.js";
5
+ import {
6
+ rawRows,
7
+ setSubjectRlsContext,
8
+ withRlsContext
9
+ } from "./chunk-GVZJ7XSR.js";
10
+ import {
11
+ apiKeys,
12
+ sessionCommandReceipts,
13
+ sessionMcpServers,
14
+ sessions
15
+ } from "./chunk-6TKL3GQO.js";
16
+ import "./chunk-PZ5AY32C.js";
17
+
18
+ // src/session-mcp-credential-rotation.ts
19
+ import { and, eq, inArray, sql } from "drizzle-orm";
20
+ import { RotateSessionMcpCredentialsReceipt, SessionTurnStatus } from "@opengeni/contracts";
21
+ var action = "session.mcp.credentials.rotate";
22
+ var liveTurnStatuses = SessionTurnStatus.exclude([
23
+ "completed",
24
+ "failed",
25
+ "cancelled",
26
+ "superseded",
27
+ "withdrawn_for_edit"
28
+ ]).options;
29
+ var SessionMcpCredentialRotationError = class extends Error {
30
+ constructor(code) {
31
+ super(code);
32
+ this.code = code;
33
+ this.name = "SessionMcpCredentialRotationError";
34
+ }
35
+ };
36
+ async function rotateSessionMcpCredentialsAtomically(db, input) {
37
+ if (!input.updates.length || input.updates.length > 64 || new Set(input.updates.map((update) => update.id)).size !== input.updates.length || !/^[a-f0-9]{64}$/.test(input.requestDigest) || !/^[a-f0-9]{64}$/.test(input.digestKeyTag)) {
38
+ throw new SessionMcpCredentialRotationError("invalid_request");
39
+ }
40
+ return withRlsContext(
41
+ db,
42
+ input,
43
+ async (tx) => {
44
+ await tx.execute(sql`select pg_advisory_xact_lock(hashtextextended(
45
+ ${`organization-membership:${input.accountId}`}, 0))`);
46
+ await tx.execute(sql`select pg_advisory_xact_lock_shared(hashtextextended(
47
+ ${`session-tenancy:${input.workspaceId}`}, 0))`);
48
+ await setSubjectRlsContext(tx, input.subjectId);
49
+ const [target] = await tx.select({ id: sessions.id }).from(sessions).where(
50
+ and(
51
+ eq(sessions.accountId, input.accountId),
52
+ eq(sessions.workspaceId, input.workspaceId),
53
+ eq(sessions.id, input.sessionId)
54
+ )
55
+ );
56
+ if (!target) throw new SessionMcpCredentialRotationError("not_found");
57
+ const locks = await lockSessionEventWriteRows(tx, {
58
+ workspaceId: input.workspaceId,
59
+ controlLock: "share",
60
+ sessionIds: [input.sessionId]
61
+ });
62
+ const session = locks.sessions[0];
63
+ if (!session || session.accountId !== input.accountId) {
64
+ throw new SessionMcpCredentialRotationError("not_found");
65
+ }
66
+ if (input.subjectId.startsWith("api_key:")) {
67
+ const keyId = input.subjectId.slice("api_key:".length);
68
+ const [key] = await tx.select({ id: apiKeys.id, permissions: apiKeys.permissions }).from(apiKeys).where(
69
+ and(
70
+ eq(apiKeys.id, keyId),
71
+ eq(apiKeys.accountId, input.accountId),
72
+ sql`(${apiKeys.workspaceId} is null or ${apiKeys.workspaceId} = ${input.workspaceId}::uuid)`,
73
+ sql`${apiKeys.revokedAt} is null`,
74
+ sql`(${apiKeys.expiresAt} is null or ${apiKeys.expiresAt} > clock_timestamp())`
75
+ )
76
+ ).for("update");
77
+ if (!key || !key.permissions.includes("workspace:admin") && (!key.permissions.includes("sessions:control") || !key.permissions.includes("mcp_servers:attach")))
78
+ throw new SessionMcpCredentialRotationError("authority_revoked");
79
+ }
80
+ await input.authorize(tx);
81
+ const [prior] = await tx.select().from(sessionCommandReceipts).where(
82
+ and(
83
+ eq(sessionCommandReceipts.accountId, input.accountId),
84
+ eq(sessionCommandReceipts.workspaceId, input.workspaceId),
85
+ eq(sessionCommandReceipts.targetSessionId, input.sessionId),
86
+ eq(sessionCommandReceipts.actorType, input.actorType),
87
+ eq(sessionCommandReceipts.actorSubjectId, input.subjectId),
88
+ eq(sessionCommandReceipts.action, action),
89
+ eq(sessionCommandReceipts.operationKey, input.operationKey)
90
+ )
91
+ );
92
+ if (prior) {
93
+ if (prior.result.digestKeyTag !== input.digestKeyTag)
94
+ throw new SessionMcpCredentialRotationError("receipt_key_unavailable");
95
+ if (prior.canonicalRequestHash !== input.requestDigest)
96
+ throw new SessionMcpCredentialRotationError("operation_reuse");
97
+ return RotateSessionMcpCredentialsReceipt.parse(prior.result.receipt);
98
+ }
99
+ const [busy] = await rawRows(
100
+ tx,
101
+ sql`select (
102
+ exists(select 1 from session_turns where workspace_id = ${input.workspaceId}::uuid
103
+ and session_id = ${input.sessionId}::uuid and status in (${sql.join(
104
+ liveTurnStatuses.map((status) => sql`${status}`),
105
+ sql`, `
106
+ )}))
107
+ or exists(select 1 from session_turn_attempts a where a.workspace_id = ${input.workspaceId}::uuid
108
+ and a.session_id = ${input.sessionId}::uuid and (a.state <> 'closed' or
109
+ (a.quiesced_at is null and exists(select 1 from session_attempt_interruptions i
110
+ where i.workspace_id = a.workspace_id and i.attempt_id = a.id))))
111
+ or exists(select 1 from session_system_updates where workspace_id = ${input.workspaceId}::uuid
112
+ and session_id = ${input.sessionId}::uuid and state = 'pending')
113
+ or exists(select 1 from sandbox_workspace_mutation_admissions where workspace_id = ${input.workspaceId}::uuid
114
+ and session_id = ${input.sessionId}::uuid and attempt_id is not null and settled_at is null)
115
+ or exists(select 1 from session_realtime_modes where workspace_id = ${input.workspaceId}::uuid
116
+ and session_id = ${input.sessionId}::uuid and state = 'active')
117
+ or exists(select 1 from session_realtime_connections where workspace_id = ${input.workspaceId}::uuid
118
+ and session_id = ${input.sessionId}::uuid and state not in ('failed', 'closed'))
119
+ ) as present`
120
+ );
121
+ if (!busy || busy.present) throw new SessionMcpCredentialRotationError("not_quiescent");
122
+ const rows = await tx.select().from(sessionMcpServers).where(
123
+ and(
124
+ eq(sessionMcpServers.workspaceId, input.workspaceId),
125
+ eq(sessionMcpServers.sessionId, input.sessionId),
126
+ inArray(
127
+ sessionMcpServers.serverId,
128
+ input.updates.map((update) => update.id)
129
+ )
130
+ )
131
+ ).orderBy(sessionMcpServers.serverId).for("update");
132
+ for (const update of input.updates) {
133
+ const row = rows.find((server) => server.serverId === update.id);
134
+ if (!row || row.accountId !== input.accountId)
135
+ throw new SessionMcpCredentialRotationError("not_found");
136
+ if (row.connectionRef) throw new SessionMcpCredentialRotationError("brokered_server");
137
+ if (row.url !== update.expectedServerUrl)
138
+ throw new SessionMcpCredentialRotationError("destination_conflict");
139
+ if (!Number.isInteger(update.expectedCredentialVersion) || update.expectedCredentialVersion < 1 || update.expectedCredentialVersion >= 2147483647 || row.credentialVersion !== update.expectedCredentialVersion)
140
+ throw new SessionMcpCredentialRotationError("version_conflict");
141
+ }
142
+ const receipt = {
143
+ operationKey: input.operationKey,
144
+ sessionId: input.sessionId,
145
+ appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
146
+ servers: input.updates.map((update) => ({
147
+ id: update.id,
148
+ credentialVersion: update.expectedCredentialVersion + 1
149
+ }))
150
+ };
151
+ for (const update of input.updates) {
152
+ await tx.update(sessionMcpServers).set({
153
+ headersEncrypted: update.headersEncrypted,
154
+ credentialVersion: update.expectedCredentialVersion + 1,
155
+ updatedAt: new Date(receipt.appliedAt)
156
+ }).where(
157
+ and(
158
+ eq(sessionMcpServers.workspaceId, input.workspaceId),
159
+ eq(sessionMcpServers.sessionId, input.sessionId),
160
+ eq(sessionMcpServers.serverId, update.id)
161
+ )
162
+ );
163
+ }
164
+ await tx.insert(sessionCommandReceipts).values({
165
+ accountId: input.accountId,
166
+ workspaceId: input.workspaceId,
167
+ actorType: input.actorType,
168
+ actorSubjectId: input.subjectId,
169
+ action,
170
+ targetSessionId: input.sessionId,
171
+ operationKey: input.operationKey,
172
+ canonicalRequestHash: input.requestDigest,
173
+ result: { digestKeyTag: input.digestKeyTag, receipt }
174
+ });
175
+ return receipt;
176
+ },
177
+ void 0,
178
+ "none"
179
+ );
180
+ }
181
+ export {
182
+ SessionMcpCredentialRotationError,
183
+ rotateSessionMcpCredentialsAtomically
184
+ };
185
+ //# sourceMappingURL=session-mcp-credential-rotation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/session-mcp-credential-rotation.ts"],"sourcesContent":["import { and, eq, inArray, sql } from \"drizzle-orm\";\nimport { RotateSessionMcpCredentialsReceipt, SessionTurnStatus } from \"@opengeni/contracts\";\nimport { rawRows, setSubjectRlsContext, withRlsContext, type Database } from \"./database\";\nimport { lockSessionEventWriteRows } from \"./session-control\";\nimport * as schema from \"./schema\";\n\nconst action = \"session.mcp.credentials.rotate\";\nconst liveTurnStatuses = SessionTurnStatus.exclude([\n \"completed\",\n \"failed\",\n \"cancelled\",\n \"superseded\",\n \"withdrawn_for_edit\",\n]).options;\n\nexport class SessionMcpCredentialRotationError extends Error {\n constructor(\n readonly code:\n | \"invalid_request\"\n | \"not_found\"\n | \"not_quiescent\"\n | \"version_conflict\"\n | \"destination_conflict\"\n | \"brokered_server\"\n | \"operation_reuse\"\n | \"receipt_key_unavailable\"\n | \"authority_revoked\",\n ) {\n super(code);\n this.name = \"SessionMcpCredentialRotationError\";\n }\n}\n\nexport type AtomicSessionMcpCredentialRotationInput = {\n accountId: string;\n workspaceId: string;\n sessionId: string;\n subjectId: string;\n actorType: \"human\" | \"service\";\n operationKey: string;\n requestDigest: string;\n digestKeyTag: string;\n updates: Array<{\n id: string;\n expectedCredentialVersion: number;\n expectedServerUrl: string;\n headersEncrypted: Record<string, string>;\n }>;\n /** Trusted request authorizer, mandatory even for a committed receipt replay.\n * Revalidate the original authenticated subject and permissions on this tx. */\n authorize: (tx: Database) => Promise<void>;\n};\n\n/** Existing command receipts own durable idempotency; this is not a new queue\n * command. No session/event/history/attempt/control/wake rows are mutated. */\nexport async function rotateSessionMcpCredentialsAtomically(\n db: Database,\n input: AtomicSessionMcpCredentialRotationInput,\n): Promise<RotateSessionMcpCredentialsReceipt> {\n if (\n !input.updates.length ||\n input.updates.length > 64 ||\n new Set(input.updates.map((update) => update.id)).size !== input.updates.length ||\n !/^[a-f0-9]{64}$/.test(input.requestDigest) ||\n !/^[a-f0-9]{64}$/.test(input.digestKeyTag)\n ) {\n throw new SessionMcpCredentialRotationError(\"invalid_request\");\n }\n return withRlsContext(\n db,\n input,\n async (tx) => {\n // Membership before tenancy/control/session is the canonical claim/removal\n // order. External continuation authorization reacquires this exclusive\n // membership fence, so taking shared first would introduce an upgrade.\n // Do not upgrade a previously acquired shared tenancy lock.\n await tx.execute(sql`select pg_advisory_xact_lock(hashtextextended(\n ${`organization-membership:${input.accountId}`}, 0))`);\n await tx.execute(sql`select pg_advisory_xact_lock_shared(hashtextextended(\n ${`session-tenancy:${input.workspaceId}`}, 0))`);\n await setSubjectRlsContext(tx, input.subjectId);\n const [target] = await tx\n .select({ id: schema.sessions.id })\n .from(schema.sessions)\n .where(\n and(\n eq(schema.sessions.accountId, input.accountId),\n eq(schema.sessions.workspaceId, input.workspaceId),\n eq(schema.sessions.id, input.sessionId),\n ),\n );\n if (!target) throw new SessionMcpCredentialRotationError(\"not_found\");\n const locks = await lockSessionEventWriteRows(tx, {\n workspaceId: input.workspaceId,\n controlLock: \"share\",\n sessionIds: [input.sessionId],\n });\n const session = locks.sessions[0];\n if (!session || session.accountId !== input.accountId) {\n throw new SessionMcpCredentialRotationError(\"not_found\");\n }\n // Hold a current API key through commit. The ordinary access resolver's\n // last-used write alone does not recheck a revocation racing its first read.\n // Take UPDATE directly because fresh resolution writes lastUsedAt.\n if (input.subjectId.startsWith(\"api_key:\")) {\n const keyId = input.subjectId.slice(\"api_key:\".length);\n const [key] = await tx\n .select({ id: schema.apiKeys.id, permissions: schema.apiKeys.permissions })\n .from(schema.apiKeys)\n .where(\n and(\n eq(schema.apiKeys.id, keyId),\n eq(schema.apiKeys.accountId, input.accountId),\n sql`(${schema.apiKeys.workspaceId} is null or ${schema.apiKeys.workspaceId} = ${input.workspaceId}::uuid)`,\n sql`${schema.apiKeys.revokedAt} is null`,\n sql`(${schema.apiKeys.expiresAt} is null or ${schema.apiKeys.expiresAt} > clock_timestamp())`,\n ),\n )\n .for(\"update\");\n if (\n !key ||\n (!key.permissions.includes(\"workspace:admin\") &&\n (!key.permissions.includes(\"sessions:control\") ||\n !key.permissions.includes(\"mcp_servers:attach\")))\n )\n throw new SessionMcpCredentialRotationError(\"authority_revoked\");\n }\n await input.authorize(tx);\n const [prior] = await tx\n .select()\n .from(schema.sessionCommandReceipts)\n .where(\n and(\n eq(schema.sessionCommandReceipts.accountId, input.accountId),\n eq(schema.sessionCommandReceipts.workspaceId, input.workspaceId),\n eq(schema.sessionCommandReceipts.targetSessionId, input.sessionId),\n eq(schema.sessionCommandReceipts.actorType, input.actorType),\n eq(schema.sessionCommandReceipts.actorSubjectId, input.subjectId),\n eq(schema.sessionCommandReceipts.action, action),\n eq(schema.sessionCommandReceipts.operationKey, input.operationKey),\n ),\n );\n if (prior) {\n if (prior.result.digestKeyTag !== input.digestKeyTag)\n throw new SessionMcpCredentialRotationError(\"receipt_key_unavailable\");\n if (prior.canonicalRequestHash !== input.requestDigest)\n throw new SessionMcpCredentialRotationError(\"operation_reuse\");\n return RotateSessionMcpCredentialsReceipt.parse(prior.result.receipt);\n }\n // Session lock serializes admission, claim, realtime start, interruption\n // settlement, and attempt-owned provider admission. Dormant goals/schedules\n // and unrelated viewers are deliberately not credential consumers.\n // Retained run snapshots and tool receipts need a live turn/attempt to\n // consume credentials; their historical presence alone is not activity.\n const [busy] = await rawRows<{ present: boolean }>(\n tx,\n sql`select (\n exists(select 1 from session_turns where workspace_id = ${input.workspaceId}::uuid\n and session_id = ${input.sessionId}::uuid and status in (${sql.join(\n liveTurnStatuses.map((status) => sql`${status}`),\n sql`, `,\n )}))\n or exists(select 1 from session_turn_attempts a where a.workspace_id = ${input.workspaceId}::uuid\n and a.session_id = ${input.sessionId}::uuid and (a.state <> 'closed' or\n (a.quiesced_at is null and exists(select 1 from session_attempt_interruptions i\n where i.workspace_id = a.workspace_id and i.attempt_id = a.id))))\n or exists(select 1 from session_system_updates where workspace_id = ${input.workspaceId}::uuid\n and session_id = ${input.sessionId}::uuid and state = 'pending')\n or exists(select 1 from sandbox_workspace_mutation_admissions where workspace_id = ${input.workspaceId}::uuid\n and session_id = ${input.sessionId}::uuid and attempt_id is not null and settled_at is null)\n or exists(select 1 from session_realtime_modes where workspace_id = ${input.workspaceId}::uuid\n and session_id = ${input.sessionId}::uuid and state = 'active')\n or exists(select 1 from session_realtime_connections where workspace_id = ${input.workspaceId}::uuid\n and session_id = ${input.sessionId}::uuid and state not in ('failed', 'closed'))\n ) as present`,\n );\n if (!busy || busy.present) throw new SessionMcpCredentialRotationError(\"not_quiescent\");\n const rows = await tx\n .select()\n .from(schema.sessionMcpServers)\n .where(\n and(\n eq(schema.sessionMcpServers.workspaceId, input.workspaceId),\n eq(schema.sessionMcpServers.sessionId, input.sessionId),\n inArray(\n schema.sessionMcpServers.serverId,\n input.updates.map((update) => update.id),\n ),\n ),\n )\n .orderBy(schema.sessionMcpServers.serverId)\n .for(\"update\");\n for (const update of input.updates) {\n const row = rows.find((server) => server.serverId === update.id);\n if (!row || row.accountId !== input.accountId)\n throw new SessionMcpCredentialRotationError(\"not_found\");\n if (row.connectionRef) throw new SessionMcpCredentialRotationError(\"brokered_server\");\n if (row.url !== update.expectedServerUrl)\n throw new SessionMcpCredentialRotationError(\"destination_conflict\");\n if (\n !Number.isInteger(update.expectedCredentialVersion) ||\n update.expectedCredentialVersion < 1 ||\n update.expectedCredentialVersion >= 2_147_483_647 ||\n row.credentialVersion !== update.expectedCredentialVersion\n )\n throw new SessionMcpCredentialRotationError(\"version_conflict\");\n }\n const receipt: RotateSessionMcpCredentialsReceipt = {\n operationKey: input.operationKey,\n sessionId: input.sessionId,\n appliedAt: new Date().toISOString(),\n servers: input.updates.map((update) => ({\n id: update.id,\n credentialVersion: update.expectedCredentialVersion + 1,\n })),\n };\n for (const update of input.updates) {\n await tx\n .update(schema.sessionMcpServers)\n .set({\n headersEncrypted: update.headersEncrypted,\n credentialVersion: update.expectedCredentialVersion + 1,\n updatedAt: new Date(receipt.appliedAt),\n })\n .where(\n and(\n eq(schema.sessionMcpServers.workspaceId, input.workspaceId),\n eq(schema.sessionMcpServers.sessionId, input.sessionId),\n eq(schema.sessionMcpServers.serverId, update.id),\n ),\n );\n }\n await tx.insert(schema.sessionCommandReceipts).values({\n accountId: input.accountId,\n workspaceId: input.workspaceId,\n actorType: input.actorType,\n actorSubjectId: input.subjectId,\n action,\n targetSessionId: input.sessionId,\n operationKey: input.operationKey,\n canonicalRequestHash: input.requestDigest,\n result: { digestKeyTag: input.digestKeyTag, receipt },\n });\n return receipt;\n },\n undefined,\n \"none\",\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/db",
3
- "version": "4.3.3-canary.2",
3
+ "version": "4.4.0-canary.0",
4
4
  "description": "OpenGeni persistence: Drizzle schema, RLS-scoped query layer, the SQL migration runner, and role provisioning.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -19,6 +19,10 @@
19
19
  "module": "./dist/index.js",
20
20
  "types": "./dist/index.d.ts",
21
21
  "exports": {
22
+ "./session-mcp-credential-rotation": {
23
+ "types": "./dist/session-mcp-credential-rotation.d.ts",
24
+ "import": "./dist/session-mcp-credential-rotation.js"
25
+ },
22
26
  "./mcp-operations": {
23
27
  "types": "./dist/mcp-operations.d.ts",
24
28
  "import": "./dist/mcp-operations.js"
@@ -98,11 +102,11 @@
98
102
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
99
103
  },
100
104
  "dependencies": {
101
- "@opengeni/codemode": "^0.5.6-canary.2",
102
- "@opengeni/codex": "^0.2.22-canary.10",
103
- "@opengeni/config": "^1.1.2-canary.2",
104
- "@opengeni/contracts": "^3.0.2-canary.2",
105
- "@opengeni/network": "^0.3.1-canary.10",
105
+ "@opengeni/codemode": "^0.5.7-canary.0",
106
+ "@opengeni/codex": "^0.2.22-canary.12",
107
+ "@opengeni/config": "^1.2.0-canary.0",
108
+ "@opengeni/contracts": "^3.1.0-canary.0",
109
+ "@opengeni/network": "^0.3.1-canary.12",
106
110
  "drizzle-orm": "^0.45.2",
107
111
  "postgres": "^3.4.7"
108
112
  },
@@ -0,0 +1,249 @@
1
+ import { and, eq, inArray, sql } from "drizzle-orm";
2
+ import { RotateSessionMcpCredentialsReceipt, SessionTurnStatus } from "@opengeni/contracts";
3
+ import { rawRows, setSubjectRlsContext, withRlsContext, type Database } from "./database";
4
+ import { lockSessionEventWriteRows } from "./session-control";
5
+ import * as schema from "./schema";
6
+
7
+ const action = "session.mcp.credentials.rotate";
8
+ const liveTurnStatuses = SessionTurnStatus.exclude([
9
+ "completed",
10
+ "failed",
11
+ "cancelled",
12
+ "superseded",
13
+ "withdrawn_for_edit",
14
+ ]).options;
15
+
16
+ export class SessionMcpCredentialRotationError extends Error {
17
+ constructor(
18
+ readonly code:
19
+ | "invalid_request"
20
+ | "not_found"
21
+ | "not_quiescent"
22
+ | "version_conflict"
23
+ | "destination_conflict"
24
+ | "brokered_server"
25
+ | "operation_reuse"
26
+ | "receipt_key_unavailable"
27
+ | "authority_revoked",
28
+ ) {
29
+ super(code);
30
+ this.name = "SessionMcpCredentialRotationError";
31
+ }
32
+ }
33
+
34
+ export type AtomicSessionMcpCredentialRotationInput = {
35
+ accountId: string;
36
+ workspaceId: string;
37
+ sessionId: string;
38
+ subjectId: string;
39
+ actorType: "human" | "service";
40
+ operationKey: string;
41
+ requestDigest: string;
42
+ digestKeyTag: string;
43
+ updates: Array<{
44
+ id: string;
45
+ expectedCredentialVersion: number;
46
+ expectedServerUrl: string;
47
+ headersEncrypted: Record<string, string>;
48
+ }>;
49
+ /** Trusted request authorizer, mandatory even for a committed receipt replay.
50
+ * Revalidate the original authenticated subject and permissions on this tx. */
51
+ authorize: (tx: Database) => Promise<void>;
52
+ };
53
+
54
+ /** Existing command receipts own durable idempotency; this is not a new queue
55
+ * command. No session/event/history/attempt/control/wake rows are mutated. */
56
+ export async function rotateSessionMcpCredentialsAtomically(
57
+ db: Database,
58
+ input: AtomicSessionMcpCredentialRotationInput,
59
+ ): Promise<RotateSessionMcpCredentialsReceipt> {
60
+ if (
61
+ !input.updates.length ||
62
+ input.updates.length > 64 ||
63
+ new Set(input.updates.map((update) => update.id)).size !== input.updates.length ||
64
+ !/^[a-f0-9]{64}$/.test(input.requestDigest) ||
65
+ !/^[a-f0-9]{64}$/.test(input.digestKeyTag)
66
+ ) {
67
+ throw new SessionMcpCredentialRotationError("invalid_request");
68
+ }
69
+ return withRlsContext(
70
+ db,
71
+ input,
72
+ async (tx) => {
73
+ // Membership before tenancy/control/session is the canonical claim/removal
74
+ // order. External continuation authorization reacquires this exclusive
75
+ // membership fence, so taking shared first would introduce an upgrade.
76
+ // Do not upgrade a previously acquired shared tenancy lock.
77
+ await tx.execute(sql`select pg_advisory_xact_lock(hashtextextended(
78
+ ${`organization-membership:${input.accountId}`}, 0))`);
79
+ await tx.execute(sql`select pg_advisory_xact_lock_shared(hashtextextended(
80
+ ${`session-tenancy:${input.workspaceId}`}, 0))`);
81
+ await setSubjectRlsContext(tx, input.subjectId);
82
+ const [target] = await tx
83
+ .select({ id: schema.sessions.id })
84
+ .from(schema.sessions)
85
+ .where(
86
+ and(
87
+ eq(schema.sessions.accountId, input.accountId),
88
+ eq(schema.sessions.workspaceId, input.workspaceId),
89
+ eq(schema.sessions.id, input.sessionId),
90
+ ),
91
+ );
92
+ if (!target) throw new SessionMcpCredentialRotationError("not_found");
93
+ const locks = await lockSessionEventWriteRows(tx, {
94
+ workspaceId: input.workspaceId,
95
+ controlLock: "share",
96
+ sessionIds: [input.sessionId],
97
+ });
98
+ const session = locks.sessions[0];
99
+ if (!session || session.accountId !== input.accountId) {
100
+ throw new SessionMcpCredentialRotationError("not_found");
101
+ }
102
+ // Hold a current API key through commit. The ordinary access resolver's
103
+ // last-used write alone does not recheck a revocation racing its first read.
104
+ // Take UPDATE directly because fresh resolution writes lastUsedAt.
105
+ if (input.subjectId.startsWith("api_key:")) {
106
+ const keyId = input.subjectId.slice("api_key:".length);
107
+ const [key] = await tx
108
+ .select({ id: schema.apiKeys.id, permissions: schema.apiKeys.permissions })
109
+ .from(schema.apiKeys)
110
+ .where(
111
+ and(
112
+ eq(schema.apiKeys.id, keyId),
113
+ eq(schema.apiKeys.accountId, input.accountId),
114
+ sql`(${schema.apiKeys.workspaceId} is null or ${schema.apiKeys.workspaceId} = ${input.workspaceId}::uuid)`,
115
+ sql`${schema.apiKeys.revokedAt} is null`,
116
+ sql`(${schema.apiKeys.expiresAt} is null or ${schema.apiKeys.expiresAt} > clock_timestamp())`,
117
+ ),
118
+ )
119
+ .for("update");
120
+ if (
121
+ !key ||
122
+ (!key.permissions.includes("workspace:admin") &&
123
+ (!key.permissions.includes("sessions:control") ||
124
+ !key.permissions.includes("mcp_servers:attach")))
125
+ )
126
+ throw new SessionMcpCredentialRotationError("authority_revoked");
127
+ }
128
+ await input.authorize(tx);
129
+ const [prior] = await tx
130
+ .select()
131
+ .from(schema.sessionCommandReceipts)
132
+ .where(
133
+ and(
134
+ eq(schema.sessionCommandReceipts.accountId, input.accountId),
135
+ eq(schema.sessionCommandReceipts.workspaceId, input.workspaceId),
136
+ eq(schema.sessionCommandReceipts.targetSessionId, input.sessionId),
137
+ eq(schema.sessionCommandReceipts.actorType, input.actorType),
138
+ eq(schema.sessionCommandReceipts.actorSubjectId, input.subjectId),
139
+ eq(schema.sessionCommandReceipts.action, action),
140
+ eq(schema.sessionCommandReceipts.operationKey, input.operationKey),
141
+ ),
142
+ );
143
+ if (prior) {
144
+ if (prior.result.digestKeyTag !== input.digestKeyTag)
145
+ throw new SessionMcpCredentialRotationError("receipt_key_unavailable");
146
+ if (prior.canonicalRequestHash !== input.requestDigest)
147
+ throw new SessionMcpCredentialRotationError("operation_reuse");
148
+ return RotateSessionMcpCredentialsReceipt.parse(prior.result.receipt);
149
+ }
150
+ // Session lock serializes admission, claim, realtime start, interruption
151
+ // settlement, and attempt-owned provider admission. Dormant goals/schedules
152
+ // and unrelated viewers are deliberately not credential consumers.
153
+ // Retained run snapshots and tool receipts need a live turn/attempt to
154
+ // consume credentials; their historical presence alone is not activity.
155
+ const [busy] = await rawRows<{ present: boolean }>(
156
+ tx,
157
+ sql`select (
158
+ exists(select 1 from session_turns where workspace_id = ${input.workspaceId}::uuid
159
+ and session_id = ${input.sessionId}::uuid and status in (${sql.join(
160
+ liveTurnStatuses.map((status) => sql`${status}`),
161
+ sql`, `,
162
+ )}))
163
+ or exists(select 1 from session_turn_attempts a where a.workspace_id = ${input.workspaceId}::uuid
164
+ and a.session_id = ${input.sessionId}::uuid and (a.state <> 'closed' or
165
+ (a.quiesced_at is null and exists(select 1 from session_attempt_interruptions i
166
+ where i.workspace_id = a.workspace_id and i.attempt_id = a.id))))
167
+ or exists(select 1 from session_system_updates where workspace_id = ${input.workspaceId}::uuid
168
+ and session_id = ${input.sessionId}::uuid and state = 'pending')
169
+ or exists(select 1 from sandbox_workspace_mutation_admissions where workspace_id = ${input.workspaceId}::uuid
170
+ and session_id = ${input.sessionId}::uuid and attempt_id is not null and settled_at is null)
171
+ or exists(select 1 from session_realtime_modes where workspace_id = ${input.workspaceId}::uuid
172
+ and session_id = ${input.sessionId}::uuid and state = 'active')
173
+ or exists(select 1 from session_realtime_connections where workspace_id = ${input.workspaceId}::uuid
174
+ and session_id = ${input.sessionId}::uuid and state not in ('failed', 'closed'))
175
+ ) as present`,
176
+ );
177
+ if (!busy || busy.present) throw new SessionMcpCredentialRotationError("not_quiescent");
178
+ const rows = await tx
179
+ .select()
180
+ .from(schema.sessionMcpServers)
181
+ .where(
182
+ and(
183
+ eq(schema.sessionMcpServers.workspaceId, input.workspaceId),
184
+ eq(schema.sessionMcpServers.sessionId, input.sessionId),
185
+ inArray(
186
+ schema.sessionMcpServers.serverId,
187
+ input.updates.map((update) => update.id),
188
+ ),
189
+ ),
190
+ )
191
+ .orderBy(schema.sessionMcpServers.serverId)
192
+ .for("update");
193
+ for (const update of input.updates) {
194
+ const row = rows.find((server) => server.serverId === update.id);
195
+ if (!row || row.accountId !== input.accountId)
196
+ throw new SessionMcpCredentialRotationError("not_found");
197
+ if (row.connectionRef) throw new SessionMcpCredentialRotationError("brokered_server");
198
+ if (row.url !== update.expectedServerUrl)
199
+ throw new SessionMcpCredentialRotationError("destination_conflict");
200
+ if (
201
+ !Number.isInteger(update.expectedCredentialVersion) ||
202
+ update.expectedCredentialVersion < 1 ||
203
+ update.expectedCredentialVersion >= 2_147_483_647 ||
204
+ row.credentialVersion !== update.expectedCredentialVersion
205
+ )
206
+ throw new SessionMcpCredentialRotationError("version_conflict");
207
+ }
208
+ const receipt: RotateSessionMcpCredentialsReceipt = {
209
+ operationKey: input.operationKey,
210
+ sessionId: input.sessionId,
211
+ appliedAt: new Date().toISOString(),
212
+ servers: input.updates.map((update) => ({
213
+ id: update.id,
214
+ credentialVersion: update.expectedCredentialVersion + 1,
215
+ })),
216
+ };
217
+ for (const update of input.updates) {
218
+ await tx
219
+ .update(schema.sessionMcpServers)
220
+ .set({
221
+ headersEncrypted: update.headersEncrypted,
222
+ credentialVersion: update.expectedCredentialVersion + 1,
223
+ updatedAt: new Date(receipt.appliedAt),
224
+ })
225
+ .where(
226
+ and(
227
+ eq(schema.sessionMcpServers.workspaceId, input.workspaceId),
228
+ eq(schema.sessionMcpServers.sessionId, input.sessionId),
229
+ eq(schema.sessionMcpServers.serverId, update.id),
230
+ ),
231
+ );
232
+ }
233
+ await tx.insert(schema.sessionCommandReceipts).values({
234
+ accountId: input.accountId,
235
+ workspaceId: input.workspaceId,
236
+ actorType: input.actorType,
237
+ actorSubjectId: input.subjectId,
238
+ action,
239
+ targetSessionId: input.sessionId,
240
+ operationKey: input.operationKey,
241
+ canonicalRequestHash: input.requestDigest,
242
+ result: { digestKeyTag: input.digestKeyTag, receipt },
243
+ });
244
+ return receipt;
245
+ },
246
+ undefined,
247
+ "none",
248
+ );
249
+ }