History

perf(schema): hoist Schema compilers and add lint rule

Schema.is, Schema.decode*, Schema.encode*, Schema.validate*, and Schema.parse* each compile a parser/guard from a schema. Calling them inside a function body rebuilds that function on every invocation. Adds executor/no-inline-schema-compile (oxlint plugin rule) that flags these calls when they appear inside a function body, with two severities: HIGH when the schema argument is itself an inline Schema.X(...) literal (both AST and compile rebuilt), MEDIUM when only the compiler is re-called. Fixes the 44 existing violations across 22 files by hoisting the compiled function (and where applicable, its schema literal) to module scope. Notable consolidations: - oauth-service.ts: 8 sites (5 decode + 3 encode of OAuthProviderState) collapse into two hoisted helpers - jwks-cache.ts, mcp-auth.ts: per-call guard compilation removed from JWT verify error path - React Add*Source / EditOpenApiSource components: shared decodeErrorMessage / isOpenApiSourceBindingValue hoisted migrate-connections.ts has one targeted oxlint-disable for a higher-order helper whose schema is a function parameter; the compile is hoisted into the outer closure but the rule (correctly) flags any Schema compiler call inside a function body.

Rhys Sullivan committed 6 months ago

Commit 25515e31 parent bfb87a025 files · +224 −99Browse files

Unified · Split

25 files
  1. .oxlintrc.jsonc +1 −0
  2. apps/cloud/src/jwks-cache.ts +4 −1
  3. apps/cloud/src/mcp-auth.ts +2 −1
  4. apps/cloud/src/mcp-miniflare.e2e.node.test.ts +3 −1
  5. apps/cloud/src/services/sources-api.node.test.ts +4 −3
  6. apps/local/src/server/migrate-connections.ts +5 −4
  7. apps/local/src/server/migrate-mcp-bindings.test.ts +4 −2
  8. apps/local/src/server/migrate-openapi-bindings.test.ts +14 −9
  9. packages/core/api/src/oauth-popup.test.ts +6 −6
  10. packages/core/api/src/observability.ts +3 −1
  11. packages/core/config/src/config.test.ts +5 −3
  12. packages/core/sdk/src/executor.ts +5 −5
  13. packages/core/sdk/src/oauth-service.ts +15 −17
  14. packages/core/storage-core/src/factory.ts +3 −3
  15. packages/plugins/google-discovery/src/react/AddGoogleDiscoverySource.tsx +5 −7
  16. packages/plugins/graphql/src/react/AddGraphqlSource.tsx +5 −7
  17. packages/plugins/graphql/src/sdk/store.ts +8 −2
  18. packages/plugins/mcp/src/react/AddMcpSource.tsx +5 −7
  19. packages/plugins/openapi/src/react/AddOpenApiSource.tsx +5 −7
  20. packages/plugins/openapi/src/react/EditOpenApiSource.tsx +8 −9
  21. packages/plugins/openapi/src/sdk/non-json-body.test.ts +3 −2
  22. packages/plugins/openapi/src/sdk/store.ts +3 −1
  23. packages/plugins/workos-vault/src/sdk/client.ts +2 −1
  24. scripts/oxlint-plugin-executor.js +2 −0
  25. scripts/oxlint-plugin-executor/rules/no-inline-schema-compile.js +104 −0
