@opengeni/db 3.7.4 → 4.0.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.
Files changed (113) hide show
  1. package/dist/automations.d.ts +4 -0
  2. package/dist/canonical-human-identities.js +3 -3
  3. package/dist/capability-integrations.d.ts +2 -0
  4. package/dist/{chunk-6DPJA36C.js → chunk-2XEZK4LF.js} +7 -1
  5. package/dist/chunk-2XEZK4LF.js.map +1 -0
  6. package/dist/{chunk-4NAR7FBA.js → chunk-3RI33YQN.js} +67 -1
  7. package/dist/chunk-3RI33YQN.js.map +1 -0
  8. package/dist/{chunk-SZAWDXQC.js → chunk-CJROYHSC.js} +3 -3
  9. package/dist/{chunk-DZCYW2WT.js → chunk-MX6V5F65.js} +2 -2
  10. package/dist/{chunk-S3TDDCS2.js → chunk-N5VXJ4I2.js} +151 -78
  11. package/dist/chunk-N5VXJ4I2.js.map +1 -0
  12. package/dist/{chunk-U4WPPEBH.js → chunk-NLFHMPUB.js} +2 -2
  13. package/dist/{chunk-5NNCGFQY.js → chunk-P6NSEG2S.js} +2 -2
  14. package/dist/{chunk-O56DM5GZ.js → chunk-QM6YOWHZ.js} +624 -57
  15. package/dist/chunk-QM6YOWHZ.js.map +1 -0
  16. package/dist/{chunk-US4IS6IV.js → chunk-RZNOSEMC.js} +2 -2
  17. package/dist/{chunk-F6PBS6WE.js → chunk-UFAMOUWL.js} +2 -2
  18. package/dist/codemode-operations.d.ts +5 -0
  19. package/dist/codex-token-resolver.d.ts +6 -2
  20. package/dist/connection-token-resolver.d.ts +8 -0
  21. package/dist/editable-artifact-durable-export.js +2 -2
  22. package/dist/editable-artifacts.js +3 -3
  23. package/dist/generated-images.d.ts +15 -0
  24. package/dist/index.d.ts +393 -91
  25. package/dist/index.js +9992 -6069
  26. package/dist/index.js.map +1 -1
  27. package/dist/insights.d.ts +9 -0
  28. package/dist/managed-auth-session-sets.js +3 -3
  29. package/dist/managed-user-setup.d.ts +3 -0
  30. package/dist/mcp-oauth.d.ts +80 -0
  31. package/dist/migrate.d.ts +1 -0
  32. package/dist/migrate.js +3 -1
  33. package/dist/model-catalog.d.ts +153 -0
  34. package/dist/model-context-snapshots.d.ts +20 -0
  35. package/dist/new-session-drafts.d.ts +12 -0
  36. package/dist/organization-membership-lifecycle.d.ts +8 -1
  37. package/dist/organization-model-providers.d.ts +101 -0
  38. package/dist/pack-components.d.ts +1 -0
  39. package/dist/pr-review.d.ts +9 -0
  40. package/dist/provision-roles.js +1 -1
  41. package/dist/runtime-posture.d.ts +8 -8
  42. package/dist/schema.d.ts +7708 -5050
  43. package/dist/schema.js +25 -1
  44. package/dist/session-background-commands.d.ts +18 -6
  45. package/dist/session-background-commands.js +15 -9
  46. package/dist/session-control.d.ts +37 -2
  47. package/dist/session-goal-pacing.d.ts +3 -6
  48. package/dist/session-queue-commands.d.ts +17 -0
  49. package/dist/session-tenancy.js +3 -3
  50. package/dist/tool-gateway-approvals.d.ts +31 -0
  51. package/dist/video-generation.js +3 -3
  52. package/dist/workspace-artifacts.d.ts +17 -3
  53. package/dist/xai-subscription.d.ts +8 -0
  54. package/drizzle/0387_bound_host_export_session_payloads.sql +165 -0
  55. package/drizzle/0388_sandbox_provider_deadline_interactions.sql +491 -0
  56. package/drizzle/0389_model_catalog_and_gateway_custom_models.sql +241 -0
  57. package/drizzle/0390_organization_model_provider_connections.sql +197 -0
  58. package/drizzle/0391_sandbox_provider_deadline_interaction_followup.sql +295 -0
  59. package/drizzle/0392_context_compaction_pending_observability.sql +198 -0
  60. package/drizzle/0393_workspace_memory_and_learning_defaults.sql +102 -0
  61. package/drizzle/0394_session_selected_skill_activation.sql +20 -0
  62. package/drizzle/0395_scheduled_task_unclaimed_occurrence_invalidation.sql +154 -0
  63. package/drizzle/0396_model_call_equivalent_credit_cost.sql +28 -0
  64. package/drizzle/0397_sandbox_deadline_rotation_preemption.sql +113 -0
  65. package/drizzle/0398_organization_workspace_management_entry.sql +206 -0
  66. package/drizzle/0399_additional_managed_organization_creation.sql +691 -0
  67. package/drizzle/0400_session_attempt_model_context_snapshots.sql +65 -0
  68. package/drizzle/0401_organization_user_setup_token_transport.sql +178 -0
  69. package/drizzle/0402_session_input_wait_and_background_command_results.sql +1023 -0
  70. package/drizzle/0403_codex_unconditional_credential_leasing.sql +181 -0
  71. package/drizzle/0404_mcp_oauth_authorization_server.sql +471 -0
  72. package/drizzle/0405_tool_gateway_approval_capabilities.sql +184 -0
  73. package/drizzle/0406_workspace_html_site_sources.sql +71 -0
  74. package/package.json +6 -6
  75. package/src/automations.ts +9 -1
  76. package/src/capability-integrations.ts +8 -0
  77. package/src/codemode-operations.ts +31 -21
  78. package/src/codex-token-resolver.ts +16 -6
  79. package/src/connection-token-resolver.ts +52 -3
  80. package/src/generated-images.ts +70 -6
  81. package/src/index.ts +3894 -994
  82. package/src/insights-model-bundle.ts +54 -5
  83. package/src/insights.ts +53 -7
  84. package/src/managed-user-setup.ts +14 -3
  85. package/src/mcp-oauth.ts +488 -0
  86. package/src/migrate.ts +9 -1
  87. package/src/model-catalog.ts +682 -0
  88. package/src/model-context-snapshots.ts +151 -0
  89. package/src/new-session-drafts.ts +66 -21
  90. package/src/organization-membership-lifecycle.ts +47 -5
  91. package/src/organization-model-providers.ts +614 -0
  92. package/src/pack-components.ts +15 -2
  93. package/src/pr-review.ts +19 -0
  94. package/src/provision-roles.ts +4 -0
  95. package/src/runtime-posture.ts +64 -0
  96. package/src/schema.ts +666 -61
  97. package/src/session-background-commands.ts +180 -78
  98. package/src/session-control.ts +181 -12
  99. package/src/session-goal-pacing.ts +4 -8
  100. package/src/session-queue-commands.ts +157 -82
  101. package/src/tool-gateway-approvals.ts +168 -0
  102. package/src/workspace-artifacts.ts +633 -178
  103. package/src/xai-subscription.ts +66 -14
  104. package/dist/chunk-4NAR7FBA.js.map +0 -1
  105. package/dist/chunk-6DPJA36C.js.map +0 -1
  106. package/dist/chunk-O56DM5GZ.js.map +0 -1
  107. package/dist/chunk-S3TDDCS2.js.map +0 -1
  108. /package/dist/{chunk-SZAWDXQC.js.map → chunk-CJROYHSC.js.map} +0 -0
  109. /package/dist/{chunk-DZCYW2WT.js.map → chunk-MX6V5F65.js.map} +0 -0
  110. /package/dist/{chunk-U4WPPEBH.js.map → chunk-NLFHMPUB.js.map} +0 -0
  111. /package/dist/{chunk-5NNCGFQY.js.map → chunk-P6NSEG2S.js.map} +0 -0
  112. /package/dist/{chunk-US4IS6IV.js.map → chunk-RZNOSEMC.js.map} +0 -0
  113. /package/dist/{chunk-F6PBS6WE.js.map → chunk-UFAMOUWL.js.map} +0 -0
