@opengeni/db 0.28.9 → 0.31.1
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/attached-browser-devices.d.ts +42 -0
- package/dist/attempt-tool-catalogs.d.ts +16 -0
- package/dist/browser-auth.d.ts +91 -0
- package/dist/browser-identities.d.ts +128 -0
- package/dist/browser-sessions.d.ts +272 -0
- package/dist/browser-state-artifacts.d.ts +17 -0
- package/dist/capability-components.d.ts +37 -0
- package/dist/capability-integrations.d.ts +192 -0
- package/dist/chunk-F5FT7BEZ.js +3535 -0
- package/dist/chunk-F5FT7BEZ.js.map +1 -0
- package/dist/chunk-ISI2TIUG.js +639 -0
- package/dist/chunk-ISI2TIUG.js.map +1 -0
- package/dist/chunk-MWDVXQOL.js +11639 -0
- package/dist/chunk-MWDVXQOL.js.map +1 -0
- package/dist/chunk-NBE6CHRI.js +955 -0
- package/dist/chunk-NBE6CHRI.js.map +1 -0
- package/dist/{chunk-VHBFHMPC.js → chunk-YHZXQ7HP.js} +754 -11
- package/dist/chunk-YHZXQ7HP.js.map +1 -0
- package/dist/codemode-operations.d.ts +117 -0
- package/dist/codex-token-resolver.d.ts +2 -2
- package/dist/company-profile-schema.d.ts +798 -0
- package/dist/company-profile.d.ts +268 -0
- package/dist/computer-sessions.d.ts +109 -0
- package/dist/connection-token-resolver.d.ts +9 -4
- package/dist/database.d.ts +52 -3
- package/dist/editable-artifact-durable-export.d.ts +153 -0
- package/dist/editable-artifact-durable-export.js +788 -0
- package/dist/editable-artifact-durable-export.js.map +1 -0
- package/dist/editable-artifact-materialization.d.ts +123 -0
- package/dist/editable-artifacts-schema.d.ts +4152 -0
- package/dist/editable-artifacts.d.ts +610 -0
- package/dist/editable-artifacts.js +26 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/generated-images.d.ts +139 -0
- package/dist/index.d.ts +698 -111
- package/dist/index.js +27573 -10322
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +51 -4
- package/dist/integration-bindings.d.ts +70 -0
- package/dist/integration-features.d.ts +99 -0
- package/dist/interaction-revisions.d.ts +3 -0
- package/dist/interaction-schema.d.ts +5372 -0
- package/dist/knowledge-source-sync-schema.d.ts +1679 -0
- package/dist/knowledge-source-sync.d.ts +274 -0
- package/dist/memory-governance.d.ts +2 -2
- package/dist/memory-slack-delivery.d.ts +101 -0
- package/dist/new-session-drafts.d.ts +2 -2
- package/dist/pack-components.d.ts +76 -0
- package/dist/pack-installations.d.ts +86 -0
- package/dist/persistence-errors.d.ts +3 -3
- package/dist/plugin-packages.d.ts +126 -0
- package/dist/preference-registry.d.ts +1 -1
- package/dist/provision-roles.d.ts +9 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +18 -6
- package/dist/schema.d.ts +18342 -9099
- package/dist/schema.js +165 -1
- package/dist/scoped-knowledge.d.ts +97 -1
- package/dist/session-control.d.ts +4 -4
- package/dist/session-queue-commands.d.ts +19 -11
- package/dist/session-realtime-context.d.ts +2 -2
- package/dist/session-realtime-ledger.d.ts +4 -4
- package/dist/session-realtime-mirror.d.ts +2 -2
- package/dist/session-realtime-state.d.ts +1 -1
- package/dist/session-realtime-terminal.d.ts +1 -1
- package/dist/session-realtime.d.ts +7 -7
- package/dist/session-tool-call-settlement.d.ts +1 -1
- package/dist/slack-user-link-access.d.ts +54 -0
- package/dist/transcription-recordings.d.ts +2 -2
- package/dist/turn-initiator.d.ts +2 -2
- package/dist/video-generation.d.ts +216 -0
- package/dist/video-generation.js +66 -0
- package/dist/video-generation.js.map +1 -0
- package/dist/workspace-artifacts.d.ts +1 -1
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/dist/workspace-learning-policy-schema.d.ts +932 -0
- package/dist/workspace-learning-policy.d.ts +161 -0
- package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
- package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
- package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
- package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
- package/drizzle/0187_generated_image_artifacts.sql +173 -0
- package/drizzle/0188_image_generation_retention_failure.sql +25 -0
- package/drizzle/0189_retained_session_image_formats.sql +13 -0
- package/drizzle/0190_timeline_annotations.sql +31 -0
- package/drizzle/0191_editable_artifact_engine.sql +3853 -0
- package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
- package/drizzle/0193_editable_artifact_authorization.sql +190 -0
- package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
- package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
- package/drizzle/0196_rig_provider_images.sql +152 -0
- package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
- package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
- package/drizzle/0199_workspace_learning_policy.sql +980 -0
- package/drizzle/0201_company_profile_authority.sql +849 -0
- package/drizzle/0202_document_index_checkpoints.sql +85 -0
- package/drizzle/0203_durable_video_generation.sql +353 -0
- package/drizzle/0204_video_generation_funding.sql +44 -0
- package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
- package/drizzle/0206_browser_sessions.sql +646 -0
- package/drizzle/0207_browser_identities.sql +379 -0
- package/drizzle/0208_attached_browser_devices.sql +165 -0
- package/drizzle/0209_computer_sessions.sql +575 -0
- package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
- package/drizzle/0211_editable_artifact_session_links.sql +58 -0
- package/drizzle/0212_slack_installation_bindings.sql +348 -0
- package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
- package/drizzle/0214_session_activity_commit_gate.sql +262 -0
- package/drizzle/0215_capabilities_platform.sql +980 -0
- package/drizzle/0216_pack_component_ownership.sql +235 -0
- package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
- package/package.json +18 -5
- package/src/attached-browser-devices.ts +371 -0
- package/src/attempt-tool-catalogs.ts +131 -0
- package/src/browser-auth.ts +1896 -0
- package/src/browser-identities.ts +1168 -0
- package/src/browser-sessions.ts +2005 -0
- package/src/browser-state-artifacts.ts +59 -0
- package/src/capability-components.ts +166 -0
- package/src/capability-integrations.ts +1663 -0
- package/src/codemode-operations.ts +674 -0
- package/src/company-profile-schema.ts +143 -0
- package/src/company-profile.ts +968 -0
- package/src/computer-sessions.ts +1130 -0
- package/src/connection-token-resolver.ts +270 -25
- package/src/database.ts +427 -23
- package/src/editable-artifact-durable-export.ts +1201 -0
- package/src/editable-artifact-materialization.ts +893 -0
- package/src/editable-artifacts-schema.ts +879 -0
- package/src/editable-artifacts.ts +5404 -0
- package/src/generated-images.ts +740 -0
- package/src/index.ts +5917 -1182
- package/src/insights.ts +160 -13
- package/src/integration-bindings.ts +455 -0
- package/src/integration-features.ts +859 -0
- package/src/interaction-revisions.ts +40 -0
- package/src/interaction-schema.ts +1303 -0
- package/src/knowledge-source-sync-schema.ts +192 -0
- package/src/knowledge-source-sync.ts +1922 -0
- package/src/memory-slack-delivery.ts +801 -0
- package/src/pack-components.ts +954 -0
- package/src/pack-installations.ts +929 -0
- package/src/persistence-errors.ts +4 -7
- package/src/plugin-packages.ts +1186 -0
- package/src/provision-roles.ts +302 -1
- package/src/runtime-posture-cli.ts +9 -0
- package/src/runtime-posture.ts +508 -5
- package/src/schema.ts +5117 -3119
- package/src/scoped-knowledge-schema.ts +1 -1
- package/src/scoped-knowledge.ts +655 -3
- package/src/session-control.ts +8 -5
- package/src/session-queue-commands.ts +187 -14
- package/src/session-realtime-context.ts +2 -2
- package/src/session-realtime-ledger.ts +3 -3
- package/src/session-realtime.ts +7 -7
- package/src/session-tool-call-settlement.ts +9 -0
- package/src/slack-user-link-access.ts +702 -0
- package/src/video-generation.ts +1452 -0
- package/src/workspace-learning-policy-schema.ts +139 -0
- package/src/workspace-learning-policy.ts +595 -0
- package/dist/chunk-P6CUHXQZ.js +0 -7248
- package/dist/chunk-P6CUHXQZ.js.map +0 -1
- package/dist/chunk-VHBFHMPC.js.map +0 -1
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type Settings,
|
|
5
5
|
} from "@opengeni/config";
|
|
6
6
|
import type {
|
|
7
|
+
ConnectionCredentialPlacement,
|
|
7
8
|
ConnectionKind,
|
|
8
9
|
ConnectionCredentialsPort,
|
|
9
10
|
ConnectionStatus,
|
|
@@ -28,6 +29,24 @@ import { isIP } from "node:net";
|
|
|
28
29
|
import { encryptEnvironmentValue } from "./environment-crypto";
|
|
29
30
|
import type { Database } from "./database";
|
|
30
31
|
|
|
32
|
+
const MAX_CREDENTIAL_PLACEMENTS = 32;
|
|
33
|
+
const MAX_CREDENTIAL_NAME_BYTES = 256;
|
|
34
|
+
const MAX_CREDENTIAL_VALUE_BYTES = 16_384;
|
|
35
|
+
const HEADER_NAME_PATTERN = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
36
|
+
const QUERY_NAME_PATTERN = /^[A-Za-z0-9._~-]+$/;
|
|
37
|
+
const COOKIE_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
38
|
+
const FORBIDDEN_CREDENTIAL_HEADERS = new Set([
|
|
39
|
+
"connection",
|
|
40
|
+
"content-length",
|
|
41
|
+
"host",
|
|
42
|
+
"proxy-authorization",
|
|
43
|
+
"proxy-connection",
|
|
44
|
+
"te",
|
|
45
|
+
"trailer",
|
|
46
|
+
"transfer-encoding",
|
|
47
|
+
"upgrade",
|
|
48
|
+
]);
|
|
49
|
+
|
|
31
50
|
export type ConnectionCredentialForBroker = {
|
|
32
51
|
id: string;
|
|
33
52
|
accountId: string;
|
|
@@ -74,6 +93,8 @@ export type ResolveConnectionCredentialResult =
|
|
|
74
93
|
| {
|
|
75
94
|
status: "ok";
|
|
76
95
|
headers: Record<string, string>;
|
|
96
|
+
/** Present when the credential bundle or embedding host supplied explicit placements. */
|
|
97
|
+
placements?: ConnectionCredentialPlacement[];
|
|
77
98
|
connectionId: string;
|
|
78
99
|
/** Exact durable version when the credential came from the local connection store. */
|
|
79
100
|
connectionVersion?: number;
|
|
@@ -105,6 +126,8 @@ export type ResolveConnectionCredentialInput = {
|
|
|
105
126
|
connectionRef: McpServerConnectionRef;
|
|
106
127
|
/** Exact MCP destination whose request would receive the resolved headers. */
|
|
107
128
|
destinationUrl: string;
|
|
129
|
+
/** Defaults to header-only MCP transport. */
|
|
130
|
+
credentialTarget?: "mcp" | "http_api";
|
|
108
131
|
forceRefresh?: boolean;
|
|
109
132
|
};
|
|
110
133
|
|
|
@@ -119,6 +142,7 @@ export type HostMcpCredentialResolverContext = {
|
|
|
119
142
|
initiator: TurnInitiator;
|
|
120
143
|
initiatorContext: TurnInitiatorContext;
|
|
121
144
|
surface: McpCredentialsRequest["surface"];
|
|
145
|
+
allowOfficialGmailRestDestination?: boolean;
|
|
122
146
|
};
|
|
123
147
|
|
|
124
148
|
export class HostMcpCredentialScopeError extends Error {
|
|
@@ -137,13 +161,43 @@ export class HostMcpCredentialBindingError extends Error {
|
|
|
137
161
|
| "scopes"
|
|
138
162
|
| "resource"
|
|
139
163
|
| "selectedResources"
|
|
140
|
-
| "destinationUrl"
|
|
164
|
+
| "destinationUrl"
|
|
165
|
+
| "credentialPlacements",
|
|
141
166
|
) {
|
|
142
167
|
super(`host MCP credential ${field} binding mismatch`);
|
|
143
168
|
this.name = "HostMcpCredentialBindingError";
|
|
144
169
|
}
|
|
145
170
|
}
|
|
146
171
|
|
|
172
|
+
const OFFICIAL_GMAIL_MCP_RESOURCE = "https://gmailmcp.googleapis.com/mcp/v1";
|
|
173
|
+
const OFFICIAL_GMAIL_REST_HOST = "gmail.googleapis.com";
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The opt-in Gmail REST adapter reuses the exact OAuth grant created for
|
|
177
|
+
* Google's hosted Gmail MCP while the preview endpoint is unavailable. Keep
|
|
178
|
+
* this exception narrower than ordinary provider-domain binding: HTTPS only,
|
|
179
|
+
* Google's canonical Gmail API host, and the authenticated `users/me` path.
|
|
180
|
+
*/
|
|
181
|
+
function isOfficialGmailRestDestination(
|
|
182
|
+
destinationUrl: string,
|
|
183
|
+
ref: McpServerConnectionRef,
|
|
184
|
+
): boolean {
|
|
185
|
+
if (
|
|
186
|
+
ref.providerDomain.toLowerCase() !== "gmailmcp.googleapis.com" ||
|
|
187
|
+
ref.kind !== "oauth2" ||
|
|
188
|
+
ref.subjectScope !== "subject"
|
|
189
|
+
) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const destination = new URL(destinationUrl);
|
|
193
|
+
return (
|
|
194
|
+
destination.protocol === "https:" &&
|
|
195
|
+
destination.hostname.toLowerCase() === OFFICIAL_GMAIL_REST_HOST &&
|
|
196
|
+
(destination.pathname === "/gmail/v1/users/me" ||
|
|
197
|
+
destination.pathname.startsWith("/gmail/v1/users/me/"))
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
147
201
|
/**
|
|
148
202
|
* Adapts the public embedding credential port to the runtime's connection
|
|
149
203
|
* resolver contract. Scope echoes are checked before credential headers can
|
|
@@ -161,7 +215,11 @@ export function buildHostConnectionTokenResolver(
|
|
|
161
215
|
const destinationUrl = canonicalHttpUrl(input.destinationUrl);
|
|
162
216
|
if (
|
|
163
217
|
!destinationUrl ||
|
|
164
|
-
!destinationHostMatchesProvider(destinationUrl, input.connectionRef.providerDomain)
|
|
218
|
+
(!destinationHostMatchesProvider(destinationUrl, input.connectionRef.providerDomain) &&
|
|
219
|
+
!(
|
|
220
|
+
context.allowOfficialGmailRestDestination === true &&
|
|
221
|
+
isOfficialGmailRestDestination(destinationUrl, input.connectionRef)
|
|
222
|
+
))
|
|
165
223
|
) {
|
|
166
224
|
throw new HostMcpCredentialBindingError("destinationUrl");
|
|
167
225
|
}
|
|
@@ -178,6 +236,7 @@ export function buildHostConnectionTokenResolver(
|
|
|
178
236
|
initiatorContext: { ...context.initiatorContext },
|
|
179
237
|
surface: context.surface,
|
|
180
238
|
destinationUrl,
|
|
239
|
+
credentialTarget: input.credentialTarget ?? "mcp",
|
|
181
240
|
serverId: input.serverId,
|
|
182
241
|
connectionRef: {
|
|
183
242
|
providerDomain: input.connectionRef.providerDomain,
|
|
@@ -221,10 +280,30 @@ export function buildHostConnectionTokenResolver(
|
|
|
221
280
|
if (result.connectionId.length === 0) {
|
|
222
281
|
throw new Error("host MCP credential returned an empty connectionId");
|
|
223
282
|
}
|
|
283
|
+
const explicitPlacements =
|
|
284
|
+
result.placements === undefined
|
|
285
|
+
? undefined
|
|
286
|
+
: normalizedCredentialPlacements(result.placements);
|
|
287
|
+
const headers = normalizedHostCredentialHeaders(
|
|
288
|
+
result.headers,
|
|
289
|
+
explicitPlacements !== undefined,
|
|
290
|
+
);
|
|
291
|
+
if (explicitPlacements) {
|
|
292
|
+
if (!sameHeaderMap(headers, headerMapForPlacements(explicitPlacements))) {
|
|
293
|
+
throw new HostMcpCredentialBindingError("credentialPlacements");
|
|
294
|
+
}
|
|
295
|
+
if (
|
|
296
|
+
(input.credentialTarget ?? "mcp") !== "http_api" &&
|
|
297
|
+
explicitPlacements.some((placement) => placement.carrier !== "header")
|
|
298
|
+
) {
|
|
299
|
+
throw new HostMcpCredentialBindingError("credentialPlacements");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
224
302
|
const expiresAt = parseHostCredentialExpiry(result.expiresAt);
|
|
225
303
|
return {
|
|
226
304
|
status: "ok",
|
|
227
|
-
headers
|
|
305
|
+
headers,
|
|
306
|
+
...(explicitPlacements ? { placements: explicitPlacements } : {}),
|
|
228
307
|
connectionId: result.connectionId,
|
|
229
308
|
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
230
309
|
};
|
|
@@ -318,27 +397,129 @@ function assertHostMcpCredentialScope(
|
|
|
318
397
|
}
|
|
319
398
|
}
|
|
320
399
|
|
|
321
|
-
function normalizedHostCredentialHeaders(
|
|
400
|
+
function normalizedHostCredentialHeaders(
|
|
401
|
+
headers: Record<string, string>,
|
|
402
|
+
allowEmpty = false,
|
|
403
|
+
): Record<string, string> {
|
|
322
404
|
const entries = Object.entries(headers);
|
|
323
|
-
if (entries.length === 0 || entries.length > 32) {
|
|
405
|
+
if ((!allowEmpty && entries.length === 0) || entries.length > 32) {
|
|
324
406
|
throw new Error("host MCP credential returned an invalid header count");
|
|
325
407
|
}
|
|
326
408
|
const normalized: Record<string, string> = {};
|
|
327
409
|
for (const [name, value] of entries) {
|
|
328
410
|
if (
|
|
329
411
|
name.length > 256 ||
|
|
330
|
-
|
|
412
|
+
!HEADER_NAME_PATTERN.test(name) ||
|
|
331
413
|
value.length === 0 ||
|
|
332
414
|
value.length > 16_384 ||
|
|
333
|
-
/[\r\n\0]/.test(value)
|
|
415
|
+
/[\r\n\0]/.test(value) ||
|
|
416
|
+
FORBIDDEN_CREDENTIAL_HEADERS.has(name.toLowerCase()) ||
|
|
417
|
+
name.toLowerCase().startsWith("sec-")
|
|
334
418
|
) {
|
|
335
419
|
throw new Error("host MCP credential returned an invalid header");
|
|
336
420
|
}
|
|
421
|
+
if (Object.keys(normalized).some((existing) => existing.toLowerCase() === name.toLowerCase())) {
|
|
422
|
+
throw new Error("host MCP credential returned duplicate headers");
|
|
423
|
+
}
|
|
337
424
|
normalized[name] = value;
|
|
338
425
|
}
|
|
339
426
|
return normalized;
|
|
340
427
|
}
|
|
341
428
|
|
|
429
|
+
function normalizedCredentialPlacements(value: unknown): ConnectionCredentialPlacement[] {
|
|
430
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_CREDENTIAL_PLACEMENTS) {
|
|
431
|
+
throw new Error("connection credential returned an invalid placement count");
|
|
432
|
+
}
|
|
433
|
+
const seen = new Set<string>();
|
|
434
|
+
return value.map((raw) => {
|
|
435
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
436
|
+
throw new Error("connection credential returned an invalid placement");
|
|
437
|
+
}
|
|
438
|
+
const record = raw as Record<string, unknown>;
|
|
439
|
+
if (
|
|
440
|
+
Object.keys(record).some(
|
|
441
|
+
(key) => key !== "carrier" && key !== "name" && key !== "value" && key !== "prefix",
|
|
442
|
+
)
|
|
443
|
+
) {
|
|
444
|
+
throw new Error("connection credential returned an invalid placement");
|
|
445
|
+
}
|
|
446
|
+
const carrier = record.carrier;
|
|
447
|
+
const name = record.name;
|
|
448
|
+
const credentialValue = record.value;
|
|
449
|
+
const prefix = record.prefix;
|
|
450
|
+
if (
|
|
451
|
+
(carrier !== "header" && carrier !== "query" && carrier !== "cookie") ||
|
|
452
|
+
typeof name !== "string" ||
|
|
453
|
+
name.length === 0 ||
|
|
454
|
+
Buffer.byteLength(name) > MAX_CREDENTIAL_NAME_BYTES ||
|
|
455
|
+
typeof credentialValue !== "string" ||
|
|
456
|
+
credentialValue.length === 0 ||
|
|
457
|
+
(typeof prefix !== "undefined" && typeof prefix !== "string") ||
|
|
458
|
+
/[\r\n\0]/.test(name) ||
|
|
459
|
+
/[\r\n\0]/.test(credentialValue) ||
|
|
460
|
+
(typeof prefix === "string" && /[\r\n\0]/.test(prefix)) ||
|
|
461
|
+
Buffer.byteLength(`${typeof prefix === "string" ? prefix : ""}${credentialValue}`) >
|
|
462
|
+
MAX_CREDENTIAL_VALUE_BYTES
|
|
463
|
+
) {
|
|
464
|
+
throw new Error("connection credential returned an invalid placement");
|
|
465
|
+
}
|
|
466
|
+
const normalizedName = carrier === "header" ? name.toLowerCase() : name;
|
|
467
|
+
if (
|
|
468
|
+
(carrier === "header" &&
|
|
469
|
+
(!HEADER_NAME_PATTERN.test(name) ||
|
|
470
|
+
FORBIDDEN_CREDENTIAL_HEADERS.has(normalizedName) ||
|
|
471
|
+
normalizedName.startsWith("sec-"))) ||
|
|
472
|
+
(carrier === "query" && !QUERY_NAME_PATTERN.test(name)) ||
|
|
473
|
+
(carrier === "cookie" &&
|
|
474
|
+
(!COOKIE_NAME_PATTERN.test(name) || /;/.test(`${prefix ?? ""}${credentialValue}`)))
|
|
475
|
+
) {
|
|
476
|
+
throw new Error("connection credential returned an invalid placement");
|
|
477
|
+
}
|
|
478
|
+
const key = `${carrier}\0${normalizedName}`;
|
|
479
|
+
if (seen.has(key)) {
|
|
480
|
+
throw new Error("connection credential returned duplicate placements");
|
|
481
|
+
}
|
|
482
|
+
seen.add(key);
|
|
483
|
+
return {
|
|
484
|
+
carrier,
|
|
485
|
+
name,
|
|
486
|
+
value: credentialValue,
|
|
487
|
+
...(typeof prefix === "string" && prefix.length > 0 ? { prefix } : {}),
|
|
488
|
+
};
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function headerMapForPlacements(
|
|
493
|
+
placements: readonly ConnectionCredentialPlacement[],
|
|
494
|
+
): Record<string, string> {
|
|
495
|
+
const headers: Record<string, string> = {};
|
|
496
|
+
for (const placement of placements) {
|
|
497
|
+
if (placement.carrier === "header") {
|
|
498
|
+
headers[placement.name] = `${placement.prefix ?? ""}${placement.value}`;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return headers;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function sameHeaderMap(
|
|
505
|
+
left: Readonly<Record<string, string>>,
|
|
506
|
+
right: Readonly<Record<string, string>>,
|
|
507
|
+
): boolean {
|
|
508
|
+
const canonical = (headers: Readonly<Record<string, string>>) =>
|
|
509
|
+
Object.entries(headers)
|
|
510
|
+
.map(([name, value]) => [name.toLowerCase(), value] as const)
|
|
511
|
+
.sort(([leftName], [rightName]) => leftName.localeCompare(rightName));
|
|
512
|
+
const leftEntries = canonical(left);
|
|
513
|
+
const rightEntries = canonical(right);
|
|
514
|
+
return (
|
|
515
|
+
leftEntries.length === rightEntries.length &&
|
|
516
|
+
leftEntries.every(
|
|
517
|
+
([name, value], index) =>
|
|
518
|
+
rightEntries[index]?.[0] === name && rightEntries[index]?.[1] === value,
|
|
519
|
+
)
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
342
523
|
function normalizedAuthorizationUrl(value: string | undefined): string | undefined {
|
|
343
524
|
if (!value) return undefined;
|
|
344
525
|
let url: URL;
|
|
@@ -469,11 +650,12 @@ export function buildConnectionTokenResolver(
|
|
|
469
650
|
cred: ConnectionCredentialForBroker,
|
|
470
651
|
ref: McpServerConnectionRef,
|
|
471
652
|
destinationUrl: string,
|
|
653
|
+
inputCredentialTarget: "mcp" | "http_api",
|
|
472
654
|
): Promise<ResolveConnectionCredentialResult> => {
|
|
473
655
|
if (cred.status !== "active") {
|
|
474
656
|
return authNeededForStatus(cred, ref);
|
|
475
657
|
}
|
|
476
|
-
if (!connectionBindingMatches(cred, ref, destinationUrl)) {
|
|
658
|
+
if (!connectionBindingMatches(cred, ref, destinationUrl, settings.gmailRestAdapterEnabled)) {
|
|
477
659
|
return authNeeded(ref, "missing_connection", cred.id);
|
|
478
660
|
}
|
|
479
661
|
const missingScopes = missingRequestedScopes(ref.scopes, cred.grantedScopes);
|
|
@@ -491,11 +673,11 @@ export function buildConnectionTokenResolver(
|
|
|
491
673
|
: {}),
|
|
492
674
|
};
|
|
493
675
|
}
|
|
494
|
-
const
|
|
495
|
-
if (
|
|
676
|
+
const material = credentialMaterialForConnection(cred, inputCredentialTarget);
|
|
677
|
+
if (material.status !== "ok") {
|
|
496
678
|
return {
|
|
497
679
|
status: "auth_needed",
|
|
498
|
-
reason: "refresh_failed",
|
|
680
|
+
reason: material.status === "unsupported" ? "unsupported_auth" : "refresh_failed",
|
|
499
681
|
providerDomain: ref.providerDomain,
|
|
500
682
|
...(ref.provider ? { provider: ref.provider } : {}),
|
|
501
683
|
connectionId: cred.id,
|
|
@@ -509,7 +691,8 @@ export function buildConnectionTokenResolver(
|
|
|
509
691
|
await deps.recordUsed(db, cred.workspaceId, cred.id, cred.subjectId);
|
|
510
692
|
return {
|
|
511
693
|
status: "ok",
|
|
512
|
-
headers,
|
|
694
|
+
headers: material.headers,
|
|
695
|
+
...(material.placements ? { placements: material.placements } : {}),
|
|
513
696
|
connectionId: cred.id,
|
|
514
697
|
connectionVersion: cred.version,
|
|
515
698
|
expiresAt: cred.expiresAt,
|
|
@@ -604,7 +787,9 @@ export function buildConnectionTokenResolver(
|
|
|
604
787
|
// Reject an audience/destination mismatch before any provider-side refresh
|
|
605
788
|
// or usage update. Refreshing first would still create an unauthorized
|
|
606
789
|
// external side effect even though the token was never sent to the target.
|
|
607
|
-
if (
|
|
790
|
+
if (
|
|
791
|
+
!connectionBindingMatches(cred, ref, input.destinationUrl, settings.gmailRestAdapterEnabled)
|
|
792
|
+
) {
|
|
608
793
|
return authNeeded(ref, "missing_connection", cred.id);
|
|
609
794
|
}
|
|
610
795
|
if (shouldRefresh(cred, input.forceRefresh === true, deps.now())) {
|
|
@@ -643,7 +828,7 @@ export function buildConnectionTokenResolver(
|
|
|
643
828
|
return authNeeded(ref, "refresh_failed", cred.id);
|
|
644
829
|
}
|
|
645
830
|
}
|
|
646
|
-
return await snapshot(cred, ref, input.destinationUrl);
|
|
831
|
+
return await snapshot(cred, ref, input.destinationUrl, input.credentialTarget ?? "mcp");
|
|
647
832
|
};
|
|
648
833
|
}
|
|
649
834
|
|
|
@@ -651,6 +836,7 @@ function connectionBindingMatches(
|
|
|
651
836
|
cred: ConnectionCredentialForBroker,
|
|
652
837
|
ref: McpServerConnectionRef,
|
|
653
838
|
destinationUrl: string,
|
|
839
|
+
gmailRestAdapterEnabled: boolean,
|
|
654
840
|
): boolean {
|
|
655
841
|
if (cred.providerDomain.toLowerCase() !== ref.providerDomain.toLowerCase()) return false;
|
|
656
842
|
if (ref.kind && cred.kind !== ref.kind) return false;
|
|
@@ -662,7 +848,17 @@ function connectionBindingMatches(
|
|
|
662
848
|
if (!destination) return false;
|
|
663
849
|
if (boundMcpUrl) {
|
|
664
850
|
const binding = canonicalHttpUrl(boundMcpUrl);
|
|
665
|
-
if (
|
|
851
|
+
if (
|
|
852
|
+
!binding ||
|
|
853
|
+
(destination !== binding &&
|
|
854
|
+
!(
|
|
855
|
+
gmailRestAdapterEnabled &&
|
|
856
|
+
binding === canonicalHttpUrl(OFFICIAL_GMAIL_MCP_RESOURCE) &&
|
|
857
|
+
isOfficialGmailRestDestination(destination, ref)
|
|
858
|
+
))
|
|
859
|
+
) {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
666
862
|
} else if (!destinationHostMatchesProvider(destination, cred.providerDomain)) {
|
|
667
863
|
// Legacy/manual API-key rows may predate mcpUrl metadata. They are still
|
|
668
864
|
// host-bound to their canonical provider domain, never usable as an
|
|
@@ -801,20 +997,55 @@ export function normalizeBearerScheme(tokenType: string | null | undefined): str
|
|
|
801
997
|
return !tokenType || /^bearer$/i.test(tokenType) ? "Bearer" : tokenType;
|
|
802
998
|
}
|
|
803
999
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1000
|
+
type ConnectionCredentialMaterial =
|
|
1001
|
+
| {
|
|
1002
|
+
status: "ok";
|
|
1003
|
+
headers: Record<string, string>;
|
|
1004
|
+
placements?: ConnectionCredentialPlacement[];
|
|
1005
|
+
}
|
|
1006
|
+
| { status: "invalid" | "unsupported" };
|
|
1007
|
+
|
|
1008
|
+
function credentialMaterialForConnection(
|
|
1009
|
+
cred: ConnectionCredentialForBroker,
|
|
1010
|
+
target: "mcp" | "http_api",
|
|
1011
|
+
): ConnectionCredentialMaterial {
|
|
808
1012
|
if (cred.kind === "oauth2") {
|
|
809
1013
|
const accessToken = stringValue((cred.credential as { access_token?: unknown }).access_token);
|
|
810
1014
|
if (!accessToken) {
|
|
811
|
-
return
|
|
1015
|
+
return { status: "invalid" };
|
|
812
1016
|
}
|
|
813
1017
|
return {
|
|
814
|
-
|
|
1018
|
+
status: "ok",
|
|
1019
|
+
headers: {
|
|
1020
|
+
authorization: `${normalizeBearerScheme(stringValue((cred.credential as { token_type?: unknown }).token_type))} ${accessToken}`,
|
|
1021
|
+
},
|
|
815
1022
|
};
|
|
816
1023
|
}
|
|
817
|
-
|
|
1024
|
+
const rawPlacements = (cred.credential as { placements?: unknown }).placements;
|
|
1025
|
+
if (rawPlacements !== undefined) {
|
|
1026
|
+
let placements: ConnectionCredentialPlacement[];
|
|
1027
|
+
try {
|
|
1028
|
+
placements = normalizedCredentialPlacements(rawPlacements);
|
|
1029
|
+
} catch {
|
|
1030
|
+
return { status: "invalid" };
|
|
1031
|
+
}
|
|
1032
|
+
if (target !== "http_api" && placements.some((placement) => placement.carrier !== "header")) {
|
|
1033
|
+
return { status: "unsupported" };
|
|
1034
|
+
}
|
|
1035
|
+
return {
|
|
1036
|
+
status: "ok",
|
|
1037
|
+
headers: headerMapForPlacements(placements),
|
|
1038
|
+
placements,
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
try {
|
|
1042
|
+
const headers = normalizedHostCredentialHeaders(
|
|
1043
|
+
stringRecord((cred.credential as { headers?: unknown }).headers) ?? {},
|
|
1044
|
+
);
|
|
1045
|
+
return { status: "ok", headers };
|
|
1046
|
+
} catch {
|
|
1047
|
+
return { status: "invalid" };
|
|
1048
|
+
}
|
|
818
1049
|
}
|
|
819
1050
|
|
|
820
1051
|
export async function refreshOAuthConnectionCredential(
|
|
@@ -869,7 +1100,16 @@ export async function refreshOAuthConnectionCredential(
|
|
|
869
1100
|
if (clientId) {
|
|
870
1101
|
body.set("client_id", clientId);
|
|
871
1102
|
}
|
|
872
|
-
const
|
|
1103
|
+
const tokenRequestEncoding =
|
|
1104
|
+
stringValue(
|
|
1105
|
+
(cred.credential as { token_request_encoding?: unknown }).token_request_encoding,
|
|
1106
|
+
) === "json"
|
|
1107
|
+
? "json"
|
|
1108
|
+
: "form";
|
|
1109
|
+
const headers: Record<string, string> = {
|
|
1110
|
+
"content-type":
|
|
1111
|
+
tokenRequestEncoding === "json" ? "application/json" : "application/x-www-form-urlencoded",
|
|
1112
|
+
};
|
|
873
1113
|
if (clientSecret && authMethod === "client_secret_post") {
|
|
874
1114
|
body.set("client_secret", clientSecret);
|
|
875
1115
|
} else if (clientId && clientSecret && authMethod === "client_secret_basic") {
|
|
@@ -877,18 +1117,23 @@ export async function refreshOAuthConnectionCredential(
|
|
|
877
1117
|
}
|
|
878
1118
|
const resource =
|
|
879
1119
|
ref.resource ?? stringValue((cred.credential as { resource?: unknown }).resource);
|
|
880
|
-
|
|
1120
|
+
const resourceParameterSupported =
|
|
1121
|
+
(cred.credential as { resource_parameter_supported?: unknown }).resource_parameter_supported !==
|
|
1122
|
+
false;
|
|
1123
|
+
if (resource && resourceParameterSupported) {
|
|
881
1124
|
body.set("resource", resource);
|
|
882
1125
|
}
|
|
883
1126
|
if (ref.scopes?.length) {
|
|
884
1127
|
body.set("scope", ref.scopes.join(" "));
|
|
885
1128
|
}
|
|
1129
|
+
const requestBody: BodyInit =
|
|
1130
|
+
tokenRequestEncoding === "json" ? JSON.stringify(Object.fromEntries(body.entries())) : body;
|
|
886
1131
|
const response = await pinnedFetch(
|
|
887
1132
|
validatedTokenEndpoint,
|
|
888
1133
|
{
|
|
889
1134
|
method: "POST",
|
|
890
1135
|
headers,
|
|
891
|
-
body,
|
|
1136
|
+
body: requestBody,
|
|
892
1137
|
signal: AbortSignal.timeout(CONNECTION_REFRESH_TIMEOUT_MS),
|
|
893
1138
|
},
|
|
894
1139
|
settings,
|