@opengeni/db 0.28.1 → 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.
Files changed (187) hide show
  1. package/dist/attached-browser-devices.d.ts +42 -0
  2. package/dist/attempt-tool-catalogs.d.ts +16 -0
  3. package/dist/browser-auth.d.ts +91 -0
  4. package/dist/browser-identities.d.ts +128 -0
  5. package/dist/browser-sessions.d.ts +272 -0
  6. package/dist/browser-state-artifacts.d.ts +17 -0
  7. package/dist/capability-components.d.ts +37 -0
  8. package/dist/capability-integrations.d.ts +192 -0
  9. package/dist/chunk-F5FT7BEZ.js +3535 -0
  10. package/dist/chunk-F5FT7BEZ.js.map +1 -0
  11. package/dist/chunk-ISI2TIUG.js +639 -0
  12. package/dist/chunk-ISI2TIUG.js.map +1 -0
  13. package/dist/chunk-MWDVXQOL.js +11639 -0
  14. package/dist/chunk-MWDVXQOL.js.map +1 -0
  15. package/dist/chunk-NBE6CHRI.js +955 -0
  16. package/dist/chunk-NBE6CHRI.js.map +1 -0
  17. package/dist/{chunk-DBS5HPEW.js → chunk-YHZXQ7HP.js} +760 -11
  18. package/dist/chunk-YHZXQ7HP.js.map +1 -0
  19. package/dist/codemode-operations.d.ts +117 -0
  20. package/dist/codex-token-resolver.d.ts +2 -2
  21. package/dist/company-profile-schema.d.ts +798 -0
  22. package/dist/company-profile.d.ts +268 -0
  23. package/dist/computer-sessions.d.ts +109 -0
  24. package/dist/connection-token-resolver.d.ts +9 -4
  25. package/dist/database.d.ts +52 -3
  26. package/dist/editable-artifact-durable-export.d.ts +153 -0
  27. package/dist/editable-artifact-durable-export.js +788 -0
  28. package/dist/editable-artifact-durable-export.js.map +1 -0
  29. package/dist/editable-artifact-materialization.d.ts +123 -0
  30. package/dist/editable-artifacts-schema.d.ts +4152 -0
  31. package/dist/editable-artifacts.d.ts +610 -0
  32. package/dist/editable-artifacts.js +26 -0
  33. package/dist/editable-artifacts.js.map +1 -0
  34. package/dist/environment-crypto.d.ts +8 -4
  35. package/dist/generated-images.d.ts +139 -0
  36. package/dist/index.d.ts +958 -126
  37. package/dist/index.js +31204 -12172
  38. package/dist/index.js.map +1 -1
  39. package/dist/insights.d.ts +51 -4
  40. package/dist/integration-bindings.d.ts +70 -0
  41. package/dist/integration-features.d.ts +99 -0
  42. package/dist/interaction-revisions.d.ts +3 -0
  43. package/dist/interaction-schema.d.ts +5372 -0
  44. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  45. package/dist/knowledge-source-sync.d.ts +274 -0
  46. package/dist/lossless-columns.d.ts +58 -0
  47. package/dist/lossless-json.d.ts +39 -0
  48. package/dist/memory-domain.d.ts +3 -6
  49. package/dist/memory-governance.d.ts +2 -2
  50. package/dist/memory-slack-delivery.d.ts +101 -0
  51. package/dist/new-session-drafts.d.ts +2 -2
  52. package/dist/pack-components.d.ts +76 -0
  53. package/dist/pack-installations.d.ts +86 -0
  54. package/dist/persistence-errors.d.ts +7 -14
  55. package/dist/plugin-packages.d.ts +126 -0
  56. package/dist/preference-registry.d.ts +1 -1
  57. package/dist/provision-roles.d.ts +9 -1
  58. package/dist/provision-roles.js +1 -1
  59. package/dist/runtime-posture.d.ts +18 -6
  60. package/dist/schema.d.ts +19122 -8731
  61. package/dist/schema.js +173 -1
  62. package/dist/scoped-knowledge.d.ts +97 -1
  63. package/dist/session-control.d.ts +4 -4
  64. package/dist/session-queue-commands.d.ts +19 -11
  65. package/dist/session-realtime-context.d.ts +2 -2
  66. package/dist/session-realtime-ledger.d.ts +4 -4
  67. package/dist/session-realtime-mirror.d.ts +2 -2
  68. package/dist/session-realtime-state.d.ts +1 -1
  69. package/dist/session-realtime-terminal.d.ts +8 -1
  70. package/dist/session-realtime.d.ts +7 -7
  71. package/dist/session-tool-call-settlement.d.ts +1 -1
  72. package/dist/slack-user-link-access.d.ts +54 -0
  73. package/dist/transcription-recordings-schema.d.ts +44 -6
  74. package/dist/transcription-recordings.d.ts +3 -3
  75. package/dist/turn-initiator.d.ts +2 -2
  76. package/dist/video-generation.d.ts +216 -0
  77. package/dist/video-generation.js +66 -0
  78. package/dist/video-generation.js.map +1 -0
  79. package/dist/workspace-artifacts.d.ts +1 -1
  80. package/dist/workspace-instruction-policies.d.ts +1 -1
  81. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  82. package/dist/workspace-learning-policy.d.ts +161 -0
  83. package/drizzle/0065_enrollment_credential_generation.sql +10 -0
  84. package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
  85. package/drizzle/0176_lossless_canonical_json.sql +975 -0
  86. package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
  87. package/drizzle/0178_permissioned_secret_reads.sql +14 -0
  88. package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
  89. package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
  90. package/drizzle/0181_connected_machine_removal.sql +52 -0
  91. package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
  92. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  93. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  94. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  95. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  96. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  97. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  98. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  99. package/drizzle/0190_timeline_annotations.sql +31 -0
  100. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  101. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  102. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  103. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  104. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  105. package/drizzle/0196_rig_provider_images.sql +152 -0
  106. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  107. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  108. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  109. package/drizzle/0201_company_profile_authority.sql +849 -0
  110. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  111. package/drizzle/0203_durable_video_generation.sql +353 -0
  112. package/drizzle/0204_video_generation_funding.sql +44 -0
  113. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  114. package/drizzle/0206_browser_sessions.sql +646 -0
  115. package/drizzle/0207_browser_identities.sql +379 -0
  116. package/drizzle/0208_attached_browser_devices.sql +165 -0
  117. package/drizzle/0209_computer_sessions.sql +575 -0
  118. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  119. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  120. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  121. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  122. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  123. package/drizzle/0215_capabilities_platform.sql +980 -0
  124. package/drizzle/0216_pack_component_ownership.sql +235 -0
  125. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  126. package/package.json +18 -5
  127. package/src/attached-browser-devices.ts +371 -0
  128. package/src/attempt-tool-catalogs.ts +131 -0
  129. package/src/browser-auth.ts +1896 -0
  130. package/src/browser-identities.ts +1168 -0
  131. package/src/browser-sessions.ts +2005 -0
  132. package/src/browser-state-artifacts.ts +59 -0
  133. package/src/capability-components.ts +166 -0
  134. package/src/capability-integrations.ts +1663 -0
  135. package/src/codemode-operations.ts +674 -0
  136. package/src/company-profile-schema.ts +143 -0
  137. package/src/company-profile.ts +968 -0
  138. package/src/computer-sessions.ts +1130 -0
  139. package/src/connection-token-resolver.ts +270 -25
  140. package/src/database.ts +434 -24
  141. package/src/editable-artifact-durable-export.ts +1201 -0
  142. package/src/editable-artifact-materialization.ts +893 -0
  143. package/src/editable-artifacts-schema.ts +879 -0
  144. package/src/editable-artifacts.ts +5404 -0
  145. package/src/environment-crypto.ts +22 -9
  146. package/src/generated-images.ts +740 -0
  147. package/src/index.ts +10117 -2730
  148. package/src/insights.ts +160 -13
  149. package/src/integration-bindings.ts +455 -0
  150. package/src/integration-features.ts +859 -0
  151. package/src/interaction-revisions.ts +40 -0
  152. package/src/interaction-schema.ts +1303 -0
  153. package/src/knowledge-source-sync-schema.ts +192 -0
  154. package/src/knowledge-source-sync.ts +1922 -0
  155. package/src/lossless-columns.ts +35 -0
  156. package/src/lossless-json.ts +322 -0
  157. package/src/memory-domain.ts +5 -44
  158. package/src/memory-slack-delivery.ts +801 -0
  159. package/src/pack-components.ts +954 -0
  160. package/src/pack-installations.ts +929 -0
  161. package/src/persistence-errors.ts +26 -34
  162. package/src/plugin-packages.ts +1186 -0
  163. package/src/provision-roles.ts +302 -1
  164. package/src/runtime-posture-cli.ts +9 -0
  165. package/src/runtime-posture.ts +514 -5
  166. package/src/schema.ts +5177 -2957
  167. package/src/scoped-knowledge-schema.ts +1 -1
  168. package/src/scoped-knowledge.ts +655 -3
  169. package/src/session-control.ts +133 -81
  170. package/src/session-queue-commands.ts +511 -247
  171. package/src/session-realtime-context.ts +20 -7
  172. package/src/session-realtime-ledger.ts +32 -10
  173. package/src/session-realtime-mirror.ts +4 -2
  174. package/src/session-realtime-terminal.ts +89 -21
  175. package/src/session-realtime.ts +10 -9
  176. package/src/session-tool-call-settlement.ts +38 -15
  177. package/src/slack-user-link-access.ts +702 -0
  178. package/src/transcription-recordings-schema.ts +5 -2
  179. package/src/transcription-recordings.ts +70 -40
  180. package/src/video-generation.ts +1452 -0
  181. package/src/workspace-learning-policy-schema.ts +139 -0
  182. package/src/workspace-learning-policy.ts +595 -0
  183. package/dist/chunk-DBS5HPEW.js.map +0 -1
  184. package/dist/chunk-HT5T62CC.js +0 -6803
  185. package/dist/chunk-HT5T62CC.js.map +0 -1
  186. package/dist/event-payload-sanitizer.d.ts +0 -32
  187. package/src/event-payload-sanitizer.ts +0 -377
@@ -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: normalizedHostCredentialHeaders(result.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(headers: Record<string, string>): Record<string, string> {
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
- !/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(name) ||
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 headers = headersForCredential(cred);
495
- if (!headers) {
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 (!connectionBindingMatches(cred, ref, input.destinationUrl)) {
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 (!binding || destination !== binding) return false;
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
- function headersForCredential(cred: ConnectionCredentialForBroker): Record<string, string> | null {
805
- if (cred.kind === "api_key") {
806
- return stringRecord((cred.credential as { headers?: unknown }).headers);
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 null;
1015
+ return { status: "invalid" };
812
1016
  }
813
1017
  return {
814
- authorization: `${normalizeBearerScheme(stringValue((cred.credential as { token_type?: unknown }).token_type))} ${accessToken}`,
1018
+ status: "ok",
1019
+ headers: {
1020
+ authorization: `${normalizeBearerScheme(stringValue((cred.credential as { token_type?: unknown }).token_type))} ${accessToken}`,
1021
+ },
815
1022
  };
816
1023
  }
817
- return stringRecord((cred.credential as { headers?: unknown }).headers);
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 headers: Record<string, string> = { "content-type": "application/x-www-form-urlencoded" };
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
- if (resource) {
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,