Unified · Split
| @@ -1,6 +1,8 @@ | |||
| 1 | import { useCallback, useEffect, useRef, useState } from "react"; | 1 | import { useCallback, useEffect, useRef, useState } from "react"; |
| 2 | import { useAtomSet } from "@effect/atom-react"; | 2 | import { useAtomSet } from "@effect/atom-react"; |
| 3 | import { Option } from "effect"; | 3 | import * as Exit from "effect/Exit"; |
| 4 | import * as Option from "effect/Option"; | ||
| 5 | import * as Schema from "effect/Schema"; | ||
| 4 | 6 | ||
| 5 | import { ConnectionId, ScopeId, SecretId } from "@executor-js/sdk/core"; | 7 | import { ConnectionId, ScopeId, SecretId } from "@executor-js/sdk/core"; |
| 6 | import { startOAuth } from "@executor-js/react/api/atoms"; | 8 | import { startOAuth } from "@executor-js/react/api/atoms"; |
| @@ -81,6 +83,17 @@ import { · 74 unmodified lines | |||
| 81 | type ServerVariable, | 83 | type ServerVariable, |
| 82 | } from "../sdk/types"; | 84 | } from "../sdk/types"; |
| 83 | 85 | ||
| 86 | const ErrorMessage = Schema.Struct({ message: Schema.String }); | ||
| 87 | |||
| 88 | const errorMessageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string => | ||
| 89 | Option.match( | ||
| 90 | Option.flatMap(Exit.findErrorOption(exit), Schema.decodeUnknownOption(ErrorMessage)), | ||
| 91 | { | ||
| 92 | onNone: () => fallback, | ||
| 93 | onSome: ({ message }) => message, | ||
| 94 | }, | ||
| 95 | ); | ||
| 96 | |||
| 84 | export const OPENAPI_OAUTH_POPUP_NAME = "openapi-oauth"; | 97 | export const OPENAPI_OAUTH_POPUP_NAME = "openapi-oauth"; |
| 85 | export const OPENAPI_OAUTH_CALLBACK_PATH = "/api/oauth/callback"; | 98 | export const OPENAPI_OAUTH_CALLBACK_PATH = "/api/oauth/callback"; |
| 86 | 99 | ||
| @@ -109,25 +122,15 @@ export const openApiOAuthConnectionId = ( · 22 unmodified lines | |||
| 109 | */ | 122 | */ |
| 110 | export function resolveOAuthUrl(url: string, baseUrl: string): string { | 123 | export function resolveOAuthUrl(url: string, baseUrl: string): string { |
| 111 | if (!url) return url; | 124 | if (!url) return url; |
| 112 | try { | 125 | if (URL.canParse(url)) { |
| 113 | new URL(url); | ||
| 114 | return url; | 126 | return url; |
| 115 | } catch { | ||
| 116 | if (!baseUrl) return url; | ||
| 117 | try { | ||
| 118 | return new URL(url, baseUrl).toString(); | ||
| 119 | } catch { | ||
| 120 | return url; | ||
| 121 | } | ||
| 122 | } | 127 | } |
| 128 | if (!baseUrl || !URL.canParse(url, baseUrl)) return url; | ||
| 129 | return new URL(url, baseUrl).toString(); | ||
| 123 | } | 130 | } |
| 124 | 131 | ||
| 125 | export function inferOAuthIssuerUrl(authorizationUrl: string): string | null { | 132 | export function inferOAuthIssuerUrl(authorizationUrl: string): string | null { |
| 126 | try { | 133 | return URL.canParse(authorizationUrl) ? new URL(authorizationUrl).origin : null; |
| 127 | return new URL(authorizationUrl).origin; | ||
| 128 | } catch { | ||
| 129 | return null; | ||
| 130 | } | ||
| 131 | } | 134 | } |
| 132 | 135 | ||
| 133 | type StrategySelection = | 136 | type StrategySelection = |
| @@ -242,10 +245,10 @@ export default function AddOpenApiSource(props: { · 108 unmodified lines | |||
| 242 | 245 | ||
| 243 | const scopeId = useScope(); | 246 | const scopeId = useScope(); |
| 244 | const userScope = useUserScope(); | 247 | const userScope = useUserScope(); |
| 245 | const doPreview = useAtomSet(previewOpenApiSpec, { mode: "promise" }); | 248 | const doPreview = useAtomSet(previewOpenApiSpec, { mode: "promiseExit" }); |
| 246 | const doAdd = useAtomSet(addOpenApiSpec, { mode: "promise" }); | 249 | const doAdd = useAtomSet(addOpenApiSpec, { mode: "promiseExit" }); |
| 247 | const doStartOAuth = useAtomSet(startOAuth, { mode: "promise" }); | 250 | const doStartOAuth = useAtomSet(startOAuth, { mode: "promiseExit" }); |
| 248 | const doSetBinding = useAtomSet(setOpenApiSourceBinding, { mode: "promise" }); | 251 | const doSetBinding = useAtomSet(setOpenApiSourceBinding, { mode: "promiseExit" }); |
| 249 | const { beginAdd } = usePendingSources(); | 252 | const { beginAdd } = usePendingSources(); |
| 250 | const secretList = useSecretPickerSecrets(); | 253 | const secretList = useSecretPickerSecrets(); |
| 251 | const oauth = useOAuthPopupFlow<OAuthCompletionPayload>({ | 254 | const oauth = useOAuthPopupFlow<OAuthCompletionPayload>({ |
| @@ -278,7 +281,7 @@ export default function AddOpenApiSource(props: { · 26 unmodified lines | |||
| 278 | selectedServerIndex >= 0 ? (servers[selectedServerIndex] ?? null) : null; | 281 | selectedServerIndex >= 0 ? (servers[selectedServerIndex] ?? null) : null; |
| 279 | 282 | ||
| 280 | const serverVariables: Record<string, ServerVariable> = selectedServer | 283 | const serverVariables: Record<string, ServerVariable> = selectedServer |
| 281 | ? Option.getOrElse(selectedServer.variables, () => ({}) as Record<string, ServerVariable>) | 284 | ? Option.getOrElse(selectedServer.variables, () => ({})) |
| 282 | : {}; | 285 | : {}; |
| 283 | const serverVariableEntries: Array<[string, ServerVariable]> = Object.entries(serverVariables); | 286 | const serverVariableEntries: Array<[string, ServerVariable]> = Object.entries(serverVariables); |
| 284 | 287 | ||
| @@ -290,10 +293,7 @@ export default function AddOpenApiSource(props: { · 5 unmodified lines | |||
| 290 | // Helper used by analyze + server selection: build a default selection map | 293 | // Helper used by analyze + server selection: build a default selection map |
| 291 | // from a server's variable defaults. | 294 | // from a server's variable defaults. |
| 292 | const defaultSelectionsFor = (server: ServerInfo): Record<string, string> => { | 295 | const defaultSelectionsFor = (server: ServerInfo): Record<string, string> => { |
| 293 | const vars: Record<string, ServerVariable> = Option.getOrElse( | 296 | const vars: Record<string, ServerVariable> = Option.getOrElse(server.variables, () => ({})); |
| 294 | server.variables, | ||
| 295 | () => ({}) as Record<string, ServerVariable>, | ||
| 296 | ); | ||
| 297 | const out: Record<string, string> = {}; | 297 | const out: Record<string, string> = {}; |
| 298 | for (const [name, v] of Object.entries(vars)) out[name] = v.default; | 298 | for (const [name, v] of Object.entries(vars)) out[name] = v.default; |
| 299 | return out; | 299 | return out; |
| @@ -371,45 +371,47 @@ export default function AddOpenApiSource(props: { · 71 unmodified lines | |||
| 371 | setAnalyzing(true); | 371 | setAnalyzing(true); |
| 372 | setAnalyzeError(null); | 372 | setAnalyzeError(null); |
| 373 | setAddError(null); | 373 | setAddError(null); |
| 374 | try { | 374 | const credentials = serializeHttpCredentials(specFetchCredentials); |
| 375 | const credentials = serializeHttpCredentials(specFetchCredentials); | 375 | const exit = await doPreview({ |
| 376 | const result = await doPreview({ | 376 | params: { scopeId }, |
| 377 | params: { scopeId }, | 377 | payload: { |
| 378 | payload: { | 378 | spec: specUrl, |
| 379 | spec: specUrl, | 379 | specFetchCredentials: credentials, |
| 380 | specFetchCredentials: credentials, | 380 | }, |
| 381 | }, | 381 | }); |
| 382 | }); | 382 | if (Exit.isFailure(exit)) { |
| 383 | setPreview(result); | 383 | setAnalyzeError(errorMessageFromExit(exit, "Failed to parse spec")); |
| 384 | |||
| 385 | const firstServer = result.servers[0]; | ||
| 386 | if (firstServer) { | ||
| 387 | setSelectedServerIndex(0); | ||
| 388 | setVariableSelections(defaultSelectionsFor(firstServer)); | ||
| 389 | setCustomBaseUrl(""); | ||
| 390 | } else { | ||
| 391 | setSelectedServerIndex(-1); | ||
| 392 | setVariableSelections({}); | ||
| 393 | setCustomBaseUrl(""); | ||
| 394 | } | ||
| 395 | |||
| 396 | const firstPreset = result.headerPresets[0]; | ||
| 397 | if (firstPreset) { | ||
| 398 | setStrategy({ kind: "header", presetIndex: 0 }); | ||
| 399 | setCustomHeaders(entriesFromSpecPreset(firstPreset)); | ||
| 400 | } else { | ||
| 401 | // No header presets — default to "custom" so the headers editor is | ||
| 402 | // visible immediately. Specs with no `security` block (e.g. Microsoft | ||
| 403 | // Graph) would otherwise leave the user staring at just the | ||
| 404 | // Authentication heading with no way to add headers. | ||
| 405 | setStrategy({ kind: "custom" }); | ||
| 406 | setCustomHeaders([]); | ||
| 407 | } | ||
| 408 | } catch (e) { | ||
| 409 | setAnalyzeError(e instanceof Error ? e.message : "Failed to parse spec"); | ||
| 410 | } finally { | ||
| 411 | setAnalyzing(false); | 384 | setAnalyzing(false); |
| 385 | return; | ||
| 386 | } | ||
| 387 | |||
| 388 | const result = exit.value; | ||
| 389 | setPreview(result); | ||
| 390 | |||
| 391 | const firstServer = result.servers[0]; | ||
| 392 | if (firstServer) { | ||
| 393 | setSelectedServerIndex(0); | ||
| 394 | setVariableSelections(defaultSelectionsFor(firstServer)); | ||
| 395 | setCustomBaseUrl(""); | ||
| 396 | } else { | ||
| 397 | setSelectedServerIndex(-1); | ||
| 398 | setVariableSelections({}); | ||
| 399 | setCustomBaseUrl(""); | ||
| 412 | } | 400 | } |
| 401 | |||
| 402 | const firstPreset = result.headerPresets[0]; | ||
| 403 | if (firstPreset) { | ||
| 404 | setStrategy({ kind: "header", presetIndex: 0 }); | ||
| 405 | setCustomHeaders(entriesFromSpecPreset(firstPreset)); | ||
| 406 | } else { | ||
| 407 | // No header presets — default to "custom" so the headers editor is | ||
| 408 | // visible immediately. Specs with no `security` block (e.g. Microsoft | ||
| 409 | // Graph) would otherwise leave the user staring at just the | ||
| 410 | // Authentication heading with no way to add headers. | ||
| 411 | setStrategy({ kind: "custom" }); | ||
| 412 | setCustomHeaders([]); | ||
| 413 | } | ||
| 414 | setAnalyzing(false); | ||
| 413 | }; | 415 | }; |
| 414 | 416 | ||
| 415 | handleAnalyzeRef.current = handleAnalyze; | 417 | handleAnalyzeRef.current = handleAnalyze; |
| @@ -470,122 +472,126 @@ export default function AddOpenApiSource(props: { · 54 unmodified lines | |||
| 470 | if (!selectedOAuth2Preset || !oauth2ClientIdSecretId || !preview) return; | 472 | if (!selectedOAuth2Preset || !oauth2ClientIdSecretId || !preview) return; |
| 471 | oauth.cancel(); | 473 | oauth.cancel(); |
| 472 | setOauth2Error(null); | 474 | setOauth2Error(null); |
| 473 | try { | 475 | const displayName = identity.name.trim() || selectedOAuth2Preset.securitySchemeName; |
| 474 | const displayName = identity.name.trim() || selectedOAuth2Preset.securitySchemeName; | 476 | |
| 475 | 477 | const tokenUrl = resolveOAuthUrl(selectedOAuth2Preset.tokenUrl, resolvedBaseUrl); | |
| 476 | const tokenUrl = resolveOAuthUrl(selectedOAuth2Preset.tokenUrl, resolvedBaseUrl); | 478 | |
| 477 | 479 | if (selectedOAuth2Preset.flow === "clientCredentials") { | |
| 478 | if (selectedOAuth2Preset.flow === "clientCredentials") { | 480 | // RFC 6749 §4.4: no user-interactive consent step. The client_secret |
| 479 | // RFC 6749 §4.4: no user-interactive consent step. The client_secret | 481 | // is mandatory; the backend exchanges tokens inline and returns a |
| 480 | // is mandatory; the backend exchanges tokens inline and returns a | 482 | // completed OAuth2Auth we can attach to the source directly. |
| 481 | // completed OAuth2Auth we can attach to the source directly. | 483 | if (!oauth2ClientSecretSecretId) { |
| 482 | if (!oauth2ClientSecretSecretId) { | 484 | setOauth2Error("client_credentials requires a client secret"); |
| 483 | setOauth2Error("client_credentials requires a client secret"); | 485 | return; |
| 484 | return; | 486 | } |
| 485 | } | 487 | setStartingOAuth(true); |
| 486 | setStartingOAuth(true); | 488 | const connectionId = openApiOAuthConnectionId(resolvedSourceId, selectedOAuth2Preset.flow); |
| 487 | const connectionId = openApiOAuthConnectionId(resolvedSourceId, selectedOAuth2Preset.flow); | 489 | const startOAuthExit = await doStartOAuth({ |
| 488 | const response = await doStartOAuth({ | 490 | params: { scopeId }, |
| 489 | params: { scopeId }, | 491 | payload: { |
| 490 | payload: { | 492 | endpoint: tokenUrl, |
| 491 | endpoint: tokenUrl, | 493 | redirectUrl: tokenUrl, |
| 492 | redirectUrl: tokenUrl, | 494 | connectionId, |
| 493 | connectionId, | 495 | tokenScope: scopeId, |
| 494 | tokenScope: scopeId as string, | 496 | strategy: { |
| 495 | strategy: { | 497 | kind: "client-credentials", |
| 496 | kind: "client-credentials", | 498 | tokenEndpoint: tokenUrl, |
| 497 | tokenEndpoint: tokenUrl, | 499 | clientIdSecretId: oauth2ClientIdSecretId, |
| 498 | clientIdSecretId: oauth2ClientIdSecretId, | 500 | clientSecretSecretId: oauth2ClientSecretSecretId, |
| 499 | clientSecretSecretId: oauth2ClientSecretSecretId, | 501 | scopes: [...oauth2SelectedScopes], |
| 500 | scopes: [...oauth2SelectedScopes], | ||
| 501 | }, | ||
| 502 | pluginId: "openapi", | ||
| 503 | identityLabel: `${displayName} OAuth`, | ||
| 504 | }, | 502 | }, |
| 505 | }); | 503 | pluginId: "openapi", |
| 506 | setStartingOAuth(false); | 504 | identityLabel: `${displayName} OAuth`, |
| 507 | if (!response.completedConnection) { | 505 | }, |
| 508 | setOauth2Error("client_credentials flow did not mint a connection"); | 506 | }); |
| 509 | return; | 507 | setStartingOAuth(false); |
| 510 | } | 508 | if (Exit.isFailure(startOAuthExit)) { |
| 509 | setOauth2Error(errorMessageFromExit(startOAuthExit, "Failed to start OAuth")); | ||
| 510 | return; | ||
| 511 | } | ||
| 512 | const response = startOAuthExit.value; | ||
| 513 | if (!response.completedConnection) { | ||
| 514 | setOauth2Error("client_credentials flow did not mint a connection"); | ||
| 515 | return; | ||
| 516 | } | ||
| 517 | setOauth2AuthState({ | ||
| 518 | fingerprint: selectedOAuth2Fingerprint, | ||
| 519 | auth: new OAuth2Auth({ | ||
| 520 | kind: "oauth2", | ||
| 521 | connectionId: response.completedConnection.connectionId, | ||
| 522 | securitySchemeName: selectedOAuth2Preset.securitySchemeName, | ||
| 523 | flow: "clientCredentials", | ||
| 524 | tokenUrl, | ||
| 525 | authorizationUrl: null, | ||
| 526 | clientIdSecretId: oauth2ClientIdSecretId, | ||
| 527 | clientSecretSecretId: oauth2ClientSecretSecretId, | ||
| 528 | scopes: [...oauth2SelectedScopes], | ||
| 529 | }), | ||
| 530 | }); | ||
| 531 | setOauth2Error(null); | ||
| 532 | return; | ||
| 533 | } | ||
| 534 | |||
| 535 | const authorizationUrl = resolveOAuthUrl( | ||
| 536 | Option.getOrElse(selectedOAuth2Preset.authorizationUrl, () => ""), | ||
| 537 | resolvedBaseUrl, | ||
| 538 | ); | ||
| 539 | const issuerUrl = inferOAuthIssuerUrl(authorizationUrl); | ||
| 540 | const startOAuthExit = await doStartOAuth({ | ||
| 541 | params: { scopeId }, | ||
| 542 | payload: { | ||
| 543 | endpoint: authorizationUrl, | ||
| 544 | connectionId: openApiOAuthConnectionId(resolvedSourceId, selectedOAuth2Preset.flow), | ||
| 545 | tokenScope: scopeId, | ||
| 546 | redirectUrl: oauth2RedirectUrl, | ||
| 547 | strategy: { | ||
| 548 | kind: "authorization-code", | ||
| 549 | authorizationEndpoint: authorizationUrl, | ||
| 550 | tokenEndpoint: tokenUrl, | ||
| 551 | issuerUrl, | ||
| 552 | clientIdSecretId: oauth2ClientIdSecretId, | ||
| 553 | clientSecretSecretId: oauth2ClientSecretSecretId ?? null, | ||
| 554 | scopes: [...oauth2SelectedScopes], | ||
| 555 | }, | ||
| 556 | pluginId: "openapi", | ||
| 557 | identityLabel: `${displayName} OAuth`, | ||
| 558 | }, | ||
| 559 | }); | ||
| 560 | if (Exit.isFailure(startOAuthExit)) { | ||
| 561 | setOauth2Error(errorMessageFromExit(startOAuthExit, "Failed to start OAuth")); | ||
| 562 | return; | ||
| 563 | } | ||
| 564 | const response = startOAuthExit.value; | ||
| 565 | if (response.authorizationUrl === null) { | ||
| 566 | setOauth2Error("Unexpected response flow from server"); | ||
| 567 | return; | ||
| 568 | } | ||
| 569 | |||
| 570 | await oauth.openAuthorization({ | ||
| 571 | run: async () => ({ | ||
| 572 | sessionId: response.sessionId, | ||
| 573 | authorizationUrl: response.authorizationUrl, | ||
| 574 | }), | ||
| 575 | onSuccess: (result) => { | ||
| 511 | setOauth2AuthState({ | 576 | setOauth2AuthState({ |
| 512 | fingerprint: selectedOAuth2Fingerprint, | 577 | fingerprint: selectedOAuth2Fingerprint, |
| 513 | auth: new OAuth2Auth({ | 578 | auth: new OAuth2Auth({ |
| 514 | kind: "oauth2", | 579 | kind: "oauth2", |
| 515 | connectionId: response.completedConnection.connectionId, | 580 | connectionId: result.connectionId, |
| 516 | securitySchemeName: selectedOAuth2Preset.securitySchemeName, | 581 | securitySchemeName: selectedOAuth2Preset.securitySchemeName, |
| 517 | flow: "clientCredentials", | 582 | flow: "authorizationCode", |
| 518 | tokenUrl, | 583 | tokenUrl, |
| 519 | authorizationUrl: null, | 584 | authorizationUrl, |
| 585 | issuerUrl, | ||
| 520 | clientIdSecretId: oauth2ClientIdSecretId, | 586 | clientIdSecretId: oauth2ClientIdSecretId, |
| 521 | clientSecretSecretId: oauth2ClientSecretSecretId, | 587 | clientSecretSecretId: oauth2ClientSecretSecretId, |
| 522 | scopes: [...oauth2SelectedScopes], | 588 | scopes: [...oauth2SelectedScopes], |
| 523 | }), | 589 | }), |
| 524 | }); | 590 | }); |
| 525 | setOauth2Error(null); | 591 | setOauth2Error(null); |
| 526 | return; | 592 | }, |
| 527 | } | 593 | onError: setOauth2Error, |
| 528 | 594 | }); | |
| 529 | const authorizationUrl = resolveOAuthUrl( | ||
| 530 | Option.getOrElse(selectedOAuth2Preset.authorizationUrl, () => ""), | ||
| 531 | resolvedBaseUrl, | ||
| 532 | ); | ||
| 533 | const issuerUrl = inferOAuthIssuerUrl(authorizationUrl); | ||
| 534 | |||
| 535 | await oauth.openAuthorization({ | ||
| 536 | run: async () => { | ||
| 537 | const response = await doStartOAuth({ | ||
| 538 | params: { scopeId }, | ||
| 539 | payload: { | ||
| 540 | endpoint: authorizationUrl, | ||
| 541 | connectionId: openApiOAuthConnectionId(resolvedSourceId, selectedOAuth2Preset.flow), | ||
| 542 | tokenScope: scopeId as string, | ||
| 543 | redirectUrl: oauth2RedirectUrl, | ||
| 544 | strategy: { | ||
| 545 | kind: "authorization-code", | ||
| 546 | authorizationEndpoint: authorizationUrl, | ||
| 547 | tokenEndpoint: tokenUrl, | ||
| 548 | issuerUrl, | ||
| 549 | clientIdSecretId: oauth2ClientIdSecretId, | ||
| 550 | clientSecretSecretId: oauth2ClientSecretSecretId ?? null, | ||
| 551 | scopes: [...oauth2SelectedScopes], | ||
| 552 | }, | ||
| 553 | pluginId: "openapi", | ||
| 554 | identityLabel: `${displayName} OAuth`, | ||
| 555 | }, | ||
| 556 | }); | ||
| 557 | if (response.authorizationUrl === null) { | ||
| 558 | throw new Error("Unexpected response flow from server"); | ||
| 559 | } | ||
| 560 | return { | ||
| 561 | sessionId: response.sessionId, | ||
| 562 | authorizationUrl: response.authorizationUrl, | ||
| 563 | }; | ||
| 564 | }, | ||
| 565 | onSuccess: (result) => { | ||
| 566 | setOauth2AuthState({ | ||
| 567 | fingerprint: selectedOAuth2Fingerprint, | ||
| 568 | auth: new OAuth2Auth({ | ||
| 569 | kind: "oauth2", | ||
| 570 | connectionId: result.connectionId, | ||
| 571 | securitySchemeName: selectedOAuth2Preset.securitySchemeName, | ||
| 572 | flow: "authorizationCode", | ||
| 573 | tokenUrl, | ||
| 574 | authorizationUrl, | ||
| 575 | issuerUrl, | ||
| 576 | clientIdSecretId: oauth2ClientIdSecretId, | ||
| 577 | clientSecretSecretId: oauth2ClientSecretSecretId, | ||
| 578 | scopes: [...oauth2SelectedScopes], | ||
| 579 | }), | ||
| 580 | }); | ||
| 581 | setOauth2Error(null); | ||
| 582 | }, | ||
| 583 | onError: setOauth2Error, | ||
| 584 | }); | ||
| 585 | } catch (e) { | ||
| 586 | setStartingOAuth(false); | ||
| 587 | setOauth2Error(e instanceof Error ? e.message : "Failed to start OAuth"); | ||
| 588 | } | ||
| 589 | }, [ | 595 | }, [ |
| 590 | selectedOAuth2Preset, | 596 | selectedOAuth2Preset, |
| 591 | oauth2ClientIdSecretId, | 597 | oauth2ClientIdSecretId, |
| @@ -621,103 +627,123 @@ export default function AddOpenApiSource(props: { · 29 unmodified lines | |||
| 621 | kind: "openapi", | 627 | kind: "openapi", |
| 622 | url: resolvedBaseUrl || undefined, | 628 | url: resolvedBaseUrl || undefined, |
| 623 | }); | 629 | }); |
| 624 | try { | 630 | const failAdd = (message: string) => { |
| 625 | const result = await doAdd({ | 631 | setAddError(message); |
| 632 | setAdding(false); | ||
| 633 | placeholder.done(); | ||
| 634 | }; | ||
| 635 | const resultExit = await doAdd({ | ||
| 636 | params: { scopeId }, | ||
| 637 | payload: { | ||
| 638 | spec: specUrl, | ||
| 639 | specFetchCredentials: serializeHttpCredentials(specFetchCredentials), | ||
| 640 | name: identity.name.trim() || undefined, | ||
| 641 | namespace: slugifyNamespace(identity.namespace) || undefined, | ||
| 642 | baseUrl: resolvedBaseUrl || undefined, | ||
| 643 | ...(hasHeaders ? { headers: configuredHeaders } : {}), | ||
| 644 | ...(Object.keys(serializeHttpCredentials(runtimeCredentials).queryParams).length > 0 | ||
| 645 | ? { queryParams: serializeHttpCredentials(runtimeCredentials).queryParams } | ||
| 646 | : {}), | ||
| 647 | ...(configuredOAuth2 ? { oauth2: configuredOAuth2 } : {}), | ||
| 648 | }, | ||
| 649 | reactivityKeys: addSpecWriteKeys, | ||
| 650 | }); | ||
| 651 | |||
| 652 | if (Exit.isFailure(resultExit)) { | ||
| 653 | failAdd(errorMessageFromExit(resultExit, "Failed to add source")); | ||
| 654 | return; | ||
| 655 | } | ||
| 656 | |||
| 657 | const sourceId = resultExit.value.namespace; | ||
| 658 | const sourceScope = ScopeId.make(scopeId); | ||
| 659 | const bindingScope = ScopeId.make(userScope); | ||
| 660 | |||
| 661 | for (const binding of headerBindings) { | ||
| 662 | const bindingExit = await doSetBinding({ | ||
| 626 | params: { scopeId }, | 663 | params: { scopeId }, |
| 627 | payload: { | 664 | payload: { |
| 628 | spec: specUrl, | 665 | sourceId, |
| 629 | specFetchCredentials: serializeHttpCredentials(specFetchCredentials), | 666 | sourceScope, |
| 630 | name: identity.name.trim() || undefined, | 667 | scope: bindingScope, |
| 631 | namespace: slugifyNamespace(identity.namespace) || undefined, | 668 | slot: binding.slot, |
| 632 | baseUrl: resolvedBaseUrl || undefined, | 669 | value: { |
| 633 | ...(hasHeaders ? { headers: configuredHeaders } : {}), | 670 | kind: "secret", |
| 634 | ...(Object.keys(serializeHttpCredentials(runtimeCredentials).queryParams).length > 0 | 671 | secretId: SecretId.make(binding.secretId), |
| 635 | ? { queryParams: serializeHttpCredentials(runtimeCredentials).queryParams } | 672 | }, |
| 636 | : {}), | ||
| 637 | ...(configuredOAuth2 ? { oauth2: configuredOAuth2 } : {}), | ||
| 638 | }, | 673 | }, |
| 639 | reactivityKeys: addSpecWriteKeys, | 674 | reactivityKeys: bindingWriteKeys, |
| 640 | }); | 675 | }); |
| 641 | 676 | if (Exit.isFailure(bindingExit)) { | |
| 642 | const sourceId = result.namespace; | 677 | failAdd(errorMessageFromExit(bindingExit, "Failed to add source")); |
| 643 | const sourceScope = ScopeId.make(scopeId); | 678 | return; |
| 644 | const bindingScope = ScopeId.make(userScope); | ||
| 645 | |||
| 646 | for (const binding of headerBindings) { | ||
| 647 | await doSetBinding({ | ||
| 648 | params: { scopeId }, | ||
| 649 | payload: { | ||
| 650 | sourceId, | ||
| 651 | sourceScope, | ||
| 652 | scope: bindingScope, | ||
| 653 | slot: binding.slot, | ||
| 654 | value: { | ||
| 655 | kind: "secret", | ||
| 656 | secretId: SecretId.make(binding.secretId), | ||
| 657 | }, | ||
| 658 | }, | ||
| 659 | reactivityKeys: bindingWriteKeys, | ||
| 660 | }); | ||
| 661 | } | 679 | } |
| 680 | } | ||
| 662 | 681 | ||
| 663 | if (configuredOAuth2 && oauth2ClientIdSecretId) { | 682 | if (configuredOAuth2 && oauth2ClientIdSecretId) { |
| 664 | await doSetBinding({ | 683 | const clientIdBindingExit = await doSetBinding({ |
| 665 | params: { scopeId }, | 684 | params: { scopeId }, |
| 666 | payload: { | 685 | payload: { |
| 667 | sourceId, | 686 | sourceId, |
| 668 | sourceScope, | 687 | sourceScope, |
| 669 | scope: bindingScope, | 688 | scope: bindingScope, |
| 670 | slot: configuredOAuth2.clientIdSlot, | 689 | slot: configuredOAuth2.clientIdSlot, |
| 671 | value: { | 690 | value: { |
| 672 | kind: "secret", | 691 | kind: "secret", |
| 673 | secretId: SecretId.make(oauth2ClientIdSecretId), | 692 | secretId: SecretId.make(oauth2ClientIdSecretId), |
| 674 | }, | ||
| 675 | }, | 693 | }, |
| 676 | reactivityKeys: bindingWriteKeys, | 694 | }, |
| 677 | }); | 695 | reactivityKeys: bindingWriteKeys, |
| 696 | }); | ||
| 697 | if (Exit.isFailure(clientIdBindingExit)) { | ||
| 698 | failAdd(errorMessageFromExit(clientIdBindingExit, "Failed to add source")); | ||
| 699 | return; | ||
| 678 | } | 700 | } |
| 701 | } | ||
| 679 | 702 | ||
| 680 | if (configuredOAuth2?.clientSecretSlot && oauth2ClientSecretSecretId) { | 703 | if (configuredOAuth2?.clientSecretSlot && oauth2ClientSecretSecretId) { |
| 681 | await doSetBinding({ | 704 | const clientSecretBindingExit = await doSetBinding({ |
| 682 | params: { scopeId }, | 705 | params: { scopeId }, |
| 683 | payload: { | 706 | payload: { |
| 684 | sourceId, | 707 | sourceId, |
| 685 | sourceScope, | 708 | sourceScope, |
| 686 | scope: bindingScope, | 709 | scope: bindingScope, |
| 687 | slot: configuredOAuth2.clientSecretSlot, | 710 | slot: configuredOAuth2.clientSecretSlot, |
| 688 | value: { | 711 | value: { |
| 689 | kind: "secret", | 712 | kind: "secret", |
| 690 | secretId: SecretId.make(oauth2ClientSecretSecretId), | 713 | secretId: SecretId.make(oauth2ClientSecretSecretId), |
| 691 | }, | ||
| 692 | }, | 714 | }, |
| 693 | reactivityKeys: bindingWriteKeys, | 715 | }, |
| 694 | }); | 716 | reactivityKeys: bindingWriteKeys, |
| 717 | }); | ||
| 718 | if (Exit.isFailure(clientSecretBindingExit)) { | ||
| 719 | failAdd(errorMessageFromExit(clientSecretBindingExit, "Failed to add source")); | ||
| 720 | return; | ||
| 695 | } | 721 | } |
| 722 | } | ||
| 696 | 723 | ||
| 697 | if (configuredOAuth2 && oauth2Auth) { | 724 | if (configuredOAuth2 && oauth2Auth) { |
| 698 | await doSetBinding({ | 725 | const connectionBindingExit = await doSetBinding({ |
| 699 | params: { scopeId }, | 726 | params: { scopeId }, |
| 700 | payload: { | 727 | payload: { |
| 701 | sourceId, | 728 | sourceId, |
| 702 | sourceScope, | 729 | sourceScope, |
| 703 | scope: bindingScope, | 730 | scope: bindingScope, |
| 704 | slot: configuredOAuth2.connectionSlot, | 731 | slot: configuredOAuth2.connectionSlot, |
| 705 | value: { | 732 | value: { |
| 706 | kind: "connection", | 733 | kind: "connection", |
| 707 | connectionId: ConnectionId.make(oauth2Auth.connectionId), | 734 | connectionId: ConnectionId.make(oauth2Auth.connectionId), |
| 708 | }, | ||
| 709 | }, | 735 | }, |
| 710 | reactivityKeys: bindingWriteKeys, | 736 | }, |
| 711 | }); | 737 | reactivityKeys: bindingWriteKeys, |
| 738 | }); | ||
| 739 | if (Exit.isFailure(connectionBindingExit)) { | ||
| 740 | failAdd(errorMessageFromExit(connectionBindingExit, "Failed to add source")); | ||
| 741 | return; | ||
| 712 | } | 742 | } |
| 713 | |||
| 714 | props.onComplete(); | ||
| 715 | } catch (e) { | ||
| 716 | setAddError(e instanceof Error ? e.message : "Failed to add source"); | ||
| 717 | setAdding(false); | ||
| 718 | } finally { | ||
| 719 | placeholder.done(); | ||
| 720 | } | 743 | } |
| 744 | |||
| 745 | placeholder.done(); | ||
| 746 | props.onComplete(); | ||
| 721 | }; | 747 | }; |
| 722 | 748 | ||
| 723 | // ---- Render ---- | 749 | // ---- Render ---- |
| @@ -1,5 +1,8 @@ | |||
| 1 | import { useEffect, useMemo, useRef, useState } from "react"; | 1 | import { useEffect, useMemo, useRef, useState } from "react"; |
| 2 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; | 2 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; |
| 3 | import * as Exit from "effect/Exit"; | ||
| 4 | import * as Option from "effect/Option"; | ||
| 5 | import * as Schema from "effect/Schema"; | ||
| 3 | import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; | 6 | import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; |
| 4 | 7 | ||
| 5 | import { connectionsAtom, sourceAtom, startOAuth } from "@executor-js/react/api/atoms"; | 8 | import { connectionsAtom, sourceAtom, startOAuth } from "@executor-js/react/api/atoms"; |
| @@ -42,7 +45,21 @@ import { · 36 unmodified lines | |||
| 42 | resolveOAuthUrl, | 45 | resolveOAuthUrl, |
| 43 | } from "./AddOpenApiSource"; | 46 | } from "./AddOpenApiSource"; |
| 44 | import { oauth2ClientSecretSlot } from "../sdk/store"; | 47 | import { oauth2ClientSecretSlot } from "../sdk/store"; |
| 45 | import type { OpenApiSourceBindingValue } from "../sdk/types"; | 48 | import { |
| 49 | OpenApiSourceBindingValue, | ||
| 50 | type OpenApiSourceBindingValue as OpenApiSourceBindingValueType, | ||
| 51 | } from "../sdk/types"; | ||
| 52 | |||
| 53 | const ErrorMessage = Schema.Struct({ message: Schema.String }); | ||
| 54 | |||
| 55 | const errorMessageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string => | ||
| 56 | Option.match( | ||
| 57 | Option.flatMap(Exit.findErrorOption(exit), Schema.decodeUnknownOption(ErrorMessage)), | ||
| 58 | { | ||
| 59 | onNone: () => fallback, | ||
| 60 | onSome: ({ message }) => message, | ||
| 61 | }, | ||
| 62 | ); | ||
| 46 | 63 | ||
| 47 | type SlotDef = | 64 | type SlotDef = |
| 48 | | { | 65 | | { |
| @@ -79,7 +96,7 @@ const openApiOAuthConnectionId = ( · 30 unmodified lines | |||
| 79 | targetScope: ScopeId, | 96 | targetScope: ScopeId, |
| 80 | ): ConnectionId => | 97 | ): ConnectionId => |
| 81 | ConnectionId.make( | 98 | ConnectionId.make( |
| 82 | `openapi-oauth-${slugify(sourceId)}-${slugify(securitySchemeName)}-${shortHash(targetScope as string)}`, | 99 | `openapi-oauth-${slugify(sourceId)}-${slugify(securitySchemeName)}-${shortHash(targetScope)}`, |
| 83 | ); | 100 | ); |
| 84 | 101 | ||
| 85 | const bindingSecretId = (sourceId: string, slot: string, scopeId: string): string => | 102 | const bindingSecretId = (sourceId: string, slot: string, scopeId: string): string => |
| @@ -101,7 +118,7 @@ const exactBindingForScope = ( · 15 unmodified lines | |||
| 101 | ) => rows.find((row) => row.slot === slot && row.scopeId === scopeId) ?? null; | 118 | ) => rows.find((row) => row.slot === slot && row.scopeId === scopeId) ?? null; |
| 102 | 119 | ||
| 103 | const scopeRank = (ranks: ReadonlyMap<string, number>, scopeId: ScopeId): number => | 120 | const scopeRank = (ranks: ReadonlyMap<string, number>, scopeId: ScopeId): number => |
| 104 | ranks.get(scopeId as string) ?? Number.MAX_SAFE_INTEGER; | 121 | ranks.get(scopeId) ?? Number.MAX_SAFE_INTEGER; |
| 105 | 122 | ||
| 106 | const effectiveBindingForScope = ( | 123 | const effectiveBindingForScope = ( |
| 107 | rows: readonly { | 124 | rows: readonly { |
| @@ -119,21 +136,13 @@ const effectiveBindingForScope = ( · 11 unmodified lines | |||
| 119 | 136 | ||
| 120 | const isSecretBindingValue = ( | 137 | const isSecretBindingValue = ( |
| 121 | value: unknown, | 138 | value: unknown, |
| 122 | ): value is Extract<OpenApiSourceBindingValue, { readonly kind: "secret" }> => | 139 | ): value is Extract<OpenApiSourceBindingValueType, { readonly kind: "secret" }> => |
| 123 | typeof value === "object" && | 140 | Schema.is(OpenApiSourceBindingValue)(value) && value.kind === "secret"; |
| 124 | value !== null && | ||
| 125 | "kind" in value && | ||
| 126 | (value as { kind?: unknown }).kind === "secret" && | ||
| 127 | "secretId" in value; | ||
| 128 | 141 | ||
| 129 | const isConnectionBindingValue = ( | 142 | const isConnectionBindingValue = ( |
| 130 | value: unknown, | 143 | value: unknown, |
| 131 | ): value is Extract<OpenApiSourceBindingValue, { readonly kind: "connection" }> => | 144 | ): value is Extract<OpenApiSourceBindingValueType, { readonly kind: "connection" }> => |
| 132 | typeof value === "object" && | 145 | Schema.is(OpenApiSourceBindingValue)(value) && value.kind === "connection"; |
| 133 | value !== null && | ||
| 134 | "kind" in value && | ||
| 135 | (value as { kind?: unknown }).kind === "connection" && | ||
| 136 | "connectionId" in value; | ||
| 137 | 146 | ||
| 138 | export default function EditOpenApiSource(props: { | 147 | export default function EditOpenApiSource(props: { |
| 139 | readonly sourceId: string; | 148 | readonly sourceId: string; |
| @@ -150,7 +159,7 @@ export default function EditOpenApiSource(props: { · 10 unmodified lines | |||
| 150 | const sourceScopeId = sourceSummary?.scopeId ?? displayScope; | 159 | const sourceScopeId = sourceSummary?.scopeId ?? displayScope; |
| 151 | const sourceScope = ScopeId.make(sourceScopeId); | 160 | const sourceScope = ScopeId.make(sourceScopeId); |
| 152 | const scopeRanks = useMemo( | 161 | const scopeRanks = useMemo( |
| 153 | () => new Map(scopeStack.map((scope, index) => [scope.id as string, index] as const)), | 162 | () => new Map(scopeStack.map((scope, index) => [scope.id, index] as const)), |
| 154 | [scopeStack], | 163 | [scopeStack], |
| 155 | ); | 164 | ); |
| 156 | 165 | ||
| @@ -161,10 +170,10 @@ export default function EditOpenApiSource(props: { · 4 unmodified lines | |||
| 161 | const connectionsResult = useAtomValue(connectionsAtom(displayScope)); | 170 | const connectionsResult = useAtomValue(connectionsAtom(displayScope)); |
| 162 | const secretList = useSecretPickerSecrets(); | 171 | const secretList = useSecretPickerSecrets(); |
| 163 | 172 | ||
| 164 | const doUpdate = useAtomSet(updateOpenApiSource, { mode: "promise" }); | 173 | const doUpdate = useAtomSet(updateOpenApiSource, { mode: "promiseExit" }); |
| 165 | const doSetBinding = useAtomSet(setOpenApiSourceBinding, { mode: "promise" }); | 174 | const doSetBinding = useAtomSet(setOpenApiSourceBinding, { mode: "promiseExit" }); |
| 166 | const doRemoveBinding = useAtomSet(removeOpenApiSourceBinding, { mode: "promise" }); | 175 | const doRemoveBinding = useAtomSet(removeOpenApiSourceBinding, { mode: "promiseExit" }); |
| 167 | const doStartOAuth = useAtomSet(startOAuth, { mode: "promise" }); | 176 | const doStartOAuth = useAtomSet(startOAuth, { mode: "promiseExit" }); |
| 168 | const oauth = useOAuthPopupFlow<OAuthCompletionPayload>({ | 177 | const oauth = useOAuthPopupFlow<OAuthCompletionPayload>({ |
| 169 | popupName: OPENAPI_OAUTH_POPUP_NAME, | 178 | popupName: OPENAPI_OAUTH_POPUP_NAME, |
| 170 | popupBlockedMessage: "OAuth popup was blocked by the browser", | 179 | popupBlockedMessage: "OAuth popup was blocked by the browser", |
| @@ -224,28 +233,28 @@ export default function EditOpenApiSource(props: { · 53 unmodified lines | |||
| 224 | const seq = ++sourceSaveSeq.current; | 233 | const seq = ++sourceSaveSeq.current; |
| 225 | setSourceSaveState("saving"); | 234 | setSourceSaveState("saving"); |
| 226 | setError(null); | 235 | setError(null); |
| 227 | void doUpdate({ | 236 | void (async () => { |
| 228 | params: { scopeId: ScopeId.make(sourceScopeId), namespace: props.sourceId }, | 237 | const exit = await doUpdate({ |
| 229 | payload: { | 238 | params: { scopeId: ScopeId.make(sourceScopeId), namespace: props.sourceId }, |
| 230 | name: nextName || undefined, | 239 | payload: { |
| 231 | baseUrl: nextBaseUrl || undefined, | 240 | name: nextName || undefined, |
| 232 | headers: source.config.headers, | 241 | baseUrl: nextBaseUrl || undefined, |
| 233 | oauth2: source.config.oauth2, | 242 | headers: source.config.headers, |
| 234 | }, | 243 | oauth2: source.config.oauth2, |
| 235 | reactivityKeys: openApiWriteKeys, | 244 | }, |
| 236 | }) | 245 | reactivityKeys: openApiWriteKeys, |
| 237 | .then(() => { | ||
| 238 | if (sourceSaveSeq.current !== seq) return; | ||
| 239 | setSourceSaveState("saved"); | ||
| 240 | window.setTimeout(() => { | ||
| 241 | if (sourceSaveSeq.current === seq) setSourceSaveState("idle"); | ||
| 242 | }, 1600); | ||
| 243 | }) | ||
| 244 | .catch((e: unknown) => { | ||
| 245 | if (sourceSaveSeq.current !== seq) return; | ||
| 246 | setSourceSaveState("idle"); | ||
| 247 | setError(e instanceof Error ? e.message : "Failed to save source details"); | ||
| 248 | }); | 246 | }); |
| 247 | if (sourceSaveSeq.current !== seq) return; | ||
| 248 | if (Exit.isFailure(exit)) { | ||
| 249 | setSourceSaveState("idle"); | ||
| 250 | setError(errorMessageFromExit(exit, "Failed to save source details")); | ||
| 251 | return; | ||
| 252 | } | ||
| 253 | setSourceSaveState("saved"); | ||
| 254 | window.setTimeout(() => { | ||
| 255 | if (sourceSaveSeq.current === seq) setSourceSaveState("idle"); | ||
| 256 | }, 1600); | ||
| 257 | })(); | ||
| 249 | }, 600); | 258 | }, 600); |
| 250 | 259 | ||
| 251 | return () => window.clearTimeout(timeout); | 260 | return () => window.clearTimeout(timeout); |
| @@ -321,44 +330,40 @@ export default function EditOpenApiSource(props: { · 69 unmodified lines | |||
| 321 | if (!trimmed) return; | 330 | if (!trimmed) return; |
| 322 | setBusyKey(inputKey); | 331 | setBusyKey(inputKey); |
| 323 | setError(null); | 332 | setError(null); |
| 324 | try { | 333 | const exit = await doSetBinding({ |
| 325 | await doSetBinding({ | 334 | params: { scopeId: displayScope }, |
| 326 | params: { scopeId: displayScope }, | 335 | payload: { |
| 327 | payload: { | 336 | sourceId: props.sourceId, |
| 328 | sourceId: props.sourceId, | 337 | sourceScope, |
| 329 | sourceScope, | 338 | scope: targetScope, |
| 330 | scope: targetScope, | 339 | slot, |
| 331 | slot, | 340 | value: { kind: "secret", secretId: SecretId.make(trimmed) }, |
| 332 | value: { kind: "secret", secretId: SecretId.make(trimmed) }, | 341 | }, |
| 333 | }, | 342 | reactivityKeys: sourceWriteKeys, |
| 334 | reactivityKeys: sourceWriteKeys, | 343 | }); |
| 335 | }); | 344 | if (Exit.isFailure(exit)) { |
| 336 | } catch (e) { | 345 | setError(errorMessageFromExit(exit, "Failed to save credential binding")); |
| 337 | setError(e instanceof Error ? e.message : "Failed to save credential binding"); | ||
| 338 | } finally { | ||
| 339 | setBusyKey(null); | ||
| 340 | } | 346 | } |
| 347 | setBusyKey(null); | ||
| 341 | }; | 348 | }; |
| 342 | 349 | ||
| 343 | const clearBinding = async (targetScope: ScopeId, slot: string) => { | 350 | const clearBinding = async (targetScope: ScopeId, slot: string) => { |
| 344 | setBusyKey(`${targetScope}:${slot}:clear`); | 351 | setBusyKey(`${targetScope}:${slot}:clear`); |
| 345 | setError(null); | 352 | setError(null); |
| 346 | try { | 353 | const exit = await doRemoveBinding({ |
| 347 | await doRemoveBinding({ | 354 | params: { scopeId: displayScope }, |
| 348 | params: { scopeId: displayScope }, | 355 | payload: { |
| 349 | payload: { | 356 | sourceId: props.sourceId, |
| 350 | sourceId: props.sourceId, | 357 | sourceScope, |
| 351 | sourceScope, | 358 | slot, |
| 352 | slot, | 359 | scope: targetScope, |
| 353 | scope: targetScope, | 360 | }, |
| 354 | }, | 361 | reactivityKeys: sourceWriteKeys, |
| 355 | reactivityKeys: sourceWriteKeys, | 362 | }); |
| 356 | }); | 363 | if (Exit.isFailure(exit)) { |
| 357 | } catch (e) { | 364 | setError(errorMessageFromExit(exit, "Failed to clear credential binding")); |
| 358 | setError(e instanceof Error ? e.message : "Failed to clear credential binding"); | ||
| 359 | } finally { | ||
| 360 | setBusyKey(null); | ||
| 361 | } | 365 | } |
| 366 | setBusyKey(null); | ||
| 362 | }; | 367 | }; |
| 363 | 368 | ||
| 364 | const connectOAuth = async (targetScope: ScopeId) => { | 369 | const connectOAuth = async (targetScope: ScopeId) => { |
| @@ -410,35 +415,112 @@ export default function EditOpenApiSource(props: { · 45 unmodified lines | |||
| 410 | setPendingOAuthConnection({ | 415 | setPendingOAuthConnection({ |
| 411 | scopeId: targetScope, | 416 | scopeId: targetScope, |
| 412 | slot: oauth2.connectionSlot, | 417 | slot: oauth2.connectionSlot, |
| 413 | connectionId: connectionId as string, | 418 | connectionId: connectionId, |
| 414 | }); | 419 | }); |
| 415 | setError(null); | 420 | setError(null); |
| 416 | try { | 421 | const failConnect = (message: string) => { |
| 417 | const displayName = source.name; | 422 | setError(message); |
| 418 | const tokenUrl = resolveOAuthUrl(oauth2.tokenUrl, source.config.baseUrl ?? ""); | 423 | setPendingOAuthConnection(null); |
| 419 | if (oauth2.flow === "clientCredentials") { | 424 | setBusyKey(null); |
| 420 | const response = await doStartOAuth({ | 425 | }; |
| 421 | params: { scopeId: displayScope }, | 426 | const displayName = source.name; |
| 422 | payload: { | 427 | const tokenUrl = resolveOAuthUrl(oauth2.tokenUrl, source.config.baseUrl ?? ""); |
| 423 | endpoint: tokenUrl, | 428 | if (oauth2.flow === "clientCredentials") { |
| 424 | redirectUrl: tokenUrl, | 429 | const startOAuthExit = await doStartOAuth({ |
| 425 | connectionId: connectionId as string, | 430 | params: { scopeId: displayScope }, |
| 426 | tokenScope: targetScope as string, | 431 | payload: { |
| 427 | strategy: { | 432 | endpoint: tokenUrl, |
| 428 | kind: "client-credentials", | 433 | redirectUrl: tokenUrl, |
| 429 | tokenEndpoint: tokenUrl, | 434 | connectionId: connectionId, |
| 430 | clientIdSecretId, | 435 | tokenScope: targetScope, |
| 431 | clientSecretSecretId: clientSecretValue!.secretId, | 436 | strategy: { |
| 432 | scopes: [...oauth2.scopes], | 437 | kind: "client-credentials", |
| 433 | }, | 438 | tokenEndpoint: tokenUrl, |
| 434 | pluginId: "openapi", | 439 | clientIdSecretId, |
| 435 | identityLabel: `${displayName} OAuth`, | 440 | clientSecretSecretId: clientSecretValue!.secretId, |
| 441 | scopes: [...oauth2.scopes], | ||
| 436 | }, | 442 | }, |
| 437 | }); | 443 | pluginId: "openapi", |
| 438 | if (!response.completedConnection) { | 444 | identityLabel: `${displayName} OAuth`, |
| 439 | throw new Error("Unexpected OAuth response"); | 445 | }, |
| 440 | } | 446 | }); |
| 441 | await doSetBinding({ | 447 | if (Exit.isFailure(startOAuthExit)) { |
| 448 | failConnect(errorMessageFromExit(startOAuthExit, "Failed to connect OAuth")); | ||
| 449 | return; | ||
| 450 | } | ||
| 451 | const response = startOAuthExit.value; | ||
| 452 | if (!response.completedConnection) { | ||
| 453 | failConnect("Unexpected OAuth response"); | ||
| 454 | return; | ||
| 455 | } | ||
| 456 | const setBindingExit = await doSetBinding({ | ||
| 457 | params: { scopeId: displayScope }, | ||
| 458 | payload: { | ||
| 459 | sourceId: props.sourceId, | ||
| 460 | sourceScope, | ||
| 461 | scope: targetScope, | ||
| 462 | slot: oauth2.connectionSlot, | ||
| 463 | value: { | ||
| 464 | kind: "connection", | ||
| 465 | connectionId: ConnectionId.make(response.completedConnection.connectionId), | ||
| 466 | }, | ||
| 467 | }, | ||
| 468 | reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys], | ||
| 469 | }); | ||
| 470 | if (Exit.isFailure(setBindingExit)) { | ||
| 471 | failConnect(errorMessageFromExit(setBindingExit, "Failed to connect OAuth")); | ||
| 472 | return; | ||
| 473 | } | ||
| 474 | setPendingOAuthConnection(null); | ||
| 475 | setBusyKey(null); | ||
| 476 | return; | ||
| 477 | } | ||
| 478 | |||
| 479 | const authorizationUrl = resolveOAuthUrl( | ||
| 480 | oauth2.authorizationUrl ?? "", | ||
| 481 | source.config.baseUrl ?? "", | ||
| 482 | ); | ||
| 483 | const issuerUrl = oauth2.issuerUrl ?? inferOAuthIssuerUrl(authorizationUrl); | ||
| 484 | const startOAuthExit = await doStartOAuth({ | ||
| 485 | params: { scopeId: displayScope }, | ||
| 486 | payload: { | ||
| 487 | endpoint: authorizationUrl, | ||
| 488 | connectionId, | ||
| 489 | tokenScope: targetScope, | ||
| 490 | redirectUrl: oauth2RedirectUrl, | ||
| 491 | strategy: { | ||
| 492 | kind: "authorization-code", | ||
| 493 | authorizationEndpoint: authorizationUrl, | ||
| 494 | tokenEndpoint: tokenUrl, | ||
| 495 | issuerUrl, | ||
| 496 | clientIdSecretId, | ||
| 497 | clientSecretSecretId: | ||
| 498 | clientSecretBinding && isSecretBindingValue(clientSecretBinding.value) | ||
| 499 | ? clientSecretBinding.value.secretId | ||
| 500 | : null, | ||
| 501 | scopes: [...oauth2.scopes], | ||
| 502 | }, | ||
| 503 | pluginId: "openapi", | ||
| 504 | identityLabel: `${displayName} OAuth`, | ||
| 505 | }, | ||
| 506 | }); | ||
| 507 | if (Exit.isFailure(startOAuthExit)) { | ||
| 508 | failConnect(errorMessageFromExit(startOAuthExit, "Failed to connect OAuth")); | ||
| 509 | return; | ||
| 510 | } | ||
| 511 | const response = startOAuthExit.value; | ||
| 512 | if (response.authorizationUrl === null) { | ||
| 513 | failConnect("Unexpected OAuth response"); | ||
| 514 | return; | ||
| 515 | } | ||
| 516 | |||
| 517 | await oauth.openAuthorization({ | ||
| 518 | run: async () => ({ | ||
| 519 | sessionId: response.sessionId, | ||
| 520 | authorizationUrl: response.authorizationUrl, | ||
| 521 | }), | ||
| 522 | onSuccess: async (result) => { | ||
| 523 | const setBindingExit = await doSetBinding({ | ||
| 442 | params: { scopeId: displayScope }, | 524 | params: { scopeId: displayScope }, |
| 443 | payload: { | 525 | payload: { |
| 444 | sourceId: props.sourceId, | 526 | sourceId: props.sourceId, |
| @@ -447,83 +529,24 @@ export default function EditOpenApiSource(props: { · 2 unmodified lines | |||
| 447 | slot: oauth2.connectionSlot, | 529 | slot: oauth2.connectionSlot, |
| 448 | value: { | 530 | value: { |
| 449 | kind: "connection", | 531 | kind: "connection", |
| 450 | connectionId: ConnectionId.make(response.completedConnection.connectionId), | 532 | connectionId: ConnectionId.make(result.connectionId), |
| 451 | }, | 533 | }, |
| 452 | }, | 534 | }, |
| 453 | reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys], | 535 | reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys], |
| 454 | }); | 536 | }); |
| 537 | if (Exit.isFailure(setBindingExit)) { | ||
| 538 | failConnect(errorMessageFromExit(setBindingExit, "Failed to connect OAuth")); | ||
| 539 | return; | ||
| 540 | } | ||
| 455 | setPendingOAuthConnection(null); | 541 | setPendingOAuthConnection(null); |
| 456 | setBusyKey(null); | 542 | setBusyKey(null); |
| 457 | return; | 543 | }, |
| 458 | } | 544 | onError: (message) => { |
| 459 | 545 | setError(message); | |
| 460 | const authorizationUrl = resolveOAuthUrl( | 546 | setPendingOAuthConnection(null); |
| 461 | oauth2.authorizationUrl ?? "", | 547 | setBusyKey(null); |
| 462 | source.config.baseUrl ?? "", | 548 | }, |
| 463 | ); | 549 | }); |
| 464 | const issuerUrl = oauth2.issuerUrl ?? inferOAuthIssuerUrl(authorizationUrl); | ||
| 465 | await oauth.openAuthorization({ | ||
| 466 | run: async () => { | ||
| 467 | const response = await doStartOAuth({ | ||
| 468 | params: { scopeId: displayScope }, | ||
| 469 | payload: { | ||
| 470 | endpoint: authorizationUrl, | ||
| 471 | connectionId: connectionId as string, | ||
| 472 | tokenScope: targetScope as string, | ||
| 473 | redirectUrl: oauth2RedirectUrl, | ||
| 474 | strategy: { | ||
| 475 | kind: "authorization-code", | ||
| 476 | authorizationEndpoint: authorizationUrl, | ||
| 477 | tokenEndpoint: tokenUrl, | ||
| 478 | issuerUrl, | ||
| 479 | clientIdSecretId, | ||
| 480 | clientSecretSecretId: | ||
| 481 | clientSecretBinding && isSecretBindingValue(clientSecretBinding.value) | ||
| 482 | ? clientSecretBinding.value.secretId | ||
| 483 | : null, | ||
| 484 | scopes: [...oauth2.scopes], | ||
| 485 | }, | ||
| 486 | pluginId: "openapi", | ||
| 487 | identityLabel: `${displayName} OAuth`, | ||
| 488 | }, | ||
| 489 | }); | ||
| 490 | if (response.authorizationUrl === null) { | ||
| 491 | throw new Error("Unexpected OAuth response"); | ||
| 492 | } | ||
| 493 | return { | ||
| 494 | sessionId: response.sessionId, | ||
| 495 | authorizationUrl: response.authorizationUrl, | ||
| 496 | }; | ||
| 497 | }, | ||
| 498 | onSuccess: async (result) => { | ||
| 499 | await doSetBinding({ | ||
| 500 | params: { scopeId: displayScope }, | ||
| 501 | payload: { | ||
| 502 | sourceId: props.sourceId, | ||
| 503 | sourceScope, | ||
| 504 | scope: targetScope, | ||
| 505 | slot: oauth2.connectionSlot, | ||
| 506 | value: { | ||
| 507 | kind: "connection", | ||
| 508 | connectionId: ConnectionId.make(result.connectionId), | ||
| 509 | }, | ||
| 510 | }, | ||
| 511 | reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys], | ||
| 512 | }); | ||
| 513 | setPendingOAuthConnection(null); | ||
| 514 | setBusyKey(null); | ||
| 515 | }, | ||
| 516 | onError: (message) => { | ||
| 517 | setError(message); | ||
| 518 | setPendingOAuthConnection(null); | ||
| 519 | setBusyKey(null); | ||
| 520 | }, | ||
| 521 | }); | ||
| 522 | } catch (e) { | ||
| 523 | setError(e instanceof Error ? e.message : "Failed to connect OAuth"); | ||
| 524 | setPendingOAuthConnection(null); | ||
| 525 | setBusyKey(null); | ||
| 526 | } | ||
| 527 | }; | 550 | }; |
| 528 | 551 | ||
| 529 | return ( | 552 | return ( |
| @@ -590,10 +613,10 @@ export default function EditOpenApiSource(props: { · 60 unmodified lines | |||
| 590 | </CardStackEntryContent> | 613 | </CardStackEntryContent> |
| 591 | <FilterTabs | 614 | <FilterTabs |
| 592 | tabs={credentialScopes.map((entry) => ({ | 615 | tabs={credentialScopes.map((entry) => ({ |
| 593 | value: entry.scopeId as string, | 616 | value: entry.scopeId, |
| 594 | label: entry.label, | 617 | label: entry.label, |
| 595 | }))} | 618 | }))} |
| 596 | value={activeCredentialScopeId as string} | 619 | value={activeCredentialScopeId} |
| 597 | onChange={setSelectedCredentialScope} | 620 | onChange={setSelectedCredentialScope} |
| 598 | /> | 621 | /> |
| 599 | </CardStackEntry> | 622 | </CardStackEntry> |
| @@ -618,9 +641,9 @@ export default function EditOpenApiSource(props: { · 18 unmodified lines | |||
| 618 | isSecretBindingValue(effective.value); | 641 | isSecretBindingValue(effective.value); |
| 619 | const currentSecretId = | 642 | const currentSecretId = |
| 620 | exact && isSecretBindingValue(exact.value) | 643 | exact && isSecretBindingValue(exact.value) |
| 621 | ? (exact.value.secretId as string) | 644 | ? exact.value.secretId |
| 622 | : inherited && effective && isSecretBindingValue(effective.value) | 645 | : inherited && effective && isSecretBindingValue(effective.value) |
| 623 | ? (effective.value.secretId as string) | 646 | ? effective.value.secretId |
| 624 | : null; | 647 | : null; |
| 625 | return ( | 648 | return ( |
| 626 | <CardStackEntryField | 649 | <CardStackEntryField |
| @@ -1,4 +1,4 @@ | |||
| 1 | import { Effect, Schema } from "effect"; | 1 | import { Effect, Option, Schema } from "effect"; |
| 2 | 2 | ||
| 3 | import { | 3 | import { |
| 4 | ConnectionId, | 4 | ConnectionId, |
| @@ -174,9 +174,7 @@ export interface StoredSource { · 169 unmodified lines | |||
| 174 | // an encodable/decodable shape for HTTP responses. | 174 | // an encodable/decodable shape for HTTP responses. |
| 175 | // --------------------------------------------------------------------------- | 175 | // --------------------------------------------------------------------------- |
| 176 | 176 | ||
| 177 | export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>( | 177 | export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>("OpenApiStoredSource")({ |
| 178 | "OpenApiStoredSource", | ||
| 179 | )({ | ||
| 180 | namespace: Schema.String, | 178 | namespace: Schema.String, |
| 181 | name: Schema.String, | 179 | name: Schema.String, |
| 182 | config: Schema.Struct({ | 180 | config: Schema.Struct({ |
| @@ -184,20 +182,12 @@ export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>( · 1 unmodified line | |||
| 184 | sourceUrl: Schema.optional(Schema.String), | 182 | sourceUrl: Schema.optional(Schema.String), |
| 185 | baseUrl: Schema.optional(Schema.String), | 183 | baseUrl: Schema.optional(Schema.String), |
| 186 | namespace: Schema.optional(Schema.String), | 184 | namespace: Schema.optional(Schema.String), |
| 187 | headers: Schema.optional( | 185 | headers: Schema.optional(Schema.Record(Schema.String, ConfiguredHeaderValue)), |
| 188 | Schema.Record(Schema.String, ConfiguredHeaderValue), | 186 | queryParams: Schema.optional(Schema.Record(Schema.String, HeaderValue)), |
| 189 | ), | ||
| 190 | queryParams: Schema.optional( | ||
| 191 | Schema.Record(Schema.String, HeaderValue), | ||
| 192 | ), | ||
| 193 | specFetchCredentials: Schema.optional( | 187 | specFetchCredentials: Schema.optional( |
| 194 | Schema.Struct({ | 188 | Schema.Struct({ |
| 195 | headers: Schema.optional( | 189 | headers: Schema.optional(Schema.Record(Schema.String, HeaderValue)), |
| 196 | Schema.Record(Schema.String, HeaderValue), | 190 | queryParams: Schema.optional(Schema.Record(Schema.String, HeaderValue)), |
| 197 | ), | ||
| 198 | queryParams: Schema.optional( | ||
| 199 | Schema.Record(Schema.String, HeaderValue), | ||
| 200 | ), | ||
| 201 | }), | 191 | }), |
| 202 | ), | 192 | ), |
| 203 | // Canonical source-owned OAuth config. Concrete client credentials | 193 | // Canonical source-owned OAuth config. Concrete client credentials |
| @@ -221,10 +211,85 @@ export interface StoredOperation { · 17 unmodified lines | |||
| 221 | 211 | ||
| 222 | const encodeBinding = Schema.encodeSync(OperationBinding); | 212 | const encodeBinding = Schema.encodeSync(OperationBinding); |
| 223 | const decodeBinding = Schema.decodeUnknownSync(OperationBinding); | 213 | const decodeBinding = Schema.decodeUnknownSync(OperationBinding); |
| 214 | const decodeBindingJson = Schema.decodeUnknownSync(Schema.fromJsonString(OperationBinding)); | ||
| 224 | 215 | ||
| 225 | const decodeOAuth2 = Schema.decodeUnknownSync(OAuth2Auth); | 216 | const decodeOAuth2 = Schema.decodeUnknownSync(OAuth2Auth); |
| 217 | const decodeOAuth2Option = Schema.decodeUnknownOption(OAuth2Auth); | ||
| 218 | const decodeOAuth2JsonOption = Schema.decodeUnknownOption(Schema.fromJsonString(OAuth2Auth)); | ||
| 219 | const decodeOAuth2SourceConfigOption = Schema.decodeUnknownOption(OAuth2SourceConfig); | ||
| 220 | const decodeOAuth2SourceConfigJsonOption = Schema.decodeUnknownOption( | ||
| 221 | Schema.fromJsonString(OAuth2SourceConfig), | ||
| 222 | ); | ||
| 226 | const encodeOAuth2SourceConfig = Schema.encodeSync(OAuth2SourceConfig); | 223 | const encodeOAuth2SourceConfig = Schema.encodeSync(OAuth2SourceConfig); |
| 227 | 224 | ||
| 225 | const decodeHeaderValueOption = Schema.decodeUnknownOption(HeaderValue); | ||
| 226 | const UnknownRecord = Schema.Record(Schema.String, Schema.Unknown); | ||
| 227 | const decodeUnknownRecord = Schema.decodeUnknownSync(UnknownRecord); | ||
| 228 | const decodeUnknownRecordJson = Schema.decodeUnknownSync(Schema.fromJsonString(UnknownRecord)); | ||
| 229 | const decodeConfiguredHeaderBindingOption = Schema.decodeUnknownOption(ConfiguredHeaderBinding); | ||
| 230 | |||
| 231 | const NullableString = Schema.NullOr(Schema.String); | ||
| 232 | const OptionalNullableString = Schema.optional(NullableString); | ||
| 233 | |||
| 234 | const ChildStorageRow = Schema.Struct({ | ||
| 235 | name: Schema.String, | ||
| 236 | kind: Schema.Literals(["text", "secret"]), | ||
| 237 | text_value: OptionalNullableString, | ||
| 238 | secret_id: OptionalNullableString, | ||
| 239 | secret_prefix: OptionalNullableString, | ||
| 240 | }); | ||
| 241 | const decodeChildStorageRowOption = Schema.decodeUnknownOption(ChildStorageRow); | ||
| 242 | |||
| 243 | const SourceBindingStorageRow = Schema.Struct({ | ||
| 244 | source_id: Schema.String, | ||
| 245 | source_scope_id: Schema.String, | ||
| 246 | target_scope_id: Schema.String, | ||
| 247 | slot: Schema.String, | ||
| 248 | kind: Schema.Literals(["secret", "connection", "text"]), | ||
| 249 | secret_id: OptionalNullableString, | ||
| 250 | connection_id: OptionalNullableString, | ||
| 251 | text_value: OptionalNullableString, | ||
| 252 | created_at: Schema.Unknown, | ||
| 253 | updated_at: Schema.Unknown, | ||
| 254 | }); | ||
| 255 | const decodeSourceBindingStorageRow = Schema.decodeUnknownSync(SourceBindingStorageRow); | ||
| 256 | |||
| 257 | const SourceStorageRow = Schema.Struct({ | ||
| 258 | id: Schema.String, | ||
| 259 | scope_id: Schema.String, | ||
| 260 | name: Schema.String, | ||
| 261 | spec: Schema.String, | ||
| 262 | source_url: OptionalNullableString, | ||
| 263 | base_url: OptionalNullableString, | ||
| 264 | headers: Schema.optional(Schema.Unknown), | ||
| 265 | oauth2: Schema.optional(Schema.Unknown), | ||
| 266 | }); | ||
| 267 | const decodeSourceStorageRow = Schema.decodeUnknownSync(SourceStorageRow); | ||
| 268 | |||
| 269 | const OperationStorageRow = Schema.Struct({ | ||
| 270 | id: Schema.String, | ||
| 271 | source_id: Schema.String, | ||
| 272 | binding: Schema.Unknown, | ||
| 273 | }); | ||
| 274 | const decodeOperationStorageRow = Schema.decodeUnknownSync(OperationStorageRow); | ||
| 275 | |||
| 276 | const ChildUsageStorageRow = Schema.Struct({ | ||
| 277 | source_id: Schema.String, | ||
| 278 | scope_id: Schema.String, | ||
| 279 | name: Schema.String, | ||
| 280 | }); | ||
| 281 | const decodeChildUsageStorageRow = Schema.decodeUnknownSync(ChildUsageStorageRow); | ||
| 282 | |||
| 283 | const SourceNameStorageRow = Schema.Struct({ | ||
| 284 | id: Schema.String, | ||
| 285 | scope_id: Schema.String, | ||
| 286 | name: Schema.String, | ||
| 287 | }); | ||
| 288 | const decodeSourceNameStorageRow = Schema.decodeUnknownSync(SourceNameStorageRow); | ||
| 289 | |||
| 290 | const decodeStorageDate = (value: unknown): Date => | ||
| 291 | value instanceof Date ? value : new Date(Schema.decodeUnknownSync(Schema.String)(value)); | ||
| 292 | |||
| 228 | interface ChildRow { | 293 | interface ChildRow { |
| 229 | readonly id: string; | 294 | readonly id: string; |
| 230 | readonly scope_id: string; | 295 | readonly scope_id: string; |
| @@ -277,21 +342,24 @@ const childRowsToValueMap = ( · 46 unmodified lines | |||
| 277 | ): Record<string, HeaderValue> => { | 342 | ): Record<string, HeaderValue> => { |
| 278 | const out: Record<string, HeaderValue> = {}; | 343 | const out: Record<string, HeaderValue> = {}; |
| 279 | for (const row of rows) { | 344 | for (const row of rows) { |
| 280 | const name = row.name as string; | 345 | const decoded = decodeChildStorageRowOption(row); |
| 281 | if (row.kind === "secret" && typeof row.secret_id === "string") { | 346 | if (Option.isSome(decoded)) { |
| 282 | const prefix = row.secret_prefix as string | undefined | null; | 347 | const child = decoded.value; |
| 283 | out[name] = prefix | 348 | if (child.kind === "secret" && child.secret_id != null) { |
| 284 | ? { secretId: row.secret_id, prefix } | 349 | out[child.name] = |
| 285 | : { secretId: row.secret_id }; | 350 | child.secret_prefix != null |
| 286 | } else if (row.kind === "text" && typeof row.text_value === "string") { | 351 | ? { secretId: child.secret_id, prefix: child.secret_prefix } |
| 287 | out[name] = row.text_value; | 352 | : { secretId: child.secret_id }; |
| 353 | } else if (child.kind === "text" && child.text_value != null) { | ||
| 354 | out[child.name] = child.text_value; | ||
| 355 | } | ||
| 288 | } | 356 | } |
| 289 | } | 357 | } |
| 290 | return out; | 358 | return out; |
| 291 | }; | 359 | }; |
| 292 | 360 | ||
| 293 | const toJsonRecord = (value: unknown): Record<string, unknown> => | 361 | // oxlint-disable-next-line executor/no-explicit-unknown-record -- boundary: storage adapter accepts JSON object columns |
| 294 | value as Record<string, unknown>; | 362 | const toJsonRecord = (value: unknown): Record<string, unknown> => value as Record<string, unknown>; |
| 295 | 363 | ||
| 296 | const toConfiguredHeaderBinding = (value: { | 364 | const toConfiguredHeaderBinding = (value: { |
| 297 | readonly slot?: unknown; | 365 | readonly slot?: unknown; |
| @@ -303,11 +371,10 @@ const toConfiguredHeaderBinding = (value: { · 5 unmodified lines | |||
| 303 | ...(typeof value.prefix === "string" ? { prefix: value.prefix } : {}), | 371 | ...(typeof value.prefix === "string" ? { prefix: value.prefix } : {}), |
| 304 | }); | 372 | }); |
| 305 | 373 | ||
| 306 | const decodeHeaders = (value: unknown): Record<string, HeaderValue> => { | 374 | const decodeHeaders = (value: unknown): Record<string, unknown> => { |
| 307 | if (value == null) return {}; | 375 | if (value == null) return {}; |
| 308 | if (typeof value === "string") | 376 | if (typeof value === "string") return decodeUnknownRecordJson(value); |
| 309 | return JSON.parse(value) as Record<string, HeaderValue>; | 377 | return decodeUnknownRecord(value); |
| 310 | return value as Record<string, HeaderValue>; | ||
| 311 | }; | 378 | }; |
| 312 | 379 | ||
| 313 | const slugifySlotPart = (value: string): string => | 380 | const slugifySlotPart = (value: string): string => |
| @@ -344,20 +411,18 @@ const normalizeStoredHeaders = ( · 30 unmodified lines | |||
| 344 | legacy[name] = header; | 411 | legacy[name] = header; |
| 345 | continue; | 412 | continue; |
| 346 | } | 413 | } |
| 347 | if ( | 414 | const binding = decodeConfiguredHeaderBindingOption(header); |
| 348 | header && | 415 | if (Option.isSome(binding)) { |
| 349 | typeof header === "object" && | 416 | headers[name] = toConfiguredHeaderBinding(binding.value); |
| 350 | "kind" in header && | ||
| 351 | (header as { kind?: unknown }).kind === "binding" | ||
| 352 | ) { | ||
| 353 | headers[name] = toConfiguredHeaderBinding(header); | ||
| 354 | continue; | 417 | continue; |
| 355 | } | 418 | } |
| 356 | legacy[name] = header; | 419 | const legacyHeader = decodeHeaderValueOption(header); |
| 420 | if (Option.isNone(legacyHeader)) continue; | ||
| 421 | legacy[name] = legacyHeader.value; | ||
| 357 | headers[name] = new ConfiguredHeaderBinding({ | 422 | headers[name] = new ConfiguredHeaderBinding({ |
| 358 | kind: "binding", | 423 | kind: "binding", |
| 359 | slot: headerBindingSlot(name), | 424 | slot: headerBindingSlot(name), |
| 360 | prefix: header.prefix, | 425 | prefix: typeof legacyHeader.value === "string" ? undefined : legacyHeader.value.prefix, |
| 361 | }); | 426 | }); |
| 362 | } | 427 | } |
| 363 | return { headers, legacy }; | 428 | return { headers, legacy }; |
| @@ -370,13 +435,16 @@ const normalizeStoredOAuth2 = ( · 6 unmodified lines | |||
| 370 | readonly legacy?: OAuth2Auth; | 435 | readonly legacy?: OAuth2Auth; |
| 371 | } => { | 436 | } => { |
| 372 | if (value == null) return {}; | 437 | if (value == null) return {}; |
| 373 | const parsed = typeof value === "string" ? JSON.parse(value) : value; | 438 | const sourceConfig = |
| 374 | if (parsed && typeof parsed === "object" && "connectionSlot" in parsed) { | 439 | typeof value === "string" |
| 375 | return { | 440 | ? decodeOAuth2SourceConfigJsonOption(value) |
| 376 | oauth2: Schema.decodeUnknownSync(OAuth2SourceConfig)(parsed), | 441 | : decodeOAuth2SourceConfigOption(value); |
| 377 | }; | 442 | if (Option.isSome(sourceConfig)) { |
| 443 | return { oauth2: sourceConfig.value }; | ||
| 378 | } | 444 | } |
| 379 | const legacy = decodeOAuth2(parsed); | 445 | const legacyOption = |
| 446 | typeof value === "string" ? decodeOAuth2JsonOption(value) : decodeOAuth2Option(value); | ||
| 447 | const legacy = Option.isSome(legacyOption) ? legacyOption.value : decodeOAuth2(value); | ||
| 380 | return { | 448 | return { |
| 381 | legacy, | 449 | legacy, |
| 382 | oauth2: new OAuth2SourceConfig({ | 450 | oauth2: new OAuth2SourceConfig({ |
| @@ -438,10 +506,7 @@ export interface OpenapiStore { · 55 unmodified lines | |||
| 438 | scope: string, | 506 | scope: string, |
| 439 | ) => Effect.Effect<StoredSource | null, StorageFailure>; | 507 | ) => Effect.Effect<StoredSource | null, StorageFailure>; |
| 440 | 508 | ||
| 441 | readonly listSources: () => Effect.Effect< | 509 | readonly listSources: () => Effect.Effect<readonly StoredSource[], StorageFailure>; |
| 442 | readonly StoredSource[], | ||
| 443 | StorageFailure | ||
| 444 | >; | ||
| 445 | 510 | ||
| 446 | readonly getOperationByToolId: ( | 511 | readonly getOperationByToolId: ( |
| 447 | toolId: string, | 512 | toolId: string, |
| @@ -453,10 +518,7 @@ export interface OpenapiStore { · 5 unmodified lines | |||
| 453 | scope: string, | 518 | scope: string, |
| 454 | ) => Effect.Effect<readonly StoredOperation[], StorageFailure>; | 519 | ) => Effect.Effect<readonly StoredOperation[], StorageFailure>; |
| 455 | 520 | ||
| 456 | readonly removeSource: ( | 521 | readonly removeSource: (namespace: string, scope: string) => Effect.Effect<void, StorageFailure>; |
| 457 | namespace: string, | ||
| 458 | scope: string, | ||
| 459 | ) => Effect.Effect<void, StorageFailure>; | ||
| 460 | 522 | ||
| 461 | readonly listSourceBindings: ( | 523 | readonly listSourceBindings: ( |
| 462 | sourceId: string, | 524 | sourceId: string, |
| @@ -501,10 +563,7 @@ export interface OpenapiStore { · 38 unmodified lines | |||
| 501 | * `query_param:foo` or `spec_fetch_header:Authorization`. */ | 563 | * `query_param:foo` or `spec_fetch_header:Authorization`. */ |
| 502 | readonly findChildRowsBySecret: (secretId: string) => Effect.Effect< | 564 | readonly findChildRowsBySecret: (secretId: string) => Effect.Effect< |
| 503 | readonly { | 565 | readonly { |
| 504 | readonly kind: | 566 | readonly kind: "query_param" | "spec_fetch_header" | "spec_fetch_query_param"; |
| 505 | | "query_param" | ||
| 506 | | "spec_fetch_header" | ||
| 507 | | "spec_fetch_query_param"; | ||
| 508 | readonly source_id: string; | 567 | readonly source_id: string; |
| 509 | readonly scope_id: string; | 568 | readonly scope_id: string; |
| 510 | readonly name: string; | 569 | readonly name: string; |
| @@ -527,14 +586,12 @@ export const makeDefaultOpenapiStore = ({ · 16 unmodified lines | |||
| 527 | adapter, | 586 | adapter, |
| 528 | scopes, | 587 | scopes, |
| 529 | }: StorageDeps<OpenapiSchema>): OpenapiStore => { | 588 | }: StorageDeps<OpenapiSchema>): OpenapiStore => { |
| 530 | const scopeIds = scopes.map((scope) => scope.id as string); | 589 | const scopeIds = scopes.map((scope) => String(scope.id)); |
| 531 | const scopePrecedence = new Map<string, number>(); | 590 | const scopePrecedence = new Map<string, number>(); |
| 532 | scopeIds.forEach((scope, index) => scopePrecedence.set(scope, index)); | 591 | scopeIds.forEach((scope, index) => scopePrecedence.set(scope, index)); |
| 533 | const scopeRank = (scopeId: string): number => | 592 | const scopeRank = (scopeId: string): number => scopePrecedence.get(scopeId) ?? Infinity; |
| 534 | scopePrecedence.get(scopeId) ?? Infinity; | ||
| 535 | 593 | ||
| 536 | const encodeSyntheticRowIdPart = (value: string): string => | 594 | const encodeSyntheticRowIdPart = (value: string): string => encodeURIComponent(value); |
| 537 | encodeURIComponent(value); | ||
| 538 | 595 | ||
| 539 | const sourceBindingRowId = ( | 596 | const sourceBindingRowId = ( |
| 540 | sourceId: string, | 597 | sourceId: string, |
| @@ -550,47 +607,47 @@ export const makeDefaultOpenapiStore = ({ · 9 unmodified lines | |||
| 550 | encodeSyntheticRowIdPart(scopeId), | 607 | encodeSyntheticRowIdPart(scopeId), |
| 551 | ].join("::"); | 608 | ].join("::"); |
| 552 | 609 | ||
| 553 | const rowToSourceBindingValue = ( | 610 | const rowToSourceBindingValue = (row: Record<string, unknown>): OpenApiSourceBindingValue => { |
| 554 | row: Record<string, unknown>, | 611 | const decoded = decodeSourceBindingStorageRow(row); |
| 555 | ): OpenApiSourceBindingValue => { | 612 | if (decoded.kind === "secret" && decoded.secret_id != null) { |
| 556 | const kind = row.kind as string; | 613 | return { kind: "secret", secretId: SecretId.make(decoded.secret_id) }; |
| 557 | if (kind === "secret" && typeof row.secret_id === "string") { | ||
| 558 | return { kind: "secret", secretId: SecretId.make(row.secret_id) }; | ||
| 559 | } | 614 | } |
| 560 | if (kind === "connection" && typeof row.connection_id === "string") { | 615 | if (decoded.kind === "connection" && decoded.connection_id != null) { |
| 561 | return { | 616 | return { |
| 562 | kind: "connection", | 617 | kind: "connection", |
| 563 | connectionId: ConnectionId.make(row.connection_id), | 618 | connectionId: ConnectionId.make(decoded.connection_id), |
| 564 | }; | 619 | }; |
| 565 | } | 620 | } |
| 566 | // text fallback covers both well-formed text rows and any | 621 | // text fallback covers both well-formed text rows and any |
| 567 | // partial/null row that survived a malformed write — `text_value` | 622 | // partial/null row that survived a malformed write — `text_value` |
| 568 | // defaults to "" so the type stays satisfied without a throw. | 623 | // defaults to "" so the type stays satisfied without a throw. |
| 569 | return { kind: "text", text: (row.text_value as string | null) ?? "" }; | 624 | return { kind: "text", text: decoded.text_value ?? "" }; |
| 570 | }; | 625 | }; |
| 571 | 626 | ||
| 572 | const rowToSourceBinding = ( | 627 | const rowToSourceBinding = (row: Record<string, unknown>): OpenApiSourceBindingRef => { |
| 573 | row: Record<string, unknown>, | 628 | const decoded = decodeSourceBindingStorageRow(row); |
| 574 | ): OpenApiSourceBindingRef => | 629 | return new OpenApiSourceBindingRef({ |
| 575 | new OpenApiSourceBindingRef({ | 630 | sourceId: decoded.source_id, |
| 576 | sourceId: row.source_id as string, | 631 | sourceScopeId: ScopeId.make(decoded.source_scope_id), |
| 577 | sourceScopeId: ScopeId.make(row.source_scope_id as string), | 632 | scopeId: ScopeId.make(decoded.target_scope_id), |
| 578 | scopeId: ScopeId.make(row.target_scope_id as string), | 633 | slot: decoded.slot, |
| 579 | slot: row.slot as string, | ||
| 580 | value: rowToSourceBindingValue(row), | 634 | value: rowToSourceBindingValue(row), |
| 581 | createdAt: | 635 | createdAt: decodeStorageDate(decoded.created_at), |
| 582 | row.created_at instanceof Date | 636 | updatedAt: decodeStorageDate(decoded.updated_at), |
| 583 | ? row.created_at | ||
| 584 | : new Date(row.created_at as string), | ||
| 585 | updatedAt: | ||
| 586 | row.updated_at instanceof Date | ||
| 587 | ? row.updated_at | ||
| 588 | : new Date(row.updated_at as string), | ||
| 589 | }); | 637 | }); |
| 638 | }; | ||
| 639 | |||
| 640 | const sourceBindingTargetScope = (row: Record<string, unknown>): string => | ||
| 641 | decodeSourceBindingStorageRow(row).target_scope_id; | ||
| 590 | 642 | ||
| 591 | const sourceBindingValueColumns = ( | 643 | const sourceBindingValueColumns = ( |
| 592 | value: OpenApiSourceBindingValue, | 644 | value: OpenApiSourceBindingValue, |
| 593 | ): { kind: string; secret_id?: string; connection_id?: string; text_value?: string } => { | 645 | ): { |
| 646 | kind: string; | ||
| 647 | secret_id?: string; | ||
| 648 | connection_id?: string; | ||
| 649 | text_value?: string; | ||
| 650 | } => { | ||
| 594 | if (value.kind === "secret") { | 651 | if (value.kind === "secret") { |
| 595 | return { kind: "secret", secret_id: value.secretId }; | 652 | return { kind: "secret", secret_id: value.secretId }; |
| 596 | } | 653 | } |
| @@ -606,24 +663,20 @@ export const makeDefaultOpenapiStore = ({ · 9 unmodified lines | |||
| 606 | }) => | 663 | }) => |
| 607 | Effect.gen(function* () { | 664 | Effect.gen(function* () { |
| 608 | if (!scopeIds.includes(params.sourceScope)) { | 665 | if (!scopeIds.includes(params.sourceScope)) { |
| 609 | return yield* Effect.fail( | 666 | return yield* new StorageError({ |
| 610 | new StorageError({ | 667 | message: |
| 611 | message: | 668 | `OpenAPI source binding references source scope "${params.sourceScope}" ` + |
| 612 | `OpenAPI source binding references source scope "${params.sourceScope}" ` + | 669 | `which is not in the executor's scope stack [${scopeIds.join(", ")}].`, |
| 613 | `which is not in the executor's scope stack [${scopeIds.join(", ")}].`, | 670 | cause: undefined, |
| 614 | cause: undefined, | 671 | }); |
| 615 | }), | ||
| 616 | ); | ||
| 617 | } | 672 | } |
| 618 | if (!scopeIds.includes(params.targetScope)) { | 673 | if (!scopeIds.includes(params.targetScope)) { |
| 619 | return yield* Effect.fail( | 674 | return yield* new StorageError({ |
| 620 | new StorageError({ | 675 | message: |
| 621 | message: | 676 | `OpenAPI source binding targets scope "${params.targetScope}" which is not ` + |
| 622 | `OpenAPI source binding targets scope "${params.targetScope}" which is not ` + | 677 | `in the executor's scope stack [${scopeIds.join(", ")}].`, |
| 623 | `in the executor's scope stack [${scopeIds.join(", ")}].`, | 678 | cause: undefined, |
| 624 | cause: undefined, | 679 | }); |
| 625 | }), | ||
| 626 | ); | ||
| 627 | } | 680 | } |
| 628 | }); | 681 | }); |
| 629 | 682 | ||
| @@ -645,23 +698,19 @@ export const makeDefaultOpenapiStore = ({ · 15 unmodified lines | |||
| 645 | ], | 698 | ], |
| 646 | }); | 699 | }); |
| 647 | if (!source) { | 700 | if (!source) { |
| 648 | return yield* Effect.fail( | 701 | return yield* new StorageError({ |
| 649 | new StorageError({ | 702 | message: `OpenAPI source "${params.sourceId}" does not exist at scope "${params.sourceScope}"`, |
| 650 | message: `OpenAPI source "${params.sourceId}" does not exist at scope "${params.sourceScope}"`, | 703 | cause: undefined, |
| 651 | cause: undefined, | 704 | }); |
| 652 | }), | ||
| 653 | ); | ||
| 654 | } | 705 | } |
| 655 | if (scopeRank(params.targetScope) > scopeRank(params.sourceScope)) { | 706 | if (scopeRank(params.targetScope) > scopeRank(params.sourceScope)) { |
| 656 | return yield* Effect.fail( | 707 | return yield* new StorageError({ |
| 657 | new StorageError({ | 708 | message: |
| 658 | message: | 709 | `OpenAPI source bindings for "${params.sourceId}" cannot be written at ` + |
| 659 | `OpenAPI source bindings for "${params.sourceId}" cannot be written at ` + | 710 | `outer scope "${params.targetScope}" because the base source lives at ` + |
| 660 | `outer scope "${params.targetScope}" because the base source lives at ` + | 711 | `"${params.sourceScope}"`, |
| 661 | `"${params.sourceScope}"`, | 712 | cause: undefined, |
| 662 | cause: undefined, | 713 | }); |
| 663 | }), | ||
| 664 | ); | ||
| 665 | } | 714 | } |
| 666 | return source; | 715 | return source; |
| 667 | }); | 716 | }); |
| @@ -684,20 +733,15 @@ export const makeDefaultOpenapiStore = ({ · 16 unmodified lines | |||
| 684 | }) | 733 | }) |
| 685 | .pipe(Effect.map(childRowsToValueMap)); | 734 | .pipe(Effect.map(childRowsToValueMap)); |
| 686 | 735 | ||
| 687 | const rowToSource = ( | 736 | const rowToSource = (row: Record<string, unknown>): Effect.Effect<StoredSource, StorageFailure> => |
| 688 | row: Record<string, unknown>, | ||
| 689 | ): Effect.Effect<StoredSource, StorageFailure> => | ||
| 690 | Effect.gen(function* () { | 737 | Effect.gen(function* () { |
| 691 | const sourceId = row.id as string; | 738 | const sourceRow = decodeSourceStorageRow(row); |
| 692 | const scope = row.scope_id as string; | 739 | const sourceId = sourceRow.id; |
| 693 | const normalizedHeaders = normalizeStoredHeaders(row.headers); | 740 | const scope = sourceRow.scope_id; |
| 694 | const normalizedOAuth2 = normalizeStoredOAuth2(row.oauth2); | 741 | const normalizedHeaders = normalizeStoredHeaders(sourceRow.headers); |
| 742 | const normalizedOAuth2 = normalizeStoredOAuth2(sourceRow.oauth2); | ||
| 695 | 743 | ||
| 696 | const queryParams = yield* loadChildValueMap( | 744 | const queryParams = yield* loadChildValueMap("openapi_source_query_param", sourceId, scope); |
| 697 | "openapi_source_query_param", | ||
| 698 | sourceId, | ||
| 699 | scope, | ||
| 700 | ); | ||
| 701 | const specFetchHeaders = yield* loadChildValueMap( | 745 | const specFetchHeaders = yield* loadChildValueMap( |
| 702 | "openapi_source_spec_fetch_header", | 746 | "openapi_source_spec_fetch_header", |
| 703 | sourceId, | 747 | sourceId, |
| @@ -709,13 +753,10 @@ export const makeDefaultOpenapiStore = ({ · 5 unmodified lines | |||
| 709 | scope, | 753 | scope, |
| 710 | ); | 754 | ); |
| 711 | const specFetchCredentials: OpenApiSpecFetchCredentials | undefined = | 755 | const specFetchCredentials: OpenApiSpecFetchCredentials | undefined = |
| 712 | Object.keys(specFetchHeaders).length === 0 && | 756 | Object.keys(specFetchHeaders).length === 0 && Object.keys(specFetchQueryParams).length === 0 |
| 713 | Object.keys(specFetchQueryParams).length === 0 | ||
| 714 | ? undefined | 757 | ? undefined |
| 715 | : { | 758 | : { |
| 716 | ...(Object.keys(specFetchHeaders).length > 0 | 759 | ...(Object.keys(specFetchHeaders).length > 0 ? { headers: specFetchHeaders } : {}), |
| 717 | ? { headers: specFetchHeaders } | ||
| 718 | : {}), | ||
| 719 | ...(Object.keys(specFetchQueryParams).length > 0 | 760 | ...(Object.keys(specFetchQueryParams).length > 0 |
| 720 | ? { queryParams: specFetchQueryParams } | 761 | ? { queryParams: specFetchQueryParams } |
| 721 | : {}), | 762 | : {}), |
| @@ -724,38 +765,40 @@ export const makeDefaultOpenapiStore = ({ · 2 unmodified lines | |||
| 724 | return { | 765 | return { |
| 725 | namespace: sourceId, | 766 | namespace: sourceId, |
| 726 | scope, | 767 | scope, |
| 727 | name: row.name as string, | 768 | name: sourceRow.name, |
| 728 | config: { | 769 | config: { |
| 729 | spec: row.spec as string, | 770 | spec: sourceRow.spec, |
| 730 | sourceUrl: (row.source_url as string | null | undefined) ?? undefined, | 771 | sourceUrl: sourceRow.source_url ?? undefined, |
| 731 | baseUrl: (row.base_url as string | null | undefined) ?? undefined, | 772 | baseUrl: sourceRow.base_url ?? undefined, |
| 732 | headers: normalizedHeaders.headers, | 773 | headers: normalizedHeaders.headers, |
| 733 | queryParams, | 774 | queryParams, |
| 734 | specFetchCredentials, | 775 | specFetchCredentials, |
| 735 | oauth2: normalizedOAuth2.oauth2, | 776 | oauth2: normalizedOAuth2.oauth2, |
| 736 | }, | 777 | }, |
| 737 | legacy: | 778 | legacy: |
| 738 | Object.keys(normalizedHeaders.legacy).length > 0 || | 779 | Object.keys(normalizedHeaders.legacy).length > 0 || normalizedOAuth2.legacy |
| 739 | normalizedOAuth2.legacy | ||
| 740 | ? { | 780 | ? { |
| 741 | ...(Object.keys(normalizedHeaders.legacy).length > 0 | 781 | ...(Object.keys(normalizedHeaders.legacy).length > 0 |
| 742 | ? { headers: normalizedHeaders.legacy } | 782 | ? { headers: normalizedHeaders.legacy } |
| 743 | : {}), | 783 | : {}), |
| 744 | ...(normalizedOAuth2.legacy | 784 | ...(normalizedOAuth2.legacy ? { oauth2: normalizedOAuth2.legacy } : {}), |
| 745 | ? { oauth2: normalizedOAuth2.legacy } | ||
| 746 | : {}), | ||
| 747 | } | 785 | } |
| 748 | : undefined, | 786 | : undefined, |
| 749 | }; | 787 | }; |
| 750 | }); | 788 | }); |
| 751 | 789 | ||
| 752 | const rowToOperation = (row: Record<string, unknown>): StoredOperation => ({ | 790 | const rowToOperation = (row: Record<string, unknown>): StoredOperation => { |
| 753 | toolId: row.id as string, | 791 | const operationRow = decodeOperationStorageRow(row); |
| 754 | sourceId: row.source_id as string, | 792 | return { |
| 755 | binding: decodeBinding( | 793 | toolId: operationRow.id, |
| 756 | typeof row.binding === "string" ? JSON.parse(row.binding) : row.binding, | 794 | sourceId: operationRow.source_id, |
| 757 | ), | 795 | binding: decodeBinding( |
| 758 | }); | 796 | typeof operationRow.binding === "string" |
| 797 | ? decodeBindingJson(operationRow.binding) | ||
| 798 | : operationRow.binding, | ||
| 799 | ), | ||
| 800 | }; | ||
| 801 | }; | ||
| 759 | 802 | ||
| 760 | // Replace the rows of one child table for a source: delete then bulk | 803 | // Replace the rows of one child table for a source: delete then bulk |
| 761 | // insert. Single helper so upsertSource and updateSourceMeta both | 804 | // insert. Single helper so upsertSource and updateSourceMeta both |
| @@ -845,20 +888,18 @@ export const makeDefaultOpenapiStore = ({ · 83 unmodified lines | |||
| 845 | source_url: input.config.sourceUrl ?? undefined, | 888 | source_url: input.config.sourceUrl ?? undefined, |
| 846 | base_url: input.config.baseUrl ?? undefined, | 889 | base_url: input.config.baseUrl ?? undefined, |
| 847 | headers: Object.fromEntries( | 890 | headers: Object.fromEntries( |
| 848 | Object.entries(input.config.headers ?? {}).map( | 891 | Object.entries(input.config.headers ?? {}).map(([name, value]) => [ |
| 849 | ([name, value]) => [ | 892 | name, |
| 850 | name, | 893 | typeof value === "string" |
| 851 | typeof value === "string" | 894 | ? value |
| 852 | ? value | 895 | : value.kind === "binding" |
| 853 | : value.kind === "binding" | 896 | ? { |
| 854 | ? { | 897 | kind: value.kind, |
| 855 | kind: value.kind, | 898 | slot: value.slot, |
| 856 | slot: value.slot, | 899 | ...(value.prefix ? { prefix: value.prefix } : {}), |
| 857 | ...(value.prefix ? { prefix: value.prefix } : {}), | 900 | } |
| 858 | } | 901 | : value, |
| 859 | : value, | 902 | ]), |
| 860 | ], | ||
| 861 | ), | ||
| 862 | ) as Record<string, unknown>, | 903 | ) as Record<string, unknown>, |
| 863 | oauth2: input.config.oauth2 | 904 | oauth2: input.config.oauth2 |
| 864 | ? toJsonRecord(encodeOAuth2SourceConfig(input.config.oauth2)) | 905 | ? toJsonRecord(encodeOAuth2SourceConfig(input.config.oauth2)) |
| @@ -911,14 +952,10 @@ export const makeDefaultOpenapiStore = ({ · 46 unmodified lines | |||
| 911 | const existing = yield* rowToSource(existingRow); | 952 | const existing = yield* rowToSource(existingRow); |
| 912 | 953 | ||
| 913 | const nextName = patch.name?.trim() || existing.name; | 954 | const nextName = patch.name?.trim() || existing.name; |
| 914 | const nextBaseUrl = | 955 | const nextBaseUrl = patch.baseUrl !== undefined ? patch.baseUrl : existing.config.baseUrl; |
| 915 | patch.baseUrl !== undefined ? patch.baseUrl : existing.config.baseUrl; | ||
| 916 | const nextHeaders = | 956 | const nextHeaders = |
| 917 | patch.headers !== undefined | 957 | patch.headers !== undefined ? patch.headers : (existing.config.headers ?? {}); |
| 918 | ? patch.headers | 958 | const nextOAuth2 = patch.oauth2 !== undefined ? patch.oauth2 : existing.config.oauth2; |
| 919 | : (existing.config.headers ?? {}); | ||
| 920 | const nextOAuth2 = | ||
| 921 | patch.oauth2 !== undefined ? patch.oauth2 : existing.config.oauth2; | ||
| 922 | 959 | ||
| 923 | yield* adapter.update({ | 960 | yield* adapter.update({ |
| 924 | model: "openapi_source", | 961 | model: "openapi_source", |
| @@ -941,9 +978,7 @@ export const makeDefaultOpenapiStore = ({ · 16 unmodified lines | |||
| 941 | }, | 978 | }, |
| 942 | ]), | 979 | ]), |
| 943 | ) as Record<string, unknown>, | 980 | ) as Record<string, unknown>, |
| 944 | oauth2: nextOAuth2 | 981 | oauth2: nextOAuth2 ? toJsonRecord(encodeOAuth2SourceConfig(nextOAuth2)) : undefined, |
| 945 | ? toJsonRecord(encodeOAuth2SourceConfig(nextOAuth2)) | ||
| 946 | : undefined, | ||
| 947 | }, | 982 | }, |
| 948 | }); | 983 | }); |
| 949 | if (patch.queryParams !== undefined) { | 984 | if (patch.queryParams !== undefined) { |
| @@ -999,8 +1034,7 @@ export const makeDefaultOpenapiStore = ({ · 49 unmodified lines | |||
| 999 | }) | 1034 | }) |
| 1000 | .pipe(Effect.map((rows) => rows.map(rowToOperation))), | 1035 | .pipe(Effect.map((rows) => rows.map(rowToOperation))), |
| 1001 | 1036 | ||
| 1002 | removeSource: (namespace, scope) => | 1037 | removeSource: (namespace, scope) => deleteSource(namespace, scope, { includeBindings: true }), |
| 1003 | deleteSource(namespace, scope, { includeBindings: true }), | ||
| 1004 | 1038 | ||
| 1005 | listSourceBindings: (sourceId, sourceScope) => | 1039 | listSourceBindings: (sourceId, sourceScope) => |
| 1006 | Effect.gen(function* () { | 1040 | Effect.gen(function* () { |
| @@ -1017,14 +1051,10 @@ export const makeDefaultOpenapiStore = ({ · 10 unmodified lines | |||
| 1017 | ], | 1051 | ], |
| 1018 | }); | 1052 | }); |
| 1019 | return rows | 1053 | return rows |
| 1020 | .filter( | 1054 | .filter((row) => scopeRank(sourceBindingTargetScope(row)) <= sourceScopeRank) |
| 1021 | (row) => | ||
| 1022 | scopeRank(row.target_scope_id as string) <= sourceScopeRank, | ||
| 1023 | ) | ||
| 1024 | .sort( | 1055 | .sort( |
| 1025 | (a, b) => | 1056 | (a, b) => |
| 1026 | scopeRank(a.target_scope_id as string) - | 1057 | scopeRank(sourceBindingTargetScope(a)) - scopeRank(sourceBindingTargetScope(b)), |
| 1027 | scopeRank(b.target_scope_id as string), | ||
| 1028 | ) | 1058 | ) |
| 1029 | .map(rowToSourceBinding); | 1059 | .map(rowToSourceBinding); |
| 1030 | }), | 1060 | }), |
| @@ -1045,31 +1075,24 @@ export const makeDefaultOpenapiStore = ({ · 14 unmodified lines | |||
| 1045 | }); | 1075 | }); |
| 1046 | const sourceScopeRank = scopeRank(sourceScope); | 1076 | const sourceScopeRank = scopeRank(sourceScope); |
| 1047 | const row = rows | 1077 | const row = rows |
| 1048 | .filter( | 1078 | .filter((candidate) => scopeRank(sourceBindingTargetScope(candidate)) <= sourceScopeRank) |
| 1049 | (candidate) => | ||
| 1050 | scopeRank(candidate.target_scope_id as string) <= sourceScopeRank, | ||
| 1051 | ) | ||
| 1052 | .sort( | 1079 | .sort( |
| 1053 | (a, b) => | 1080 | (a, b) => |
| 1054 | scopeRank(a.target_scope_id as string) - | 1081 | scopeRank(sourceBindingTargetScope(a)) - scopeRank(sourceBindingTargetScope(b)), |
| 1055 | scopeRank(b.target_scope_id as string), | ||
| 1056 | )[0]; | 1082 | )[0]; |
| 1057 | return row ? rowToSourceBinding(row) : null; | 1083 | return row ? rowToSourceBinding(row) : null; |
| 1058 | }), | 1084 | }), |
| 1059 | 1085 | ||
| 1060 | setSourceBinding: (input) => | 1086 | setSourceBinding: (input) => |
| 1061 | Effect.gen(function* () { | 1087 | Effect.gen(function* () { |
| 1088 | const sourceScope = String(input.sourceScope); | ||
| 1089 | const targetScope = String(input.scope); | ||
| 1062 | yield* validateBindingTarget({ | 1090 | yield* validateBindingTarget({ |
| 1063 | sourceId: input.sourceId, | 1091 | sourceId: input.sourceId, |
| 1064 | sourceScope: input.sourceScope as string, | 1092 | sourceScope, |
| 1065 | targetScope: input.scope as string, | 1093 | targetScope, |
| 1066 | }); | 1094 | }); |
| 1067 | const id = sourceBindingRowId( | 1095 | const id = sourceBindingRowId(input.sourceId, sourceScope, input.slot, targetScope); |
| 1068 | input.sourceId, | ||
| 1069 | input.sourceScope as string, | ||
| 1070 | input.slot, | ||
| 1071 | input.scope as string, | ||
| 1072 | ); | ||
| 1073 | const now = new Date(); | 1096 | const now = new Date(); |
| 1074 | const valueColumns = sourceBindingValueColumns(input.value); | 1097 | const valueColumns = sourceBindingValueColumns(input.value); |
| 1075 | yield* adapter.delete({ | 1098 | yield* adapter.delete({ |
| @@ -1081,8 +1104,8 @@ export const makeDefaultOpenapiStore = ({ · 5 unmodified lines | |||
| 1081 | data: { | 1104 | data: { |
| 1082 | id, | 1105 | id, |
| 1083 | source_id: input.sourceId, | 1106 | source_id: input.sourceId, |
| 1084 | source_scope_id: input.sourceScope as string, | 1107 | source_scope_id: sourceScope, |
| 1085 | target_scope_id: input.scope as string, | 1108 | target_scope_id: targetScope, |
| 1086 | slot: input.slot, | 1109 | slot: input.slot, |
| 1087 | ...valueColumns, | 1110 | ...valueColumns, |
| 1088 | created_at: now, | 1111 | created_at: now, |
| @@ -1139,8 +1162,14 @@ export const makeDefaultOpenapiStore = ({ · 50 unmodified lines | |||
| 1139 | Effect.gen(function* () { | 1162 | Effect.gen(function* () { |
| 1140 | const tables = [ | 1163 | const tables = [ |
| 1141 | { model: "openapi_source_query_param" as const, kind: "query_param" as const }, | 1164 | { model: "openapi_source_query_param" as const, kind: "query_param" as const }, |
| 1142 | { model: "openapi_source_spec_fetch_header" as const, kind: "spec_fetch_header" as const }, | 1165 | { |
| 1143 | { model: "openapi_source_spec_fetch_query_param" as const, kind: "spec_fetch_query_param" as const }, | 1166 | model: "openapi_source_spec_fetch_header" as const, |
| 1167 | kind: "spec_fetch_header" as const, | ||
| 1168 | }, | ||
| 1169 | { | ||
| 1170 | model: "openapi_source_spec_fetch_query_param" as const, | ||
| 1171 | kind: "spec_fetch_query_param" as const, | ||
| 1172 | }, | ||
| 1144 | ]; | 1173 | ]; |
| 1145 | const perTable = yield* Effect.forEach( | 1174 | const perTable = yield* Effect.forEach( |
| 1146 | tables, | 1175 | tables, |
| @@ -1152,12 +1181,15 @@ export const makeDefaultOpenapiStore = ({ · 5 unmodified lines | |||
| 1152 | }) | 1181 | }) |
| 1153 | .pipe( | 1182 | .pipe( |
| 1154 | Effect.map((rows) => | 1183 | Effect.map((rows) => |
| 1155 | rows.map((r) => ({ | 1184 | rows.map((r) => { |
| 1156 | kind: t.kind, | 1185 | const row = decodeChildUsageStorageRow(r); |
| 1157 | source_id: r.source_id as string, | 1186 | return { |
| 1158 | scope_id: r.scope_id as string, | 1187 | kind: t.kind, |
| 1159 | name: r.name as string, | 1188 | source_id: row.source_id, |
| 1160 | })), | 1189 | scope_id: row.scope_id, |
| 1190 | name: row.name, | ||
| 1191 | }; | ||
| 1192 | }), | ||
| 1161 | ), | 1193 | ), |
| 1162 | ), | 1194 | ), |
| 1163 | { concurrency: "unbounded" }, | 1195 | { concurrency: "unbounded" }, |
| @@ -1172,8 +1204,9 @@ export const makeDefaultOpenapiStore = ({ · 8 unmodified lines | |||
| 1172 | const requested = new Set(keys); | 1204 | const requested = new Set(keys); |
| 1173 | const out = new Map<string, string>(); | 1205 | const out = new Map<string, string>(); |
| 1174 | for (const r of rows) { | 1206 | for (const r of rows) { |
| 1175 | const key = `${r.scope_id as string}:${r.id as string}`; | 1207 | const row = decodeSourceNameStorageRow(r); |
| 1176 | if (requested.has(key)) out.set(key, r.name as string); | 1208 | const key = `${row.scope_id}:${row.id}`; |
| 1209 | if (requested.has(key)) out.set(key, row.name); | ||
| 1177 | } | 1210 | } |
| 1178 | return out; | 1211 | return out; |
| 1179 | }), | 1212 | }), |