import { useCallback, useEffect, useMemo, useRef, useState } from "react";import { useAtomSet, useAtomValue } from "@effect/atom-react";import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Match from "effect/Match";import * as Option from "effect/Option";import * as Schema from "effect/Schema";import { ChevronDownIcon } from "lucide-react"; import { ConnectionId, ScopeId, SecretId, SetSourceCredentialBindingInput,} from "@executor-js/sdk/shared";import { connectionIdentityAtom, connectionsAtom, setSourceCredentialBinding, startOAuth,} from "@executor-js/react/api/atoms";import { useScope, useScopeStack } from "@executor-js/react/api/scope-context";import { connectionWriteKeys, sourceWriteKeys } from "@executor-js/react/api/reactivity-keys"; // `addSpec` with an oauth2 payload persists a source row AND (for// clientCredentials) a freshly-minted Connection + owned secrets,// because the inline token exchange happens during `startOAuth`.// Invalidate both so the source-detail page opens into its connected// state without a refresh.const addSpecWriteKeys = [...sourceWriteKeys, ...connectionWriteKeys] as const;const bindingWriteKeys = [...sourceWriteKeys, ...connectionWriteKeys] as const;import { HeadersList } from "@executor-js/react/plugins/headers-list";import { HttpCredentialsEditor, emptyHttpCredentials, serializeHttpCredentials, type HttpCredentialsState,} from "@executor-js/react/plugins/http-credentials";import { oauthCallbackUrl, useOAuthPopupFlow, type OAuthCompletionPayload,} from "@executor-js/react/plugins/oauth-sign-in";import { CreatableSecretPicker, matchPresetKey, type HeaderState,} from "@executor-js/react/plugins/secret-header-auth";import { CredentialScopeDropdown } from "@executor-js/react/plugins/credential-target-scope";import { slugifyNamespace, useSourceIdentity } from "@executor-js/react/plugins/source-identity";import { useSecretPickerSecrets } from "@executor-js/react/plugins/use-secret-picker-secrets";import { Button } from "@executor-js/react/components/button";import { CopyButton } from "@executor-js/react/components/copy-button";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@executor-js/react/components/collapsible";import { CardStack, CardStackContent, CardStackEntryField,} from "@executor-js/react/components/card-stack";import { FieldLabel } from "@executor-js/react/components/field";import { FloatActions } from "@executor-js/react/components/float-actions";import { HelpTooltip } from "@executor-js/react/components/help-tooltip";import { Info, InfoDescription, InfoTitle } from "@executor-js/react/components/info";import { Label } from "@executor-js/react/components/label";import { Textarea } from "@executor-js/react/components/textarea";import { Checkbox } from "@executor-js/react/components/checkbox";import { RadioGroup, RadioGroupItem } from "@executor-js/react/components/radio-group";import { IOSSpinner, Spinner } from "@executor-js/react/components/spinner";import { addOpenApiSpecOptimistic, previewOpenApiSpec } from "./atoms";import { OpenApiSourceDetailsFields } from "./OpenApiSourceDetailsFields";import { googleOpenApiPresets, googleStandardUserOAuthPresets, openApiPresets,} from "../sdk/presets";import { GOOGLE_BUNDLE_PRESET_ID } from "../sdk/google-presets";import type { SpecPreview, HeaderPreset, OAuth2Preset } from "../sdk/preview";import { headerBindingSlot, oauth2ClientIdSlot, oauth2ClientSecretSlot, oauth2ConnectionSlot, queryParamBindingSlot, specFetchHeaderBindingSlot, specFetchQueryParamBindingSlot,} from "../sdk/source-contracts";import { OAuth2SourceConfig, type ServerInfo } from "../sdk/types";import { expandServerUrlOptions } from "../sdk/openapi-utils";import { compactGoogleOAuthScopes, filterGoogleUserConsentOAuthScopes,} from "../sdk/google-oauth-scopes";import { googleOAuthConsentBatches } from "../sdk/google-oauth-batches"; export const OPENAPI_OAUTH_POPUP_NAME = "openapi-oauth";export const OPENAPI_OAUTH_CALLBACK_PATH = "/api/oauth/callback";const GOOGLE_BUNDLE_BASE_URL = "https://www.googleapis.com/";const GOOGLE_ICON = "https://fonts.gstatic.com/s/i/productlogos/googleg/v6/192px.svg";const GOOGLE_BUNDLE_DEFAULT_PRESET = googleOpenApiPresets[0]!;const GOOGLE_STANDARD_SERVICE_IDS = googleStandardUserOAuthPresets.map((preset) => preset.id);const GOOGLE_BROAD_SELECTION_WARNING_THRESHOLD = 6; const ErrorMessage = Schema.Struct({ message: Schema.String });const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage); const errorMessageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string => Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeErrorMessage), { onNone: () => fallback, onSome: ({ message }) => message, }); const googleOAuthErrorMessage = "Google did not approve this permission request. Try selecting fewer services, or add sensitive services as separate Google sources."; const oauthErrorMessage = (providerLabel: string, message: string, details?: string): string => { if (providerLabel !== "Google") return details ? `${message}: ${details}` : message; const combined = `${message}\n${details ?? ""}`; if ( /Authorization server returned error|Token exchange failed|invalid_grant|access_denied|Something went wrong|unknownerror/i.test( combined, ) ) { return googleOAuthErrorMessage; } return details ? `${message}: ${details}` : message;}; export const openApiOAuthConnectionId = ( namespaceSlug: string, flow: OAuth2Preset["flow"],): string => flow === "clientCredentials" ? `openapi-oauth2-app-${namespaceSlug || "default"}` : `openapi-oauth2-user-${namespaceSlug || "default"}`; /** * OpenAPI 3.x requires OAuth2 tokenUrl/authorizationUrl to be absolute, * but some specs ship relative paths like `/api/rest/v1/oauth/token`. * Resolve them against the source's chosen baseUrl so the backend can * fetch them directly and the absolute URL is what gets persisted on * OAuth2SourceConfig. */export function resolveOAuthUrl(url: string, baseUrl: string): string { if (!url) return url; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL constructor normalizes provider metadata URLs try { new URL(url); return url; } catch { if (!baseUrl) return url; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL constructor resolves relative provider metadata URLs try { return new URL(url, baseUrl).toString(); } catch { return url; } }} export function inferOAuthIssuerUrl(authorizationUrl: string): string | null { // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL constructor normalizes provider metadata URLs try { return new URL(authorizationUrl).origin; } catch { return null; }} const standardOidcIdentityScopes = ["openid", "email", "profile"] as const; const identityScopesForPreset = ( identityScopes: OAuth2Preset["identityScopes"],): readonly string[] => { if (identityScopes === false) return []; return identityScopes === "auto" ? standardOidcIdentityScopes : identityScopes;}; const resolvedOAuthScopes = ( apiScopes: Iterable<string>, identityScopes: OAuth2Preset["identityScopes"],): string[] => { const merged = new Set(apiScopes); for (const scope of identityScopesForPreset(identityScopes)) merged.add(scope); return [...merged];}; const splitOAuthScopes = (value: string | null): Set<string> => new Set(value?.split(/\s+/).filter(Boolean) ?? []); const mergeOAuthScopes = (...values: readonly Iterable<string>[]): string[] => { const merged = new Set<string>(); for (const scopes of values) { for (const scope of scopes) { if (scope.trim()) merged.add(scope); } } return [...merged];}; const missingOAuthScopesFromGranted = ( granted: ReadonlySet<string>, requiredApiScopes: Iterable<string>,): readonly string[] => { return [...requiredApiScopes].filter((scope) => !granted.has(scope));}; const isGoogleOAuthScope = (scope: string): boolean => scope === "https://mail.google.com/" || scope.startsWith("https://www.googleapis.com/auth/") || scope.startsWith("https://www.google.com/m8/feeds/"); const hasGoogleOAuthScope = (connection: { readonly oauthScope: string | null }): boolean => [...splitOAuthScopes(connection.oauthScope)].some(isGoogleOAuthScope); const isGoogleOAuthUrl = (url: string): boolean => { if (!URL.canParse(url)) return false; const host = new URL(url).hostname.toLowerCase(); return host === "accounts.google.com" || host === "oauth2.googleapis.com";}; const isGoogleOAuthTarget = (preset: OAuth2Preset, baseUrl: string, specUrl: string): boolean => { if (isGoogleDiscoveryUrl(specUrl)) return true; if (Object.keys(preset.scopes).some(isGoogleOAuthScope)) return true; if (isGoogleOAuthUrl(resolveOAuthUrl(preset.tokenUrl, baseUrl))) return true; const authorizationUrl = Option.getOrElse(preset.authorizationUrl, () => ""); return authorizationUrl ? isGoogleOAuthUrl(resolveOAuthUrl(authorizationUrl, baseUrl)) : false;}; const googleAuthorizationParams = (enabled: boolean): Record<string, string> | undefined => enabled ? { access_type: "offline", } : undefined; type OAuthConnectionChoice = { readonly id: ConnectionId; readonly scopeId: ScopeId; readonly provider: string; readonly identityLabel: string | null; readonly oauthScope: string | null; readonly missingApiScopes: readonly string[];}; type GoogleServicePreviewState = | { readonly status: "loading" } | { readonly status: "success"; readonly preview: SpecPreview; readonly baseUrl: string; } | { readonly status: "error"; readonly message: string }; type GoogleServiceAddItem = { readonly preset: (typeof googleOpenApiPresets)[number]; readonly preview: SpecPreview;}; const scopesForGoogleServiceItem = (item: GoogleServiceAddItem): readonly string[] => { const oauth2Preset = item.preview.oauth2Presets[0]; return filterGoogleUserConsentOAuthScopes(Object.keys(oauth2Preset?.scopes ?? {}));}; const specInputForAdd = (input: string) => { const value = input.trim(); const parsed = Effect.runSyncExit( Effect.try({ try: () => new URL(value), catch: () => null, }), ); return Exit.isSuccess(parsed) ? isGoogleDiscoveryUrl(value) ? { kind: "googleDiscovery" as const, url: value } : { kind: "url" as const, url: value } : { kind: "blob" as const, value };}; const isGoogleDiscoveryUrl = (url: string): boolean => { const trimmed = url.trim(); if (!URL.canParse(trimmed)) return false; const parsed = new URL(trimmed); const host = parsed.hostname.toLowerCase(); if (!host.endsWith("googleapis.com")) return false; return parsed.pathname.includes("/discovery/") || parsed.pathname.includes("$discovery");}; const normalizePresetUrl = (url: string): string => { const trimmed = url.trim(); if (!URL.canParse(trimmed)) return trimmed.replace(/\/$/, ""); const parsed = new URL(trimmed); parsed.hash = ""; parsed.searchParams.sort(); return parsed.toString().replace(/\/$/, "");}; const googlePresetForSpec = ( presetId: string | undefined, specUrl: string,): (typeof googleOpenApiPresets)[number] | null => { const normalizedSpecUrl = normalizePresetUrl(specUrl); const byUrl = googleOpenApiPresets.find( (preset) => preset.url && normalizePresetUrl(preset.url) === normalizedSpecUrl, ); if (byUrl) return byUrl; const byId = presetId ? googleOpenApiPresets.find((preset) => preset.id === presetId) : undefined; if (byId) return byId; return null;}; const firstBaseUrlForPreview = (preview: SpecPreview): string => { const firstServer = preview.servers[0]; return firstServer ? (expandServerUrlOptions(firstServer)[0] ?? "") : "";}; type StrategySelection = | { readonly kind: "none" } | { readonly kind: "custom" } | { readonly kind: "header"; readonly presetIndex: number } | { readonly kind: "oauth2"; readonly presetIndex: number }; const serializeStrategy = (s: StrategySelection): string => Match.value(s).pipe( Match.when({ kind: "none" }, () => "none"), Match.when({ kind: "custom" }, () => "custom"), Match.when({ kind: "header" }, (sel) => `header:${sel.presetIndex}`), Match.when({ kind: "oauth2" }, (sel) => `oauth2:${sel.presetIndex}`), Match.exhaustive, ); const parseStrategy = (value: string): StrategySelection => { if (value === "none") return { kind: "none" }; if (value === "custom") return { kind: "custom" }; if (value.startsWith("header:")) { return { kind: "header", presetIndex: Number(value.slice("header:".length)), }; } if (value.startsWith("oauth2:")) { return { kind: "oauth2", presetIndex: Number(value.slice("oauth2:".length)), }; } return { kind: "none" };}; // ---------------------------------------------------------------------------// Helpers// --------------------------------------------------------------------------- function prefixForHeader(preset: HeaderPreset, headerName: string): string | undefined { const label = preset.label.toLowerCase(); if (headerName.toLowerCase() === "authorization") { if (label.includes("bearer")) return "Bearer "; if (label.includes("basic")) return "Basic "; } return undefined;} function entriesFromSpecPreset(preset: HeaderPreset): HeaderState[] { return preset.secretHeaders.map((headerName) => { const prefix = prefixForHeader(preset, headerName); return { name: headerName, secretId: null, prefix, presetKey: matchPresetKey(headerName, prefix), fromPreset: true, }; });} const oauth2ConfigForPreset = (input: { readonly preset: OAuth2Preset; readonly baseUrl: string; readonly scopes: Iterable<string>; readonly identityScopes: OAuth2Preset["identityScopes"];}): OAuth2SourceConfig => OAuth2SourceConfig.make({ kind: "oauth2", securitySchemeName: input.preset.securitySchemeName, flow: input.preset.flow, tokenUrl: resolveOAuthUrl(input.preset.tokenUrl, input.baseUrl), authorizationUrl: input.preset.flow === "authorizationCode" ? resolveOAuthUrl( Option.getOrElse(input.preset.authorizationUrl, () => ""), input.baseUrl, ) || null : null, clientIdSlot: oauth2ClientIdSlot(input.preset.securitySchemeName), clientSecretSlot: oauth2ClientSecretSlot(input.preset.securitySchemeName), connectionSlot: oauth2ConnectionSlot(input.preset.securitySchemeName), scopes: [...input.scopes], identityScopes: input.identityScopes, }); function OAuthConnectedAccount(props: { readonly scopeId: ScopeId; readonly connectionId: string; readonly scopeSummary: string; readonly sourceName: string; readonly onSetSourceName: (name: string) => void;}) { const identityResult = useAtomValue( connectionIdentityAtom(props.scopeId, ConnectionId.make(props.connectionId)), ); const identityResponse = AsyncResult.isSuccess(identityResult) ? identityResult.value : null; const identity = identityResponse?.status === "available" ? identityResponse : null; const accountLabel = identity?.email ?? identity?.name ?? identity?.username ?? null; const sourceNameWithAccount = accountLabel && !props.sourceName.includes(accountLabel) ? `${props.sourceName} - ${accountLabel}` : props.sourceName; const accountIsInSourceName = accountLabel !== null && props.sourceName === sourceNameWithAccount; return ( <div className="flex min-w-0 flex-1 items-center gap-3"> <div className="min-w-0 flex-1"> <div className="flex min-w-0 items-center gap-2"> {identity?.picture ? ( <img src={identity.picture} alt="" referrerPolicy="no-referrer" className="size-5 shrink-0 rounded-full" /> ) : null} <span className="min-w-0 truncate text-foreground"> {accountLabel ? `Connected as ${accountLabel}` : "Connected"} <span className="text-muted-foreground"> · {props.scopeSummary}</span> </span> </div> {accountLabel ? ( <div className="mt-0.5 truncate text-[11px] text-muted-foreground"> Source name: {sourceNameWithAccount} </div> ) : null} </div> {accountLabel ? ( <Button variant="secondary" size="sm" className="h-6 shrink-0 px-2 text-[11px]" disabled={accountIsInSourceName} onClick={() => props.onSetSourceName(sourceNameWithAccount)} > {accountIsInSourceName ? "Name includes account" : "Add account name to source name"} </Button> ) : null} </div> );} function ExistingOAuthConnectionOption(props: { readonly connection: OAuthConnectionChoice; readonly selected: boolean; readonly onSelect: () => void; readonly providerLabel: string;}) { const identityResult = useAtomValue( connectionIdentityAtom(props.connection.scopeId, props.connection.id), ); const identity = AsyncResult.isSuccess(identityResult) && identityResult.value.status === "available" ? identityResult.value : null; const label = identity?.email ?? identity?.name ?? props.connection.identityLabel ?? props.connection.id; const picture = identity?.picture; const needsPermission = props.connection.missingApiScopes.length > 0; return ( <Button type="button" variant="ghost" onClick={props.onSelect} className={`h-auto w-full justify-between gap-3 rounded-md border px-3 py-2 text-left transition-colors ${ props.selected ? "border-primary/40 bg-primary/10" : "border-border/60 bg-background hover:border-border hover:bg-muted/30" }`} > <span className="flex min-w-0 items-center gap-2"> {picture ? ( <img src={picture} alt="" referrerPolicy="no-referrer" className="size-5 shrink-0 rounded-full" /> ) : ( <span className="grid size-5 shrink-0 place-items-center rounded-full bg-muted text-[10px] text-muted-foreground"> {label.slice(0, 1).toUpperCase()} </span>Lines 1–500 of 2549. Load more
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useAtomSet, useAtomValue } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Match from "effect/Match"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { ChevronDownIcon } from "lucide-react"; import { ConnectionId, ScopeId, SecretId, SetSourceCredentialBindingInput, } from "@executor-js/sdk/shared"; import { connectionIdentityAtom, connectionsAtom, setSourceCredentialBinding, startOAuth, } from "@executor-js/react/api/atoms"; import { useScope, useScopeStack } from "@executor-js/react/api/scope-context"; import { connectionWriteKeys, sourceWriteKeys } from "@executor-js/react/api/reactivity-keys"; // `addSpec` with an oauth2 payload persists a source row AND (for // clientCredentials) a freshly-minted Connection + owned secrets, // because the inline token exchange happens during `startOAuth`. // Invalidate both so the source-detail page opens into its connected // state without a refresh. const addSpecWriteKeys = [...sourceWriteKeys, ...connectionWriteKeys] as const; const bindingWriteKeys = [...sourceWriteKeys, ...connectionWriteKeys] as const; import { HeadersList } from "@executor-js/react/plugins/headers-list"; import { HttpCredentialsEditor, emptyHttpCredentials, serializeHttpCredentials, type HttpCredentialsState, } from "@executor-js/react/plugins/http-credentials"; import { oauthCallbackUrl, useOAuthPopupFlow, type OAuthCompletionPayload, } from "@executor-js/react/plugins/oauth-sign-in"; import { CreatableSecretPicker, matchPresetKey, type HeaderState, } from "@executor-js/react/plugins/secret-header-auth"; import { CredentialScopeDropdown } from "@executor-js/react/plugins/credential-target-scope"; import { slugifyNamespace, useSourceIdentity } from "@executor-js/react/plugins/source-identity"; import { useSecretPickerSecrets } from "@executor-js/react/plugins/use-secret-picker-secrets"; import { Button } from "@executor-js/react/components/button"; import { CopyButton } from "@executor-js/react/components/copy-button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@executor-js/react/components/collapsible"; import { CardStack, CardStackContent, CardStackEntryField, } from "@executor-js/react/components/card-stack"; import { FieldLabel } from "@executor-js/react/components/field"; import { FloatActions } from "@executor-js/react/components/float-actions"; import { HelpTooltip } from "@executor-js/react/components/help-tooltip"; import { Info, InfoDescription, InfoTitle } from "@executor-js/react/components/info"; import { Label } from "@executor-js/react/components/label"; import { Textarea } from "@executor-js/react/components/textarea"; import { Checkbox } from "@executor-js/react/components/checkbox"; import { RadioGroup, RadioGroupItem } from "@executor-js/react/components/radio-group"; import { IOSSpinner, Spinner } from "@executor-js/react/components/spinner"; import { addOpenApiSpecOptimistic, previewOpenApiSpec } from "./atoms"; import { OpenApiSourceDetailsFields } from "./OpenApiSourceDetailsFields"; import { googleOpenApiPresets, googleStandardUserOAuthPresets, openApiPresets, } from "../sdk/presets"; import { GOOGLE_BUNDLE_PRESET_ID } from "../sdk/google-presets"; import type { SpecPreview, HeaderPreset, OAuth2Preset } from "../sdk/preview"; import { headerBindingSlot, oauth2ClientIdSlot, oauth2ClientSecretSlot, oauth2ConnectionSlot, queryParamBindingSlot, specFetchHeaderBindingSlot, specFetchQueryParamBindingSlot, } from "../sdk/source-contracts"; import { OAuth2SourceConfig, type ServerInfo } from "../sdk/types"; import { expandServerUrlOptions } from "../sdk/openapi-utils"; import { compactGoogleOAuthScopes, filterGoogleUserConsentOAuthScopes, } from "../sdk/google-oauth-scopes"; import { googleOAuthConsentBatches } from "../sdk/google-oauth-batches"; export const OPENAPI_OAUTH_POPUP_NAME = "openapi-oauth"; export const OPENAPI_OAUTH_CALLBACK_PATH = "/api/oauth/callback"; const GOOGLE_BUNDLE_BASE_URL = "https://www.googleapis.com/"; const GOOGLE_ICON = "https://fonts.gstatic.com/s/i/productlogos/googleg/v6/192px.svg"; const GOOGLE_BUNDLE_DEFAULT_PRESET = googleOpenApiPresets[0]!; const GOOGLE_STANDARD_SERVICE_IDS = googleStandardUserOAuthPresets.map((preset) => preset.id); const GOOGLE_BROAD_SELECTION_WARNING_THRESHOLD = 6; const ErrorMessage = Schema.Struct({ message: Schema.String }); const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage); const errorMessageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string => Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeErrorMessage), { onNone: () => fallback, onSome: ({ message }) => message, }); const googleOAuthErrorMessage = "Google did not approve this permission request. Try selecting fewer services, or add sensitive services as separate Google sources."; const oauthErrorMessage = (providerLabel: string, message: string, details?: string): string => { if (providerLabel !== "Google") return details ? `${message}: ${details}` : message; const combined = `${message}\n${details ?? ""}`; if ( /Authorization server returned error|Token exchange failed|invalid_grant|access_denied|Something went wrong|unknownerror/i.test( combined, ) ) { return googleOAuthErrorMessage; } return details ? `${message}: ${details}` : message; }; export const openApiOAuthConnectionId = ( namespaceSlug: string, flow: OAuth2Preset["flow"], ): string => flow === "clientCredentials" ? `openapi-oauth2-app-${namespaceSlug || "default"}` : `openapi-oauth2-user-${namespaceSlug || "default"}`; /** * OpenAPI 3.x requires OAuth2 tokenUrl/authorizationUrl to be absolute, * but some specs ship relative paths like `/api/rest/v1/oauth/token`. * Resolve them against the source's chosen baseUrl so the backend can * fetch them directly and the absolute URL is what gets persisted on * OAuth2SourceConfig. */ export function resolveOAuthUrl(url: string, baseUrl: string): string { if (!url) return url; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL constructor normalizes provider metadata URLs try { new URL(url); return url; } catch { if (!baseUrl) return url; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL constructor resolves relative provider metadata URLs try { return new URL(url, baseUrl).toString(); } catch { return url; } } } export function inferOAuthIssuerUrl(authorizationUrl: string): string | null { // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL constructor normalizes provider metadata URLs try { return new URL(authorizationUrl).origin; } catch { return null; } } const standardOidcIdentityScopes = ["openid", "email", "profile"] as const; const identityScopesForPreset = ( identityScopes: OAuth2Preset["identityScopes"], ): readonly string[] => { if (identityScopes === false) return []; return identityScopes === "auto" ? standardOidcIdentityScopes : identityScopes; }; const resolvedOAuthScopes = ( apiScopes: Iterable<string>, identityScopes: OAuth2Preset["identityScopes"], ): string[] => { const merged = new Set(apiScopes); for (const scope of identityScopesForPreset(identityScopes)) merged.add(scope); return [...merged]; }; const splitOAuthScopes = (value: string | null): Set<string> => new Set(value?.split(/\s+/).filter(Boolean) ?? []); const mergeOAuthScopes = (...values: readonly Iterable<string>[]): string[] => { const merged = new Set<string>(); for (const scopes of values) { for (const scope of scopes) { if (scope.trim()) merged.add(scope); } } return [...merged]; }; const missingOAuthScopesFromGranted = ( granted: ReadonlySet<string>, requiredApiScopes: Iterable<string>, ): readonly string[] => { return [...requiredApiScopes].filter((scope) => !granted.has(scope)); }; const isGoogleOAuthScope = (scope: string): boolean => scope === "https://mail.google.com/" || scope.startsWith("https://www.googleapis.com/auth/") || scope.startsWith("https://www.google.com/m8/feeds/"); const hasGoogleOAuthScope = (connection: { readonly oauthScope: string | null }): boolean => [...splitOAuthScopes(connection.oauthScope)].some(isGoogleOAuthScope); const isGoogleOAuthUrl = (url: string): boolean => { if (!URL.canParse(url)) return false; const host = new URL(url).hostname.toLowerCase(); return host === "accounts.google.com" || host === "oauth2.googleapis.com"; }; const isGoogleOAuthTarget = (preset: OAuth2Preset, baseUrl: string, specUrl: string): boolean => { if (isGoogleDiscoveryUrl(specUrl)) return true; if (Object.keys(preset.scopes).some(isGoogleOAuthScope)) return true; if (isGoogleOAuthUrl(resolveOAuthUrl(preset.tokenUrl, baseUrl))) return true; const authorizationUrl = Option.getOrElse(preset.authorizationUrl, () => ""); return authorizationUrl ? isGoogleOAuthUrl(resolveOAuthUrl(authorizationUrl, baseUrl)) : false; }; const googleAuthorizationParams = (enabled: boolean): Record<string, string> | undefined => enabled ? { access_type: "offline", } : undefined; type OAuthConnectionChoice = { readonly id: ConnectionId; readonly scopeId: ScopeId; readonly provider: string; readonly identityLabel: string | null; readonly oauthScope: string | null; readonly missingApiScopes: readonly string[]; }; type GoogleServicePreviewState = | { readonly status: "loading" } | { readonly status: "success"; readonly preview: SpecPreview; readonly baseUrl: string; } | { readonly status: "error"; readonly message: string }; type GoogleServiceAddItem = { readonly preset: (typeof googleOpenApiPresets)[number]; readonly preview: SpecPreview; }; const scopesForGoogleServiceItem = (item: GoogleServiceAddItem): readonly string[] => { const oauth2Preset = item.preview.oauth2Presets[0]; return filterGoogleUserConsentOAuthScopes(Object.keys(oauth2Preset?.scopes ?? {})); }; const specInputForAdd = (input: string) => { const value = input.trim(); const parsed = Effect.runSyncExit( Effect.try({ try: () => new URL(value), catch: () => null, }), ); return Exit.isSuccess(parsed) ? isGoogleDiscoveryUrl(value) ? { kind: "googleDiscovery" as const, url: value } : { kind: "url" as const, url: value } : { kind: "blob" as const, value }; }; const isGoogleDiscoveryUrl = (url: string): boolean => { const trimmed = url.trim(); if (!URL.canParse(trimmed)) return false; const parsed = new URL(trimmed); const host = parsed.hostname.toLowerCase(); if (!host.endsWith("googleapis.com")) return false; return parsed.pathname.includes("/discovery/") || parsed.pathname.includes("$discovery"); }; const normalizePresetUrl = (url: string): string => { const trimmed = url.trim(); if (!URL.canParse(trimmed)) return trimmed.replace(/\/$/, ""); const parsed = new URL(trimmed); parsed.hash = ""; parsed.searchParams.sort(); return parsed.toString().replace(/\/$/, ""); }; const googlePresetForSpec = ( presetId: string | undefined, specUrl: string, ): (typeof googleOpenApiPresets)[number] | null => { const normalizedSpecUrl = normalizePresetUrl(specUrl); const byUrl = googleOpenApiPresets.find( (preset) => preset.url && normalizePresetUrl(preset.url) === normalizedSpecUrl, ); if (byUrl) return byUrl; const byId = presetId ? googleOpenApiPresets.find((preset) => preset.id === presetId) : undefined; if (byId) return byId; return null; }; const firstBaseUrlForPreview = (preview: SpecPreview): string => { const firstServer = preview.servers[0]; return firstServer ? (expandServerUrlOptions(firstServer)[0] ?? "") : ""; }; type StrategySelection = | { readonly kind: "none" } | { readonly kind: "custom" } | { readonly kind: "header"; readonly presetIndex: number } | { readonly kind: "oauth2"; readonly presetIndex: number }; const serializeStrategy = (s: StrategySelection): string => Match.value(s).pipe( Match.when({ kind: "none" }, () => "none"), Match.when({ kind: "custom" }, () => "custom"), Match.when({ kind: "header" }, (sel) => `header:${sel.presetIndex}`), Match.when({ kind: "oauth2" }, (sel) => `oauth2:${sel.presetIndex}`), Match.exhaustive, ); const parseStrategy = (value: string): StrategySelection => { if (value === "none") return { kind: "none" }; if (value === "custom") return { kind: "custom" }; if (value.startsWith("header:")) { return { kind: "header", presetIndex: Number(value.slice("header:".length)), }; } if (value.startsWith("oauth2:")) { return { kind: "oauth2", presetIndex: Number(value.slice("oauth2:".length)), }; } return { kind: "none" }; }; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function prefixForHeader(preset: HeaderPreset, headerName: string): string | undefined { const label = preset.label.toLowerCase(); if (headerName.toLowerCase() === "authorization") { if (label.includes("bearer")) return "Bearer "; if (label.includes("basic")) return "Basic "; } return undefined; } function entriesFromSpecPreset(preset: HeaderPreset): HeaderState[] { return preset.secretHeaders.map((headerName) => { const prefix = prefixForHeader(preset, headerName); return { name: headerName, secretId: null, prefix, presetKey: matchPresetKey(headerName, prefix), fromPreset: true, }; }); } const oauth2ConfigForPreset = (input: { readonly preset: OAuth2Preset; readonly baseUrl: string; readonly scopes: Iterable<string>; readonly identityScopes: OAuth2Preset["identityScopes"]; }): OAuth2SourceConfig => OAuth2SourceConfig.make({ kind: "oauth2", securitySchemeName: input.preset.securitySchemeName, flow: input.preset.flow, tokenUrl: resolveOAuthUrl(input.preset.tokenUrl, input.baseUrl), authorizationUrl: input.preset.flow === "authorizationCode" ? resolveOAuthUrl( Option.getOrElse(input.preset.authorizationUrl, () => ""), input.baseUrl, ) || null : null, clientIdSlot: oauth2ClientIdSlot(input.preset.securitySchemeName), clientSecretSlot: oauth2ClientSecretSlot(input.preset.securitySchemeName), connectionSlot: oauth2ConnectionSlot(input.preset.securitySchemeName), scopes: [...input.scopes], identityScopes: input.identityScopes, }); function OAuthConnectedAccount(props: { readonly scopeId: ScopeId; readonly connectionId: string; readonly scopeSummary: string; readonly sourceName: string; readonly onSetSourceName: (name: string) => void; }) { const identityResult = useAtomValue( connectionIdentityAtom(props.scopeId, ConnectionId.make(props.connectionId)), ); const identityResponse = AsyncResult.isSuccess(identityResult) ? identityResult.value : null; const identity = identityResponse?.status === "available" ? identityResponse : null; const accountLabel = identity?.email ?? identity?.name ?? identity?.username ?? null; const sourceNameWithAccount = accountLabel && !props.sourceName.includes(accountLabel) ? `${props.sourceName} - ${accountLabel}` : props.sourceName; const accountIsInSourceName = accountLabel !== null && props.sourceName === sourceNameWithAccount; return ( <div className="flex min-w-0 flex-1 items-center gap-3"> <div className="min-w-0 flex-1"> <div className="flex min-w-0 items-center gap-2"> {identity?.picture ? ( <img src={identity.picture} alt="" referrerPolicy="no-referrer" className="size-5 shrink-0 rounded-full" /> ) : null} <span className="min-w-0 truncate text-foreground"> {accountLabel ? `Connected as ${accountLabel}` : "Connected"} <span className="text-muted-foreground"> · {props.scopeSummary}</span> </span> </div> {accountLabel ? ( <div className="mt-0.5 truncate text-[11px] text-muted-foreground"> Source name: {sourceNameWithAccount} </div> ) : null} </div> {accountLabel ? ( <Button variant="secondary" size="sm" className="h-6 shrink-0 px-2 text-[11px]" disabled={accountIsInSourceName} onClick={() => props.onSetSourceName(sourceNameWithAccount)} > {accountIsInSourceName ? "Name includes account" : "Add account name to source name"} </Button> ) : null} </div> ); } function ExistingOAuthConnectionOption(props: { readonly connection: OAuthConnectionChoice; readonly selected: boolean; readonly onSelect: () => void; readonly providerLabel: string; }) { const identityResult = useAtomValue( connectionIdentityAtom(props.connection.scopeId, props.connection.id), ); const identity = AsyncResult.isSuccess(identityResult) && identityResult.value.status === "available" ? identityResult.value : null; const label = identity?.email ?? identity?.name ?? props.connection.identityLabel ?? props.connection.id; const picture = identity?.picture; const needsPermission = props.connection.missingApiScopes.length > 0; return ( <Button type="button" variant="ghost" onClick={props.onSelect} className={`h-auto w-full justify-between gap-3 rounded-md border px-3 py-2 text-left transition-colors ${ props.selected ? "border-primary/40 bg-primary/10" : "border-border/60 bg-background hover:border-border hover:bg-muted/30" }`} > <span className="flex min-w-0 items-center gap-2"> {picture ? ( <img src={picture} alt="" referrerPolicy="no-referrer" className="size-5 shrink-0 rounded-full" /> ) : ( <span className="grid size-5 shrink-0 place-items-center rounded-full bg-muted text-[10px] text-muted-foreground"> {label.slice(0, 1).toUpperCase()} </span>