@@ -0,0 +1,488 @@
1
+ import { Permission, ToolGatewayIdentity, type AccessGrant } from "@opengeni/contracts";
2
+ import { sql } from "drizzle-orm";
3
+ import { rawRows, type Database, withWorkspaceSubjectRls } from "./database";
4
+ import { nestedPostgresSqlState } from "./persistence-errors";
5
+ import { subjectHasLiveWorkspaceAuthorityInScope } from "./workspace-authority";
6
+
7
+ export class McpOAuthClientRegistrationRateLimitError extends Error {
8
+ readonly name = "McpOAuthClientRegistrationRateLimitError";
9
+ readonly code = "mcp_oauth_client_registration_rate_limited";
10
+ }
11
+
12
+ export type McpOAuthClient = {
13
+ clientId: string;
14
+ redirectUris: string[];
15
+ clientName: string | null;
16
+ grantTypes: Array<"authorization_code" | "refresh_token">;
17
+ responseTypes: ["code"];
18
+ createdAt: Date;
19
+ };
20
+
21
+ export type McpOAuthGrantSnapshot = {
22
+ accountId: string;
23
+ workspaceId: string;
24
+ subjectId: string;
25
+ resource: string;
26
+ permissions: AccessGrant["permissions"];
27
+ toolIdentities: Array<{ serverId: string; toolName: string }>;
28
+ };
29
+
30
+ export type McpOAuthAuthorizationRequest = McpOAuthGrantSnapshot & {
31
+ requestHash: string;
32
+ clientId: string;
33
+ redirectUri: string;
34
+ codeChallenge: string;
35
+ state: string | null;
36
+ expiresAt: Date;
37
+ };
38
+
39
+ export type McpOAuthAccess = McpOAuthGrantSnapshot & {
40
+ tokenHash: string;
41
+ clientId: string;
42
+ refreshFamilyId: string;
43
+ refreshGeneration: number;
44
+ expiresAt: Date;
45
+ };
46
+
47
+ type ClientRow = {
48
+ client_id: string;
49
+ redirect_uris: unknown;
50
+ client_name: string | null;
51
+ grant_types: unknown;
52
+ response_types: unknown;
53
+ created_at: Date | string;
54
+ };
55
+
56
+ type GrantRow = {
57
+ account_id: string;
58
+ workspace_id: string;
59
+ subject_id: string;
60
+ resource: string;
61
+ permissions: unknown;
62
+ tool_identities: unknown;
63
+ };
64
+
65
+ export async function registerMcpOAuthClient(
66
+ db: Database,
67
+ input: {
68
+ clientId: string;
69
+ redirectUris: string[];
70
+ clientName: string | null;
71
+ grantTypes: Array<"authorization_code" | "refresh_token">;
72
+ responseTypes: ["code"];
73
+ registrationScopeHash: string;
74
+ },
75
+ ): Promise<McpOAuthClient> {
76
+ try {
77
+ const [row] = await rawRows<ClientRow>(
78
+ db,
79
+ sql`select client_id, redirect_uris, client_name, grant_types, response_types, created_at
80
+ from opengeni_private.register_mcp_oauth_client(
81
+ ${input.clientId}, ${JSON.stringify(input.redirectUris)}::jsonb, ${input.clientName},
82
+ ${JSON.stringify(input.grantTypes)}::jsonb, ${JSON.stringify(input.responseTypes)}::jsonb,
83
+ ${input.registrationScopeHash}
84
+ )`,
85
+ );
86
+ if (!row) throw new Error("MCP OAuth client registration was not persisted");
87
+ return mapClient(row);
88
+ } catch (error) {
89
+ if (nestedPostgresSqlState(error) === "P0004") {
90
+ throw new McpOAuthClientRegistrationRateLimitError();
91
+ }
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ export async function getMcpOAuthClient(
97
+ db: Database,
98
+ clientId: string,
99
+ ): Promise<McpOAuthClient | null> {
100
+ const [row] = await rawRows<ClientRow>(
101
+ db,
102
+ sql`with reaped as (
103
+ select opengeni_private.reap_mcp_oauth_state(128)
104
+ )
105
+ select client.client_id, client.redirect_uris, client.client_name,
106
+ client.grant_types, client.response_types, client.created_at
107
+ from mcp_oauth_clients client
108
+ cross join reaped
109
+ where client.client_id = ${clientId}
110
+ and client.expires_at > clock_timestamp()
111
+ limit 1`,
112
+ );
113
+ return row ? mapClient(row) : null;
114
+ }
115
+
116
+ async function extendMcpOAuthClientRetention(db: Database, clientId: string): Promise<void> {
117
+ await db.execute(sql`update mcp_oauth_clients
118
+ set expires_at = greatest(expires_at, clock_timestamp() + interval '31 days')
119
+ where client_id = ${clientId}
120
+ and expires_at > clock_timestamp()`);
121
+ }
122
+
123
+ export async function createMcpOAuthAuthorizationRequest(
124
+ db: Database,
125
+ input: McpOAuthAuthorizationRequest,
126
+ ): Promise<void> {
127
+ await db.execute(sql`insert into mcp_oauth_authorization_requests (
128
+ request_hash, client_id, account_id, workspace_id, subject_id, resource,
129
+ redirect_uri, code_challenge, state, permissions, tool_identities, expires_at
130
+ ) values (
131
+ ${input.requestHash}, ${input.clientId}, ${input.accountId}, ${input.workspaceId},
132
+ ${input.subjectId}, ${input.resource}, ${input.redirectUri}, ${input.codeChallenge},
133
+ ${input.state}, ${JSON.stringify(input.permissions)}::jsonb,
134
+ ${JSON.stringify(input.toolIdentities)}::jsonb, ${input.expiresAt.toISOString()}::timestamptz
135
+ )`);
136
+ }
137
+
138
+ export async function getMcpOAuthAuthorizationRequest(
139
+ db: Database,
140
+ requestHash: string,
141
+ ): Promise<McpOAuthAuthorizationRequest | null> {
142
+ const [row] = await rawRows<
143
+ GrantRow & {
144
+ request_hash: string;
145
+ client_id: string;
146
+ redirect_uri: string;
147
+ code_challenge: string;
148
+ state: string | null;
149
+ expires_at: Date | string;
150
+ }
151
+ >(
152
+ db,
153
+ sql`select request_hash, client_id, account_id, workspace_id, subject_id, resource,
154
+ redirect_uri, code_challenge, state, permissions, tool_identities, expires_at
155
+ from mcp_oauth_authorization_requests
156
+ where request_hash = ${requestHash} and expires_at > clock_timestamp()`,
157
+ );
158
+ return row
159
+ ? {
160
+ requestHash: row.request_hash,
161
+ clientId: row.client_id,
162
+ ...mapGrant(row),
163
+ redirectUri: row.redirect_uri,
164
+ codeChallenge: row.code_challenge,
165
+ state: row.state,
166
+ expiresAt: new Date(row.expires_at),
167
+ }
168
+ : null;
169
+ }
170
+
171
+ export async function deleteMcpOAuthAuthorizationRequest(
172
+ db: Database,
173
+ requestHash: string,
174
+ ): Promise<void> {
175
+ await db.execute(
176
+ sql`delete from mcp_oauth_authorization_requests where request_hash = ${requestHash}`,
177
+ );
178
+ }
179
+
180
+ export async function consumeMcpOAuthAuthorizationRequest(
181
+ db: Database,
182
+ input: {
183
+ requestHash: string;
184
+ subjectId: string;
185
+ codeHash: string;
186
+ codeExpiresAt: Date;
187
+ },
188
+ ): Promise<McpOAuthAuthorizationRequest | null> {
189
+ return await db.transaction(async (tx) => {
190
+ const [row] = await rawRows<
191
+ GrantRow & {
192
+ request_hash: string;
193
+ client_id: string;
194
+ redirect_uri: string;
195
+ code_challenge: string;
196
+ state: string | null;
197
+ expires_at: Date | string;
198
+ }
199
+ >(
200
+ tx,
201
+ sql`delete from mcp_oauth_authorization_requests
202
+ where request_hash = ${input.requestHash}
203
+ and subject_id = ${input.subjectId}
204
+ and expires_at > clock_timestamp()
205
+ returning request_hash, client_id, account_id, workspace_id, subject_id, resource,
206
+ redirect_uri, code_challenge, state, permissions, tool_identities, expires_at`,
207
+ );
208
+ if (!row) return null;
209
+ await extendMcpOAuthClientRetention(tx, row.client_id);
210
+ await tx.execute(sql`insert into mcp_oauth_authorization_codes (
211
+ code_hash, client_id, account_id, workspace_id, subject_id, resource,
212
+ redirect_uri, code_challenge, permissions, tool_identities, expires_at
213
+ ) values (
214
+ ${input.codeHash}, ${row.client_id}, ${row.account_id}, ${row.workspace_id},
215
+ ${row.subject_id}, ${row.resource}, ${row.redirect_uri}, ${row.code_challenge},
216
+ ${JSON.stringify(row.permissions)}::jsonb, ${JSON.stringify(row.tool_identities)}::jsonb,
217
+ ${input.codeExpiresAt.toISOString()}::timestamptz
218
+ )`);
219
+ return {
220
+ requestHash: row.request_hash,
221
+ clientId: row.client_id,
222
+ ...mapGrant(row),
223
+ redirectUri: row.redirect_uri,
224
+ codeChallenge: row.code_challenge,
225
+ state: row.state,
226
+ expiresAt: new Date(row.expires_at),
227
+ };
228
+ });
229
+ }
230
+
231
+ export async function exchangeMcpOAuthAuthorizationCode(
232
+ db: Database,
233
+ input: {
234
+ codeHash: string;
235
+ clientId: string;
236
+ redirectUri: string;
237
+ resource: string;
238
+ codeChallenge: string;
239
+ accessTokenHash: string;
240
+ refreshTokenHash: string | null;
241
+ accessExpiresAt: Date;
242
+ refreshExpiresAt: Date;
243
+ },
244
+ ): Promise<McpOAuthAccess | null> {
245
+ return await db.transaction(async (tx) => {
246
+ const [row] = await rawRows<GrantRow & { client_id: string }>(
247
+ tx,
248
+ sql`delete from mcp_oauth_authorization_codes
249
+ where code_hash = ${input.codeHash}
250
+ and client_id = ${input.clientId}
251
+ and redirect_uri = ${input.redirectUri}
252
+ and resource = ${input.resource}
253
+ and code_challenge = ${input.codeChallenge}
254
+ and expires_at > clock_timestamp()
255
+ returning client_id, account_id, workspace_id, subject_id, resource,
256
+ permissions, tool_identities`,
257
+ );
258
+ if (!row) return null;
259
+ await extendMcpOAuthClientRetention(tx, row.client_id);
260
+ const familyId = crypto.randomUUID();
261
+ if (input.refreshTokenHash) {
262
+ await tx.execute(sql`insert into mcp_oauth_refresh_tokens (
263
+ token_hash, family_id, generation, client_id, account_id, workspace_id,
264
+ subject_id, resource, permissions, tool_identities, expires_at
265
+ ) values (
266
+ ${input.refreshTokenHash}, ${familyId}, 1, ${row.client_id}, ${row.account_id},
267
+ ${row.workspace_id}, ${row.subject_id}, ${row.resource},
268
+ ${JSON.stringify(row.permissions)}::jsonb, ${JSON.stringify(row.tool_identities)}::jsonb,
269
+ ${input.refreshExpiresAt.toISOString()}::timestamptz
270
+ )`);
271
+ }
272
+ await tx.execute(sql`insert into mcp_oauth_access_tokens (
273
+ token_hash, refresh_family_id, refresh_generation, client_id, account_id,
274
+ workspace_id, subject_id, resource, permissions, tool_identities, expires_at
275
+ ) values (
276
+ ${input.accessTokenHash}, ${familyId}, 1, ${row.client_id}, ${row.account_id},
277
+ ${row.workspace_id}, ${row.subject_id}, ${row.resource},
278
+ ${JSON.stringify(row.permissions)}::jsonb, ${JSON.stringify(row.tool_identities)}::jsonb,
279
+ ${input.accessExpiresAt.toISOString()}::timestamptz
280
+ )`);
281
+ return {
282
+ tokenHash: input.accessTokenHash,
283
+ clientId: row.client_id,
284
+ refreshFamilyId: familyId,
285
+ refreshGeneration: 1,
286
+ ...mapGrant(row),
287
+ expiresAt: input.accessExpiresAt,
288
+ };
289
+ });
290
+ }
291
+
292
+ export async function rotateMcpOAuthRefreshToken(
293
+ db: Database,
294
+ input: {
295
+ refreshTokenHash: string;
296
+ clientId: string;
297
+ resource: string;
298
+ accessTokenHash: string;
299
+ nextRefreshTokenHash: string;
300
+ accessExpiresAt: Date;
301
+ refreshExpiresAt: Date;
302
+ },
303
+ ): Promise<McpOAuthAccess | null> {
304
+ return await db.transaction(async (tx) => {
305
+ const [family] = await rawRows<{ family_id: string }>(
306
+ tx,
307
+ sql`select family_id
308
+ from mcp_oauth_refresh_tokens
309
+ where token_hash = ${input.refreshTokenHash}
310
+ and client_id = ${input.clientId}
311
+ and resource = ${input.resource}`,
312
+ );
313
+ if (!family) return null;
314
+ await tx.execute(
315
+ sql`select pg_advisory_xact_lock(
316
+ hashtextextended(${`mcp-oauth-refresh-family:${family.family_id}`}, 0)
317
+ )`,
318
+ );
319
+ const [row] = await rawRows<
320
+ GrantRow & {
321
+ client_id: string;
322
+ family_id: string;
323
+ generation: number;
324
+ revoked_at: Date | string | null;
325
+ active: boolean;
326
+ }
327
+ >(
328
+ tx,
329
+ sql`select client_id, family_id, generation, account_id, workspace_id,
330
+ subject_id, resource, permissions, tool_identities, revoked_at,
331
+ expires_at > clock_timestamp() as active
332
+ from mcp_oauth_refresh_tokens
333
+ where token_hash = ${input.refreshTokenHash}
334
+ and client_id = ${input.clientId}
335
+ and resource = ${input.resource}
336
+ for update`,
337
+ );
338
+ if (!row) return null;
339
+ if (row.revoked_at !== null) {
340
+ await revokeMcpOAuthRefreshFamily(tx, row.family_id);
341
+ return null;
342
+ }
343
+ if (!row.active) return null;
344
+ await extendMcpOAuthClientRetention(tx, row.client_id);
345
+ await tx.execute(sql`update mcp_oauth_refresh_tokens
346
+ set revoked_at = clock_timestamp()
347
+ where token_hash = ${input.refreshTokenHash}`);
348
+ const generation = Number(row.generation) + 1;
349
+ await tx.execute(sql`insert into mcp_oauth_refresh_tokens (
350
+ token_hash, family_id, generation, client_id, account_id, workspace_id,
351
+ subject_id, resource, permissions, tool_identities, expires_at
352
+ ) values (
353
+ ${input.nextRefreshTokenHash}, ${row.family_id}, ${generation}, ${row.client_id},
354
+ ${row.account_id}, ${row.workspace_id}, ${row.subject_id}, ${row.resource},
355
+ ${JSON.stringify(row.permissions)}::jsonb, ${JSON.stringify(row.tool_identities)}::jsonb,
356
+ ${input.refreshExpiresAt.toISOString()}::timestamptz
357
+ )`);
358
+ await tx.execute(sql`insert into mcp_oauth_access_tokens (
359
+ token_hash, refresh_family_id, refresh_generation, client_id, account_id,
360
+ workspace_id, subject_id, resource, permissions, tool_identities, expires_at
361
+ ) values (
362
+ ${input.accessTokenHash}, ${row.family_id}, ${generation}, ${row.client_id},
363
+ ${row.account_id}, ${row.workspace_id}, ${row.subject_id}, ${row.resource},
364
+ ${JSON.stringify(row.permissions)}::jsonb, ${JSON.stringify(row.tool_identities)}::jsonb,
365
+ ${input.accessExpiresAt.toISOString()}::timestamptz
366
+ )`);
367
+ return {
368
+ tokenHash: input.accessTokenHash,
369
+ clientId: row.client_id,
370
+ refreshFamilyId: row.family_id,
371
+ refreshGeneration: generation,
372
+ ...mapGrant(row),
373
+ expiresAt: input.accessExpiresAt,
374
+ };
375
+ });
376
+ }
377
+
378
+ async function revokeMcpOAuthRefreshFamily(db: Database, familyId: string): Promise<void> {
379
+ await db.execute(sql`update mcp_oauth_refresh_tokens
380
+ set revoked_at = coalesce(revoked_at, clock_timestamp())
381
+ where family_id = ${familyId}`);
382
+ await db.execute(sql`update mcp_oauth_access_tokens
383
+ set revoked_at = coalesce(revoked_at, clock_timestamp())
384
+ where refresh_family_id = ${familyId}`);
385
+ }
386
+
387
+ export async function resolveMcpOAuthAccessToken(
388
+ db: Database,
389
+ tokenHash: string,
390
+ ): Promise<McpOAuthAccess | null> {
391
+ const [row] = await rawRows<
392
+ GrantRow & {
393
+ token_hash: string;
394
+ client_id: string;
395
+ refresh_family_id: string;
396
+ refresh_generation: number;
397
+ expires_at: Date | string;
398
+ }
399
+ >(
400
+ db,
401
+ sql`select token_hash, client_id, refresh_family_id, refresh_generation,
402
+ account_id, workspace_id, subject_id, resource, permissions, tool_identities, expires_at
403
+ from mcp_oauth_access_tokens
404
+ where token_hash = ${tokenHash}
405
+ and revoked_at is null
406
+ and expires_at > clock_timestamp()`,
407
+ );
408
+ return row
409
+ ? {
410
+ tokenHash: row.token_hash,
411
+ clientId: row.client_id,
412
+ refreshFamilyId: row.refresh_family_id,
413
+ refreshGeneration: Number(row.refresh_generation),
414
+ ...mapGrant(row),
415
+ expiresAt: new Date(row.expires_at),
416
+ }
417
+ : null;
418
+ }
419
+
420
+ export async function resolveLiveMcpOAuthGrant(
421
+ db: Database,
422
+ access: McpOAuthAccess,
423
+ ): Promise<AccessGrant | null> {
424
+ return await withWorkspaceSubjectRls(
425
+ db,
426
+ access.workspaceId,
427
+ access.subjectId,
428
+ async (scopedDb) => {
429
+ if (!(await subjectHasLiveWorkspaceAuthorityInScope(scopedDb, access))) return null;
430
+ const [membership] = await rawRows<{
431
+ permissions: unknown;
432
+ account_id: string;
433
+ }>(
434
+ scopedDb,
435
+ sql`select membership.permissions, workspace.account_id
436
+ from workspace_memberships membership
437
+ join workspaces workspace on workspace.id = membership.workspace_id
438
+ where membership.workspace_id = ${access.workspaceId}
439
+ and membership.subject_id = ${access.subjectId}
440
+ limit 1`,
441
+ );
442
+ if (membership && membership.account_id !== access.accountId) return null;
443
+ const livePermissions = membership
444
+ ? Permission.array().parse(membership.permissions)
445
+ : access.permissions;
446
+ const liveSet = new Set(livePermissions);
447
+ const permissions = access.permissions.filter((permission) => liveSet.has(permission));
448
+ if (!permissions.includes("workspace:read")) return null;
449
+ return {
450
+ accountId: access.accountId,
451
+ workspaceId: access.workspaceId,
452
+ subjectId: access.subjectId,
453
+ permissions,
454
+ principalKind: "human_session",
455
+ metadata: { mcpOAuth: true, refreshFamilyId: access.refreshFamilyId },
456
+ };
457
+ },
458
+ );
459
+ }
460
+
461
+ function mapClient(row: ClientRow): McpOAuthClient {
462
+ return {
463
+ clientId: row.client_id,
464
+ redirectUris: stringArray(row.redirect_uris),
465
+ clientName: row.client_name,
466
+ grantTypes: stringArray(row.grant_types) as McpOAuthClient["grantTypes"],
467
+ responseTypes: stringArray(row.response_types) as ["code"],
468
+ createdAt: new Date(row.created_at),
469
+ };
470
+ }
471
+
472
+ function mapGrant(row: GrantRow): McpOAuthGrantSnapshot {
473
+ return {
474
+ accountId: row.account_id,
475
+ workspaceId: row.workspace_id,
476
+ subjectId: row.subject_id,
477
+ resource: row.resource,
478
+ permissions: Permission.array().parse(row.permissions),
479
+ toolIdentities: ToolGatewayIdentity.array().parse(row.tool_identities),
480
+ };
481
+ }
482
+
483
+ function stringArray(value: unknown): string[] {
484
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
485
+ throw new Error("Invalid MCP OAuth string array persisted in database");
486
+ }
487
+ return value;
488
+ }
package/src/migrate.ts CHANGED
@@ -253,7 +253,7 @@ function deploymentDepthPolicy(
253
253
  return { maxNestedAgentDepth: value, source: "deployment" };
254
254
  }
255
255
 
256
- function migrationApplicationRoles(
256
+ export function migrationApplicationRoles(
257
257
  schema: string | undefined,
258
258
  options: MigrationRuntimeOptions | undefined,
259
259
  ): string[] {
@@ -281,6 +281,14 @@ function migrationApplicationRoles(
281
281
  return [DEFAULT_APPLICATION_DATABASE_ROLE];
282
282
  }
283
283
 
284
+ if (
285
+ configured !== undefined &&
286
+ configured.some((role) => typeof role !== "string" || role !== role.trim())
287
+ ) {
288
+ throw new Error(
289
+ "MigrationRuntimeOptions.applicationDatabaseRoles must contain canonical Postgres role names without surrounding whitespace",
290
+ );
291
+ }
284
292
  if (explicit.length < 1 || explicit.length > MAX_MIGRATION_APPLICATION_ROLES) {
285
293
  throw new Error(
286
294
  `Migration application database roles must contain 1-${MAX_MIGRATION_APPLICATION_ROLES} entries`,