History

Fix OpenAPI boundary lint

Rhys Sullivan committed 6 months ago

Commit 9494cb41 parent b039b6e3 files · +711 −629Browse files

Unified · Split

3 files
  1. packages/plugins/openapi/src/react/AddOpenApiSource.tsx +272 −246
  2. packages/plugins/openapi/src/react/EditOpenApiSource.tsx +196 −173
  3. packages/plugins/openapi/src/sdk/store.ts +243 −210
packages/plugins/openapi/src/react/AddOpenApiSource.tsx+272−246
@@ -1,6 +1,8 @@
11 import { useCallback, useEffect, useRef, useState } from "react";
22 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";
46
57 import { ConnectionId, ScopeId, SecretId } from "@executor-js/sdk/core";
68 import { startOAuth } from "@executor-js/react/api/atoms";
@@ -81,6 +83,17 @@ import { · 74 unmodified lines
8183 type ServerVariable,
8284 } from "../sdk/types";
8385
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+
8497 export const OPENAPI_OAUTH_POPUP_NAME = "openapi-oauth";
8598 export const OPENAPI_OAUTH_CALLBACK_PATH = "/api/oauth/callback";
8699
@@ -109,25 +122,15 @@ export const openApiOAuthConnectionId = ( · 22 unmodified lines
109122 */
110123 export function resolveOAuthUrl(url: string, baseUrl: string): string {
111124 if (!url) return url;
112− try {
113− new URL(url);
125+ if (URL.canParse(url)) {
114126 return url;
115− } catch {
116− if (!baseUrl) return url;
117− try {
118− return new URL(url, baseUrl).toString();
119− } catch {
120− return url;
121− }
122127 }
128+ if (!baseUrl || !URL.canParse(url, baseUrl)) return url;
129+ return new URL(url, baseUrl).toString();
123130 }
124131
125132 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;
131134 }
132135
133136 type StrategySelection =
@@ -242,10 +245,10 @@ export default function AddOpenApiSource(props: { · 108 unmodified lines
242245
243246 const scopeId = useScope();
244247 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" });
249252 const { beginAdd } = usePendingSources();
250253 const secretList = useSecretPickerSecrets();
251254 const oauth = useOAuthPopupFlow<OAuthCompletionPayload>({
@@ -278,7 +281,7 @@ export default function AddOpenApiSource(props: { · 26 unmodified lines
278281 selectedServerIndex >= 0 ? (servers[selectedServerIndex] ?? null) : null;
279282
280283 const serverVariables: Record<string, ServerVariable> = selectedServer
281− ? Option.getOrElse(selectedServer.variables, () => ({}) as Record<string, ServerVariable>)
284+ ? Option.getOrElse(selectedServer.variables, () => ({}))
282285 : {};
283286 const serverVariableEntries: Array<[string, ServerVariable]> = Object.entries(serverVariables);
284287
@@ -290,10 +293,7 @@ export default function AddOpenApiSource(props: { · 5 unmodified lines
290293 // Helper used by analyze + server selection: build a default selection map
291294 // from a server's variable defaults.
292295 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, () => ({}));
297297 const out: Record<string, string> = {};
298298 for (const [name, v] of Object.entries(vars)) out[name] = v.default;
299299 return out;
@@ -371,45 +371,47 @@ export default function AddOpenApiSource(props: { · 71 unmodified lines
371371 setAnalyzing(true);
372372 setAnalyzeError(null);
373373 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"));
411384 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("");
412400 }
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);
413415 };
414416
415417 handleAnalyzeRef.current = handleAnalyze;
@@ -470,122 +472,126 @@ export default function AddOpenApiSource(props: { · 54 unmodified lines
470472 if (!selectedOAuth2Preset || !oauth2ClientIdSecretId || !preview) return;
471473 oauth.cancel();
472474 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],
504502 },
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) => {
511576 setOauth2AuthState({
512577 fingerprint: selectedOAuth2Fingerprint,
513578 auth: new OAuth2Auth({
514579 kind: "oauth2",
515− connectionId: response.completedConnection.connectionId,
580+ connectionId: result.connectionId,
516581 securitySchemeName: selectedOAuth2Preset.securitySchemeName,
517− flow: "clientCredentials",
582+ flow: "authorizationCode",
518583 tokenUrl,
519− authorizationUrl: null,
584+ authorizationUrl,
585+ issuerUrl,
520586 clientIdSecretId: oauth2ClientIdSecretId,
521587 clientSecretSecretId: oauth2ClientSecretSecretId,
522588 scopes: [...oauth2SelectedScopes],
523589 }),
524590 });
525591 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+ });
589595 }, [
590596 selectedOAuth2Preset,
591597 oauth2ClientIdSecretId,
@@ -621,103 +627,123 @@ export default function AddOpenApiSource(props: { · 29 unmodified lines
621627 kind: "openapi",
622628 url: resolvedBaseUrl || undefined,
623629 });
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({
626663 params: { scopeId },
627664 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+ },
638673 },
639− reactivityKeys: addSpecWriteKeys,
674+ reactivityKeys: bindingWriteKeys,
640675 });
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;
661679 }
680+ }
662681
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),
675693 },
676− reactivityKeys: bindingWriteKeys,
677− });
694+ },
695+ reactivityKeys: bindingWriteKeys,
696+ });
697+ if (Exit.isFailure(clientIdBindingExit)) {
698+ failAdd(errorMessageFromExit(clientIdBindingExit, "Failed to add source"));
699+ return;
678700 }
701+ }
679702
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),
692714 },
693− reactivityKeys: bindingWriteKeys,
694− });
715+ },
716+ reactivityKeys: bindingWriteKeys,
717+ });
718+ if (Exit.isFailure(clientSecretBindingExit)) {
719+ failAdd(errorMessageFromExit(clientSecretBindingExit, "Failed to add source"));
720+ return;
695721 }
722+ }
696723
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),
709735 },
710− reactivityKeys: bindingWriteKeys,
711− });
736+ },
737+ reactivityKeys: bindingWriteKeys,
738+ });
739+ if (Exit.isFailure(connectionBindingExit)) {
740+ failAdd(errorMessageFromExit(connectionBindingExit, "Failed to add source"));
741+ return;
712742 }
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();
720743 }
744+
745+ placeholder.done();
746+ props.onComplete();
721747 };
722748
723749 // ---- Render ----
packages/plugins/openapi/src/react/EditOpenApiSource.tsx+196−173
@@ -1,5 +1,8 @@
11 import { useEffect, useMemo, useRef, useState } from "react";
22 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";
36 import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
47
58 import { connectionsAtom, sourceAtom, startOAuth } from "@executor-js/react/api/atoms";
@@ -42,7 +45,21 @@ import { · 36 unmodified lines
4245 resolveOAuthUrl,
4346 } from "./AddOpenApiSource";
4447 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+ );
4663
4764 type SlotDef =
4865 | {
@@ -79,7 +96,7 @@ const openApiOAuthConnectionId = ( · 30 unmodified lines
7996 targetScope: ScopeId,
8097 ): ConnectionId =>
8198 ConnectionId.make(
82− `openapi-oauth-${slugify(sourceId)}-${slugify(securitySchemeName)}-${shortHash(targetScope as string)}`,
99+ `openapi-oauth-${slugify(sourceId)}-${slugify(securitySchemeName)}-${shortHash(targetScope)}`,
83100 );
84101
85102 const bindingSecretId = (sourceId: string, slot: string, scopeId: string): string =>
@@ -101,7 +118,7 @@ const exactBindingForScope = ( · 15 unmodified lines
101118 ) => rows.find((row) => row.slot === slot && row.scopeId === scopeId) ?? null;
102119
103120 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;
105122
106123 const effectiveBindingForScope = (
107124 rows: readonly {
@@ -119,21 +136,13 @@ const effectiveBindingForScope = ( · 11 unmodified lines
119136
120137 const isSecretBindingValue = (
121138 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";
128141
129142 const isConnectionBindingValue = (
130143 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";
137146
138147 export default function EditOpenApiSource(props: {
139148 readonly sourceId: string;
@@ -150,7 +159,7 @@ export default function EditOpenApiSource(props: { · 10 unmodified lines
150159 const sourceScopeId = sourceSummary?.scopeId ?? displayScope;
151160 const sourceScope = ScopeId.make(sourceScopeId);
152161 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)),
154163 [scopeStack],
155164 );
156165
@@ -161,10 +170,10 @@ export default function EditOpenApiSource(props: { · 4 unmodified lines
161170 const connectionsResult = useAtomValue(connectionsAtom(displayScope));
162171 const secretList = useSecretPickerSecrets();
163172
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" });
168177 const oauth = useOAuthPopupFlow<OAuthCompletionPayload>({
169178 popupName: OPENAPI_OAUTH_POPUP_NAME,
170179 popupBlockedMessage: "OAuth popup was blocked by the browser",
@@ -224,28 +233,28 @@ export default function EditOpenApiSource(props: { · 53 unmodified lines
224233 const seq = ++sourceSaveSeq.current;
225234 setSourceSaveState("saving");
226235 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,
248246 });
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+ })();
249258 }, 600);
250259
251260 return () => window.clearTimeout(timeout);
@@ -321,44 +330,40 @@ export default function EditOpenApiSource(props: { · 69 unmodified lines
321330 if (!trimmed) return;
322331 setBusyKey(inputKey);
323332 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"));
340346 }
347+ setBusyKey(null);
341348 };
342349
343350 const clearBinding = async (targetScope: ScopeId, slot: string) => {
344351 setBusyKey(`${targetScope}:${slot}:clear`);
345352 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"));
361365 }
366+ setBusyKey(null);
362367 };
363368
364369 const connectOAuth = async (targetScope: ScopeId) => {
@@ -410,35 +415,112 @@ export default function EditOpenApiSource(props: { · 45 unmodified lines
410415 setPendingOAuthConnection({
411416 scopeId: targetScope,
412417 slot: oauth2.connectionSlot,
413− connectionId: connectionId as string,
418+ connectionId: connectionId,
414419 });
415420 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],
436442 },
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({
442524 params: { scopeId: displayScope },
443525 payload: {
444526 sourceId: props.sourceId,
@@ -447,83 +529,24 @@ export default function EditOpenApiSource(props: { · 2 unmodified lines
447529 slot: oauth2.connectionSlot,
448530 value: {
449531 kind: "connection",
450− connectionId: ConnectionId.make(response.completedConnection.connectionId),
532+ connectionId: ConnectionId.make(result.connectionId),
451533 },
452534 },
453535 reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys],
454536 });
537+ if (Exit.isFailure(setBindingExit)) {
538+ failConnect(errorMessageFromExit(setBindingExit, "Failed to connect OAuth"));
539+ return;
540+ }
455541 setPendingOAuthConnection(null);
456542 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+ });
527550 };
528551
529552 return (
@@ -590,10 +613,10 @@ export default function EditOpenApiSource(props: { · 60 unmodified lines
590613 </CardStackEntryContent>
591614 <FilterTabs
592615 tabs={credentialScopes.map((entry) => ({
593− value: entry.scopeId as string,
616+ value: entry.scopeId,
594617 label: entry.label,
595618 }))}
596− value={activeCredentialScopeId as string}
619+ value={activeCredentialScopeId}
597620 onChange={setSelectedCredentialScope}
598621 />
599622 </CardStackEntry>
@@ -618,9 +641,9 @@ export default function EditOpenApiSource(props: { · 18 unmodified lines
618641 isSecretBindingValue(effective.value);
619642 const currentSecretId =
620643 exact && isSecretBindingValue(exact.value)
621− ? (exact.value.secretId as string)
644+ ? exact.value.secretId
622645 : inherited && effective && isSecretBindingValue(effective.value)
623− ? (effective.value.secretId as string)
646+ ? effective.value.secretId
624647 : null;
625648 return (
626649 <CardStackEntryField
packages/plugins/openapi/src/sdk/store.ts+243−210
@@ -1,4 +1,4 @@
1−import { Effect, Schema } from "effect";
1+import { Effect, Option, Schema } from "effect";
22
33 import {
44 ConnectionId,
@@ -174,9 +174,7 @@ export interface StoredSource { · 169 unmodified lines
174174 // an encodable/decodable shape for HTTP responses.
175175 // ---------------------------------------------------------------------------
176176
177−export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>(
178− "OpenApiStoredSource",
179−)({
177+export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>("OpenApiStoredSource")({
180178 namespace: Schema.String,
181179 name: Schema.String,
182180 config: Schema.Struct({
@@ -184,20 +182,12 @@ export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>( · 1 unmodified line
184182 sourceUrl: Schema.optional(Schema.String),
185183 baseUrl: Schema.optional(Schema.String),
186184 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)),
193187 specFetchCredentials: Schema.optional(
194188 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)),
201191 }),
202192 ),
203193 // Canonical source-owned OAuth config. Concrete client credentials
@@ -221,10 +211,85 @@ export interface StoredOperation { · 17 unmodified lines
221211
222212 const encodeBinding = Schema.encodeSync(OperationBinding);
223213 const decodeBinding = Schema.decodeUnknownSync(OperationBinding);
214+const decodeBindingJson = Schema.decodeUnknownSync(Schema.fromJsonString(OperationBinding));
224215
225216 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+);
226223 const encodeOAuth2SourceConfig = Schema.encodeSync(OAuth2SourceConfig);
227224
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+
228293 interface ChildRow {
229294 readonly id: string;
230295 readonly scope_id: string;
@@ -277,21 +342,24 @@ const childRowsToValueMap = ( · 46 unmodified lines
277342 ): Record<string, HeaderValue> => {
278343 const out: Record<string, HeaderValue> = {};
279344 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+ }
288356 }
289357 }
290358 return out;
291359 };
292360
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>;
295363
296364 const toConfiguredHeaderBinding = (value: {
297365 readonly slot?: unknown;
@@ -303,11 +371,10 @@ const toConfiguredHeaderBinding = (value: { · 5 unmodified lines
303371 ...(typeof value.prefix === "string" ? { prefix: value.prefix } : {}),
304372 });
305373
306−const decodeHeaders = (value: unknown): Record<string, HeaderValue> => {
374+const decodeHeaders = (value: unknown): Record<string, unknown> => {
307375 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);
311378 };
312379
313380 const slugifySlotPart = (value: string): string =>
@@ -344,20 +411,18 @@ const normalizeStoredHeaders = ( · 30 unmodified lines
344411 legacy[name] = header;
345412 continue;
346413 }
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);
354417 continue;
355418 }
356− legacy[name] = header;
419+ const legacyHeader = decodeHeaderValueOption(header);
420+ if (Option.isNone(legacyHeader)) continue;
421+ legacy[name] = legacyHeader.value;
357422 headers[name] = new ConfiguredHeaderBinding({
358423 kind: "binding",
359424 slot: headerBindingSlot(name),
360− prefix: header.prefix,
425+ prefix: typeof legacyHeader.value === "string" ? undefined : legacyHeader.value.prefix,
361426 });
362427 }
363428 return { headers, legacy };
@@ -370,13 +435,16 @@ const normalizeStoredOAuth2 = ( · 6 unmodified lines
370435 readonly legacy?: OAuth2Auth;
371436 } => {
372437 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 };
378444 }
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);
380448 return {
381449 legacy,
382450 oauth2: new OAuth2SourceConfig({
@@ -438,10 +506,7 @@ export interface OpenapiStore { · 55 unmodified lines
438506 scope: string,
439507 ) => Effect.Effect<StoredSource | null, StorageFailure>;
440508
441− readonly listSources: () => Effect.Effect<
442− readonly StoredSource[],
443− StorageFailure
444− >;
509+ readonly listSources: () => Effect.Effect<readonly StoredSource[], StorageFailure>;
445510
446511 readonly getOperationByToolId: (
447512 toolId: string,
@@ -453,10 +518,7 @@ export interface OpenapiStore { · 5 unmodified lines
453518 scope: string,
454519 ) => Effect.Effect<readonly StoredOperation[], StorageFailure>;
455520
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>;
460522
461523 readonly listSourceBindings: (
462524 sourceId: string,
@@ -501,10 +563,7 @@ export interface OpenapiStore { · 38 unmodified lines
501563 * `query_param:foo` or `spec_fetch_header:Authorization`. */
502564 readonly findChildRowsBySecret: (secretId: string) => Effect.Effect<
503565 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";
508567 readonly source_id: string;
509568 readonly scope_id: string;
510569 readonly name: string;
@@ -527,14 +586,12 @@ export const makeDefaultOpenapiStore = ({ · 16 unmodified lines
527586 adapter,
528587 scopes,
529588 }: StorageDeps<OpenapiSchema>): OpenapiStore => {
530− const scopeIds = scopes.map((scope) => scope.id as string);
589+ const scopeIds = scopes.map((scope) => String(scope.id));
531590 const scopePrecedence = new Map<string, number>();
532591 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;
535593
536− const encodeSyntheticRowIdPart = (value: string): string =>
537− encodeURIComponent(value);
594+ const encodeSyntheticRowIdPart = (value: string): string => encodeURIComponent(value);
538595
539596 const sourceBindingRowId = (
540597 sourceId: string,
@@ -550,47 +607,47 @@ export const makeDefaultOpenapiStore = ({ · 9 unmodified lines
550607 encodeSyntheticRowIdPart(scopeId),
551608 ].join("::");
552609
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) };
559614 }
560− if (kind === "connection" && typeof row.connection_id === "string") {
615+ if (decoded.kind === "connection" && decoded.connection_id != null) {
561616 return {
562617 kind: "connection",
563− connectionId: ConnectionId.make(row.connection_id),
618+ connectionId: ConnectionId.make(decoded.connection_id),
564619 };
565620 }
566621 // text fallback covers both well-formed text rows and any
567622 // partial/null row that survived a malformed write — `text_value`
568623 // 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 ?? "" };
570625 };
571626
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,
580634 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),
589637 });
638+ };
639+
640+ const sourceBindingTargetScope = (row: Record<string, unknown>): string =>
641+ decodeSourceBindingStorageRow(row).target_scope_id;
590642
591643 const sourceBindingValueColumns = (
592644 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+ } => {
594651 if (value.kind === "secret") {
595652 return { kind: "secret", secret_id: value.secretId };
596653 }
@@ -606,24 +663,20 @@ export const makeDefaultOpenapiStore = ({ · 9 unmodified lines
606663 }) =>
607664 Effect.gen(function* () {
608665 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+ });
617672 }
618673 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+ });
627680 }
628681 });
629682
@@ -645,23 +698,19 @@ export const makeDefaultOpenapiStore = ({ · 15 unmodified lines
645698 ],
646699 });
647700 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+ });
654705 }
655706 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+ });
665714 }
666715 return source;
667716 });
@@ -684,20 +733,15 @@ export const makeDefaultOpenapiStore = ({ · 16 unmodified lines
684733 })
685734 .pipe(Effect.map(childRowsToValueMap));
686735
687− const rowToSource = (
688− row: Record<string, unknown>,
689− ): Effect.Effect<StoredSource, StorageFailure> =>
736+ const rowToSource = (row: Record<string, unknown>): Effect.Effect<StoredSource, StorageFailure> =>
690737 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);
695743
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);
701745 const specFetchHeaders = yield* loadChildValueMap(
702746 "openapi_source_spec_fetch_header",
703747 sourceId,
@@ -709,13 +753,10 @@ export const makeDefaultOpenapiStore = ({ · 5 unmodified lines
709753 scope,
710754 );
711755 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
714757 ? undefined
715758 : {
716− ...(Object.keys(specFetchHeaders).length > 0
717− ? { headers: specFetchHeaders }
718− : {}),
759+ ...(Object.keys(specFetchHeaders).length > 0 ? { headers: specFetchHeaders } : {}),
719760 ...(Object.keys(specFetchQueryParams).length > 0
720761 ? { queryParams: specFetchQueryParams }
721762 : {}),
@@ -724,38 +765,40 @@ export const makeDefaultOpenapiStore = ({ · 2 unmodified lines
724765 return {
725766 namespace: sourceId,
726767 scope,
727− name: row.name as string,
768+ name: sourceRow.name,
728769 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,
732773 headers: normalizedHeaders.headers,
733774 queryParams,
734775 specFetchCredentials,
735776 oauth2: normalizedOAuth2.oauth2,
736777 },
737778 legacy:
738− Object.keys(normalizedHeaders.legacy).length > 0 ||
739− normalizedOAuth2.legacy
779+ Object.keys(normalizedHeaders.legacy).length > 0 || normalizedOAuth2.legacy
740780 ? {
741781 ...(Object.keys(normalizedHeaders.legacy).length > 0
742782 ? { headers: normalizedHeaders.legacy }
743783 : {}),
744− ...(normalizedOAuth2.legacy
745− ? { oauth2: normalizedOAuth2.legacy }
746− : {}),
784+ ...(normalizedOAuth2.legacy ? { oauth2: normalizedOAuth2.legacy } : {}),
747785 }
748786 : undefined,
749787 };
750788 });
751789
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+ };
759802
760803 // Replace the rows of one child table for a source: delete then bulk
761804 // insert. Single helper so upsertSource and updateSourceMeta both
@@ -845,20 +888,18 @@ export const makeDefaultOpenapiStore = ({ · 83 unmodified lines
845888 source_url: input.config.sourceUrl ?? undefined,
846889 base_url: input.config.baseUrl ?? undefined,
847890 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+ ]),
862903 ) as Record<string, unknown>,
863904 oauth2: input.config.oauth2
864905 ? toJsonRecord(encodeOAuth2SourceConfig(input.config.oauth2))
@@ -911,14 +952,10 @@ export const makeDefaultOpenapiStore = ({ · 46 unmodified lines
911952 const existing = yield* rowToSource(existingRow);
912953
913954 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;
916956 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;
922959
923960 yield* adapter.update({
924961 model: "openapi_source",
@@ -941,9 +978,7 @@ export const makeDefaultOpenapiStore = ({ · 16 unmodified lines
941978 },
942979 ]),
943980 ) as Record<string, unknown>,
944− oauth2: nextOAuth2
945− ? toJsonRecord(encodeOAuth2SourceConfig(nextOAuth2))
946− : undefined,
981+ oauth2: nextOAuth2 ? toJsonRecord(encodeOAuth2SourceConfig(nextOAuth2)) : undefined,
947982 },
948983 });
949984 if (patch.queryParams !== undefined) {
@@ -999,8 +1034,7 @@ export const makeDefaultOpenapiStore = ({ · 49 unmodified lines
9991034 })
10001035 .pipe(Effect.map((rows) => rows.map(rowToOperation))),
10011036
1002− removeSource: (namespace, scope) =>
1003− deleteSource(namespace, scope, { includeBindings: true }),
1037+ removeSource: (namespace, scope) => deleteSource(namespace, scope, { includeBindings: true }),
10041038
10051039 listSourceBindings: (sourceId, sourceScope) =>
10061040 Effect.gen(function* () {
@@ -1017,14 +1051,10 @@ export const makeDefaultOpenapiStore = ({ · 10 unmodified lines
10171051 ],
10181052 });
10191053 return rows
1020− .filter(
1021− (row) =>
1022− scopeRank(row.target_scope_id as string) <= sourceScopeRank,
1023− )
1054+ .filter((row) => scopeRank(sourceBindingTargetScope(row)) <= sourceScopeRank)
10241055 .sort(
10251056 (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)),
10281058 )
10291059 .map(rowToSourceBinding);
10301060 }),
@@ -1045,31 +1075,24 @@ export const makeDefaultOpenapiStore = ({ · 14 unmodified lines
10451075 });
10461076 const sourceScopeRank = scopeRank(sourceScope);
10471077 const row = rows
1048− .filter(
1049− (candidate) =>
1050− scopeRank(candidate.target_scope_id as string) <= sourceScopeRank,
1051− )
1078+ .filter((candidate) => scopeRank(sourceBindingTargetScope(candidate)) <= sourceScopeRank)
10521079 .sort(
10531080 (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)),
10561082 )[0];
10571083 return row ? rowToSourceBinding(row) : null;
10581084 }),
10591085
10601086 setSourceBinding: (input) =>
10611087 Effect.gen(function* () {
1088+ const sourceScope = String(input.sourceScope);
1089+ const targetScope = String(input.scope);
10621090 yield* validateBindingTarget({
10631091 sourceId: input.sourceId,
1064− sourceScope: input.sourceScope as string,
1065− targetScope: input.scope as string,
1092+ sourceScope,
1093+ targetScope,
10661094 });
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);
10731096 const now = new Date();
10741097 const valueColumns = sourceBindingValueColumns(input.value);
10751098 yield* adapter.delete({
@@ -1081,8 +1104,8 @@ export const makeDefaultOpenapiStore = ({ · 5 unmodified lines
10811104 data: {
10821105 id,
10831106 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,
10861109 slot: input.slot,
10871110 ...valueColumns,
10881111 created_at: now,
@@ -1139,8 +1162,14 @@ export const makeDefaultOpenapiStore = ({ · 50 unmodified lines
11391162 Effect.gen(function* () {
11401163 const tables = [
11411164 { 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+ },
11441173 ];
11451174 const perTable = yield* Effect.forEach(
11461175 tables,
@@ -1152,12 +1181,15 @@ export const makeDefaultOpenapiStore = ({ · 5 unmodified lines
11521181 })
11531182 .pipe(
11541183 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+ }),
11611193 ),
11621194 ),
11631195 { concurrency: "unbounded" },
@@ -1172,8 +1204,9 @@ export const makeDefaultOpenapiStore = ({ · 8 unmodified lines
11721204 const requested = new Set(keys);
11731205 const out = new Map<string, string>();
11741206 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);
11771210 }
11781211 return out;
11791212 }),