.oxlintrc.jsonc+1−0
@@ -11,6 +11,7 @@ · 10 unmodified lines
1111 "executor/no-effect-escape-hatch": "error",
1212 "executor/no-effect-internal-tags": "error",
1313 "executor/no-error-constructor": "error",
14+ "executor/no-inline-schema-compile": "error",
1415 "executor/no-instanceof-error": "error",
1516 "executor/no-instanceof-tagged-error": "error",
1617 "executor/no-json-parse": "error",
apps/cloud/src/jwks-cache.ts+4−1
@@ -61,8 +61,11 @@ const JsonWebKeySetPayload = Schema.Struct({ · 60 unmodified lines
6161 });
6262 const decodeJsonWebKeySetPayload = Schema.decodeUnknownPromise(JsonWebKeySetPayload);
6363
64+const ErrorWithCode = Schema.Struct({ code: Schema.String });
65+const isErrorWithCode = Schema.is(ErrorWithCode);
66+
6467 const isJwksNoMatchingKey = (cause: unknown): boolean =>
65− Schema.is(Schema.Struct({ code: Schema.String }))(cause) && cause.code === JWKSNoMatchingKey.code;
68+ isErrorWithCode(cause) && cause.code === JWKSNoMatchingKey.code;
6669
6770 interface CacheEntry {
6871 jwks: JSONWebKeySet;
apps/cloud/src/mcp-auth.ts+2−1
@@ -15,9 +15,10 @@ export class McpJwtVerificationError extends Data.TaggedError("McpJwtVerificatio · 14 unmodified lines
1515 }> {}
1616
1717 const JoseErrorCode = Schema.Struct({ code: Schema.String });
18+const isJoseErrorCodeShape = Schema.is(JoseErrorCode);
1819
1920 const getJoseErrorCode = (cause: unknown): string | null =>
20− Schema.is(JoseErrorCode)(cause) ? cause.code : null;
21+ isJoseErrorCodeShape(cause) ? cause.code : null;
2122
2223 const isJoseErrorCode = (code: string): boolean => code.startsWith("ERR_J");
2324
apps/cloud/src/mcp-miniflare.e2e.node.test.ts+3−1
@@ -186,6 +186,8 @@ const OtlpPayloadFromJson = Schema.fromJsonString( · 185 unmodified lines
186186 }),
187187 );
188188
189+const decodeOtlpPayload = Schema.decodeUnknownOption(OtlpPayloadFromJson);
190+
189191 const unwrapAttrValue = (v?: OtlpAttributeValue): unknown => {
190192 if (!v) return undefined;
191193 if (v.stringValue !== undefined) return v.stringValue;
@@ -218,7 +220,7 @@ const TelemetryReceiverLive = Layer.effect(TelemetryReceiver)( · 26 unmodified lines
218220 body += chunk;
219221 });
220222 req.on("end", () => {
221− const maybePayload = Schema.decodeUnknownOption(OtlpPayloadFromJson)(body);
223+ const maybePayload = decodeOtlpPayload(body);
222224 if (Option.isSome(maybePayload)) {
223225 const payload = maybePayload.value;
224226 for (const rs of payload.resourceSpans ?? []) {
apps/cloud/src/services/sources-api.node.test.ts+4−3
@@ -177,6 +177,9 @@ const GraphqlRequestSchema = Schema.Struct({ · 176 unmodified lines
177177 variables: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
178178 });
179179
180+const GraphqlRequestFromJson = Schema.fromJsonString(GraphqlRequestSchema);
181+const decodeGraphqlRequest = Schema.decodeUnknownPromise(GraphqlRequestFromJson);
182+
180183 const startGraphqlServer = () => {
181184 const requests: Array<{ readonly query: string; readonly variables: unknown }> = [];
182185 const server = http.createServer(async (req, res) => {
@@ -186,9 +189,7 @@ const startGraphqlServer = () => { · 3 unmodified lines
186189 return;
187190 }
188191
189− const parsed = await Schema.decodeUnknownPromise(Schema.fromJsonString(GraphqlRequestSchema))(
190− await readBody(req),
191− );
192+ const parsed = await decodeGraphqlRequest(await readBody(req));
192193 const query = parsed.query ?? "";
193194 requests.push({ query, variables: parsed.variables ?? null });
194195
apps/local/src/server/migrate-connections.ts+5−4
@@ -30,10 +30,11 @@ const isString = (v: unknown): v is string => typeof v === "string"; · 29 unmodified lines
3030 const JsonObject = Schema.Record(Schema.String, Schema.Unknown);
3131 const JsonObjectFromString = Schema.fromJsonString(JsonObject);
3232
33−const decodeUnknownOptionAs =
34− <A>(schema: Schema.Decoder<A>) =>
35− (input: unknown): Option.Option<A> =>
36− Schema.decodeUnknownOption(schema)(input);
33+const decodeUnknownOptionAs = <A>(schema: Schema.Decoder<A>) => {
34+ // oxlint-disable-next-line executor/no-inline-schema-compile -- schema bound by parameter; compiler hoisted into closure
35+ const decode = Schema.decodeUnknownOption(schema);
36+ return (input: unknown): Option.Option<A> => decode(input);
37+};
3738
3839 const decodeJsonObjectString = Schema.decodeUnknownOption(JsonObjectFromString);
3940
apps/local/src/server/migrate-mcp-bindings.test.ts+4−2
@@ -26,6 +26,8 @@ const ConfigJson = Schema.fromJsonString( · 25 unmodified lines
2626 }),
2727 );
2828
29+const decodeConfigJson = Schema.decodeUnknownSync(ConfigJson);
30+
2931 const tempDirs: Array<string> = [];
3032
3133 const makeDbPath = () => {
@@ -87,7 +89,7 @@ describe("0007_normalize_plugin_secret_refs (mcp)", () => { · 55 unmodified lines
8789 expect(row.auth_secret_id).toBe("tok-secret");
8890 expect(row.auth_secret_prefix).toBe("Bearer ");
8991 // The auth key should be stripped from config json after migration.
90− const config = Schema.decodeUnknownSync(ConfigJson)(row.config);
92+ const config = decodeConfigJson(row.config);
9193 expect(config.auth).toBeUndefined();
9294 expect(config.transport).toBe("remote");
9395 expect(config.endpoint).toBe("https://example.com/mcp");
@@ -200,7 +202,7 @@ describe("0007_normalize_plugin_secret_refs (mcp)", () => { · 106 unmodified lines
200202 };
201203 expect(row.auth_kind).toBe("none");
202204 expect(row.auth_secret_id).toBeNull();
203− const config = Schema.decodeUnknownSync(ConfigJson)(row.config);
205+ const config = decodeConfigJson(row.config);
204206 expect(config.transport).toBe("stdio");
205207 expect(config.command).toBe("/usr/bin/server");
206208 after.close();
apps/local/src/server/migrate-openapi-bindings.test.ts+14−9
@@ -53,6 +53,13 @@ const CountRow = Schema.Struct({ · 52 unmodified lines
5353 n: Schema.Number,
5454 });
5555
56+const decodeBindingRows = Schema.decodeUnknownSync(Schema.Array(BindingRow));
57+const decodeQueryParamRows = Schema.decodeUnknownSync(Schema.Array(QueryParamRow));
58+const decodeFetchHeaderRows = Schema.decodeUnknownSync(Schema.Array(FetchHeaderRow));
59+const decodeFetchQueryParamRows = Schema.decodeUnknownSync(Schema.Array(FetchQueryParamRow));
60+const decodeTableInfoRows = Schema.decodeUnknownSync(Schema.Array(TableInfoRow));
61+const decodeCountRow = Schema.decodeUnknownSync(CountRow);
62+
5663 describe("0007_normalize_plugin_secret_refs (openapi)", () => {
5764 let dir: string;
5865 let dbPath: string;
@@ -139,7 +146,7 @@ describe("0007_normalize_plugin_secret_refs (openapi)", () => { · 80 unmodified lines
139146 closeDatabase(drizzleSqlite);
140147
141148 const after = openDatabase(dbPath, { readonly: true });
142− const rows = Schema.decodeUnknownSync(Schema.Array(BindingRow))(
149+ const rows = decodeBindingRows(
143150 after
144151 .prepare(
145152 "SELECT id, kind, secret_id, connection_id, text_value FROM openapi_source_binding ORDER BY id",
@@ -169,7 +176,7 @@ describe("0007_normalize_plugin_secret_refs (openapi)", () => { · 23 unmodified lines
169176 text_value: "literal",
170177 });
171178 // value json column dropped.
172− const cols = Schema.decodeUnknownSync(Schema.Array(TableInfoRow))(
179+ const cols = decodeTableInfoRows(
173180 after.prepare("PRAGMA table_info('openapi_source_binding')").all(),
174181 );
175182 expect(cols.some((c) => c.name === "value")).toBe(false);
@@ -213,7 +220,7 @@ describe("0007_normalize_plugin_secret_refs (openapi)", () => { · 37 unmodified lines
213220
214221 const after = openDatabase(dbPath, { readonly: true });
215222
216− const qpRows = Schema.decodeUnknownSync(Schema.Array(QueryParamRow))(
223+ const qpRows = decodeQueryParamRows(
217224 after
218225 .prepare(
219226 "SELECT name, kind, text_value, secret_id FROM openapi_source_query_param WHERE source_id = ? ORDER BY name",
@@ -231,7 +238,7 @@ describe("0007_normalize_plugin_secret_refs (openapi)", () => { · 11 unmodified lines
231238 text_value: "true",
232239 });
233240
234− const fetchHeaders = Schema.decodeUnknownSync(Schema.Array(FetchHeaderRow))(
241+ const fetchHeaders = decodeFetchHeaderRows(
235242 after
236243 .prepare(
237244 "SELECT name, kind, secret_id, secret_prefix FROM openapi_source_spec_fetch_header WHERE source_id = ?",
@@ -246,7 +253,7 @@ describe("0007_normalize_plugin_secret_refs (openapi)", () => { · 8 unmodified lines
246253 secret_prefix: "Bearer ",
247254 });
248255
249− const fetchQp = Schema.decodeUnknownSync(Schema.Array(FetchQueryParamRow))(
256+ const fetchQp = decodeFetchQueryParamRows(
250257 after
251258 .prepare(
252259 "SELECT name, secret_id FROM openapi_source_spec_fetch_query_param WHERE source_id = ?",
@@ -257,9 +264,7 @@ describe("0007_normalize_plugin_secret_refs (openapi)", () => { · 4 unmodified lines
257264 expect(fetchQp[0]).toMatchObject({ name: "token", secret_id: "fetch-qp" });
258265
259266 // Old json columns dropped.
260− const cols = Schema.decodeUnknownSync(Schema.Array(TableInfoRow))(
261− after.prepare("PRAGMA table_info('openapi_source')").all(),
262− );
267+ const cols = decodeTableInfoRows(after.prepare("PRAGMA table_info('openapi_source')").all());
263268 expect(cols.some((c) => c.name === "query_params")).toBe(false);
264269 expect(cols.some((c) => c.name === "invocation_config")).toBe(false);
265270 });
@@ -281,7 +286,7 @@ describe("0007_normalize_plugin_secret_refs (openapi)", () => { · 15 unmodified lines
281286 closeDatabase(drizzleSqlite);
282287
283288 const after = openDatabase(dbPath, { readonly: true });
284− const qpCount = Schema.decodeUnknownSync(CountRow)(
289+ const qpCount = decodeCountRow(
285290 after
286291 .prepare("SELECT count(*) as n FROM openapi_source_query_param WHERE source_id = ?")
287292 .get("bare"),
packages/core/api/src/oauth-popup.test.ts+6−6
@@ -21,6 +21,12 @@ type GoogleAuth = { · 20 unmodified lines
2121 refreshTokenSecretId: string | null;
2222 };
2323
24+const DomainErrorShape = Schema.Struct({
25+ _tag: Schema.Literal("DomainError"),
26+ message: Schema.String,
27+});
28+const isDomainError = Schema.is(DomainErrorShape);
29+
2430 // ---------------------------------------------------------------------------
2531 // popupDocument
2632 // ---------------------------------------------------------------------------
@@ -172,12 +178,6 @@ describe("runOAuthCallback", () => { · 145 unmodified lines
172178 class DomainError extends Data.TaggedError("DomainError")<{
173179 readonly message: string;
174180 }> {}
175− const isDomainError = Schema.is(
176− Schema.Struct({
177− _tag: Schema.Literal("DomainError"),
178− message: Schema.String,
179− }),
180− );
181181 const html = await Effect.runPromise(
182182 runOAuthCallback<GoogleAuth, DomainError, never>({
183183 complete: () => Effect.fail(new DomainError({ message: "Code expired" })),
packages/core/api/src/observability.ts+3−1
@@ -115,12 +115,14 @@ export const capture = <A, E, R>( · 114 unmodified lines
115115 * widened `YieldableError` channel on `ExecutionEngineService` is
116116 * narrowed to `InternalError` before leaving the handler body.
117117 */
118+const isInternalError = Schema.is(InternalError);
119+
118120 export const captureEngineError = <A, R>(
119121 eff: Effect.Effect<A, Cause.YieldableError, R>,
120122 ): Effect.Effect<A, InternalError, R> =>
121123 eff.pipe(
122124 Effect.catch((err) =>
123− Schema.is(InternalError)(err)
125+ isInternalError(err)
124126 ? Effect.fail(err)
125127 : resolveCapture.pipe(
126128 Effect.flatMap((c) => c.captureException(Cause.fail(err))),
packages/core/config/src/config.test.ts+5−3
@@ -14,6 +14,8 @@ import { · 13 unmodified lines
1414 removeSecretFromConfig,
1515 } from "./write";
1616
17+const decodeExecutorFileConfig = Schema.decodeUnknownSync(ExecutorFileConfig);
18+
1719 const withTmpDir = <A, E>(fn: (dir: string) => Effect.Effect<A, E, FileSystem.FileSystem>) =>
1820 Effect.gen(function* () {
1921 const fs = yield* FileSystem.FileSystem;
@@ -26,7 +28,7 @@ const withTmpDir = <A, E>(fn: (dir: string) => Effect.Effect<A, E, FileSystem.Fi · 6 unmodified lines
2628 describe("ExecutorFileConfig schema", () => {
2729 it("decodes a minimal config", () => {
2830 const raw = { sources: [] };
29− const result = Schema.decodeUnknownSync(ExecutorFileConfig)(raw);
31+ const result = decodeExecutorFileConfig(raw);
3032 expect(result.sources).toEqual([]);
3133 });
3234
@@ -70,7 +72,7 @@ describe("ExecutorFileConfig schema", () => { · 37 unmodified lines
7072 },
7173 };
7274
73− const result = Schema.decodeUnknownSync(ExecutorFileConfig)(raw);
75+ const result = decodeExecutorFileConfig(raw);
7476 expect(result.sources).toHaveLength(4);
7577 expect(result.name).toBe("test");
7678 expect(result.secrets!["my-token"]!.name).toBe("My Token");
@@ -80,7 +82,7 @@ describe("ExecutorFileConfig schema", () => { · 3 unmodified lines
8082 const raw = {
8183 sources: [{ kind: "invalid", endpoint: "http://example.com" }],
8284 };
83− expect(() => Schema.decodeUnknownSync(ExecutorFileConfig)(raw)).toThrow();
85+ expect(() => decodeExecutorFileConfig(raw)).toThrow();
8486 });
8587 });
8688
packages/core/sdk/src/executor.ts+5−5
@@ -347,14 +347,16 @@ const staticDeclToSource = (decl: StaticSourceDecl, pluginId: string): Source => · 346 unmodified lines
347347 runtime: true,
348348 });
349349
350+const decodeJsonFromString = Schema.decodeUnknownOption(Schema.UnknownFromJsonString);
351+
350352 const decodeJsonColumn = (value: unknown): unknown => {
351353 if (value === null || value === undefined) return undefined;
352354 if (typeof value !== "string") return value;
353− return Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(value).pipe(
354− Option.getOrElse(() => value),
355− );
355+ return decodeJsonFromString(value).pipe(Option.getOrElse(() => value));
356356 };
357357
358+const decodeProviderState = Schema.decodeUnknownOption(ConnectionProviderState);
359+
358360 const rowToTool = (row: ToolRow, annotations?: ToolAnnotations): Tool => ({
359361 id: row.id,
360362 sourceId: row.source_id,
@@ -1177,8 +1179,6 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = []> · 816 unmodified lines
11771179 // behavior under the new SDK orchestration stays identical.
11781180 const CONNECTION_REFRESH_SKEW_MS = 60_000;
11791181
1180− const decodeProviderState = Schema.decodeUnknownOption(ConnectionProviderState);
1181−
11821182 const rowToConnection = (row: ConnectionRow): ConnectionRef =>
11831183 new ConnectionRef({
11841184 id: ConnectionId.make(row.id),
packages/core/sdk/src/oauth-service.ts+15−17
@@ -137,11 +137,15 @@ type OAuthSessionPayload = typeof OAuthSessionPayload.Type; · 136 unmodified lines
137137 const decodeSessionPayload = Schema.decodeUnknownSync(OAuthSessionPayload);
138138 const encodeSessionPayload = Schema.encodeSync(OAuthSessionPayload);
139139
140+const UnknownFromJsonString = Schema.fromJsonString(Schema.Unknown);
141+const decodeUnknownJsonOption = Schema.decodeUnknownOption(UnknownFromJsonString);
142+
143+const decodeProviderStateSync = Schema.decodeUnknownSync(OAuthProviderStateSchema);
144+const encodeProviderStateSync = Schema.encodeSync(OAuthProviderStateSchema);
145+
140146 const coerceJson = (value: unknown): unknown => {
141147 if (typeof value !== "string") return value;
142− return Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown))(value).pipe(
143− Option.getOrElse(() => value),
144− );
148+ return decodeUnknownJsonOption(value).pipe(Option.getOrElse(() => value));
145149 };
146150
147151 const stringArray = (value: unknown): readonly string[] =>
@@ -167,7 +171,7 @@ const decodeProviderState = (value: unknown): OAuthProviderState => { · 19 unmodified lines
167171 if (record && !("kind" in record) && "flow" in record && "tokenUrl" in record) {
168172 const flow = record.flow;
169173 if (flow === "authorizationCode") {
170− return Schema.decodeUnknownSync(OAuthProviderStateSchema)({
174+ return decodeProviderStateSync({
171175 kind: "authorization-code",
172176 tokenEndpoint: record.tokenUrl,
173177 issuerUrl: originOrNull(record.authorizationEndpoint),
@@ -178,7 +182,7 @@ const decodeProviderState = (value: unknown): OAuthProviderState => { · 4 unmodified lines
178182 });
179183 }
180184 if (flow === "clientCredentials") {
181− return Schema.decodeUnknownSync(OAuthProviderStateSchema)({
185+ return decodeProviderStateSync({
182186 kind: "client-credentials",
183187 tokenEndpoint: record.tokenUrl,
184188 clientIdSecretId: record.clientIdSecretId,
@@ -192,7 +196,7 @@ const decodeProviderState = (value: unknown): OAuthProviderState => { · 7 unmodified lines
192196
193197 if (record && !("kind" in record) && "clientIdSecretId" in record && "scopes" in record) {
194198 const scopes = stringArray(record.scopes);
195− return Schema.decodeUnknownSync(OAuthProviderStateSchema)({
199+ return decodeProviderStateSync({
196200 kind: "authorization-code",
197201 tokenEndpoint: "https://oauth2.googleapis.com/token",
198202 issuerUrl: "https://accounts.google.com",
@@ -208,7 +212,7 @@ const decodeProviderState = (value: unknown): OAuthProviderState => { · 9 unmodified lines
208212 const authorizationServerMetadata = isRecord(record.authorizationServerMetadata)
209213 ? record.authorizationServerMetadata
210214 : null;
211− return Schema.decodeUnknownSync(OAuthProviderStateSchema)({
215+ return decodeProviderStateSync({
212216 kind: "dynamic-dcr",
213217 tokenEndpoint:
214218 typeof record.tokenEndpoint === "string"
@@ -233,7 +237,7 @@ const decodeProviderState = (value: unknown): OAuthProviderState => { · 18 unmodified lines
233237 });
234238 }
235239
236− return Schema.decodeUnknownSync(OAuthProviderStateSchema)(raw);
240+ return decodeProviderStateSync(raw);
237241 };
238242
239243 // ---------------------------------------------------------------------------
@@ -605,10 +609,7 @@ export const makeOAuth2Service = ( · 365 unmodified lines
605609 refreshToken: null,
606610 expiresAt,
607611 oauthScope: tokens.scope ?? null,
608− providerState: Schema.encodeSync(OAuthProviderStateSchema)(providerState) as Record<
609− string,
610− unknown
611− >,
612+ providerState: encodeProviderStateSync(providerState) as Record<string, unknown>,
612613 }),
613614 )
614615 .pipe(
@@ -847,10 +848,7 @@ export const makeOAuth2Service = ( · 232 unmodified lines
847848 : null,
848849 expiresAt: connectionExpiresAt,
849850 oauthScope: exchangeResult.tokens.scope ?? null,
850− providerState: Schema.encodeSync(OAuthProviderStateSchema)(providerState) as Record<
851− string,
852− unknown
853− >,
851+ providerState: encodeProviderStateSync(providerState) as Record<string, unknown>,
854852 }),
855853 )
856854 .pipe(
@@ -1219,7 +1217,7 @@ export const makeOAuth2Service = ( · 362 unmodified lines
12191217 refreshToken: tokens.refresh_token,
12201218 expiresAt,
12211219 oauthScope: tokens.scope ?? input.oauthScope,
1222− providerState: Schema.encodeSync(OAuthProviderStateSchema)({
1220+ providerState: encodeProviderStateSync({
12231221 ...state,
12241222 tokenEndpoint,
12251223 scope: tokens.scope ?? state.scope,
packages/core/storage-core/src/factory.ts+3−3
@@ -81,6 +81,8 @@ const withApplyDefault = ( · 80 unmodified lines
8181 return value;
8282 };
8383
84+const decodeJsonFromString = Schema.decodeUnknownOption(Schema.UnknownFromJsonString);
85+
8486 // ---------------------------------------------------------------------------
8587 // Factory
8688 // ---------------------------------------------------------------------------
@@ -199,9 +201,7 @@ export const createAdapter = (options: CreateAdapterOptions): DBAdapter => { · 112 unmodified lines
199201 };
200202
201203 const decodeJsonFallback = (value: string): unknown =>
202− Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(value).pipe(
203− Option.getOrElse((): unknown => value),
204− );
204+ decodeJsonFromString(value).pipe(Option.getOrElse((): unknown => value));
205205
206206 const decodeValue = (attr: DBFieldAttribute | undefined, value: unknown): unknown => {
207207 if (value === undefined || value === null) return value;
packages/plugins/google-discovery/src/react/AddGoogleDiscoverySource.tsx+5−7
@@ -51,15 +51,13 @@ import { GOOGLE_DISCOVERY_OAUTH_POPUP_NAME, googleDiscoveryOAuthStrategy } from · 50 unmodified lines
5151 import { googleDiscoveryPresets, type GoogleDiscoveryPreset } from "../sdk/presets";
5252
5353 const ErrorMessage = Schema.Struct({ message: Schema.String });
54+const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage);
5455
5556 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− );
57+ Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeErrorMessage), {
58+ onNone: () => fallback,
59+ onSome: ({ message }) => message,
60+ });
6361
6462 type GoogleAuthKind = "none" | "oauth2";
6563
packages/plugins/graphql/src/react/AddGraphqlSource.tsx+5−7
@@ -40,15 +40,13 @@ import { initialGraphqlCredentials } from "./defaults"; · 39 unmodified lines
4040 import type { HeaderValue } from "../sdk/types";
4141
4242 const ErrorMessage = Schema.Struct({ message: Schema.String });
43+const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage);
4344
4445 const errorMessageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string =>
45− Option.match(
46− Option.flatMap(Exit.findErrorOption(exit), Schema.decodeUnknownOption(ErrorMessage)),
47− {
48− onNone: () => fallback,
49− onSome: ({ message }) => message,
50− },
51− );
46+ Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeErrorMessage), {
47+ onNone: () => fallback,
48+ onSome: ({ message }) => message,
49+ });
5250
5351 type AuthMode = "none" | "oauth2";
5452
packages/plugins/graphql/src/sdk/store.ts+8−2
@@ -110,11 +110,17 @@ export interface StoredOperation { · 109 unmodified lines
110110 readonly binding: OperationBinding;
111111 }
112112
113+const OperationBindingFromJsonString = Schema.fromJsonString(OperationBinding);
114+const decodeOperationBindingFromJsonString = Schema.decodeUnknownSync(
115+ OperationBindingFromJsonString,
116+);
117+const decodeOperationBinding = Schema.decodeUnknownSync(OperationBinding);
118+
113119 const decodeBinding = (value: unknown): OperationBinding => {
114120 if (typeof value === "string") {
115− return Schema.decodeUnknownSync(Schema.fromJsonString(OperationBinding))(value);
121+ return decodeOperationBindingFromJsonString(value);
116122 }
117− return Schema.decodeUnknownSync(OperationBinding)(value);
123+ return decodeOperationBinding(value);
118124 };
119125
120126 const encodeBinding = Schema.encodeSync(OperationBinding);
packages/plugins/mcp/src/react/AddMcpSource.tsx+5−7
@@ -56,15 +56,13 @@ import { probeMcpEndpoint, addMcpSourceOptimistic } from "./atoms"; · 55 unmodified lines
5656 import { mcpPresets, type McpPreset } from "../sdk/presets";
5757
5858 const ErrorMessage = Schema.Struct({ message: Schema.String });
59+const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage);
5960
6061 const errorMessageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string =>
61− Option.match(
62− Option.flatMap(Exit.findErrorOption(exit), Schema.decodeUnknownOption(ErrorMessage)),
63− {
64− onNone: () => fallback,
65− onSome: ({ message }) => message,
66− },
67− );
62+ Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeErrorMessage), {
63+ onNone: () => fallback,
64+ onSome: ({ message }) => message,
65+ });
6866
6967 // ---------------------------------------------------------------------------
7068 // Preset lookup
packages/plugins/openapi/src/react/AddOpenApiSource.tsx+5−7
@@ -86,15 +86,13 @@ export const OPENAPI_OAUTH_POPUP_NAME = "openapi-oauth"; · 85 unmodified lines
8686 export const OPENAPI_OAUTH_CALLBACK_PATH = "/api/oauth/callback";
8787
8888 const ErrorMessage = Schema.Struct({ message: Schema.String });
89+const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage);
8990
9091 const errorMessageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string =>
91− Option.match(
92− Option.flatMap(Exit.findErrorOption(exit), Schema.decodeUnknownOption(ErrorMessage)),
93− {
94− onNone: () => fallback,
95− onSome: ({ message }) => message,
96− },
97− );
92+ Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeErrorMessage), {
93+ onNone: () => fallback,
94+ onSome: ({ message }) => message,
95+ });
9896
9997 const substituteUrlVariables = (url: string, values: Record<string, string>): string => {
10098 let out = url;
packages/plugins/openapi/src/react/EditOpenApiSource.tsx+8−9
@@ -51,15 +51,14 @@ import { · 50 unmodified lines
5151 } from "../sdk/types";
5252
5353 const ErrorMessage = Schema.Struct({ message: Schema.String });
54+const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage);
55+const isOpenApiSourceBindingValue = Schema.is(OpenApiSourceBindingValue);
5456
5557 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− );
58+ Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeErrorMessage), {
59+ onNone: () => fallback,
60+ onSome: ({ message }) => message,
61+ });
6362
6463 type SlotDef =
6564 | {
@@ -137,12 +136,12 @@ const effectiveBindingForScope = ( · 71 unmodified lines
137136 const isSecretBindingValue = (
138137 value: unknown,
139138 ): value is Extract<OpenApiSourceBindingValueType, { readonly kind: "secret" }> =>
140− Schema.is(OpenApiSourceBindingValue)(value) && value.kind === "secret";
139+ isOpenApiSourceBindingValue(value) && value.kind === "secret";
141140
142141 const isConnectionBindingValue = (
143142 value: unknown,
144143 ): value is Extract<OpenApiSourceBindingValueType, { readonly kind: "connection" }> =>
145− Schema.is(OpenApiSourceBindingValue)(value) && value.kind === "connection";
144+ isOpenApiSourceBindingValue(value) && value.kind === "connection";
146145
147146 export default function EditOpenApiSource(props: {
148147 readonly sourceId: string;
packages/plugins/openapi/src/sdk/non-json-body.test.ts+3−2
@@ -33,6 +33,7 @@ const JsonNameBody = Schema.fromJsonString( · 32 unmodified lines
3333 name: Schema.String,
3434 }),
3535 );
36+const decodeJsonNameBody = Schema.decodeUnknownSync(JsonNameBody);
3637
3738 const memoryProvider: SecretProvider = (() => {
3839 const store = new Map<string, string>();
@@ -216,7 +217,7 @@ describe("OpenAPI non-JSON request body dispatch", () => { · 177 unmodified lines
216217 expect(captured.contentType).toBe("text/xml");
217218 const body = captured.body.toString("utf8");
218219 expect(body).not.toBe("[object Object]");
219− expect(Schema.decodeUnknownSync(JsonNameBody)(body)).toEqual({ name: "Acme" });
220+ expect(decodeJsonNameBody(body)).toEqual({ name: "Acme" });
220221 }),
221222 );
222223
@@ -364,7 +365,7 @@ describe("OpenAPI non-JSON request body dispatch", () => { · 141 unmodified lines
364365 );
365366
366367 expect(captured.contentType).toBe("application/json");
367− expect(Schema.decodeUnknownSync(JsonNameBody)(captured.body.toString("utf8"))).toEqual({
368+ expect(decodeJsonNameBody(captured.body.toString("utf8"))).toEqual({
368369 name: "Acme",
369370 });
370371 }),
packages/plugins/openapi/src/sdk/store.ts+3−1
@@ -287,8 +287,10 @@ const SourceNameStorageRow = Schema.Struct({ · 286 unmodified lines
287287 });
288288 const decodeSourceNameStorageRow = Schema.decodeUnknownSync(SourceNameStorageRow);
289289
290+const decodeStorageString = Schema.decodeUnknownSync(Schema.String);
291+
290292 const decodeStorageDate = (value: unknown): Date =>
291− value instanceof Date ? value : new Date(Schema.decodeUnknownSync(Schema.String)(value));
293+ value instanceof Date ? value : new Date(decodeStorageString(value));
292294
293295 interface ChildRow {
294296 readonly id: string;
packages/plugins/workos-vault/src/sdk/client.ts+2−1
@@ -26,12 +26,13 @@ const WORKOS_KEK_NOT_READY_MESSAGE = · 25 unmodified lines
2626 const CauseWithStatusSchema = Schema.Struct({
2727 status: Schema.Number,
2828 });
29+const decodeCauseWithStatusOption = Schema.decodeUnknownOption(CauseWithStatusSchema);
2930
3031 const statusFromWorkOSCause = (cause: unknown): number | undefined => {
3132 if (cause instanceof GenericServerException || cause instanceof NotFoundException) {
3233 return cause.status;
3334 }
34− return Option.match(Schema.decodeUnknownOption(CauseWithStatusSchema)(cause), {
35+ return Option.match(decodeCauseWithStatusOption(cause), {
3536 onNone: () => undefined,
3637 onSome: (decoded) => decoded.status,
3738 });
scripts/oxlint-plugin-executor.js+2−0
@@ -5,6 +5,7 @@ import noEffectEscapeHatch from "./oxlint-plugin-executor/rules/no-effect-escape · 4 unmodified lines
55 import noEffectInternalTags from "./oxlint-plugin-executor/rules/no-effect-internal-tags.js";
66 import noErrorConstructor from "./oxlint-plugin-executor/rules/no-error-constructor.js";
77 import noInlineObjectTypeAssertion from "./oxlint-plugin-executor/rules/no-inline-object-type-assertion.js";
8+import noInlineSchemaCompile from "./oxlint-plugin-executor/rules/no-inline-schema-compile.js";
89 import noInstanceofError from "./oxlint-plugin-executor/rules/no-instanceof-error.js";
910 import noInstanceofTaggedError from "./oxlint-plugin-executor/rules/no-instanceof-tagged-error.js";
1011 import noJsonParse from "./oxlint-plugin-executor/rules/no-json-parse.js";
@@ -42,6 +43,7 @@ export default { · 31 unmodified lines
4243 "no-error-constructor": noErrorConstructor,
4344 "no-ts-nocheck": noTsNocheck,
4445 "no-inline-object-type-assertion": noInlineObjectTypeAssertion,
46+ "no-inline-schema-compile": noInlineSchemaCompile,
4547 "no-instanceof-error": noInstanceofError,
4648 "no-instanceof-tagged-error": noInstanceofTaggedError,
4749 "no-json-parse": noJsonParse,
scripts/oxlint-plugin-executor/rules/no-inline-schema-compile.js+104−0
@@ -0,0 +1,104 @@
1+import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.js";
2+
3+// Schema methods that compile a parser/guard from a schema. Calling these
4+// allocates a new function per invocation; the result should be hoisted to
5+// module (or at least closure) scope.
6+const COMPILER_METHODS = new Set([
7+ "is",
8+ "asserts",
9+ "decode",
10+ "decodeSync",
11+ "decodePromise",
12+ "decodeOption",
13+ "decodeEither",
14+ "decodeUnknown",
15+ "decodeUnknownSync",
16+ "decodeUnknownPromise",
17+ "decodeUnknownOption",
18+ "decodeUnknownEither",
19+ "encode",
20+ "encodeSync",
21+ "encodePromise",
22+ "encodeOption",
23+ "encodeEither",
24+ "encodeUnknown",
25+ "encodeUnknownSync",
26+ "encodeUnknownPromise",
27+ "encodeUnknownOption",
28+ "encodeUnknownEither",
29+ "validate",
30+ "validateSync",
31+ "validatePromise",
32+ "validateOption",
33+ "validateEither",
34+ "parse",
35+ "parseSync",
36+ "parsePromise",
37+ "parseOption",
38+ "parseEither",
39+]);
40+
41+const getSchemaCompilerMethod = (callee) => {
42+ const expression = unwrapExpression(callee);
43+ if (expression?.type !== "MemberExpression") return undefined;
44+ const object = unwrapExpression(expression.object);
45+ if (!isIdentifier(object, "Schema")) return undefined;
46+ const method = getPropertyName(expression.property);
47+ return method && COMPILER_METHODS.has(method) ? method : undefined;
48+};
49+
50+const isNestedSchemaCall = (node) => {
51+ const expression = unwrapExpression(node);
52+ if (expression?.type !== "CallExpression") return false;
53+ const callee = unwrapExpression(expression.callee);
54+ if (callee?.type !== "MemberExpression") return false;
55+ const object = unwrapExpression(callee.object);
56+ return isIdentifier(object, "Schema");
57+};
58+
59+const messageHigh = (method) =>
60+ `Hoist Schema.${method}(...) to module scope: both the inline schema literal and the compiled function are rebuilt on every call. Move the compiled function to a module-level const.`;
61+
62+const messageMedium = (method) =>
63+ `Hoist Schema.${method}(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.`;
64+
65+export default {
66+ meta: {
67+ type: "problem",
68+ docs: {
69+ description:
70+ "Disallow Schema compiler calls (Schema.is/decode*/encode*/validate*/parse*) inside function bodies; hoist them to module scope.",
71+ },
72+ },
73+ create(context) {
74+ let functionDepth = 0;
75+
76+ const enterFunction = () => {
77+ functionDepth++;
78+ };
79+ const exitFunction = () => {
80+ functionDepth--;
81+ };
82+
83+ return {
84+ FunctionDeclaration: enterFunction,
85+ "FunctionDeclaration:exit": exitFunction,
86+ FunctionExpression: enterFunction,
87+ "FunctionExpression:exit": exitFunction,
88+ ArrowFunctionExpression: enterFunction,
89+ "ArrowFunctionExpression:exit": exitFunction,
90+
91+ CallExpression(node) {
92+ if (functionDepth === 0) return;
93+ const method = getSchemaCompilerMethod(node.callee);
94+ if (!method) return;
95+ const firstArg = node.arguments[0];
96+ const high = firstArg && isNestedSchemaCall(firstArg);
97+ context.report({
98+ node: node.callee,
99+ message: high ? messageHigh(method) : messageMedium(method),
100+ });
101+ },
102+ };
103+ },
104+};