@executor-js/sdk 1.5.15 → 1.5.17

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.
@@ -62,6 +62,40 @@ var providerAuthorizeExtras = (authorizationUrl) => {
62
62
  }
63
63
  return {};
64
64
  };
65
+ var hostnameFromCallbackDomain = (callbackDomain) => {
66
+ const trimmed = callbackDomain.trim();
67
+ if (trimmed.length === 0) return void 0;
68
+ const candidate = trimmed.includes("://") ? trimmed : `https://${trimmed}`;
69
+ if (!URL.canParse(candidate)) return void 0;
70
+ const url = new URL(candidate);
71
+ if (url.port !== "" || url.username !== "" || url.password !== "") return void 0;
72
+ if (url.pathname !== "/" && url.pathname !== "") return void 0;
73
+ return url.hostname.toLowerCase();
74
+ };
75
+ var siblingParentDomainOf = (hostname) => {
76
+ const labels = hostname.split(".");
77
+ if (labels.length < 3) return void 0;
78
+ const parent = labels.slice(1).join(".");
79
+ return parent.includes(".") ? parent : void 0;
80
+ };
81
+ var rebindTokenEndpointHostToCallbackDomain = (configuredTokenUrl, callbackDomain) => {
82
+ if (!callbackDomain) return configuredTokenUrl;
83
+ if (!URL.canParse(configuredTokenUrl)) return configuredTokenUrl;
84
+ const configured = new URL(configuredTokenUrl);
85
+ if (configured.protocol !== "https:") return configuredTokenUrl;
86
+ const targetHost = hostnameFromCallbackDomain(callbackDomain);
87
+ if (!targetHost) return configuredTokenUrl;
88
+ const configuredHost = configured.hostname.toLowerCase();
89
+ if (targetHost === configuredHost) return configuredTokenUrl;
90
+ const configuredParent = siblingParentDomainOf(configuredHost);
91
+ const targetParent = siblingParentDomainOf(targetHost);
92
+ if (!configuredParent || !targetParent || configuredParent !== targetParent) {
93
+ return configuredTokenUrl;
94
+ }
95
+ const rebound = new URL(configuredTokenUrl);
96
+ rebound.hostname = targetHost;
97
+ return rebound.toString();
98
+ };
65
99
  var isOAuth2Error = Predicate.isTagged("OAuth2Error");
