@opengeni/db 5.0.1 → 6.0.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 +0 -2
- package/dist/canonical-human-identities.d.ts +4 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-components.d.ts +5 -13
- package/dist/capability-integrations.d.ts +23 -8
- package/dist/capability-reconciliation.d.ts +5 -0
- package/dist/capability-reconciliation.js +21 -0
- package/dist/capability-reconciliation.js.map +1 -0
- package/dist/{chunk-6T2GGYWE.js → chunk-4UJCXBR2.js} +4 -5800
- package/dist/chunk-4UJCXBR2.js.map +1 -0
- package/dist/{chunk-MWMXWHJD.js → chunk-7JCDSMX2.js} +2 -2
- package/dist/chunk-AJA2ZIUW.js +219 -0
- package/dist/chunk-AJA2ZIUW.js.map +1 -0
- package/dist/chunk-CDAT6EKW.js +247 -0
- package/dist/chunk-CDAT6EKW.js.map +1 -0
- package/dist/{chunk-DPONPYJD.js → chunk-CJI6G254.js} +70 -18
- package/dist/chunk-CJI6G254.js.map +1 -0
- package/dist/chunk-G32NVMRJ.js +276 -0
- package/dist/chunk-G32NVMRJ.js.map +1 -0
- package/dist/chunk-GABXCG3N.js +5811 -0
- package/dist/chunk-GABXCG3N.js.map +1 -0
- package/dist/{chunk-ERKVSPDX.js → chunk-LVY33EWL.js} +3 -3
- package/dist/{chunk-45CP2YKY.js → chunk-MAMBI2HR.js} +9 -3
- package/dist/{chunk-45CP2YKY.js.map → chunk-MAMBI2HR.js.map} +1 -1
- package/dist/{chunk-F73UKJY6.js → chunk-MN5PZMEK.js} +91 -108
- package/dist/chunk-MN5PZMEK.js.map +1 -0
- package/dist/chunk-PJI4MUZ7.js +131 -0
- package/dist/chunk-PJI4MUZ7.js.map +1 -0
- package/dist/{chunk-T6BWIGWJ.js → chunk-RIYHP2WU.js} +2 -2
- package/dist/{chunk-7HMK3B45.js → chunk-RL2MUXM3.js} +16 -7
- package/dist/chunk-RL2MUXM3.js.map +1 -0
- package/dist/{chunk-HQDVI2DB.js → chunk-SYKHLPTK.js} +4 -3
- package/dist/{chunk-HQDVI2DB.js.map → chunk-SYKHLPTK.js.map} +1 -1
- package/dist/{chunk-T6YLHJLS.js → chunk-XIXVVBW2.js} +198 -51
- package/dist/chunk-XIXVVBW2.js.map +1 -0
- package/dist/chunk-ZPJ6V2VG.js +156 -0
- package/dist/chunk-ZPJ6V2VG.js.map +1 -0
- package/dist/codex-capacity-recovery.d.ts +12 -0
- package/dist/codex-failure-eligibility.d.ts +9 -0
- package/dist/company-profile-schema.d.ts +4 -2
- package/dist/connect-attempts.d.ts +15 -0
- package/dist/connect-attempts.js +25 -0
- package/dist/connect-attempts.js.map +1 -0
- package/dist/connection-authority.d.ts +7 -63
- package/dist/connection-refresh-lock.d.ts +4 -0
- package/dist/connection-token-resolver.d.ts +8 -43
- package/dist/database-timing.d.ts +10 -0
- package/dist/database.d.ts +4 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.d.ts +9 -0
- package/dist/editable-artifacts.js +7 -5
- package/dist/index.d.ts +447 -78
- package/dist/index.js +12705 -12872
- package/dist/index.js.map +1 -1
- package/dist/insights-usage-bundle.d.ts +3 -0
- package/dist/integration-bindings.d.ts +1 -1
- package/dist/integration-facets.d.ts +18 -1
- package/dist/knowledge-entries-schema.d.ts +2 -0
- package/dist/knowledge-entries.d.ts +1 -0
- package/dist/knowledge-indexing.d.ts +1 -0
- package/dist/live-session-attempt.d.ts +35 -137
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/mcp-account-bindings.d.ts +4 -0
- package/dist/mcp-oauth.d.ts +11 -0
- package/dist/mcp-operations.js +2 -2
- package/dist/migrate.js +1 -1
- package/dist/organization-integration-policy.d.ts +51 -0
- package/dist/organization-integration-policy.js +18 -0
- package/dist/organization-integration-policy.js.map +1 -0
- package/dist/organization-usage.d.ts +20 -0
- package/dist/plugin-packages.d.ts +9 -12
- package/dist/pr-review.d.ts +0 -11
- package/dist/preference-registry-schema.d.ts +17 -0
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.d.ts +124 -6
- package/dist/retained-provider-commands.js +29 -97
- package/dist/retained-provider-commands.js.map +1 -1
- package/dist/retired-session-create-metadata.d.ts +4 -0
- package/dist/runtime-posture.d.ts +7 -5
- package/dist/schema.d.ts +876 -656
- package/dist/schema.js +5 -7
- package/dist/session-admission-block.d.ts +32 -0
- package/dist/session-attempt-writers.d.ts +4 -0
- package/dist/session-background-commands.js +5 -4
- package/dist/session-command-output.js +8 -126
- package/dist/session-command-output.js.map +1 -1
- package/dist/session-control.d.ts +5 -1
- package/dist/session-event-slices.d.ts +10 -0
- package/dist/session-event-slices.js +8 -222
- package/dist/session-event-slices.js.map +1 -1
- package/dist/session-file-attachments.d.ts +28 -0
- package/dist/session-goal-reports.d.ts +43 -0
- package/dist/session-mcp-approval.d.ts +4 -0
- package/dist/session-mcp-credential-rotation.js +5 -4
- package/dist/session-mcp-credential-rotation.js.map +1 -1
- package/dist/session-message-search.d.ts +22 -0
- package/dist/session-queue-commands.d.ts +2 -6
- package/dist/session-retry.d.ts +36 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/skill-source-release-impact.d.ts +18 -0
- package/dist/skill-source-release.d.ts +9 -0
- package/dist/skills.d.ts +7 -1
- package/dist/user-resource-authority.d.ts +0 -8
- package/dist/video-generation.js +3 -3
- package/dist/workspace-tool-defaults.js +5 -4
- package/dist/workspace-tool-defaults.js.map +1 -1
- package/drizzle/0468_knowledge_relationship_projection.sql +30 -0
- package/drizzle/0469_knowledge_source_discovery.sql +85 -0
- package/drizzle/0470_organization_integration_policy.sql +151 -0
- package/drizzle/0471_usage_events_account_recent_index.sql +4 -0
- package/drizzle/0472_usage_events_workspace_recent_index.sql +4 -0
- package/drizzle/0473_organization_usage_analytical_capability.sql +237 -0
- package/drizzle/0474_goal_report_requirements.sql +156 -0
- package/drizzle/0475_session_event_history_statistics.sql +15 -0
- package/drizzle/0476_session_event_history_policy_planning.sql +29 -0
- package/drizzle/0477_managed_sign_in_methods.sql +303 -0
- package/drizzle/0478_sender_owned_connections.sql +2772 -0
- package/drizzle/0479_connection_create_idempotency.sql +16 -0
- package/drizzle/0480_private_session_organization_key_administration.sql +154 -0
- package/drizzle/0481_personal_connection_visibility_cleanup.sql +45 -0
- package/drizzle/0482_remove_packs.sql +417 -0
- package/drizzle/0483_preclaim_admission_block.sql +85 -0
- package/drizzle/0484_insights_usage_projection.sql +211 -0
- package/drizzle/0485_inherited_mcp_approval_policies.sql +6 -0
- package/drizzle/0486_reasoning_configuration_message_forks.sql +35 -0
- package/drizzle/0487_skill_catalog_message_forks.sql +26 -0
- package/drizzle/0488_permanent_skill_removal.sql +207 -0
- package/drizzle/0489_modal_command_byte_offsets.sql +17 -0
- package/drizzle/0490_periodic_capture_attempt_cadence.sql +6 -0
- package/drizzle/0491_warm_capture_holder_reclamation.sql +57 -0
- package/drizzle/0492_codex_accepted_source_authority.sql +249 -0
- package/drizzle/0493_stopping_command_error_containment.sql +58 -0
- package/drizzle/0494_mcp_account_bindings.sql +334 -0
- package/drizzle/0495_consented_sandbox_recovery.sql +220 -0
- package/drizzle/0496_supervised_command_settlement.sql +386 -0
- package/drizzle/0497_modal_inventory_owner_read_capability.sql +112 -0
- package/drizzle/0498_enrollment_membership_fence.sql +87 -0
- package/drizzle/0499_session_attachment_access.sql +224 -0
- package/drizzle/0500_session_execution_epoch.sql +37 -0
- package/drizzle/0501_session_sharing_execution.sql +591 -0
- package/drizzle/0502_active_message_boundary_forks.sql +30 -0
- package/package.json +17 -5
- package/src/automations.ts +17 -73
- package/src/canonical-human-identities.ts +17 -5
- package/src/capability-components.ts +7 -24
- package/src/capability-integrations.ts +301 -21
- package/src/capability-reconciliation.ts +19 -0
- package/src/codex-capacity-recovery.ts +47 -0
- package/src/codex-failure-eligibility.ts +93 -0
- package/src/connect-attempts.ts +49 -18
- package/src/connection-authority.ts +12 -158
- package/src/connection-refresh-lock.ts +22 -0
- package/src/connection-token-resolver.ts +58 -530
- package/src/database-timing.ts +43 -0
- package/src/database.ts +61 -26
- package/src/editable-artifacts.ts +1 -1
- package/src/index.ts +3578 -1273
- package/src/insights-model-bundle.ts +19 -7
- package/src/insights-usage-bundle.ts +16 -19
- package/src/integration-bindings.ts +2 -7
- package/src/integration-facets.ts +73 -6
- package/src/knowledge-entries.ts +8 -1
- package/src/live-session-attempt.ts +0 -22
- package/src/mcp-account-bindings.ts +12 -0
- package/src/mcp-oauth.ts +26 -0
- package/src/organization-integration-policy.ts +235 -0
- package/src/organization-usage.ts +83 -0
- package/src/plugin-packages.ts +226 -42
- package/src/pr-review.ts +7 -69
- package/src/preference-registry-schema.ts +1 -0
- package/src/provision-roles.ts +50 -37
- package/src/retained-provider-commands.ts +286 -9
- package/src/retired-session-create-metadata.ts +16 -0
- package/src/runtime-posture.ts +217 -13
- package/src/schema.ts +104 -120
- package/src/session-admission-block.ts +29 -0
- package/src/session-attempt-writers.ts +48 -0
- package/src/session-control.ts +21 -5
- package/src/session-event-slices.ts +76 -26
- package/src/session-file-attachments.ts +66 -0
- package/src/session-goal-reports.ts +255 -0
- package/src/session-mcp-approval.ts +30 -0
- package/src/session-message-search.ts +348 -0
- package/src/session-queue-commands.ts +60 -36
- package/src/session-retry.ts +319 -0
- package/src/skill-config-migration.ts +11 -2
- package/src/skill-source-release-impact.ts +33 -0
- package/src/skill-source-release.ts +67 -26
- package/src/skills.ts +44 -6
- package/src/user-resource-authority.ts +0 -56
- package/dist/chunk-6T2GGYWE.js.map +0 -1
- package/dist/chunk-7HMK3B45.js.map +0 -1
- package/dist/chunk-DPONPYJD.js.map +0 -1
- package/dist/chunk-F73UKJY6.js.map +0 -1
- package/dist/chunk-T6YLHJLS.js.map +0 -1
- package/dist/host-mcp-binding-match.d.ts +0 -5
- package/dist/host-mcp-bindings.d.ts +0 -79
- package/dist/host-mcp-resolvers.d.ts +0 -47
- package/dist/host-mcp-task-authority.d.ts +0 -43
- package/dist/host-selection-identity.d.ts +0 -6
- package/dist/pack-components.d.ts +0 -83
- package/dist/pack-installations.d.ts +0 -86
- package/src/host-mcp-binding-match.ts +0 -34
- package/src/host-mcp-bindings.ts +0 -777
- package/src/host-mcp-resolvers.ts +0 -266
- package/src/host-mcp-task-authority.ts +0 -384
- package/src/host-selection-identity.ts +0 -29
- package/src/pack-components.ts +0 -980
- package/src/pack-installations.ts +0 -950
- /package/dist/{chunk-MWMXWHJD.js.map → chunk-7JCDSMX2.js.map} +0 -0
- /package/dist/{chunk-ERKVSPDX.js.map → chunk-LVY33EWL.js.map} +0 -0
- /package/dist/{chunk-T6BWIGWJ.js.map → chunk-RIYHP2WU.js.map} +0 -0
|
@@ -6,23 +6,12 @@ import {
|
|
|
6
6
|
import type {
|
|
7
7
|
ConnectionCredentialPlacement,
|
|
8
8
|
ConnectionKind,
|
|
9
|
-
ConnectionCredentialsPort,
|
|
10
9
|
ConnectionStatus,
|
|
11
10
|
McpConnectionResourceScope,
|
|
12
11
|
McpCredentialAuthNeededReason,
|
|
13
|
-
McpCredentialsRequest,
|
|
14
|
-
McpCredentialResolution,
|
|
15
|
-
McpGatewayCredentialsRequest,
|
|
16
|
-
McpGatewayCredentialResolution,
|
|
17
|
-
TurnInitiator,
|
|
18
|
-
TurnInitiatorContext,
|
|
19
12
|
} from "@opengeni/contracts";
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
ConnectionUseAuthoritySnapshot,
|
|
23
|
-
type ConnectionUseAttribution,
|
|
24
|
-
type ConnectionUseAuthorizationResult,
|
|
25
|
-
} from "@opengeni/contracts/connection-authority";
|
|
13
|
+
|
|
14
|
+
import { type ConnectionUseAttribution } from "@opengeni/contracts/connection-authority";
|
|
26
15
|
import {
|
|
27
16
|
PERSONAL_GITHUB_REQUESTED_SCOPES,
|
|
28
17
|
PERSONAL_GITHUB_PROVIDER_DOMAIN,
|
|
@@ -41,15 +30,14 @@ import {
|
|
|
41
30
|
} from "@opengeni/network";
|
|
42
31
|
export { isPrivateAddress } from "@opengeni/network";
|
|
43
32
|
import { Buffer } from "node:buffer";
|
|
44
|
-
import { isIP } from "node:net";
|
|
45
33
|
import { encryptEnvironmentValue } from "./environment-crypto";
|
|
46
34
|
import type { Database } from "./database";
|
|
35
|
+
import { withConnectionRefreshLock } from "./connection-refresh-lock";
|
|
47
36
|
import type {
|
|
48
37
|
AcceptedConnectionUseContext,
|
|
49
38
|
AcceptedConnectionUseResolution,
|
|
50
39
|
} from "./connection-authority";
|
|
51
40
|
import { connectionScopeKey } from "./connection-scopes";
|
|
52
|
-
import { hostMcpBindingMatchesSelection } from "@opengeni/contracts/host-mcp-bindings";
|
|
53
41
|
|
|
54
42
|
const MAX_CREDENTIAL_PLACEMENTS = 32;
|
|
55
43
|
const MAX_CREDENTIAL_NAME_BYTES = 256;
|
|
@@ -173,58 +161,12 @@ export type ResolveConnectionCredentialInput = {
|
|
|
173
161
|
credentialResolutionMode?: "execution" | "preflight";
|
|
174
162
|
/** Frozen provider authority generation captured by the calling integration/catalog. */
|
|
175
163
|
expectedAuthorityGeneration?: number;
|
|
176
|
-
/**
|
|
177
|
-
|
|
164
|
+
/** Attribution already authorized for forwarding to a host resolver; never authorizes local use. */
|
|
165
|
+
hostConnectionUseAttribution?: ConnectionUseAttribution;
|
|
178
166
|
/** Exact accepted attempt plus one stable physical-provider request id. */
|
|
179
167
|
connectionUseContext?: AcceptedConnectionUseContext;
|
|
180
168
|
};
|
|
181
169
|
|
|
182
|
-
export type HostMcpCredentialResolverContext = {
|
|
183
|
-
accountId: string;
|
|
184
|
-
workspaceId: string;
|
|
185
|
-
sessionId: string;
|
|
186
|
-
rootSessionId: string;
|
|
187
|
-
turnId: string;
|
|
188
|
-
attemptId: string | null;
|
|
189
|
-
executionGeneration: number;
|
|
190
|
-
initiator: TurnInitiator;
|
|
191
|
-
initiatorContext: TurnInitiatorContext;
|
|
192
|
-
surface: McpCredentialsRequest["surface"];
|
|
193
|
-
/** Backend-owned attempt liveness, independent of binding delegation. */
|
|
194
|
-
authorizeExecution?: (request: McpCredentialsRequest) => Promise<boolean>;
|
|
195
|
-
/** Backend-owned live validator. Never populate from caller JSON or a host
|
|
196
|
-
* credential response. Omission denies every explicit durable reference. */
|
|
197
|
-
authorizeDurableBinding?: (request: McpCredentialsRequest) => Promise<boolean>;
|
|
198
|
-
/** Backend-only accepted-work lookup, not a host response or registry fallback. */
|
|
199
|
-
resolveAcceptedBinding?: (
|
|
200
|
-
request: McpCredentialsRequest,
|
|
201
|
-
) => Promise<McpServerConnectionRef | null>;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
export class HostMcpCredentialScopeError extends Error {
|
|
205
|
-
constructor(field: "accountId" | "workspaceId" | "sessionId") {
|
|
206
|
-
super(`host MCP credential ${field} scope mismatch`);
|
|
207
|
-
this.name = "HostMcpCredentialScopeError";
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export class HostMcpCredentialBindingError extends Error {
|
|
212
|
-
constructor(
|
|
213
|
-
field:
|
|
214
|
-
| "provider"
|
|
215
|
-
| "providerDomain"
|
|
216
|
-
| "connectionId"
|
|
217
|
-
| "scopes"
|
|
218
|
-
| "resource"
|
|
219
|
-
| "selectedResources"
|
|
220
|
-
| "destinationUrl"
|
|
221
|
-
| "credentialPlacements",
|
|
222
|
-
) {
|
|
223
|
-
super(`host MCP credential ${field} binding mismatch`);
|
|
224
|
-
this.name = "HostMcpCredentialBindingError";
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
170
|
const OFFICIAL_GMAIL_MCP_RESOURCE = "https://gmailmcp.googleapis.com/mcp/v1";
|
|
229
171
|
const OFFICIAL_GMAIL_REST_HOST = "gmail.googleapis.com";
|
|
230
172
|
|
|
@@ -261,344 +203,12 @@ function isOfficialGmailRestDestination(
|
|
|
261
203
|
* reach a request; the returned object is a fresh copy so a host cannot mutate
|
|
262
204
|
* headers after resolution.
|
|
263
205
|
*/
|
|
264
|
-
export function buildHostConnectionTokenResolver(
|
|
265
|
-
resolve: NonNullable<ConnectionCredentialsPort["mcpCredentials"]>,
|
|
266
|
-
suppliedContext: HostMcpCredentialResolverContext,
|
|
267
|
-
): (input: ResolveConnectionCredentialInput) => Promise<ResolveConnectionCredentialResult> {
|
|
268
|
-
// Bind the accepted execution once. A caller retaining its constructor
|
|
269
|
-
// object must not retarget later requests or replace the post-use validator.
|
|
270
|
-
const context: HostMcpCredentialResolverContext = {
|
|
271
|
-
...suppliedContext,
|
|
272
|
-
initiator: structuredClone(suppliedContext.initiator),
|
|
273
|
-
initiatorContext: structuredClone(suppliedContext.initiatorContext),
|
|
274
|
-
};
|
|
275
|
-
return async (input) => {
|
|
276
|
-
if (input.workspaceId !== context.workspaceId) {
|
|
277
|
-
throw new HostMcpCredentialScopeError("workspaceId");
|
|
278
|
-
}
|
|
279
|
-
const destinationUrl = canonicalHttpUrl(input.destinationUrl);
|
|
280
|
-
if (
|
|
281
|
-
!destinationUrl ||
|
|
282
|
-
(!destinationHostMatchesProvider(destinationUrl, input.connectionRef.providerDomain) &&
|
|
283
|
-
!isOfficialGmailRestDestination(destinationUrl, input.connectionRef))
|
|
284
|
-
) {
|
|
285
|
-
throw new HostMcpCredentialBindingError("destinationUrl");
|
|
286
|
-
}
|
|
287
|
-
const toolName = input.toolName ?? input.toolId;
|
|
288
|
-
const request: McpCredentialsRequest = {
|
|
289
|
-
accountId: context.accountId,
|
|
290
|
-
workspaceId: context.workspaceId,
|
|
291
|
-
sessionId: context.sessionId,
|
|
292
|
-
rootSessionId: context.rootSessionId,
|
|
293
|
-
turnId: context.turnId,
|
|
294
|
-
attemptId: context.attemptId,
|
|
295
|
-
executionGeneration: context.executionGeneration,
|
|
296
|
-
initiator: context.initiator,
|
|
297
|
-
initiatorContext: { ...context.initiatorContext },
|
|
298
|
-
surface: context.surface,
|
|
299
|
-
destinationUrl,
|
|
300
|
-
credentialTarget: input.credentialTarget ?? "mcp",
|
|
301
|
-
serverId: input.serverId,
|
|
302
|
-
connectionRef: {
|
|
303
|
-
providerDomain: input.connectionRef.providerDomain,
|
|
304
|
-
...(input.connectionRef.authoritySource
|
|
305
|
-
? { authoritySource: input.connectionRef.authoritySource }
|
|
306
|
-
: {}),
|
|
307
|
-
...(input.connectionRef.hostBinding
|
|
308
|
-
? { hostBinding: { ...input.connectionRef.hostBinding } }
|
|
309
|
-
: {}),
|
|
310
|
-
...(input.connectionRef.provider ? { provider: input.connectionRef.provider } : {}),
|
|
311
|
-
...(input.connectionRef.connectionId
|
|
312
|
-
? { connectionId: input.connectionRef.connectionId }
|
|
313
|
-
: {}),
|
|
314
|
-
...(input.connectionRef.kind ? { kind: input.connectionRef.kind } : {}),
|
|
315
|
-
...(input.connectionRef.scopes ? { scopes: [...input.connectionRef.scopes] } : {}),
|
|
316
|
-
...(input.connectionRef.resource ? { resource: input.connectionRef.resource } : {}),
|
|
317
|
-
...(input.connectionRef.selectedResources
|
|
318
|
-
? { selectedResources: copySelectedResources(input.connectionRef.selectedResources) }
|
|
319
|
-
: {}),
|
|
320
|
-
...(input.connectionRef.subjectScope
|
|
321
|
-
? { subjectScope: input.connectionRef.subjectScope }
|
|
322
|
-
: {}),
|
|
323
|
-
},
|
|
324
|
-
forceRefresh: input.forceRefresh === true,
|
|
325
|
-
...(input.connectionUseAuthority !== undefined
|
|
326
|
-
? { connectionUseAuthority: input.connectionUseAuthority }
|
|
327
|
-
: {}),
|
|
328
|
-
...(input.connectionUseContext
|
|
329
|
-
? { connectionUseRequestId: input.connectionUseContext.physicalRequestId }
|
|
330
|
-
: {}),
|
|
331
|
-
...(toolName ? { toolName } : {}),
|
|
332
|
-
...(input.subjectId ? { callerSubjectId: input.subjectId } : {}),
|
|
333
|
-
};
|
|
334
|
-
// Freeze configuration before any asynchronous authority lookup.
|
|
335
|
-
let requestedRef = structuredClone(request.connectionRef);
|
|
336
|
-
const credentialTarget = input.credentialTarget ?? "mcp";
|
|
337
|
-
const durableDenial = (
|
|
338
|
-
reason: McpCredentialAuthNeededReason,
|
|
339
|
-
): ResolveConnectionCredentialResult => ({
|
|
340
|
-
status: "auth_needed",
|
|
341
|
-
reason,
|
|
342
|
-
providerDomain: requestedRef.providerDomain,
|
|
343
|
-
authoritySource: "host",
|
|
344
|
-
...(requestedRef.connectionId ? { connectionId: requestedRef.connectionId } : {}),
|
|
345
|
-
});
|
|
346
|
-
if (requestedRef.hostBinding && "selection" in requestedRef.hostBinding) {
|
|
347
|
-
if (!context.resolveAcceptedBinding || !context.authorizeDurableBinding)
|
|
348
|
-
return durableDenial("unsupported_auth");
|
|
349
|
-
try {
|
|
350
|
-
const concrete = await context.resolveAcceptedBinding(structuredClone(request));
|
|
351
|
-
if (!concrete?.hostBinding || "selection" in concrete.hostBinding)
|
|
352
|
-
return durableDenial("personal_authority_unavailable");
|
|
353
|
-
const { hostBinding, ...connectionRef } = concrete;
|
|
354
|
-
if (
|
|
355
|
-
!hostMcpBindingMatchesSelection(
|
|
356
|
-
{
|
|
357
|
-
id: hostBinding.bindingId,
|
|
358
|
-
generation: hostBinding.generation,
|
|
359
|
-
definition: { serverId: request.serverId, destinationUrl, connectionRef },
|
|
360
|
-
},
|
|
361
|
-
request,
|
|
362
|
-
)
|
|
363
|
-
)
|
|
364
|
-
return durableDenial("personal_authority_unavailable");
|
|
365
|
-
request.connectionRef = structuredClone(concrete);
|
|
366
|
-
requestedRef = structuredClone(concrete);
|
|
367
|
-
} catch {
|
|
368
|
-
return durableDenial("refresh_failed");
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
const snapshot = structuredClone(request);
|
|
372
|
-
const authorized = async () => {
|
|
373
|
-
try {
|
|
374
|
-
if (
|
|
375
|
-
context.authorizeExecution &&
|
|
376
|
-
!(await context.authorizeExecution(structuredClone(snapshot)))
|
|
377
|
-
)
|
|
378
|
-
return "resource_scope_unavailable" as const;
|
|
379
|
-
if (
|
|
380
|
-
requestedRef.hostBinding &&
|
|
381
|
-
!(await context.authorizeDurableBinding?.(structuredClone(snapshot)))
|
|
382
|
-
)
|
|
383
|
-
return "personal_authority_unavailable" as const;
|
|
384
|
-
return null;
|
|
385
|
-
} catch {
|
|
386
|
-
return "refresh_failed" as const;
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
if (requestedRef.hostBinding && !context.authorizeDurableBinding)
|
|
390
|
-
return durableDenial("unsupported_auth");
|
|
391
|
-
const requiresAuthorization = Boolean(requestedRef.hostBinding || context.authorizeExecution);
|
|
392
|
-
if (requiresAuthorization) {
|
|
393
|
-
const denial = await authorized();
|
|
394
|
-
if (denial) return durableDenial(denial);
|
|
395
|
-
}
|
|
396
|
-
const result = await resolve(structuredClone(snapshot));
|
|
397
|
-
const routeGuard = result.status === "ok" ? result[hostMcpCredentialUseGuard] : undefined;
|
|
398
|
-
const routeAuthorized = async () => {
|
|
399
|
-
try {
|
|
400
|
-
return routeGuard ? await routeGuard() : true;
|
|
401
|
-
} catch {
|
|
402
|
-
return false;
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
|
-
if (requiresAuthorization) {
|
|
406
|
-
const denial = await authorized();
|
|
407
|
-
if (denial) return durableDenial(denial);
|
|
408
|
-
}
|
|
409
|
-
if (routeGuard && !(await routeAuthorized())) return durableDenial("refresh_failed");
|
|
410
|
-
assertHostMcpCredentialScope(result, context);
|
|
411
|
-
const normalized = normalizeHostCredentialResolution(result, requestedRef, credentialTarget);
|
|
412
|
-
return normalized.status === "ok" && (requiresAuthorization || routeGuard)
|
|
413
|
-
? {
|
|
414
|
-
...normalized,
|
|
415
|
-
authorizeProviderRequest: async () =>
|
|
416
|
-
(await authorized()) === null && (await routeAuthorized()),
|
|
417
|
-
}
|
|
418
|
-
: normalized;
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/** Authenticated request-time adapter. No synthetic session/turn authority and
|
|
423
|
-
* no durable binding admission; every physical use must retain live API proof. */
|
|
424
|
-
export function buildHostGatewayConnectionTokenResolver(
|
|
425
|
-
resolve: NonNullable<ConnectionCredentialsPort["mcpGatewayCredentials"]>,
|
|
426
|
-
suppliedContext: Pick<McpGatewayCredentialsRequest, "accountId" | "workspaceId" | "authority">,
|
|
427
|
-
reauthorize: () => Promise<void>,
|
|
428
|
-
): (input: ResolveConnectionCredentialInput) => Promise<ResolveConnectionCredentialResult> {
|
|
429
|
-
const context = structuredClone(suppliedContext);
|
|
430
|
-
return async (input) => {
|
|
431
|
-
const ref = structuredClone(input.connectionRef);
|
|
432
|
-
const deny = (reason: McpCredentialAuthNeededReason): ResolveConnectionCredentialResult => ({
|
|
433
|
-
status: "auth_needed",
|
|
434
|
-
reason,
|
|
435
|
-
authoritySource: "host",
|
|
436
|
-
providerDomain: ref.providerDomain,
|
|
437
|
-
...(ref.connectionId ? { connectionId: ref.connectionId } : {}),
|
|
438
|
-
});
|
|
439
|
-
if (input.workspaceId !== context.workspaceId)
|
|
440
|
-
throw new HostMcpCredentialScopeError("workspaceId");
|
|
441
|
-
if (ref.authoritySource !== "host" || ref.hostBinding) return deny("unsupported_auth");
|
|
442
|
-
const destinationUrl = canonicalHttpUrl(input.destinationUrl);
|
|
443
|
-
if (
|
|
444
|
-
!destinationUrl ||
|
|
445
|
-
(!destinationHostMatchesProvider(destinationUrl, ref.providerDomain) &&
|
|
446
|
-
!isOfficialGmailRestDestination(destinationUrl, ref))
|
|
447
|
-
)
|
|
448
|
-
throw new HostMcpCredentialBindingError("destinationUrl");
|
|
449
|
-
const request: McpGatewayCredentialsRequest = {
|
|
450
|
-
...structuredClone(context),
|
|
451
|
-
surface: "workspace_gateway",
|
|
452
|
-
requestId: crypto.randomUUID(),
|
|
453
|
-
destinationUrl,
|
|
454
|
-
credentialTarget: input.credentialTarget ?? "mcp",
|
|
455
|
-
serverId: input.serverId,
|
|
456
|
-
connectionRef: ref,
|
|
457
|
-
forceRefresh: input.forceRefresh === true,
|
|
458
|
-
...((input.toolName ?? input.toolId) ? { toolName: input.toolName ?? input.toolId } : {}),
|
|
459
|
-
};
|
|
460
|
-
await reauthorize();
|
|
461
|
-
const result = await resolve(structuredClone(request));
|
|
462
|
-
await reauthorize();
|
|
463
|
-
const routeGuard = result.status === "ok" ? result[hostMcpCredentialUseGuard] : undefined;
|
|
464
|
-
if (routeGuard) {
|
|
465
|
-
try {
|
|
466
|
-
if (!(await routeGuard())) return deny("refresh_failed");
|
|
467
|
-
} catch {
|
|
468
|
-
return deny("refresh_failed");
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
for (const field of ["accountId", "workspaceId", "requestId"] as const) {
|
|
472
|
-
if (result[field] !== request[field])
|
|
473
|
-
throw new Error(`host gateway credential ${field} scope mismatch`);
|
|
474
|
-
}
|
|
475
|
-
const normalized = normalizeHostCredentialResolution(
|
|
476
|
-
result,
|
|
477
|
-
ref,
|
|
478
|
-
request.credentialTarget ?? "mcp",
|
|
479
|
-
);
|
|
480
|
-
if (normalized.status !== "ok") return normalized;
|
|
481
|
-
return {
|
|
482
|
-
...normalized,
|
|
483
|
-
authorizeProviderRequest: async () => {
|
|
484
|
-
try {
|
|
485
|
-
await reauthorize();
|
|
486
|
-
return routeGuard ? await routeGuard() : true;
|
|
487
|
-
} catch {
|
|
488
|
-
return false;
|
|
489
|
-
}
|
|
490
|
-
},
|
|
491
|
-
};
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function normalizeHostCredentialResolution(
|
|
496
|
-
result: McpCredentialResolution | McpGatewayCredentialResolution,
|
|
497
|
-
requestedRef: McpServerConnectionRef,
|
|
498
|
-
credentialTarget: "mcp" | "http_api",
|
|
499
|
-
): ResolveConnectionCredentialResult {
|
|
500
|
-
assertHostMcpCredentialBinding(result, requestedRef);
|
|
501
|
-
if (result.status === "auth_needed") {
|
|
502
|
-
const authorizationUrl = normalizedAuthorizationUrl(result.authorizationUrl);
|
|
503
|
-
return {
|
|
504
|
-
status: "auth_needed",
|
|
505
|
-
reason: result.reason,
|
|
506
|
-
providerDomain: result.providerDomain,
|
|
507
|
-
authoritySource: "host",
|
|
508
|
-
...(result.provider ? { provider: result.provider } : {}),
|
|
509
|
-
...(result.connectionId ? { connectionId: result.connectionId } : {}),
|
|
510
|
-
...(result.scopes ? { scopes: [...result.scopes] } : {}),
|
|
511
|
-
...(result.resource ? { resource: result.resource } : {}),
|
|
512
|
-
...(result.selectedResources
|
|
513
|
-
? { selectedResources: copySelectedResources(result.selectedResources) }
|
|
514
|
-
: {}),
|
|
515
|
-
...(authorizationUrl ? { authorizationUrl } : {}),
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
if (result.connectionId.length === 0) {
|
|
519
|
-
throw new Error("host MCP credential returned an empty connectionId");
|
|
520
|
-
}
|
|
521
|
-
const explicitPlacements =
|
|
522
|
-
result.placements === undefined ? undefined : normalizedCredentialPlacements(result.placements);
|
|
523
|
-
const headers = normalizedHostCredentialHeaders(result.headers, explicitPlacements !== undefined);
|
|
524
|
-
if (explicitPlacements) {
|
|
525
|
-
if (!sameHeaderMap(headers, headerMapForPlacements(explicitPlacements))) {
|
|
526
|
-
throw new HostMcpCredentialBindingError("credentialPlacements");
|
|
527
|
-
}
|
|
528
|
-
if (
|
|
529
|
-
credentialTarget !== "http_api" &&
|
|
530
|
-
explicitPlacements.some((placement) => placement.carrier !== "header")
|
|
531
|
-
) {
|
|
532
|
-
throw new HostMcpCredentialBindingError("credentialPlacements");
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
const expiresAt = parseHostCredentialExpiry(result.expiresAt);
|
|
536
|
-
return {
|
|
537
|
-
status: "ok",
|
|
538
|
-
headers,
|
|
539
|
-
authoritySource: "host",
|
|
540
|
-
...(explicitPlacements ? { placements: explicitPlacements } : {}),
|
|
541
|
-
connectionId: result.connectionId,
|
|
542
|
-
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function assertHostMcpCredentialBinding(
|
|
547
|
-
result: McpCredentialResolution | McpGatewayCredentialResolution,
|
|
548
|
-
requested: McpServerConnectionRef,
|
|
549
|
-
): void {
|
|
550
|
-
if (result.providerDomain !== requested.providerDomain) {
|
|
551
|
-
throw new HostMcpCredentialBindingError("providerDomain");
|
|
552
|
-
}
|
|
553
|
-
if (result.provider !== requested.provider) {
|
|
554
|
-
throw new HostMcpCredentialBindingError("provider");
|
|
555
|
-
}
|
|
556
|
-
if (requested.connectionId && result.connectionId !== requested.connectionId) {
|
|
557
|
-
throw new HostMcpCredentialBindingError("connectionId");
|
|
558
|
-
}
|
|
559
|
-
if (!sameSelectedResources(result.selectedResources, requested.selectedResources)) {
|
|
560
|
-
throw new HostMcpCredentialBindingError("selectedResources");
|
|
561
|
-
}
|
|
562
|
-
if (result.status === "ok") {
|
|
563
|
-
if (!sameStringSet(result.scopes, requested.scopes)) {
|
|
564
|
-
throw new HostMcpCredentialBindingError("scopes");
|
|
565
|
-
}
|
|
566
|
-
if (result.resource !== requested.resource) {
|
|
567
|
-
throw new HostMcpCredentialBindingError("resource");
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
function sameStringSet(left: string[] | undefined, right: string[] | undefined): boolean {
|
|
573
|
-
if (left === undefined || right === undefined) return left === right;
|
|
574
|
-
if (left.length !== right.length) return false;
|
|
575
|
-
const sortedLeft = [...left].sort();
|
|
576
|
-
const sortedRight = [...right].sort();
|
|
577
|
-
return sortedLeft.every((value, index) => value === sortedRight[index]);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function sameSelectedResources(
|
|
581
|
-
left: McpConnectionResourceScope[] | undefined,
|
|
582
|
-
right: McpConnectionResourceScope[] | undefined,
|
|
583
|
-
): boolean {
|
|
584
|
-
if (left === undefined || right === undefined) return left === right;
|
|
585
|
-
const leftKeys = copySelectedResources(left)
|
|
586
|
-
.map((resource) => `${resource.kind}\0${resource.id}`)
|
|
587
|
-
.sort();
|
|
588
|
-
const rightKeys = copySelectedResources(right)
|
|
589
|
-
.map((resource) => `${resource.kind}\0${resource.id}`)
|
|
590
|
-
.sort();
|
|
591
|
-
return (
|
|
592
|
-
leftKeys.length === rightKeys.length &&
|
|
593
|
-
leftKeys.every((value, index) => value === rightKeys[index])
|
|
594
|
-
);
|
|
595
|
-
}
|
|
596
206
|
|
|
597
207
|
function copySelectedResources(
|
|
598
208
|
resources: McpConnectionResourceScope[],
|
|
599
209
|
): McpConnectionResourceScope[] {
|
|
600
210
|
if (resources.length === 0 || resources.length > 256) {
|
|
601
|
-
throw new Error("
|
|
211
|
+
throw new Error("connection credential returned an invalid selected resource count");
|
|
602
212
|
}
|
|
603
213
|
const seen = new Set<string>();
|
|
604
214
|
return resources.map((resource) => {
|
|
@@ -608,35 +218,24 @@ function copySelectedResources(
|
|
|
608
218
|
resource.id.length === 0 ||
|
|
609
219
|
resource.id.length > 512
|
|
610
220
|
) {
|
|
611
|
-
throw new Error("
|
|
221
|
+
throw new Error("connection credential returned an invalid selected resource");
|
|
612
222
|
}
|
|
613
223
|
const key = `${resource.kind}\0${resource.id}`;
|
|
614
224
|
if (seen.has(key)) {
|
|
615
|
-
throw new Error("
|
|
225
|
+
throw new Error("connection credential returned duplicate selected resources");
|
|
616
226
|
}
|
|
617
227
|
seen.add(key);
|
|
618
228
|
return { kind: resource.kind, id: resource.id };
|
|
619
229
|
});
|
|
620
230
|
}
|
|
621
231
|
|
|
622
|
-
function
|
|
623
|
-
result: { accountId: string; workspaceId: string; sessionId: string },
|
|
624
|
-
context: HostMcpCredentialResolverContext,
|
|
625
|
-
): void {
|
|
626
|
-
for (const field of ["accountId", "workspaceId", "sessionId"] as const) {
|
|
627
|
-
if (result[field] !== context[field]) {
|
|
628
|
-
throw new HostMcpCredentialScopeError(field);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
export function normalizedHostCredentialHeaders(
|
|
232
|
+
export function normalizedCredentialHeaders(
|
|
634
233
|
headers: Record<string, string>,
|
|
635
234
|
allowEmpty = false,
|
|
636
235
|
): Record<string, string> {
|
|
637
236
|
const entries = Object.entries(headers);
|
|
638
237
|
if ((!allowEmpty && entries.length === 0) || entries.length > 32) {
|
|
639
|
-
throw new Error("
|
|
238
|
+
throw new Error("connection credential returned an invalid header count");
|
|
640
239
|
}
|
|
641
240
|
const normalized: Record<string, string> = {};
|
|
642
241
|
for (const [name, value] of entries) {
|
|
@@ -649,10 +248,10 @@ export function normalizedHostCredentialHeaders(
|
|
|
649
248
|
FORBIDDEN_CREDENTIAL_HEADERS.has(name.toLowerCase()) ||
|
|
650
249
|
name.toLowerCase().startsWith("sec-")
|
|
651
250
|
) {
|
|
652
|
-
throw new Error("
|
|
251
|
+
throw new Error("connection credential returned an invalid header");
|
|
653
252
|
}
|
|
654
253
|
if (Object.keys(normalized).some((existing) => existing.toLowerCase() === name.toLowerCase())) {
|
|
655
|
-
throw new Error("
|
|
254
|
+
throw new Error("connection credential returned duplicate headers");
|
|
656
255
|
}
|
|
657
256
|
normalized[name] = value;
|
|
658
257
|
}
|
|
@@ -734,56 +333,6 @@ function headerMapForPlacements(
|
|
|
734
333
|
return headers;
|
|
735
334
|
}
|
|
736
335
|
|
|
737
|
-
function sameHeaderMap(
|
|
738
|
-
left: Readonly<Record<string, string>>,
|
|
739
|
-
right: Readonly<Record<string, string>>,
|
|
740
|
-
): boolean {
|
|
741
|
-
const canonical = (headers: Readonly<Record<string, string>>) =>
|
|
742
|
-
Object.entries(headers)
|
|
743
|
-
.map(([name, value]) => [name.toLowerCase(), value] as const)
|
|
744
|
-
.sort(([leftName], [rightName]) => leftName.localeCompare(rightName));
|
|
745
|
-
const leftEntries = canonical(left);
|
|
746
|
-
const rightEntries = canonical(right);
|
|
747
|
-
return (
|
|
748
|
-
leftEntries.length === rightEntries.length &&
|
|
749
|
-
leftEntries.every(
|
|
750
|
-
([name, value], index) =>
|
|
751
|
-
rightEntries[index]?.[0] === name && rightEntries[index]?.[1] === value,
|
|
752
|
-
)
|
|
753
|
-
);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
function normalizedAuthorizationUrl(value: string | undefined): string | undefined {
|
|
757
|
-
if (!value) return undefined;
|
|
758
|
-
let url: URL;
|
|
759
|
-
try {
|
|
760
|
-
url = new URL(value);
|
|
761
|
-
} catch {
|
|
762
|
-
throw new Error("host MCP credential returned an invalid authorizationUrl");
|
|
763
|
-
}
|
|
764
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) {
|
|
765
|
-
throw new Error("host MCP credential returned an invalid authorizationUrl");
|
|
766
|
-
}
|
|
767
|
-
return url.toString();
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function isLoopbackHost(hostname: string): boolean {
|
|
771
|
-
if (hostname === "localhost" || hostname === "::1") return true;
|
|
772
|
-
if (isIP(hostname) !== 4) return false;
|
|
773
|
-
const [first] = hostname.split(".");
|
|
774
|
-
return first === "127";
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
function parseHostCredentialExpiry(value: string | null | undefined): Date | null | undefined {
|
|
778
|
-
if (value === undefined) return undefined;
|
|
779
|
-
if (value === null) return null;
|
|
780
|
-
const parsed = new Date(value);
|
|
781
|
-
if (!Number.isFinite(parsed.getTime())) {
|
|
782
|
-
throw new Error("host MCP credential returned an invalid expiresAt");
|
|
783
|
-
}
|
|
784
|
-
return parsed;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
336
|
export type ConnectionBrokerDeps = {
|
|
788
337
|
loadCredential: (
|
|
789
338
|
db: Database,
|
|
@@ -805,13 +354,11 @@ export type ConnectionBrokerDeps = {
|
|
|
805
354
|
subjectId?: string | null,
|
|
806
355
|
) => Promise<void>;
|
|
807
356
|
refresh: typeof refreshOAuthConnectionCredential;
|
|
357
|
+
/** Test/adapter seam; standalone defaults to the database-backed refresh lock. */
|
|
358
|
+
withRefreshLock?: typeof withConnectionRefreshLock;
|
|
808
359
|
encrypt: typeof encryptEnvironmentValue;
|
|
809
360
|
keyBytes: typeof environmentsEncryptionKeyBytes;
|
|
810
361
|
now: () => Date;
|
|
811
|
-
authorizeUse?: (
|
|
812
|
-
db: Database,
|
|
813
|
-
input: { snapshot: unknown },
|
|
814
|
-
) => Promise<ConnectionUseAuthorizationResult>;
|
|
815
362
|
authorizeAcceptedUse?: (
|
|
816
363
|
db: Database,
|
|
817
364
|
input: AcceptedConnectionUseContext & {
|
|
@@ -869,6 +416,7 @@ export function buildConnectionTokenResolver(
|
|
|
869
416
|
> & { expectedAuthorityGeneration?: number };
|
|
870
417
|
const load = async (
|
|
871
418
|
input: CredentialLookupInput,
|
|
419
|
+
lookupDb: Database = db,
|
|
872
420
|
): Promise<ConnectionCredentialForBroker | null> => {
|
|
873
421
|
const subjectOwned = input.connectionRef.subjectScope === "subject";
|
|
874
422
|
if (subjectOwned && !input.subjectId) {
|
|
@@ -889,7 +437,7 @@ export function buildConnectionTokenResolver(
|
|
|
889
437
|
if (input.connectionRef.kind !== undefined) {
|
|
890
438
|
request.kind = input.connectionRef.kind;
|
|
891
439
|
}
|
|
892
|
-
const credential = await deps.loadCredential(
|
|
440
|
+
const credential = await deps.loadCredential(lookupDb, settings, request);
|
|
893
441
|
if (!credential) return null;
|
|
894
442
|
if (
|
|
895
443
|
input.expectedAuthorityGeneration !== undefined &&
|
|
@@ -968,6 +516,7 @@ export function buildConnectionTokenResolver(
|
|
|
968
516
|
const performRefresh = async (
|
|
969
517
|
cred: ConnectionCredentialForBroker,
|
|
970
518
|
ref: McpServerConnectionRef,
|
|
519
|
+
lockedDb: Database,
|
|
971
520
|
): Promise<ConnectionCredentialForBroker> => {
|
|
972
521
|
const key = deps.keyBytes(settings);
|
|
973
522
|
if (!key) {
|
|
@@ -989,22 +538,28 @@ export function buildConnectionTokenResolver(
|
|
|
989
538
|
if (refreshed.metadata !== undefined) {
|
|
990
539
|
refreshRecord.metadata = refreshed.metadata;
|
|
991
540
|
}
|
|
992
|
-
const persisted = await deps.recordRefresh(
|
|
541
|
+
const persisted = await deps.recordRefresh(lockedDb, refreshRecord);
|
|
993
542
|
if (persisted) {
|
|
994
|
-
const current = await load(
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
543
|
+
const current = await load(
|
|
544
|
+
{
|
|
545
|
+
workspaceId: cred.workspaceId,
|
|
546
|
+
connectionRef: { ...ref, connectionId: cred.id },
|
|
547
|
+
...(cred.subjectId ? { subjectId: cred.subjectId } : {}),
|
|
548
|
+
},
|
|
549
|
+
lockedDb,
|
|
550
|
+
);
|
|
999
551
|
if (current) {
|
|
1000
552
|
return current;
|
|
1001
553
|
}
|
|
1002
554
|
}
|
|
1003
|
-
const winner = await load(
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
555
|
+
const winner = await load(
|
|
556
|
+
{
|
|
557
|
+
workspaceId: cred.workspaceId,
|
|
558
|
+
connectionRef: { ...ref, connectionId: cred.id },
|
|
559
|
+
...(cred.subjectId ? { subjectId: cred.subjectId } : {}),
|
|
560
|
+
},
|
|
561
|
+
lockedDb,
|
|
562
|
+
);
|
|
1008
563
|
if (winner?.status === "active") {
|
|
1009
564
|
return winner;
|
|
1010
565
|
}
|
|
@@ -1020,7 +575,26 @@ export function buildConnectionTokenResolver(
|
|
|
1020
575
|
if (existing) {
|
|
1021
576
|
return existing;
|
|
1022
577
|
}
|
|
1023
|
-
const promise =
|
|
578
|
+
const promise = (deps.withRefreshLock ?? withConnectionRefreshLock)(
|
|
579
|
+
db,
|
|
580
|
+
cred,
|
|
581
|
+
async (lockedDb) => {
|
|
582
|
+
// A different worker may have rotated while this request waited. Never
|
|
583
|
+
// exchange the old token again, or switch to another authority generation.
|
|
584
|
+
const current = await load(
|
|
585
|
+
{
|
|
586
|
+
workspaceId: cred.workspaceId,
|
|
587
|
+
connectionRef: { ...ref, connectionId: cred.id },
|
|
588
|
+
...(cred.subjectId ? { subjectId: cred.subjectId } : {}),
|
|
589
|
+
},
|
|
590
|
+
lockedDb,
|
|
591
|
+
);
|
|
592
|
+
if (!current || current.authorityGeneration !== cred.authorityGeneration)
|
|
593
|
+
throw new Error("Connection authority changed before token refresh");
|
|
594
|
+
if (current.status !== "active" || current.version !== cred.version) return current;
|
|
595
|
+
return performRefresh(current, ref, lockedDb);
|
|
596
|
+
},
|
|
597
|
+
).finally(() => {
|
|
1024
598
|
if (inflight.get(key) === promise) {
|
|
1025
599
|
inflight.delete(key);
|
|
1026
600
|
}
|
|
@@ -1095,54 +669,6 @@ export function buildConnectionTokenResolver(
|
|
|
1095
669
|
kind: authorization.connectionKind,
|
|
1096
670
|
subjectScope: expectedPersonal ? "subject" : "workspace",
|
|
1097
671
|
};
|
|
1098
|
-
} else if (input.connectionUseAuthority !== undefined) {
|
|
1099
|
-
const authority = ConnectionUseAuthoritySnapshot.parse(input.connectionUseAuthority);
|
|
1100
|
-
const expectedPersonal = authority.scope === "user";
|
|
1101
|
-
if (
|
|
1102
|
-
authority.targetWorkspaceId !== input.workspaceId ||
|
|
1103
|
-
authority.providerDomain.toLowerCase() !== ref.providerDomain.toLowerCase() ||
|
|
1104
|
-
(ref.connectionId !== undefined && ref.connectionId !== authority.connectionId) ||
|
|
1105
|
-
(ref.kind !== undefined && ref.kind !== authority.connectionKind) ||
|
|
1106
|
-
(ref.subjectScope === "subject") !== expectedPersonal ||
|
|
1107
|
-
!deps.authorizeUse
|
|
1108
|
-
) {
|
|
1109
|
-
return authNeeded(
|
|
1110
|
-
ref,
|
|
1111
|
-
expectedPersonal ? "personal_authority_unavailable" : "missing_connection",
|
|
1112
|
-
ref.connectionId,
|
|
1113
|
-
);
|
|
1114
|
-
}
|
|
1115
|
-
const authorization = await deps.authorizeUse(db, { snapshot: authority });
|
|
1116
|
-
if (authorization.status === "denied") {
|
|
1117
|
-
return authNeeded(
|
|
1118
|
-
ref,
|
|
1119
|
-
expectedPersonal ? "personal_authority_unavailable" : "missing_connection",
|
|
1120
|
-
authority.connectionId,
|
|
1121
|
-
);
|
|
1122
|
-
}
|
|
1123
|
-
if (
|
|
1124
|
-
expectedAuthorityGeneration !== undefined &&
|
|
1125
|
-
authority.connectionGeneration !== expectedAuthorityGeneration
|
|
1126
|
-
) {
|
|
1127
|
-
return authNeeded(ref, authorityReasonForScope(expectedPersonal), authority.connectionId);
|
|
1128
|
-
}
|
|
1129
|
-
connectionUseAttribution = authorization.attribution;
|
|
1130
|
-
expectedAuthorityGeneration = authority.connectionGeneration;
|
|
1131
|
-
// Personal resources are organization-user owned and may originate in a
|
|
1132
|
-
// different workspace from the session using them. Authorization is
|
|
1133
|
-
// evaluated against the target workspace above; the exact credential is
|
|
1134
|
-
// then loaded from its frozen physical origin, never rediscovered in the
|
|
1135
|
-
// target workspace.
|
|
1136
|
-
credentialWorkspaceId = expectedPersonal
|
|
1137
|
-
? authority.originWorkspaceId
|
|
1138
|
-
: authority.targetWorkspaceId;
|
|
1139
|
-
subjectId = authority.ownerSubjectId ?? undefined;
|
|
1140
|
-
ref = {
|
|
1141
|
-
...ref,
|
|
1142
|
-
connectionId: authority.connectionId,
|
|
1143
|
-
kind: authority.connectionKind,
|
|
1144
|
-
subjectScope: expectedPersonal ? "subject" : "workspace",
|
|
1145
|
-
};
|
|
1146
672
|
}
|
|
1147
673
|
if (ref.subjectScope === "subject" && !subjectId) {
|
|
1148
674
|
return authNeeded(ref, "personal_authority_unavailable", ref.connectionId);
|
|
@@ -1207,7 +733,9 @@ export function buildConnectionTokenResolver(
|
|
|
1207
733
|
}
|
|
1208
734
|
if (!handled) {
|
|
1209
735
|
await deps
|
|
1210
|
-
|
|
736
|
+
// A personal credential may originate outside the workspace
|
|
737
|
+
// consuming it. Apply the CAS to the same row we refreshed.
|
|
738
|
+
.setStatus(db, cred.workspaceId, "needs_reauth", error.message, {
|
|
1211
739
|
id: cred.id,
|
|
1212
740
|
version: cred.version,
|
|
1213
741
|
subjectId: cred.subjectId,
|
|
@@ -1448,7 +976,7 @@ function credentialMaterialForConnection(
|
|
|
1448
976
|
};
|
|
1449
977
|
}
|
|
1450
978
|
try {
|
|
1451
|
-
const headers =
|
|
979
|
+
const headers = normalizedCredentialHeaders(
|
|
1452
980
|
stringRecord((cred.credential as { headers?: unknown }).headers) ?? {},
|
|
1453
981
|
);
|
|
1454
982
|
return { status: "ok", headers };
|