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