@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.
- package/dist/automations.d.ts +4 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-integrations.d.ts +2 -0
- package/dist/{chunk-6DPJA36C.js → chunk-2XEZK4LF.js} +7 -1
- package/dist/chunk-2XEZK4LF.js.map +1 -0
- package/dist/{chunk-4NAR7FBA.js → chunk-3RI33YQN.js} +67 -1
- package/dist/chunk-3RI33YQN.js.map +1 -0
- package/dist/{chunk-SZAWDXQC.js → chunk-CJROYHSC.js} +3 -3
- package/dist/{chunk-DZCYW2WT.js → chunk-MX6V5F65.js} +2 -2
- package/dist/{chunk-S3TDDCS2.js → chunk-N5VXJ4I2.js} +151 -78
- package/dist/chunk-N5VXJ4I2.js.map +1 -0
- package/dist/{chunk-U4WPPEBH.js → chunk-NLFHMPUB.js} +2 -2
- package/dist/{chunk-5NNCGFQY.js → chunk-P6NSEG2S.js} +2 -2
- package/dist/{chunk-O56DM5GZ.js → chunk-QM6YOWHZ.js} +624 -57
- package/dist/chunk-QM6YOWHZ.js.map +1 -0
- package/dist/{chunk-US4IS6IV.js → chunk-RZNOSEMC.js} +2 -2
- package/dist/{chunk-F6PBS6WE.js → chunk-UFAMOUWL.js} +2 -2
- package/dist/codemode-operations.d.ts +5 -0
- package/dist/codex-token-resolver.d.ts +6 -2
- package/dist/connection-token-resolver.d.ts +8 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/generated-images.d.ts +15 -0
- package/dist/index.d.ts +393 -91
- package/dist/index.js +9992 -6069
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +9 -0
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/managed-user-setup.d.ts +3 -0
- package/dist/mcp-oauth.d.ts +80 -0
- package/dist/migrate.d.ts +1 -0
- package/dist/migrate.js +3 -1
- package/dist/model-catalog.d.ts +153 -0
- package/dist/model-context-snapshots.d.ts +20 -0
- package/dist/new-session-drafts.d.ts +12 -0
- package/dist/organization-membership-lifecycle.d.ts +8 -1
- package/dist/organization-model-providers.d.ts +101 -0
- package/dist/pack-components.d.ts +1 -0
- package/dist/pr-review.d.ts +9 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +8 -8
- package/dist/schema.d.ts +7708 -5050
- package/dist/schema.js +25 -1
- package/dist/session-background-commands.d.ts +18 -6
- package/dist/session-background-commands.js +15 -9
- package/dist/session-control.d.ts +37 -2
- package/dist/session-goal-pacing.d.ts +3 -6
- package/dist/session-queue-commands.d.ts +17 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/tool-gateway-approvals.d.ts +31 -0
- package/dist/video-generation.js +3 -3
- package/dist/workspace-artifacts.d.ts +17 -3
- package/dist/xai-subscription.d.ts +8 -0
- package/drizzle/0387_bound_host_export_session_payloads.sql +165 -0
- package/drizzle/0388_sandbox_provider_deadline_interactions.sql +491 -0
- package/drizzle/0389_model_catalog_and_gateway_custom_models.sql +241 -0
- package/drizzle/0390_organization_model_provider_connections.sql +197 -0
- package/drizzle/0391_sandbox_provider_deadline_interaction_followup.sql +295 -0
- package/drizzle/0392_context_compaction_pending_observability.sql +198 -0
- package/drizzle/0393_workspace_memory_and_learning_defaults.sql +102 -0
- package/drizzle/0394_session_selected_skill_activation.sql +20 -0
- package/drizzle/0395_scheduled_task_unclaimed_occurrence_invalidation.sql +154 -0
- package/drizzle/0396_model_call_equivalent_credit_cost.sql +28 -0
- package/drizzle/0397_sandbox_deadline_rotation_preemption.sql +113 -0
- package/drizzle/0398_organization_workspace_management_entry.sql +206 -0
- package/drizzle/0399_additional_managed_organization_creation.sql +691 -0
- package/drizzle/0400_session_attempt_model_context_snapshots.sql +65 -0
- package/drizzle/0401_organization_user_setup_token_transport.sql +178 -0
- package/drizzle/0402_session_input_wait_and_background_command_results.sql +1023 -0
- package/drizzle/0403_codex_unconditional_credential_leasing.sql +181 -0
- package/drizzle/0404_mcp_oauth_authorization_server.sql +471 -0
- package/drizzle/0405_tool_gateway_approval_capabilities.sql +184 -0
- package/drizzle/0406_workspace_html_site_sources.sql +71 -0
- package/package.json +6 -6
- package/src/automations.ts +9 -1
- package/src/capability-integrations.ts +8 -0
- package/src/codemode-operations.ts +31 -21
- package/src/codex-token-resolver.ts +16 -6
- package/src/connection-token-resolver.ts +52 -3
- package/src/generated-images.ts +70 -6
- package/src/index.ts +3894 -994
- package/src/insights-model-bundle.ts +54 -5
- package/src/insights.ts +53 -7
- package/src/managed-user-setup.ts +14 -3
- package/src/mcp-oauth.ts +488 -0
- package/src/migrate.ts +9 -1
- package/src/model-catalog.ts +682 -0
- package/src/model-context-snapshots.ts +151 -0
- package/src/new-session-drafts.ts +66 -21
- package/src/organization-membership-lifecycle.ts +47 -5
- package/src/organization-model-providers.ts +614 -0
- package/src/pack-components.ts +15 -2
- package/src/pr-review.ts +19 -0
- package/src/provision-roles.ts +4 -0
- package/src/runtime-posture.ts +64 -0
- package/src/schema.ts +666 -61
- package/src/session-background-commands.ts +180 -78
- package/src/session-control.ts +181 -12
- package/src/session-goal-pacing.ts +4 -8
- package/src/session-queue-commands.ts +157 -82
- package/src/tool-gateway-approvals.ts +168 -0
- package/src/workspace-artifacts.ts +633 -178
- package/src/xai-subscription.ts +66 -14
- package/dist/chunk-4NAR7FBA.js.map +0 -1
- package/dist/chunk-6DPJA36C.js.map +0 -1
- package/dist/chunk-O56DM5GZ.js.map +0 -1
- package/dist/chunk-S3TDDCS2.js.map +0 -1
- /package/dist/{chunk-SZAWDXQC.js.map → chunk-CJROYHSC.js.map} +0 -0
- /package/dist/{chunk-DZCYW2WT.js.map → chunk-MX6V5F65.js.map} +0 -0
- /package/dist/{chunk-U4WPPEBH.js.map → chunk-NLFHMPUB.js.map} +0 -0
- /package/dist/{chunk-5NNCGFQY.js.map → chunk-P6NSEG2S.js.map} +0 -0
- /package/dist/{chunk-US4IS6IV.js.map → chunk-RZNOSEMC.js.map} +0 -0
- /package/dist/{chunk-F6PBS6WE.js.map → chunk-UFAMOUWL.js.map} +0 -0
package/src/mcp-oauth.ts
ADDED
|
@@ -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`,
|