66
100
  var responseFromOAuthErrorCause = (cause) => {
67
101
  if (cause instanceof Response) return cause;
@@ -296,9 +330,10 @@ export {
296
330
  createOAuthState,
297
331
  buildAuthorizationUrl,
298
332
  providerAuthorizeExtras,
333
+ rebindTokenEndpointHostToCallbackDomain,
299
334
  exchangeAuthorizationCode,
300
335
  exchangeClientCredentials,
301
336
  refreshAccessToken,
302
337
  shouldRefreshToken
303
338
  };
304
- //# sourceMappingURL=chunk-EIHWBY6T.js.map
339
+ //# sourceMappingURL=chunk-4XPVLX62.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/oauth-helpers.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// OAuth 2.0 helpers — generic, isomorphic building blocks.\n//\n// Thin wrappers around `oauth4webapi` (stateless; pure Web Crypto +\n// `fetch`, no deps; runs unchanged in Node, CF Workers, and browsers).\n// Each public helper is a single `Effect.tryPromise` call that delegates\n// the RFC work to the library and normalises the failure surface into\n// `OAuth2Error`.\n//\n// What stays hand-rolled:\n// - `OAuth2Error` — our tagged error; we want a stable shape across\n// every token-endpoint call\n// - `shouldRefreshToken` — skew check, trivial\n// - `buildAuthorizationUrl` — the library doesn't expose a raw\n// authorization-URL builder (it prefers PAR); a 30-line manual\n// construction keeps the call sync and lets callers opt out of PAR\n// ---------------------------------------------------------------------------\n\nimport { Data, Effect, Predicate } from \"effect\";\nimport * as oauth from \"oauth4webapi\";\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\nexport class OAuth2Error extends Data.TaggedError(\"OAuth2Error\")<{\n readonly message: string;\n /**\n * RFC 6749 §5.2 error code, when the token endpoint returned one\n * (`invalid_grant`, `invalid_client`, `unauthorized_client`, ...).\n * Callers use this to distinguish terminal failures (a refresh token\n * the AS no longer honours → re-auth required) from transient ones.\n */\n readonly error?: string;\n readonly cause?: unknown;\n}> {}\n\n// ---------------------------------------------------------------------------\n// Token response shape (RFC 6749 §5.1)\n// ---------------------------------------------------------------------------\n\nexport type OAuth2TokenResponse = {\n readonly access_token: string;\n readonly token_type?: string;\n readonly refresh_token?: string;\n readonly expires_in?: number;\n readonly scope?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Refresh tokens this many ms before expiry to avoid mid-request expiration. */\nexport const OAUTH2_REFRESH_SKEW_MS = 60_000;\n\n/** Default token-endpoint timeout. */\nexport const OAUTH2_DEFAULT_TIMEOUT_MS = 20_000;\n\nexport interface OAuthEndpointUrlPolicy {\n readonly allowHttp?: boolean;\n}\n\nconst isLoopbackHttpUrl = (value: string): boolean => {\n if (!URL.canParse(value)) return false;\n const url = new URL(value);\n if (url.protocol !== \"http:\") return false;\n const hostname = url.hostname.toLowerCase();\n return (\n hostname === \"localhost\" ||\n hostname === \"0.0.0.0\" ||\n hostname === \"::1\" ||\n hostname === \"[::1]\" ||\n hostname.startsWith(\"127.\")\n );\n};\n\nexport const isSupportedOAuthEndpointUrl = (\n value: string,\n policy: OAuthEndpointUrlPolicy = {},\n): boolean => {\n if (!URL.canParse(value)) return false;\n const url = new URL(value);\n return (\n url.protocol === \"https:\" ||\n isLoopbackHttpUrl(value) ||\n (url.protocol === \"http:\" && policy.allowHttp === true)\n );\n};\n\nexport const assertSupportedOAuthEndpointUrl = (\n value: string,\n label = \"OAuth endpoint URL\",\n policy: OAuthEndpointUrlPolicy = {},\n): string => {\n if (isSupportedOAuthEndpointUrl(value, policy)) return value;\n // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: synchronous assertion helper used by URL constructors and Effect.try wrappers\n throw new TypeError(`${label} must use https: or loopback http:`);\n};\n\n// ---------------------------------------------------------------------------\n// PKCE (RFC 7636) — straight delegation to `oauth4webapi`\n// ---------------------------------------------------------------------------\n\nexport const createPkceCodeVerifier = (): string => oauth.generateRandomCodeVerifier();\n\nexport const createPkceCodeChallenge = (verifier: string): Promise<string> =>\n oauth.calculatePKCECodeChallenge(verifier);\n\n/** RFC 6749 `state` — an unguessable correlation token minted by `oauth.start`\n * and redeemed by `oauth.complete`. */\nexport const createOAuthState = (): string => oauth.generateRandomState();\n\n// ---------------------------------------------------------------------------\n// Authorization URL builder\n// ---------------------------------------------------------------------------\n\nexport type BuildAuthorizationUrlInput = {\n readonly authorizationUrl: string;\n readonly clientId: string;\n readonly redirectUrl: string;\n readonly scopes: readonly string[];\n readonly state: string;\n /** Pre-computed base64url S256 challenge (from `createPkceCodeChallenge`). */\n readonly codeChallenge: string;\n /** Separator between scopes. RFC 6749 says space; some providers use comma. */\n readonly scopeSeparator?: string;\n /** RFC 8707 Resource Indicator. MCP Authorization 2025-06-18 §\"Resource\n * Parameter Implementation\" requires clients to send this on every\n * authorization request, regardless of AS support. */\n readonly resource?: string;\n /** Provider-specific extras (e.g. Google's `access_type=offline`). */\n readonly extraParams?: Readonly<Record<string, string>>;\n readonly endpointUrlPolicy?: OAuthEndpointUrlPolicy;\n};\n\n/** Build an RFC 6749 §4.1.1 authorization URL. Sync; pre-computed\n * challenge lets this stay out of the Promise world. */\nexport const buildAuthorizationUrl = (input: BuildAuthorizationUrlInput): string => {\n const url = new URL(\n assertSupportedOAuthEndpointUrl(\n input.authorizationUrl,\n \"Authorization URL\",\n input.endpointUrlPolicy,\n ),\n );\n // Benign default kept by design: a single space is the RFC 6749 scope\n // separator. Callers targeting a legacy comma-separated provider pass\n // `scopeSeparator` explicitly (see the field's JSDoc).\n const separator = input.scopeSeparator ?? \" \";\n url.searchParams.set(\"client_id\", input.clientId);\n url.searchParams.set(\"redirect_uri\", input.redirectUrl);\n url.searchParams.set(\"response_type\", \"code\");\n if (input.scopes.length > 0) {\n url.searchParams.set(\"scope\", input.scopes.join(separator));\n }\n url.searchParams.set(\"state\", input.state);\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n url.searchParams.set(\"code_challenge\", input.codeChallenge);\n if (input.resource) {\n url.searchParams.set(\"resource\", input.resource);\n }\n if (input.extraParams) {\n for (const [k, v] of Object.entries(input.extraParams)) {\n url.searchParams.set(k, v);\n }\n }\n return url.toString();\n};\n\n/** Provider-specific authorize-URL extras that are NOT RFC 6749 params, so the\n * generic flow must add them per-provider (keyed off the authorization host).\n *\n * Google: `access_type=offline` + `prompt=consent` are required to receive (and\n * keep receiving, across reconnects / scope changes) a REFRESH TOKEN — without\n * them Google issues an access-token-only grant that dies in ~1h and a\n * re-consent can silently keep the old scope set. Do not add\n * `include_granted_scopes=true` here: with historical grants on the same Google\n * consent app, Google folds those unrelated scopes into the new consent flow and\n * can fail inside accounts.google.com before returning to our callback. */\nexport const providerAuthorizeExtras = (\n authorizationUrl: string,\n): Readonly<Record<string, string>> => {\n // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL() throws on invalid input → no provider extras\n try {\n const host = new URL(authorizationUrl).host.toLowerCase();\n if (host === \"accounts.google.com\") {\n return { access_type: \"offline\", prompt: \"consent\" };\n }\n } catch {\n // Unparseable authorization URL — let buildAuthorizationUrl surface the error.\n }\n return {};\n};\n\n// ---------------------------------------------------------------------------\n// Regional token-endpoint rebind\n//\n// Some authorization servers publish a single static metadata document that\n// advertises one region's token endpoint, but issue authorization codes that\n// are only redeemable at the *regional* host the user's org actually lives on.\n// The region comes back on the callback as a non-standard `domain` (or `site`)\n// query param: Datadog returns `domain=us5.datadoghq.com` while its metadata\n// statically advertises `app.datadoghq.com`. Redeeming the code at the\n// advertised host then fails with `invalid_grant`.\n//\n// `rebindTokenEndpointHostToCallbackDomain` swaps ONLY the hostname of the\n// configured token URL to the callback-supplied host, and ONLY when that host\n// is a sibling subdomain of the configured one (same parent after stripping the\n// leftmost DNS label, e.g. `app.datadoghq.com` and `us5.datadoghq.com` both\n// reduce to `datadoghq.com`). The token request carries the client secret, the\n// code, and the PKCE verifier, so an attacker-influenced `domain` must never be\n// able to point it at an arbitrary origin. Anything that fails the sibling\n// check, fails to parse, or isn't https falls back to the configured URL\n// unchanged.\n// ---------------------------------------------------------------------------\n\nconst hostnameFromCallbackDomain = (callbackDomain: string): string | undefined => {\n const trimmed = callbackDomain.trim();\n if (trimmed.length === 0) return undefined;\n // Datadog sends `domain` as a bare host and `site` as a full origin; accept\n // either by tolerating an optional scheme, then taking only the hostname.\n const candidate = trimmed.includes(\"://\") ? trimmed : `https://${trimmed}`;\n if (!URL.canParse(candidate)) return undefined;\n const url = new URL(candidate);\n // A legitimate regional host carries no port, credentials, or path.\n if (url.port !== \"\" || url.username !== \"\" || url.password !== \"\") return undefined;\n if (url.pathname !== \"/\" && url.pathname !== \"\") return undefined;\n return url.hostname.toLowerCase();\n};\n\n/** Parent domain after stripping the leftmost DNS label, or `undefined` when\n * the host has no sibling space (a single label, or a parent that is a bare\n * TLD). `app.datadoghq.com` -> `datadoghq.com`; `foo.com` -> undefined. */\nconst siblingParentDomainOf = (hostname: string): string | undefined => {\n const labels = hostname.split(\".\");\n if (labels.length < 3) return undefined;\n const parent = labels.slice(1).join(\".\");\n // Require the parent to itself be multi-label so a 2-label configured host\n // can never rebind across an entire TLD (e.g. foo.com -> bar.com).\n return parent.includes(\".\") ? parent : undefined;\n};\n\nexport const rebindTokenEndpointHostToCallbackDomain = (\n configuredTokenUrl: string,\n callbackDomain: string | null | undefined,\n): string => {\n if (!callbackDomain) return configuredTokenUrl;\n if (!URL.canParse(configuredTokenUrl)) return configuredTokenUrl;\n const configured = new URL(configuredTokenUrl);\n if (configured.protocol !== \"https:\") return configuredTokenUrl;\n const targetHost = hostnameFromCallbackDomain(callbackDomain);\n if (!targetHost) return configuredTokenUrl;\n const configuredHost = configured.hostname.toLowerCase();\n if (targetHost === configuredHost) return configuredTokenUrl;\n const configuredParent = siblingParentDomainOf(configuredHost);\n const targetParent = siblingParentDomainOf(targetHost);\n if (!configuredParent || !targetParent || configuredParent !== targetParent) {\n return configuredTokenUrl;\n }\n const rebound = new URL(configuredTokenUrl);\n rebound.hostname = targetHost;\n return rebound.toString();\n};\n\n// ---------------------------------------------------------------------------\n// Error mapping — `oauth4webapi`'s `process*Response` failure shapes are\n// either a WWW-Authenticate challenge or an RFC 6749 §5.2 error body,\n// both exposed via `.error` / `.error_description`. Probing the envelope\n// preserves RFC 6749 error-code semantics (e.g., mapping `invalid_grant`\n// to reauth-required) across wrappers.\n// ---------------------------------------------------------------------------\n\nconst isOAuth2Error = Predicate.isTagged(\"OAuth2Error\") as (cause: unknown) => cause is OAuth2Error;\n\nconst responseFromOAuthErrorCause = (cause: unknown): Response | undefined => {\n if (cause instanceof Response) return cause;\n if (typeof cause !== \"object\" || cause === null) return undefined;\n const envelope = cause as {\n readonly cause?: unknown;\n readonly response?: unknown;\n };\n if (envelope.response instanceof Response) return envelope.response;\n if (envelope.cause instanceof Response) return envelope.cause;\n return undefined;\n};\n\nconst redactTokenEndpointBody = (body: string): string =>\n body\n .replaceAll(\n /(\"(?:access_token|refresh_token|id_token|client_secret)\"\\s*:\\s*\")[^\"]*(\")/gi,\n \"$1[redacted]$2\",\n )\n .replaceAll(\n /((?:access_token|refresh_token|id_token|client_secret|code)=)[^&\\s]*/gi,\n \"$1[redacted]\",\n );\n\nconst tokenEndpointHttpSummary = async (response: Response): Promise<string> => {\n const status = `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : \"\"}`;\n const contentType = response.headers.get(\"content-type\");\n const url = response.url ? ` from ${response.url}` : \"\";\n const parts = [`${status}${url}`];\n if (contentType) parts.push(`content-type ${contentType}`);\n const preview = await bodyPreviewFromResponse(response);\n if (preview) parts.push(`body: ${preview}`);\n return parts.join(\"; \");\n};\n\nconst bodyPreviewFromResponse = async (response: Response): Promise<string | undefined> => {\n const text = await Promise.resolve()\n .then(() => response.clone().text())\n .then(\n (value) => value.trim(),\n () => \"\",\n );\n if (!text) return undefined;\n const redacted = redactTokenEndpointBody(text.replaceAll(/\\s+/g, \" \"));\n return redacted.length > 500 ? `${redacted.slice(0, 500)}...` : redacted;\n};\n\nconst toOAuth2Error = (cause: unknown): OAuth2Error => {\n if (isOAuth2Error(cause)) return cause;\n if (typeof cause === \"object\" && cause !== null) {\n const c = cause as {\n error?: unknown;\n error_description?: unknown;\n message?: unknown;\n };\n const code = typeof c.error === \"string\" ? c.error : undefined;\n const description =\n typeof c.error_description === \"string\"\n ? c.error_description\n : typeof c.message === \"string\"\n ? c.message\n : undefined;\n return new OAuth2Error({\n message: `OAuth token exchange failed: ${description ?? code ?? \"unknown error\"}`,\n error: code,\n cause,\n });\n }\n return new OAuth2Error({\n message: \"OAuth token exchange failed\",\n cause,\n });\n};\n\nconst toOAuth2ErrorWithHttpSummary = (cause: unknown): Effect.Effect<OAuth2Error> => {\n if (isOAuth2Error(cause)) return Effect.succeed(cause);\n const base = toOAuth2Error(cause);\n const response = responseFromOAuthErrorCause(cause);\n if (!response) return Effect.succeed(base);\n return Effect.promise(() => tokenEndpointHttpSummary(response)).pipe(\n Effect.map(\n (summary) =>\n new OAuth2Error({\n message: `${base.message} (${summary})`,\n error: base.error,\n cause,\n }),\n ),\n );\n};\n\nconst failOAuth2WithHttpSummary = (cause: unknown): Effect.Effect<never, OAuth2Error> =>\n toOAuth2ErrorWithHttpSummary(cause).pipe(Effect.flatMap((error) => Effect.fail(error)));\n\n// ---------------------------------------------------------------------------\n// oauth4webapi adapter helpers\n// ---------------------------------------------------------------------------\n\nexport type ClientAuthMethod = \"body\" | \"basic\";\n\n/**\n * The token-endpoint client-auth transport used when a caller doesn't specify\n * one. `\"body\"` is `client_secret_post` (the secret in the form body) — the\n * method our DCR registers (`token_endpoint_auth_method: client_secret_post`)\n * and the one every confidential client in the v2 model uses. EXPLICIT and\n * documented rather than a hidden inline `?? \"body\"`: callers that need\n * `client_secret_basic` pass `clientAuth: \"basic\"`. For PUBLIC clients (no\n * secret) the method is irrelevant — `pickClientAuth` returns `None()`.\n */\nexport const DEFAULT_CLIENT_AUTH_METHOD: ClientAuthMethod = \"body\";\n\nconst asFromTokenUrl = (\n tokenUrl: string,\n endpointUrlPolicy: OAuthEndpointUrlPolicy = {},\n): oauth.AuthorizationServer => {\n assertSupportedOAuthEndpointUrl(tokenUrl, \"Token URL\", endpointUrlPolicy);\n const url = new URL(tokenUrl);\n return {\n issuer: `${url.protocol}//${url.host}`,\n token_endpoint: tokenUrl,\n };\n};\n\nconst asFromTokenUrlAndIssuer = (\n tokenUrl: string,\n issuerUrl: string | null | undefined,\n options: {\n readonly idTokenSigningAlgValuesSupported?: readonly string[];\n readonly endpointUrlPolicy?: OAuthEndpointUrlPolicy;\n } = {},\n): oauth.AuthorizationServer => {\n const as = asFromTokenUrl(tokenUrl, options.endpointUrlPolicy);\n const withIssuer = issuerUrl ? { ...as, issuer: issuerUrl } : as;\n return options.idTokenSigningAlgValuesSupported\n ? {\n ...withIssuer,\n id_token_signing_alg_values_supported: [...options.idTokenSigningAlgValuesSupported],\n }\n : withIssuer;\n};\n\nconst oauth4webapiRequestOptions = (\n targetUrl: string,\n timeoutMs: number | undefined,\n endpointUrlPolicy: OAuthEndpointUrlPolicy = {},\n): Record<string, unknown> => {\n const options: Record<string, unknown> = {\n signal: AbortSignal.timeout(timeoutMs ?? OAUTH2_DEFAULT_TIMEOUT_MS),\n };\n if (\n isLoopbackHttpUrl(targetUrl) ||\n (URL.canParse(targetUrl) &&\n new URL(targetUrl).protocol === \"http:\" &&\n endpointUrlPolicy.allowHttp === true)\n ) {\n (options as { [oauth.allowInsecureRequests]?: boolean })[oauth.allowInsecureRequests] = true;\n }\n return options;\n};\n\n// Select the token-endpoint client authentication. The secret's presence is the\n// EXPLICIT public-vs-confidential discriminator in the v2 model: a registered\n// client either has a secret (confidential — authenticate it) or has none\n// (public PKCE — `None()`, RFC 7636). This is not a silent guess: `loadClient`\n// persists a non-empty secret for confidential clients and null/\"\" for public\n// ones, so an absent secret here unambiguously means \"public client\". The\n// `method` only chooses HOW a present secret is sent (post vs basic).\nconst pickClientAuth = (\n clientSecret: string | null | undefined,\n method: ClientAuthMethod,\n): oauth.ClientAuth => {\n if (!clientSecret) return oauth.None();\n return method === \"basic\"\n ? oauth.ClientSecretBasic(clientSecret)\n : oauth.ClientSecretPost(clientSecret);\n};\n\nconst tokenResponseFrom = (r: oauth.TokenEndpointResponse): OAuth2TokenResponse => ({\n access_token: r.access_token,\n token_type: r.token_type,\n refresh_token: r.refresh_token,\n expires_in: typeof r.expires_in === \"number\" ? r.expires_in : undefined,\n scope: r.scope,\n});\n\n// MCP source connections are pure OAuth 2.0 — we never request `openid` and\n// never consume `id_token`. Some providers (PostHog, etc.) front an OIDC\n// backend and emit an `id_token` anyway; oauth4webapi then strict-validates\n// its claims against the AS metadata and rejects mismatches we don't care\n// about. Strip the field before delegation.\nconst stripIdToken = async (response: Response): Promise<Response> => {\n const body = await response\n .clone()\n .json()\n .then(\n (value: unknown) => value,\n () => null,\n );\n if (!body || typeof body !== \"object\" || !(\"id_token\" in (body as Record<string, unknown>))) {\n return response;\n }\n const { id_token: _ignored, ...rest } = body as Record<string, unknown>;\n return new Response(JSON.stringify(rest), {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n};\n\nconst processTokenEndpointResponse = async (\n as: oauth.AuthorizationServer,\n client: oauth.Client,\n response: Response,\n): Promise<OAuth2TokenResponse> =>\n tokenResponseFrom(\n await oauth.processGenericTokenEndpointResponse(as, client, await stripIdToken(response)),\n );\n\n// ---------------------------------------------------------------------------\n// Exchange authorization code → tokens\n// ---------------------------------------------------------------------------\n\nexport type ExchangeAuthorizationCodeInput = {\n readonly tokenUrl: string;\n readonly issuerUrl?: string | null;\n readonly clientId: string;\n readonly clientSecret?: string | null;\n readonly redirectUrl: string;\n readonly codeVerifier: string;\n readonly code: string;\n readonly clientAuth?: ClientAuthMethod;\n readonly idTokenSigningAlgValuesSupported?: readonly string[];\n /** RFC 8707 Resource Indicator. MCP Auth spec MUST-requires this on\n * the token request when the client knows the resource it intends\n * to call. */\n readonly resource?: string;\n readonly timeoutMs?: number;\n readonly endpointUrlPolicy?: OAuthEndpointUrlPolicy;\n};\n\nexport const exchangeAuthorizationCode = (\n input: ExchangeAuthorizationCodeInput,\n): Effect.Effect<OAuth2TokenResponse, OAuth2Error> =>\n Effect.tryPromise({\n try: async () => {\n const as = asFromTokenUrlAndIssuer(input.tokenUrl, input.issuerUrl, {\n idTokenSigningAlgValuesSupported: input.idTokenSigningAlgValuesSupported,\n endpointUrlPolicy: input.endpointUrlPolicy,\n });\n const client: oauth.Client = { client_id: input.clientId };\n const clientAuth = pickClientAuth(\n input.clientSecret,\n input.clientAuth ?? DEFAULT_CLIENT_AUTH_METHOD,\n );\n // `authorizationCodeGrantRequest` requires its `callbackParameters`\n // to have been returned from `validateAuthResponse`. Our public API\n // takes the `code` directly (the UI already validated `state` by\n // looking up the session), so skip the library's state-validation\n // rail and go through the generic grant request instead.\n const params = new URLSearchParams({\n code: input.code,\n redirect_uri: input.redirectUrl,\n code_verifier: input.codeVerifier,\n });\n if (input.resource) {\n params.set(\"resource\", input.resource);\n }\n const response = await oauth.genericTokenEndpointRequest(\n as,\n client,\n clientAuth,\n \"authorization_code\",\n params,\n oauth4webapiRequestOptions(input.tokenUrl, input.timeoutMs, input.endpointUrlPolicy),\n );\n return await processTokenEndpointResponse(as, client, response);\n },\n catch: (cause) => cause,\n }).pipe(Effect.catch(failOAuth2WithHttpSummary));\n\n// ---------------------------------------------------------------------------\n// Exchange client credentials → tokens (RFC 6749 §4.4)\n// ---------------------------------------------------------------------------\n\nexport type ExchangeClientCredentialsInput = {\n readonly tokenUrl: string;\n readonly clientId: string;\n readonly clientSecret: string;\n readonly scopes?: readonly string[];\n readonly scopeSeparator?: string;\n readonly clientAuth?: ClientAuthMethod;\n /** RFC 8707 Resource Indicator. MCP Authorization 2025-06-18 requires this\n * on token requests when the client knows the protected resource. */\n readonly resource?: string;\n readonly timeoutMs?: number;\n readonly endpointUrlPolicy?: OAuthEndpointUrlPolicy;\n};\n\nexport const exchangeClientCredentials = (\n input: ExchangeClientCredentialsInput,\n): Effect.Effect<OAuth2TokenResponse, OAuth2Error> =>\n Effect.tryPromise({\n try: async () => {\n const as = asFromTokenUrl(input.tokenUrl, input.endpointUrlPolicy);\n const client: oauth.Client = { client_id: input.clientId };\n const clientAuth = pickClientAuth(\n input.clientSecret,\n input.clientAuth ?? DEFAULT_CLIENT_AUTH_METHOD,\n );\n const params = new URLSearchParams();\n if (input.scopes && input.scopes.length > 0) {\n params.set(\"scope\", input.scopes.join(input.scopeSeparator ?? \" \"));\n }\n if (input.resource) {\n params.set(\"resource\", input.resource);\n }\n const response = await oauth.clientCredentialsGrantRequest(\n as,\n client,\n clientAuth,\n params,\n oauth4webapiRequestOptions(input.tokenUrl, input.timeoutMs, input.endpointUrlPolicy),\n );\n const result = await oauth.processClientCredentialsResponse(as, client, response);\n return tokenResponseFrom(result);\n },\n catch: (cause) => cause,\n }).pipe(Effect.catch(failOAuth2WithHttpSummary));\n\n// ---------------------------------------------------------------------------\n// Refresh access token\n// ---------------------------------------------------------------------------\n\nexport type RefreshAccessTokenInput = {\n readonly tokenUrl: string;\n readonly issuerUrl?: string | null;\n readonly clientId: string;\n readonly clientSecret?: string | null;\n readonly refreshToken: string;\n readonly scopes?: readonly string[];\n readonly scopeSeparator?: string;\n readonly clientAuth?: ClientAuthMethod;\n readonly idTokenSigningAlgValuesSupported?: readonly string[];\n /** RFC 8707 Resource Indicator — MCP spec MUST-requires this on\n * refresh requests so the new access token's audience is bound to\n * the same resource. */\n readonly resource?: string;\n readonly timeoutMs?: number;\n readonly endpointUrlPolicy?: OAuthEndpointUrlPolicy;\n};\n\nexport const refreshAccessToken = (\n input: RefreshAccessTokenInput,\n): Effect.Effect<OAuth2TokenResponse, OAuth2Error> =>\n Effect.tryPromise({\n try: async () => {\n const as = asFromTokenUrlAndIssuer(input.tokenUrl, input.issuerUrl, {\n idTokenSigningAlgValuesSupported: input.idTokenSigningAlgValuesSupported,\n endpointUrlPolicy: input.endpointUrlPolicy,\n });\n const client: oauth.Client = { client_id: input.clientId };\n const clientAuth = pickClientAuth(\n input.clientSecret,\n input.clientAuth ?? DEFAULT_CLIENT_AUTH_METHOD,\n );\n const extraParams = new URLSearchParams();\n if (input.scopes && input.scopes.length > 0) {\n extraParams.set(\"scope\", input.scopes.join(input.scopeSeparator ?? \" \"));\n }\n if (input.resource) {\n extraParams.set(\"resource\", input.resource);\n }\n const additionalParameters =\n Array.from(extraParams.keys()).length > 0 ? extraParams : undefined;\n const response = await oauth.refreshTokenGrantRequest(\n as,\n client,\n clientAuth,\n input.refreshToken,\n {\n ...oauth4webapiRequestOptions(input.tokenUrl, input.timeoutMs, input.endpointUrlPolicy),\n additionalParameters,\n },\n );\n const result = await oauth.processRefreshTokenResponse(\n as,\n client,\n await stripIdToken(response),\n );\n return tokenResponseFrom(result);\n },\n catch: (cause) => cause,\n }).pipe(Effect.catch(failOAuth2WithHttpSummary));\n\n// ---------------------------------------------------------------------------\n// Refresh-needed predicate\n// ---------------------------------------------------------------------------\n\nexport const shouldRefreshToken = (input: {\n readonly expiresAt: number | null;\n readonly now?: number;\n readonly skewMs?: number;\n}): boolean => {\n if (input.expiresAt === null) return false;\n const now = input.now ?? Date.now();\n const skew = input.skewMs ?? OAUTH2_REFRESH_SKEW_MS;\n return input.expiresAt <= now + skew;\n};\n"],"mappings":"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","names":[]}
@@ -24,7 +24,7 @@ import {
24
24
  isValidPattern,
25
25
  resolveEffectivePolicy,
26
26
  rowToToolPolicy
27
- } from "./chunk-6EED5LAL.js";
27
+ } from "./chunk-PCSRC6WP.js";
28
28
  import {
29
29
  OAUTH2_DEFAULT_TIMEOUT_MS,
30
30
  assertSupportedOAuthEndpointUrl,
@@ -35,9 +35,10 @@ import {
35
35
  exchangeAuthorizationCode,
36
36
  exchangeClientCredentials,
37
37
  providerAuthorizeExtras,
38
+ rebindTokenEndpointHostToCallbackDomain,
38
39
  refreshAccessToken,
39
40
  shouldRefreshToken
40
- } from "./chunk-EIHWBY6T.js";
41
+ } from "./chunk-4XPVLX62.js";
41
42
  import {
42
43
  AuthTemplateSlug,
43
44
  ConnectionAddress,
@@ -549,6 +550,11 @@ var coreTables = defineTables({
549
550
  refresh_item_id: nullableTextColumn("refresh_item_id"),
550
551
  expires_at: nullableBigintColumn("expires_at"),
551
552
  oauth_scope: nullableTextColumn("oauth_scope"),
553
+ // Per-connection token endpoint override. Set only when the code was
554
+ // redeemed at a region other than the oauth_client's configured token host
555
+ // (multi-site providers like Datadog signal the org's region on the
556
+ // callback). Null means refresh uses the oauth_client's `token_url`.
557
+ oauth_token_url: nullableTextColumn("oauth_token_url"),
552
558
  provider_state: nullableJsonColumn("provider_state"),
553
559
  created_at: dateColumn("created_at"),
554
560
  updated_at: dateColumn("updated_at")
@@ -3114,10 +3120,26 @@ var ConnectionOutput = Schema2.Struct({
3114
3120
  });
3115
3121
  var ConnectionsListInput = Schema2.Struct({
3116
3122
  integration: Schema2.optional(Schema2.String),
3117
- owner: Schema2.optional(OwnerSchema)
3123
+ owner: Schema2.optional(OwnerSchema),
3124
+ verbose: Schema2.optional(Schema2.Boolean)
3125
+ });
3126
+ var ConnectionListItem = Schema2.Struct({
3127
+ owner: OwnerSchema,
3128
+ name: Schema2.String,
3129
+ integration: Schema2.String,
3130
+ template: Schema2.String,
3131
+ provider: Schema2.String,
3132
+ address: Schema2.String,
3133
+ identityLabel: Schema2.optional(Schema2.NullOr(Schema2.String)),
3134
+ description: Schema2.optional(Schema2.NullOr(Schema2.String)),
3135
+ expiresAt: Schema2.NullOr(Schema2.Number),
3136
+ oauthClient: Schema2.NullOr(Schema2.String),
3137
+ oauthClientOwner: Schema2.NullOr(OwnerSchema),
3138
+ oauthScopeCount: Schema2.NullOr(Schema2.Number),
3139
+ oauthScope: Schema2.optional(Schema2.NullOr(Schema2.String))
3118
3140
  });
3119
3141
  var ConnectionsListOutput = Schema2.Struct({
3120
- connections: Schema2.Array(ConnectionOutput)
3142
+ connections: Schema2.Array(ConnectionListItem)
3121
3143
  });
3122
3144
  var ConnectionCreateHandoffInput = Schema2.Struct({
3123
3145
  integration: Schema2.String,
@@ -3332,6 +3354,22 @@ var connectionToOutput = (connection) => ({
3332
3354
  oauthClientOwner: connection.oauthClientOwner ?? null,
3333
3355
  oauthScope: connection.oauthScope ?? null
3334
3356
  });
3357
+ var oauthScopeCount = (scope) => scope == null ? null : scope.split(/\s+/).filter(Boolean).length;
3358
+ var connectionToListItem = (connection, verbose) => ({
3359
+ owner: connection.owner,
3360
+ name: String(connection.name),
3361
+ integration: String(connection.integration),
3362
+ template: String(connection.template),
3363
+ provider: String(connection.provider),
3364
+ address: String(connection.address),
3365
+ identityLabel: connection.identityLabel ?? null,
3366
+ description: connection.description ?? null,
3367
+ expiresAt: connection.expiresAt ?? null,
3368
+ oauthClient: connection.oauthClient == null ? null : String(connection.oauthClient),
3369
+ oauthClientOwner: connection.oauthClientOwner ?? null,
3370
+ oauthScopeCount: oauthScopeCount(connection.oauthScope),
3371
+ ...verbose ? { oauthScope: connection.oauthScope ?? null } : {}
3372
+ });
3335
3373
  var toolToOutput = (toolRow) => ({
3336
3374
  address: String(toolRow.address),
3337
3375
  owner: toolRow.owner,
@@ -3430,7 +3468,7 @@ var coreToolsPlugin = definePlugin((options = {}) => ({
3430
3468
  }),
3431
3469
  tool({
3432
3470
  name: "connections.list",
3433
- description: "List saved connections (the credential for one integration). Never returns the credential value. Optionally filter by integration or owner.",
3471
+ description: "List saved connections (the credential for one integration). Never returns the credential value. Optionally filter by integration or owner. OAuth scopes are summarized as `oauthScopeCount` by default; pass `verbose: true` to include the full `oauthScope` grant string per connection.",
3434
3472
  inputSchema: ConnectionsListInputStd,
3435
3473
  outputSchema: ConnectionsListOutputStd,
3436
3474
  execute: (input, { ctx }) => Effect5.map(
@@ -3439,7 +3477,9 @@ var coreToolsPlugin = definePlugin((options = {}) => ({
3439
3477
  owner: input.owner === void 0 ? void 0 : input.owner
3440
3478
  }),
3441
3479
  (connections) => ({
3442
- connections: connections.map(connectionToOutput)
3480
+ connections: connections.map(
3481
+ (connection) => connectionToListItem(connection, input.verbose === true)
3482
+ )
3443
3483
  })
3444
3484
  )
3445
3485
  }),
@@ -4041,6 +4081,13 @@ var looseDb = (db) => db;
4041
4081
  var accessItemId = (owner, integration, name) => `oauth:${owner}:${integration}:${name}`;
4042
4082
  var refreshItemIdFor = (accessId) => `${accessId}:refresh`;
4043
4083
  var dedupeScopes = (scopes) => [...new Set(scopes)];
4084
+ var recordedOAuthScope = (token, requestedScopes) => {
4085
+ if (token.scope == null) return requestedScopes.join(" ") || null;
4086
+ const granted = token.scope.split(/\s+/).filter(Boolean);
4087
+ const coveredByRefreshToken = token.refresh_token && requestedScopes.includes("offline_access") ? ["offline_access"] : [];
4088
+ const recorded = dedupeScopes([...granted, ...coveredByRefreshToken]);
4089
+ return recorded.join(" ") || null;
4090
+ };
4044
4091
  var decodeJsonPayload = Schema4.decodeUnknownOption(Schema4.UnknownFromJsonString);
4045
4092
  var requestedScopesFromPayload = (payload) => {
4046
4093
  const decoded = typeof payload === "string" ? decodeJsonPayload(payload).pipe(Option2.getOrElse(() => payload)) : payload;
@@ -4332,7 +4379,9 @@ var makeOAuthService = (deps) => {
4332
4379
  client,
4333
4380
  token,
4334
4381
  requestedScopes,
4335
- input.clientOwner
4382
+ input.clientOwner,
4383
+ // client_credentials has no callback, so no regional rebind applies.
4384
+ null
4336
4385
  ).pipe(
4337
4386
  Effect7.mapError(
4338
4387
  (cause) => new OAuthStartError({
@@ -4445,8 +4494,12 @@ var makeOAuthService = (deps) => {
4445
4494
  restartRequired: true
4446
4495
  });
4447
4496
  }
4497
+ const tokenUrl = rebindTokenEndpointHostToCallbackDomain(
4498
+ client.tokenUrl,
4499
+ input.callbackDomain
4500
+ );
4448
4501
  const token = yield* exchangeAuthorizationCode({
4449
- tokenUrl: client.tokenUrl,
4502
+ tokenUrl,
4450
4503
  clientId: client.clientId,
4451
4504
  clientSecret: client.clientSecret,
4452
4505
  redirectUrl: session.redirectUrl,
@@ -4476,7 +4529,10 @@ var makeOAuthService = (deps) => {
4476
4529
  // The scopes `start` requested (the integration's declared set), persisted
4477
4530
  // on the session. Empty only for a corrupt/legacy session with no payload.
4478
4531
  session.requestedScopes ?? [],
4479
- session.clientOwner
4532
+ session.clientOwner,
4533
+ // Persist the regional token endpoint ONLY when it differs from the
4534
+ // client's configured one, so refresh redeems against the same region.
4535
+ tokenUrl === client.tokenUrl ? null : tokenUrl
4480
4536
  ).pipe(
4481
4537
  Effect7.mapError(
4482
4538
  (cause) => new OAuthCompleteError({
@@ -4489,7 +4545,7 @@ var makeOAuthService = (deps) => {
4489
4545
  yield* deleteSession(input.state);
4490
4546
  return connection;
4491
4547
  });
4492
- const mintFromToken = (target, client, token, requestedScopes, clientOwner) => Effect7.gen(function* () {
4548
+ const mintFromToken = (target, client, token, requestedScopes, clientOwner, oauthTokenUrl) => Effect7.gen(function* () {
4493
4549
  const provider = deps.defaultWritableProvider();
4494
4550
  if (!provider || !provider.set) {
4495
4551
  return yield* new StorageError({
@@ -4516,12 +4572,12 @@ var makeOAuthService = (deps) => {
4516
4572
  oauthClientOwner: clientOwner,
4517
4573
  refreshItemId,
4518
4574
  expiresAt: expiresAtFrom(token),
4519
- // Benign fallback (kept by design): record the granted scope the AS
4520
- // echoed back; when it omits `scope` (some servers do), fall back to the
4521
- // scopes we requested (declared client). This only affects the recorded
4522
- // scope label, not what the token can do, so a guess here masks no
4523
- // misconfiguration.
4524
- oauthScope: token.scope ?? (requestedScopes.join(" ") || null)
4575
+ // Record the granted scope the AS echoed back. Some providers, including
4576
+ // Microsoft, issue a refresh token for `offline_access` but omit that
4577
+ // non-resource scope from the token `scope` string, so preserve it when
4578
+ // the refresh token proves it was granted.
4579
+ oauthScope: recordedOAuthScope(token, requestedScopes),
4580
+ oauthTokenUrl
4525
4581
  });
4526
4582
  });
4527
4583
  const deleteSession = (state) => deps.fuma.use(
@@ -4573,6 +4629,7 @@ var makeOAuthService = (deps) => {
4573
4629
 
4574
4630
  // src/executor.ts
4575
4631
  var PLUGIN_STORAGE_DELETE_KEY_BATCH_SIZE = 90;
4632
+ var PLUGIN_STORAGE_CREATE_ROW_BATCH_SIZE = 90;
4576
4633
  var MAX_APPROVAL_ARGUMENT_PREVIEW_CHARS = 4e3;
4577
4634
  var acceptAllHandler = () => Effect8.succeed(ElicitationResponse.make({ action: "accept" }));
4578
4635
  var resolveElicitationHandler = (onElicitation) => onElicitation === "accept-all" ? acceptAllHandler : onElicitation;
@@ -4970,20 +5027,26 @@ var makePluginStorageFacade = (input) => {
4970
5027
  if (uniqueEntries.length === 0) return;
4971
5028
  yield* deleteManyImpl(owner, os.subject, uniqueEntries);
4972
5029
  const now = /* @__PURE__ */ new Date();
4973
- yield* input.core.createMany(
4974
- "plugin_storage",
4975
- uniqueEntries.map((entry) => ({
4976
- tenant,
4977
- owner: os.owner,
4978
- subject: os.subject,
4979
- plugin_id: input.pluginId,
4980
- collection: entry.collection,
4981
- key: entry.key,
4982
- data: entry.data,
4983
- created_at: now,
4984
- updated_at: now
4985
- }))
4986
- );
5030
+ for (let offset = 0; offset < uniqueEntries.length; offset += PLUGIN_STORAGE_CREATE_ROW_BATCH_SIZE) {
5031
+ const batchEntries = uniqueEntries.slice(
5032
+ offset,
5033
+ offset + PLUGIN_STORAGE_CREATE_ROW_BATCH_SIZE
5034
+ );
5035
+ yield* input.core.createMany(
5036
+ "plugin_storage",
5037
+ batchEntries.map((entry) => ({
5038
+ tenant,
5039
+ owner: os.owner,
5040
+ subject: os.subject,
5041
+ plugin_id: input.pluginId,
5042
+ collection: entry.collection,
5043
+ key: entry.key,
5044
+ data: entry.data,
5045
+ created_at: now,
5046
+ updated_at: now
5047
+ }))
5048
+ );
5049
+ }
4987
5050
  });
4988
5051
  const removeManyImpl = (owner, entries) => Effect8.gen(function* () {
4989
5052
  const os = ownerSubject(owner);
@@ -5236,8 +5299,9 @@ var createExecutor = (config) => Effect8.gen(function* () {
5236
5299
  }
5237
5300
  const clientSecret = clientRow.client_secret_item_id ? (yield* provider.get(ProviderItemId.make(String(clientRow.client_secret_item_id)))) ?? "" : "";
5238
5301
  const grantedScopes = row.oauth_scope ? String(row.oauth_scope).split(/\s+/).filter(Boolean) : [];
5302
+ const tokenUrl = row.oauth_token_url ? String(row.oauth_token_url) : String(clientRow.token_url);
5239
5303
  const token = String(clientRow.grant) === "client_credentials" ? yield* exchangeClientCredentials({
5240
- tokenUrl: String(clientRow.token_url),
5304
+ tokenUrl,
5241
5305
  clientId: String(clientRow.client_id),
5242
5306
  clientSecret,
5243
5307
  scopes: grantedScopes,
@@ -5264,7 +5328,7 @@ var createExecutor = (config) => Effect8.gen(function* () {
5264
5328
  return yield* reauth("Stored refresh token could not be resolved.");
5265
5329
  }
5266
5330
  return yield* refreshAccessToken({
5267
- tokenUrl: String(clientRow.token_url),
5331
+ tokenUrl,
5268
5332
  clientId: String(clientRow.client_id),
5269
5333
  clientSecret,
5270
5334
  refreshToken,
@@ -5804,6 +5868,7 @@ var createExecutor = (config) => Effect8.gen(function* () {
5804
5868
  refresh_item_id: input.refreshItemId,
5805
5869
  expires_at: input.expiresAt,
5806
5870
  oauth_scope: input.oauthScope,
5871
+ oauth_token_url: input.oauthTokenUrl ?? null,
5807
5872
  updated_at: now
5808
5873
  };
5809
5874
  if (existing) {
@@ -5834,6 +5899,7 @@ var createExecutor = (config) => Effect8.gen(function* () {
5834
5899
  refresh_item_id: input.refreshItemId,
5835
5900
  expires_at: input.expiresAt,
5836
5901
  oauth_scope: input.oauthScope,
5902
+ oauth_token_url: input.oauthTokenUrl ?? null,
5837
5903
  provider_state: null,
5838
5904
  created_at: now,
5839
5905
  updated_at: now
@@ -5861,6 +5927,7 @@ var createExecutor = (config) => Effect8.gen(function* () {
5861
5927
  refresh_item_id: input.refreshItemId,
5862
5928
  expires_at: input.expiresAt,
5863
5929
  oauth_scope: input.oauthScope,
5930
+ oauth_token_url: input.oauthTokenUrl ?? null,
5864
5931
  provider_state: null,
5865
5932
  created_at: now,
5866
5933
  updated_at: now
@@ -6672,4 +6739,4 @@ export {
6672
6739
  collectTables,
6673
6740
  createExecutor
6674
6741
  };
6675
- //# sourceMappingURL=chunk-QEKKFEJL.js.map
6742
+ //# sourceMappingURL=chunk-5Q35SELN.js.map