History

Merge remote-tracking branch 'origin/codex/batch18-openapi-boundaries' into codex/effect-lint-integration

# Conflicts: # packages/plugins/openapi/src/react/AddOpenApiSource.tsx # packages/plugins/openapi/src/react/EditOpenApiSource.tsx

Rhys Sullivan committed 6 months ago

Commit 7257c892 parents 3f7841a 9494cb43 files · +407 −401Browse files

Unified · Split

3 files
  1. packages/plugins/openapi/src/react/AddOpenApiSource.tsx +82 −100
  2. packages/plugins/openapi/src/react/EditOpenApiSource.tsx +82 −91
  3. packages/plugins/openapi/src/sdk/store.ts +243 −210
packages/plugins/openapi/src/react/AddOpenApiSource.tsx+82−100
@@ -1,6 +1,8 @@
11 import { useCallback, useEffect, useRef, useState } from "react";
22 import { useAtomSet } from "@effect/atom-react";
3−import { Effect, Exit, Option, Schema } 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,33 +83,19 @@ import { · 74 unmodified lines
8183 type ServerVariable,
8284 } from "../sdk/types";
8385
84−export const OPENAPI_OAUTH_POPUP_NAME = "openapi-oauth";
85−export const OPENAPI_OAUTH_CALLBACK_PATH = "/api/oauth/callback";
86−const PublicErrorMessage = Schema.Struct({
87− _tag: Schema.Literals(["OpenApiParseError", "OpenApiExtractionError", "OpenApiOAuthError"]),
88− message: Schema.String,
89−});
90−
91−const messageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string => {
92− const error = Exit.findErrorOption(exit);
93− if (Option.isNone(error)) return fallback;
94− const errorMessage = Schema.decodeUnknownOption(PublicErrorMessage)(error.value);
95− return Option.match(errorMessage, {
96− onNone: () => fallback,
97− onSome: (value) => value.message,
98− });
99−};
86+const ErrorMessage = Schema.Struct({ message: Schema.String });
10087
101−const failPromise = <A,>(message: string): Promise<A> => Effect.runPromise(Effect.fail(message));
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+ );
10296
103−const parseUrlOption = (url: string, baseUrl?: string): URL | null => {
104− // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL constructor is the platform URL parser
105− try {
106− return baseUrl === undefined ? new URL(url) : new URL(url, baseUrl);
107− } catch {
108− return null;
109− }
110−};
97+export const OPENAPI_OAUTH_POPUP_NAME = "openapi-oauth";
98+export const OPENAPI_OAUTH_CALLBACK_PATH = "/api/oauth/callback";
11199
112100 const substituteUrlVariables = (url: string, values: Record<string, string>): string => {
113101 let out = url;
@@ -134,15 +122,15 @@ export const openApiOAuthConnectionId = ( · 20 unmodified lines
134122 */
135123 export function resolveOAuthUrl(url: string, baseUrl: string): string {
136124 if (!url) return url;
137− if (parseUrlOption(url)) {
125+ if (URL.canParse(url)) {
138126 return url;
139127 }
140− if (!baseUrl) return url;
141− return parseUrlOption(url, baseUrl)?.toString() ?? url;
128+ if (!baseUrl || !URL.canParse(url, baseUrl)) return url;
129+ return new URL(url, baseUrl).toString();
142130 }
143131
144132 export function inferOAuthIssuerUrl(authorizationUrl: string): string | null {
145− return parseUrlOption(authorizationUrl)?.origin ?? null;
133+ return URL.canParse(authorizationUrl) ? new URL(authorizationUrl).origin : null;
146134 }
147135
148136 type StrategySelection =
@@ -293,7 +281,7 @@ export default function AddOpenApiSource(props: { · 144 unmodified lines
293281 selectedServerIndex >= 0 ? (servers[selectedServerIndex] ?? null) : null;
294282
295283 const serverVariables: Record<string, ServerVariable> = selectedServer
296− ? Option.getOrElse(selectedServer.variables, () => ({}) as Record<string, ServerVariable>)
284+ ? Option.getOrElse(selectedServer.variables, () => ({}))
297285 : {};
298286 const serverVariableEntries: Array<[string, ServerVariable]> = Object.entries(serverVariables);
299287
@@ -305,10 +293,7 @@ export default function AddOpenApiSource(props: { · 5 unmodified lines
305293 // Helper used by analyze + server selection: build a default selection map
306294 // from a server's variable defaults.
307295 const defaultSelectionsFor = (server: ServerInfo): Record<string, string> => {
308− const vars: Record<string, ServerVariable> = Option.getOrElse(
309− server.variables,
310− () => ({}) as Record<string, ServerVariable>,
311− );
296+ const vars: Record<string, ServerVariable> = Option.getOrElse(server.variables, () => ({}));
312297 const out: Record<string, string> = {};
313298 for (const [name, v] of Object.entries(vars)) out[name] = v.default;
314299 return out;
@@ -387,19 +372,20 @@ export default function AddOpenApiSource(props: { · 72 unmodified lines
387372 setAnalyzeError(null);
388373 setAddError(null);
389374 const credentials = serializeHttpCredentials(specFetchCredentials);
390− const previewExit = await doPreview({
375+ const exit = await doPreview({
391376 params: { scopeId },
392377 payload: {
393378 spec: specUrl,
394379 specFetchCredentials: credentials,
395380 },
396381 });
397− if (Exit.isFailure(previewExit)) {
398− setAnalyzeError(messageFromExit(previewExit, "Failed to parse spec"));
382+ if (Exit.isFailure(exit)) {
383+ setAnalyzeError(errorMessageFromExit(exit, "Failed to parse spec"));
399384 setAnalyzing(false);
400385 return;
401386 }
402− const result = previewExit.value;
387+
388+ const result = exit.value;
403389 setPreview(result);
404390
405391 const firstServer = result.servers[0];
@@ -500,7 +486,7 @@ export default function AddOpenApiSource(props: { · 94 unmodified lines
500486 }
501487 setStartingOAuth(true);
502488 const connectionId = openApiOAuthConnectionId(resolvedSourceId, selectedOAuth2Preset.flow);
503− const startExit = await doStartOAuth({
489+ const startOAuthExit = await doStartOAuth({
504490 params: { scopeId },
505491 payload: {
506492 endpoint: tokenUrl,
@@ -519,11 +505,11 @@ export default function AddOpenApiSource(props: { · 12 unmodified lines
519505 },
520506 });
521507 setStartingOAuth(false);
522− if (Exit.isFailure(startExit)) {
523− setOauth2Error(messageFromExit(startExit, "Failed to start OAuth"));
508+ if (Exit.isFailure(startOAuthExit)) {
509+ setOauth2Error(errorMessageFromExit(startOAuthExit, "Failed to start OAuth"));
524510 return;
525511 }
526− const response = startExit.value;
512+ const response = startOAuthExit.value;
527513 if (!response.completedConnection) {
528514 setOauth2Error("client_credentials flow did not mint a connection");
529515 return;
@@ -551,41 +537,41 @@ export default function AddOpenApiSource(props: { · 21 unmodified lines
551537 resolvedBaseUrl,
552538 );
553539 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+ }
554569
555570 await oauth.openAuthorization({
556− run: async () => {
557− const startExit = await doStartOAuth({
558− params: { scopeId },
559− payload: {
560− endpoint: authorizationUrl,
561− connectionId: openApiOAuthConnectionId(resolvedSourceId, selectedOAuth2Preset.flow),
562− tokenScope: scopeId,
563− redirectUrl: oauth2RedirectUrl,
564− strategy: {
565− kind: "authorization-code",
566− authorizationEndpoint: authorizationUrl,
567− tokenEndpoint: tokenUrl,
568− issuerUrl,
569− clientIdSecretId: oauth2ClientIdSecretId,
570− clientSecretSecretId: oauth2ClientSecretSecretId ?? null,
571− scopes: [...oauth2SelectedScopes],
572− },
573− pluginId: "openapi",
574− identityLabel: `${displayName} OAuth`,
575− },
576− });
577− if (Exit.isFailure(startExit)) {
578− return failPromise(messageFromExit(startExit, "Failed to start OAuth"));
579− }
580− const response = startExit.value;
581− if (response.authorizationUrl === null) {
582− return failPromise("Unexpected response flow from server");
583− }
584− return {
585− sessionId: response.sessionId,
586− authorizationUrl: response.authorizationUrl,
587− };
588− },
571+ run: async () => ({
572+ sessionId: response.sessionId,
573+ authorizationUrl: response.authorizationUrl,
574+ }),
589575 onSuccess: (result) => {
590576 setOauth2AuthState({
591577 fingerprint: selectedOAuth2Fingerprint,
@@ -641,7 +627,12 @@ export default function AddOpenApiSource(props: { · 49 unmodified lines
641627 kind: "openapi",
642628 url: resolvedBaseUrl || undefined,
643629 });
644− const addExit = await doAdd({
630+ const failAdd = (message: string) => {
631+ setAddError(message);
632+ setAdding(false);
633+ placeholder.done();
634+ };
635+ const resultExit = await doAdd({
645636 params: { scopeId },
646637 payload: {
647638 spec: specUrl,
@@ -657,14 +648,13 @@ export default function AddOpenApiSource(props: { · 9 unmodified lines
657648 },
658649 reactivityKeys: addSpecWriteKeys,
659650 });
660− if (Exit.isFailure(addExit)) {
661− placeholder.done();
662− setAddError(messageFromExit(addExit, "Failed to add source"));
663− setAdding(false);
651+
652+ if (Exit.isFailure(resultExit)) {
653+ failAdd(errorMessageFromExit(resultExit, "Failed to add source"));
664654 return;
665655 }
666656
667− const sourceId = addExit.value.namespace;
657+ const sourceId = resultExit.value.namespace;
668658 const sourceScope = ScopeId.make(scopeId);
669659 const bindingScope = ScopeId.make(userScope);
670660
@@ -684,15 +674,13 @@ export default function AddOpenApiSource(props: { · 13 unmodified lines
684674 reactivityKeys: bindingWriteKeys,
685675 });
686676 if (Exit.isFailure(bindingExit)) {
687− placeholder.done();
688− setAddError(messageFromExit(bindingExit, "Failed to add source"));
689− setAdding(false);
677+ failAdd(errorMessageFromExit(bindingExit, "Failed to add source"));
690678 return;
691679 }
692680 }
693681
694682 if (configuredOAuth2 && oauth2ClientIdSecretId) {
695− const bindingExit = await doSetBinding({
683+ const clientIdBindingExit = await doSetBinding({
696684 params: { scopeId },
697685 payload: {
698686 sourceId,
@@ -706,16 +694,14 @@ export default function AddOpenApiSource(props: { · 7 unmodified lines
706694 },
707695 reactivityKeys: bindingWriteKeys,
708696 });
709− if (Exit.isFailure(bindingExit)) {
710− placeholder.done();
711− setAddError(messageFromExit(bindingExit, "Failed to add source"));
712− setAdding(false);
697+ if (Exit.isFailure(clientIdBindingExit)) {
698+ failAdd(errorMessageFromExit(clientIdBindingExit, "Failed to add source"));
713699 return;
714700 }
715701 }
716702
717703 if (configuredOAuth2?.clientSecretSlot && oauth2ClientSecretSecretId) {
718− const bindingExit = await doSetBinding({
704+ const clientSecretBindingExit = await doSetBinding({
719705 params: { scopeId },
720706 payload: {
721707 sourceId,
@@ -729,16 +715,14 @@ export default function AddOpenApiSource(props: { · 7 unmodified lines
729715 },
730716 reactivityKeys: bindingWriteKeys,
731717 });
732− if (Exit.isFailure(bindingExit)) {
733− placeholder.done();
734− setAddError(messageFromExit(bindingExit, "Failed to add source"));
735− setAdding(false);
718+ if (Exit.isFailure(clientSecretBindingExit)) {
719+ failAdd(errorMessageFromExit(clientSecretBindingExit, "Failed to add source"));
736720 return;
737721 }
738722 }
739723
740724 if (configuredOAuth2 && oauth2Auth) {
741− const bindingExit = await doSetBinding({
725+ const connectionBindingExit = await doSetBinding({
742726 params: { scopeId },
743727 payload: {
744728 sourceId,
@@ -752,10 +736,8 @@ export default function AddOpenApiSource(props: { · 7 unmodified lines
752736 },
753737 reactivityKeys: bindingWriteKeys,
754738 });
755− if (Exit.isFailure(bindingExit)) {
756− placeholder.done();
757− setAddError(messageFromExit(bindingExit, "Failed to add source"));
758− setAdding(false);
739+ if (Exit.isFailure(connectionBindingExit)) {
740+ failAdd(errorMessageFromExit(connectionBindingExit, "Failed to add source"));
759741 return;
760742 }
761743 }
packages/plugins/openapi/src/react/EditOpenApiSource.tsx+82−91
@@ -1,6 +1,8 @@
11 import { useEffect, useMemo, useRef, useState } from "react";
22 import { useAtomSet, useAtomValue } from "@effect/atom-react";
3−import { Effect, Exit, Option, Schema } from "effect";
3+import * as Exit from "effect/Exit";
4+import * as Option from "effect/Option";
5+import * as Schema from "effect/Schema";
46 import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
57
68 import { connectionsAtom, sourceAtom, startOAuth } from "@executor-js/react/api/atoms";
@@ -43,7 +45,21 @@ import { · 36 unmodified lines
4345 resolveOAuthUrl,
4446 } from "./AddOpenApiSource";
4547 import { oauth2ClientSecretSlot } from "../sdk/store";
46−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+ );
4763
4864 type SlotDef =
4965 | {
@@ -86,31 +102,6 @@ const openApiOAuthConnectionId = ( · 36 unmodified lines
86102 const bindingSecretId = (sourceId: string, slot: string, scopeId: string): string =>
87103 `source-binding-${slugify(sourceId)}-${slugify(slot)}-${slugify(scopeId)}`;
88104
89−const PublicErrorMessage = Schema.Struct({
90− _tag: Schema.Literals(["OpenApiParseError", "OpenApiExtractionError", "OpenApiOAuthError"]),
91− message: Schema.String,
92−});
93−const SecretBindingValue = Schema.Struct({
94− kind: Schema.Literal("secret"),
95− secretId: Schema.String,
96−});
97−const ConnectionBindingValue = Schema.Struct({
98− kind: Schema.Literal("connection"),
99− connectionId: Schema.String,
100−});
101−
102−const messageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string => {
103− const error = Exit.findErrorOption(exit);
104− if (Option.isNone(error)) return fallback;
105− const errorMessage = Schema.decodeUnknownOption(PublicErrorMessage)(error.value);
106− return Option.match(errorMessage, {
107− onNone: () => fallback,
108− onSome: (value) => value.message,
109− });
110−};
111−
112−const failPromise = <A,>(message: string): Promise<A> => Effect.runPromise(Effect.fail(message));
113−
114105 const effectiveClientSecretSlot = (oauth2: {
115106 readonly securitySchemeName: string;
116107 readonly clientSecretSlot: string | null;
@@ -145,13 +136,13 @@ const effectiveBindingForScope = ( · 28 unmodified lines
145136
146137 const isSecretBindingValue = (
147138 value: unknown,
148−): value is Extract<OpenApiSourceBindingValue, { readonly kind: "secret" }> =>
149− Option.isSome(Schema.decodeUnknownOption(SecretBindingValue)(value));
139+): value is Extract<OpenApiSourceBindingValueType, { readonly kind: "secret" }> =>
140+ Schema.is(OpenApiSourceBindingValue)(value) && value.kind === "secret";
150141
151142 const isConnectionBindingValue = (
152143 value: unknown,
153−): value is Extract<OpenApiSourceBindingValue, { readonly kind: "connection" }> =>
154− Option.isSome(Schema.decodeUnknownOption(ConnectionBindingValue)(value));
144+): value is Extract<OpenApiSourceBindingValueType, { readonly kind: "connection" }> =>
145+ Schema.is(OpenApiSourceBindingValue)(value) && value.kind === "connection";
155146
156147 export default function EditOpenApiSource(props: {
157148 readonly sourceId: string;
@@ -253,13 +244,12 @@ export default function EditOpenApiSource(props: { · 95 unmodified lines
253244 },
254245 reactivityKeys: openApiWriteKeys,
255246 });
247+ if (sourceSaveSeq.current !== seq) return;
256248 if (Exit.isFailure(exit)) {
257− if (sourceSaveSeq.current !== seq) return;
258249 setSourceSaveState("idle");
259− setError(messageFromExit(exit, "Failed to save source details"));
250+ setError(errorMessageFromExit(exit, "Failed to save source details"));
260251 return;
261252 }
262− if (sourceSaveSeq.current !== seq) return;
263253 setSourceSaveState("saved");
264254 window.setTimeout(() => {
265255 if (sourceSaveSeq.current === seq) setSourceSaveState("idle");
@@ -352,7 +342,7 @@ export default function EditOpenApiSource(props: { · 86 unmodified lines
352342 reactivityKeys: sourceWriteKeys,
353343 });
354344 if (Exit.isFailure(exit)) {
355− setError(messageFromExit(exit, "Failed to save credential binding"));
345+ setError(errorMessageFromExit(exit, "Failed to save credential binding"));
356346 }
357347 setBusyKey(null);
358348 };
@@ -371,7 +361,7 @@ export default function EditOpenApiSource(props: { · 12 unmodified lines
371361 reactivityKeys: sourceWriteKeys,
372362 });
373363 if (Exit.isFailure(exit)) {
374− setError(messageFromExit(exit, "Failed to clear credential binding"));
364+ setError(errorMessageFromExit(exit, "Failed to clear credential binding"));
375365 }
376366 setBusyKey(null);
377367 };
@@ -425,18 +415,23 @@ export default function EditOpenApiSource(props: { · 47 unmodified lines
425415 setPendingOAuthConnection({
426416 scopeId: targetScope,
427417 slot: oauth2.connectionSlot,
428− connectionId,
418+ connectionId: connectionId,
429419 });
430420 setError(null);
421+ const failConnect = (message: string) => {
422+ setError(message);
423+ setPendingOAuthConnection(null);
424+ setBusyKey(null);
425+ };
431426 const displayName = source.name;
432427 const tokenUrl = resolveOAuthUrl(oauth2.tokenUrl, source.config.baseUrl ?? "");
433428 if (oauth2.flow === "clientCredentials") {
434− const startExit = await doStartOAuth({
429+ const startOAuthExit = await doStartOAuth({
435430 params: { scopeId: displayScope },
436431 payload: {
437432 endpoint: tokenUrl,
438433 redirectUrl: tokenUrl,
439− connectionId,
434+ connectionId: connectionId,
440435 tokenScope: targetScope,
441436 strategy: {
442437 kind: "client-credentials",
@@ -449,20 +444,16 @@ export default function EditOpenApiSource(props: { · 6 unmodified lines
449444 identityLabel: `${displayName} OAuth`,
450445 },
451446 });
452− if (Exit.isFailure(startExit)) {
453− setError(messageFromExit(startExit, "Failed to connect OAuth"));
454− setPendingOAuthConnection(null);
455− setBusyKey(null);
447+ if (Exit.isFailure(startOAuthExit)) {
448+ failConnect(errorMessageFromExit(startOAuthExit, "Failed to connect OAuth"));
456449 return;
457450 }
458− const response = startExit.value;
451+ const response = startOAuthExit.value;
459452 if (!response.completedConnection) {
460− setError("Unexpected OAuth response");
461− setPendingOAuthConnection(null);
462− setBusyKey(null);
453+ failConnect("Unexpected OAuth response");
463454 return;
464455 }
465− const bindingExit = await doSetBinding({
456+ const setBindingExit = await doSetBinding({
466457 params: { scopeId: displayScope },
467458 payload: {
468459 sourceId: props.sourceId,
@@ -476,8 +467,9 @@ export default function EditOpenApiSource(props: { · 7 unmodified lines
476467 },
477468 reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys],
478469 });
479− if (Exit.isFailure(bindingExit)) {
480− setError(messageFromExit(bindingExit, "Failed to connect OAuth"));
470+ if (Exit.isFailure(setBindingExit)) {
471+ failConnect(errorMessageFromExit(setBindingExit, "Failed to connect OAuth"));
472+ return;
481473 }
482474 setPendingOAuthConnection(null);
483475 setBusyKey(null);
@@ -489,45 +481,46 @@ export default function EditOpenApiSource(props: { · 5 unmodified lines
489481 source.config.baseUrl ?? "",
490482 );
491483 const issuerUrl = oauth2.issuerUrl ?? inferOAuthIssuerUrl(authorizationUrl);
492− await oauth.openAuthorization({
493− run: async () => {
494− const startExit = await doStartOAuth({
495− params: { scopeId: displayScope },
496− payload: {
497− endpoint: authorizationUrl,
498− connectionId,
499− tokenScope: targetScope,
500− redirectUrl: oauth2RedirectUrl,
501− strategy: {
502− kind: "authorization-code",
503− authorizationEndpoint: authorizationUrl,
504− tokenEndpoint: tokenUrl,
505− issuerUrl,
506− clientIdSecretId,
507− clientSecretSecretId:
508− clientSecretBinding && isSecretBindingValue(clientSecretBinding.value)
509− ? clientSecretBinding.value.secretId
510− : null,
511− scopes: [...oauth2.scopes],
512− },
513− pluginId: "openapi",
514− identityLabel: `${displayName} OAuth`,
515− },
516− });
517− if (Exit.isFailure(startExit)) {
518− return failPromise(messageFromExit(startExit, "Failed to connect OAuth"));
519− }
520− const response = startExit.value;
521− if (response.authorizationUrl === null) {
522− return failPromise("Unexpected OAuth response");
523− }
524− return {
525− sessionId: response.sessionId,
526− authorizationUrl: response.authorizationUrl,
527− };
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`,
528505 },
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+ }),
529522 onSuccess: async (result) => {
530− const bindingExit = await doSetBinding({
523+ const setBindingExit = await doSetBinding({
531524 params: { scopeId: displayScope },
532525 payload: {
533526 sourceId: props.sourceId,
@@ -541,10 +534,8 @@ export default function EditOpenApiSource(props: { · 7 unmodified lines
541534 },
542535 reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys],
543536 });
544− if (Exit.isFailure(bindingExit)) {
545− setError(messageFromExit(bindingExit, "Failed to connect OAuth"));
546− setPendingOAuthConnection(null);
547− setBusyKey(null);
537+ if (Exit.isFailure(setBindingExit)) {
538+ failConnect(errorMessageFromExit(setBindingExit, "Failed to connect OAuth"));
548539 return;
549540 }
550541 setPendingOAuthConnection(null);
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 }),

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t