Add real protocol testing fixtures
Unified · Split
| @@ -0,0 +1,58 @@ | |||
| 1 | --- | ||
| 2 | name: wrdn-effect-raw-fetch-boundary | ||
| 3 | description: Route HTTP through Effect boundaries instead of raw fetch. Use when lint flags executor/no-raw-fetch or when adding networked protocol/provider code. | ||
| 4 | allowed-tools: Read Grep Glob Bash | ||
| 5 | --- | ||
| 6 | |||
| 7 | HTTP in core SDK and protocol plugins should go through Effect services so tests | ||
| 8 | can replace real networks with local protocol fixtures or mock `HttpClient` | ||
| 9 | layers. | ||
| 10 | |||
| 11 | ## Fix Shape | ||
| 12 | |||
| 13 | - Prefer `effect/unstable/http` `HttpClient` and `HttpClientRequest` for | ||
| 14 | ordinary HTTP calls. | ||
| 15 | - Accept a `Layer.Layer<HttpClient.HttpClient>` option on plugin/provider code | ||
| 16 | when callers need to inject a test client. | ||
| 17 | - In tests, use real local servers plus `FetchHttpClient.layer` or a captured | ||
| 18 | `HttpClient` service from the test layer. | ||
| 19 | - Do not add fetch-shaped abstractions in SDK or plugin seams. If a third-party | ||
| 20 | library truly only accepts `fetch`, keep the adapter in the owning package, | ||
| 21 | name the forced boundary explicitly, and delegate internally to Effect | ||
| 22 | `HttpClient`. | ||
| 23 | - Do not type new protocol/plugin seams as `typeof globalThis.fetch`; keep the | ||
| 24 | ambient runtime boundary out of domain and test APIs. | ||
| 25 | - Do not patch `globalThis.fetch`. Replace those tests with a local server, | ||
| 26 | `HttpClient` layer, or the approved Effect-backed adapter. | ||
| 27 | - Do not add a broad allowlist entry unless the file is a platform entrypoint | ||
| 28 | or a temporary migration target. | ||
| 29 | |||
| 30 | ## Approved Boundaries | ||
| 31 | |||
| 32 | - Worker/handler objects whose public API must expose a `fetch` method. | ||
| 33 | - Test calls to a worker or Miniflare binding's `.fetch(...)` method. | ||
| 34 | - Small adapters for libraries that only accept `fetch`, if the implementation | ||
| 35 | delegates to Effect `HttpClient`. | ||
| 36 | - Browser UI event handlers may remain raw only until app-side boundaries are | ||
| 37 | classified; prefer SDK/client APIs where available. | ||
| 38 | |||
| 39 | ## Bad | ||
| 40 | |||
| 41 | ```ts | ||
| 42 | const response = await fetch(url); | ||
| 43 | ``` | ||
| 44 | |||
| 45 | ```ts | ||
| 46 | const fetchImpl = options.fetch ?? globalThis.fetch; | ||
| 47 | ``` | ||
| 48 | |||
| 49 | ## Good | ||
| 50 | |||
| 51 | ```ts | ||
| 52 | const client = yield * HttpClient.HttpClient; | ||
| 53 | const response = yield * client.execute(HttpClientRequest.get(url)); | ||
| 54 | ``` | ||
| 55 | |||
| 56 | ```ts | ||
| 57 | const plugin = graphqlPlugin({ httpClientLayer: testHttpClientLayer }); | ||
| 58 | ``` | ||
| @@ -17,6 +17,7 @@ · 16 unmodified lines | |||
| 17 | "executor/no-manual-tag-check": "error", | 17 | "executor/no-manual-tag-check": "error", |
| 18 | "executor/no-promise-catch": "error", | 18 | "executor/no-promise-catch": "error", |
| 19 | "executor/no-promise-reject": "error", | 19 | "executor/no-promise-reject": "error", |
| 20 | "executor/no-raw-fetch": "error", | ||
| 20 | "executor/no-redundant-primitive-cast": "error", | 21 | "executor/no-redundant-primitive-cast": "error", |
| 21 | "executor/no-redundant-error-factory": "error", | 22 | "executor/no-redundant-error-factory": "error", |
| 22 | "executor/no-ts-nocheck": "error", | 23 | "executor/no-ts-nocheck": "error", |
| @@ -601,6 +601,8 @@ · 600 unmodified lines | |||
| 601 | "@executor-js/config": "workspace:*", | 601 | "@executor-js/config": "workspace:*", |
| 602 | "@executor-js/sdk": "workspace:*", | 602 | "@executor-js/sdk": "workspace:*", |
| 603 | "effect": "catalog:", | 603 | "effect": "catalog:", |
| 604 | "graphql": "^16.12.0", | ||
| 605 | "graphql-yoga": "^5.17.0", | ||
| 604 | }, | 606 | }, |
| 605 | "devDependencies": { | 607 | "devDependencies": { |
| 606 | "@effect/atom-react": "catalog:", | 608 | "@effect/atom-react": "catalog:", |
| @@ -1157,6 +1159,12 @@ · 550 unmodified lines | |||
| 1157 | 1159 | ||
| 1158 | "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="], | 1160 | "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="], |
| 1159 | 1161 | ||
| 1162 | "@envelop/core": ["@envelop/core@5.5.1", "", { "dependencies": { "@envelop/instrumentation": "^1.0.0", "@envelop/types": "^5.2.1", "@whatwg-node/promise-helpers": "^1.2.4", "tslib": "^2.5.0" } }, "sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw=="], | ||
| 1163 | |||
| 1164 | "@envelop/instrumentation": ["@envelop/instrumentation@1.0.0", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.2.1", "tslib": "^2.5.0" } }, "sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw=="], | ||
| 1165 | |||
| 1166 | "@envelop/types": ["@envelop/types@5.2.1", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.5.0" } }, "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg=="], | ||
| 1167 | |||
| 1160 | "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], | 1168 | "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], |
| 1161 | 1169 | ||
| 1162 | "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], | 1170 | "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], |
| @@ -1277,6 +1285,8 @@ · 114 unmodified lines | |||
| 1277 | 1285 | ||
| 1278 | "@executor-js/vite-plugin": ["@executor-js/vite-plugin@workspace:packages/core/vite-plugin"], | 1286 | "@executor-js/vite-plugin": ["@executor-js/vite-plugin@workspace:packages/core/vite-plugin"], |
| 1279 | 1287 | ||
| 1288 | "@fastify/busboy": ["@fastify/busboy@3.2.0", "", {}, "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA=="], | ||
| 1289 | |||
| 1280 | "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], | 1290 | "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], |
| 1281 | 1291 | ||
| 1282 | "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], | 1292 | "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], |
| @@ -1289,6 +1299,22 @@ · 6 unmodified lines | |||
| 1289 | 1299 | ||
| 1290 | "@giscus/react": ["@giscus/react@3.1.0", "", { "dependencies": { "giscus": "^1.6.0" }, "peerDependencies": { "react": "^16 || ^17 || ^18 || ^19", "react-dom": "^16 || ^17 || ^18 || ^19" } }, "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg=="], | 1300 | "@giscus/react": ["@giscus/react@3.1.0", "", { "dependencies": { "giscus": "^1.6.0" }, "peerDependencies": { "react": "^16 || ^17 || ^18 || ^19", "react-dom": "^16 || ^17 || ^18 || ^19" } }, "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg=="], |
| 1291 | 1301 | ||
| 1302 | "@graphql-tools/executor": ["@graphql-tools/executor@1.5.3", "", { "dependencies": { "@graphql-tools/utils": "^11.1.0", "@graphql-typed-document-node/core": "^3.2.0", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mgBFC0bsrZPZLu9EnydpMnAuQ8Iiq0CEbUcsmvXsm2/iYektGHDN/+bmb7hicA6dWZtdPfklYJmr21WD0GnOfA=="], | ||
| 1303 | |||
| 1304 | "@graphql-tools/merge": ["@graphql-tools/merge@9.1.9", "", { "dependencies": { "@graphql-tools/utils": "^11.1.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iHUWNjRHeQRYdgIMIuChThOwoKzA9vrzYeslgfBo5eUYEyHGZCoDPjAavssoYXLwstYt1dZj2J22jSzc2DrN0Q=="], | ||
| 1305 | |||
| 1306 | "@graphql-tools/schema": ["@graphql-tools/schema@10.0.33", "", { "dependencies": { "@graphql-tools/merge": "^9.1.9", "@graphql-tools/utils": "^11.1.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-O6P3RIftO0jafnSsFAqpjurUuUxJ43s/AdPVLQsBkI6y4Ic/tKm4C1Qm1KKQsCDTOxXPJClh/v3g7k7yLKCFBQ=="], | ||
| 1307 | |||
| 1308 | "@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], | ||
| 1309 | |||
| 1310 | "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], | ||
| 1311 | |||
| 1312 | "@graphql-yoga/logger": ["@graphql-yoga/logger@2.0.1", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA=="], | ||
| 1313 | |||
| 1314 | "@graphql-yoga/subscription": ["@graphql-yoga/subscription@5.0.5", "", { "dependencies": { "@graphql-yoga/typed-event-target": "^3.0.2", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/events": "^0.1.0", "tslib": "^2.8.1" } }, "sha512-oCMWOqFs6QV96/NZRt/ZhTQvzjkGB4YohBOpKM4jH/lDT4qb7Lex/aGCxpi/JD9njw3zBBtMqxbaC22+tFHVvw=="], | ||
| 1315 | |||
| 1316 | "@graphql-yoga/typed-event-target": ["@graphql-yoga/typed-event-target@3.0.2", "", { "dependencies": { "@repeaterjs/repeater": "^3.0.4", "tslib": "^2.8.1" } }, "sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA=="], | ||
| 1317 | |||
| 1292 | "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], | 1318 | "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], |
| 1293 | 1319 | ||
| 1294 | "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], | 1320 | "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], |
| @@ -1939,6 +1965,8 @@ · 644 unmodified lines | |||
| 1939 | 1965 | ||
| 1940 | "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], | 1966 | "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], |
| 1941 | 1967 | ||
| 1968 | "@repeaterjs/repeater": ["@repeaterjs/repeater@3.0.6", "", {}, "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA=="], | ||
| 1969 | |||
| 1942 | "@rhyssul/portless": ["@rhyssul/portless@0.13.3", "", { "os": [ "linux", "win32", "darwin", ], "bin": { "portless": "dist/cli.js" } }, "sha512-oPvwXJIIkRg2i1CUEUsz8sAdFSf0Fzzv+NmaacAsi6ZZTHxDOofbO6fGInVnbESIFOu4k8a/rXOZRF0aqLAUgw=="], | 1970 | "@rhyssul/portless": ["@rhyssul/portless@0.13.3", "", { "os": [ "linux", "win32", "darwin", ], "bin": { "portless": "dist/cli.js" } }, "sha512-oPvwXJIIkRg2i1CUEUsz8sAdFSf0Fzzv+NmaacAsi6ZZTHxDOofbO6fGInVnbESIFOu4k8a/rXOZRF0aqLAUgw=="], |
| 1943 | 1971 | ||
| 1944 | "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="], | 1972 | "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="], |
| @@ -2337,6 +2365,18 @@ · 392 unmodified lines | |||
| 2337 | 2365 | ||
| 2338 | "@vitest/utils": ["@vitest/utils@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug=="], | 2366 | "@vitest/utils": ["@vitest/utils@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug=="], |
| 2339 | 2367 | ||
| 2368 | "@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="], | ||
| 2369 | |||
| 2370 | "@whatwg-node/events": ["@whatwg-node/events@0.1.2", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ=="], | ||
| 2371 | |||
| 2372 | "@whatwg-node/fetch": ["@whatwg-node/fetch@0.10.13", "", { "dependencies": { "@whatwg-node/node-fetch": "^0.8.3", "urlpattern-polyfill": "^10.0.0" } }, "sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q=="], | ||
| 2373 | |||
| 2374 | "@whatwg-node/node-fetch": ["@whatwg-node/node-fetch@0.8.5", "", { "dependencies": { "@fastify/busboy": "^3.1.1", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/promise-helpers": "^1.3.2", "tslib": "^2.6.3" } }, "sha512-4xzCl/zphPqlp9tASLVeUhB5+WJHbuWGYpfoC2q1qh5dw0AqZBW7L27V5roxYWijPxj4sspRAAoOH3d2ztaHUQ=="], | ||
| 2375 | |||
| 2376 | "@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@1.3.2", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA=="], | ||
| 2377 | |||
| 2378 | "@whatwg-node/server": ["@whatwg-node/server@0.10.18", "", { "dependencies": { "@envelop/instrumentation": "^1.0.0", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/fetch": "^0.10.13", "@whatwg-node/promise-helpers": "^1.3.2", "tslib": "^2.6.3" } }, "sha512-kMwLlxUbduttIgaPdSkmEarFpP+mSY8FEm+QWMBRJwxOHWkri+cxd8KZHO9EMrB9vgUuz+5WEaCawaL5wGVoXg=="], | ||
| 2379 | |||
| 2340 | "@workos-inc/node": ["@workos-inc/node@8.13.0", "", { "dependencies": { "eventemitter3": "^5.0.4" } }, "sha512-NgQKHpwh8AbT4KvAsW91Y+4f4jja2IvFPQ5atcy5NUxUMVRgXzRFEee3erawfXrTmiCVqJjd9PljHySKBXmHKQ=="], | 2380 | "@workos-inc/node": ["@workos-inc/node@8.13.0", "", { "dependencies": { "eventemitter3": "^5.0.4" } }, "sha512-NgQKHpwh8AbT4KvAsW91Y+4f4jja2IvFPQ5atcy5NUxUMVRgXzRFEee3erawfXrTmiCVqJjd9PljHySKBXmHKQ=="], |
| 2341 | 2381 | ||
| 2342 | "@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="], | 2382 | "@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="], |
| @@ -2637,6 +2677,8 @@ · 294 unmodified lines | |||
| 2637 | 2677 | ||
| 2638 | "cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="], | 2678 | "cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="], |
| 2639 | 2679 | ||
| 2680 | "cross-inspect": ["cross-inspect@1.0.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A=="], | ||
| 2681 | |||
| 2640 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], | 2682 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], |
| 2641 | 2683 | ||
| 2642 | "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], | 2684 | "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], |
| @@ -3083,6 +3125,10 @@ · 440 unmodified lines | |||
| 3083 | 3125 | ||
| 3084 | "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], | 3126 | "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], |
| 3085 | 3127 | ||
| 3128 | "graphql": ["graphql@16.14.0", "", {}, "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q=="], | ||
| 3129 | |||
| 3130 | "graphql-yoga": ["graphql-yoga@5.21.0", "", { "dependencies": { "@envelop/core": "^5.5.1", "@envelop/instrumentation": "^1.0.0", "@graphql-tools/executor": "^1.5.0", "@graphql-tools/schema": "^10.0.11", "@graphql-tools/utils": "^10.11.0", "@graphql-yoga/logger": "^2.0.1", "@graphql-yoga/subscription": "^5.0.5", "@whatwg-node/fetch": "^0.10.6", "@whatwg-node/promise-helpers": "^1.3.2", "@whatwg-node/server": "^0.10.14", "lru-cache": "^10.0.0", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^15.2.0 || ^16.0.0" } }, "sha512-PS37UoDihx8209RRl1ogttzWevNYDnGvP7beHkwHzUpUdfZTHsVRTVe1ysGXre1EjwUAePbpez302YSrq70Ngw=="], | ||
| 3131 | |||
| 3086 | "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], | 3132 | "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], |
| 3087 | 3133 | ||
| 3088 | "h3-v2": ["h3@2.0.1-rc.20", "", { "dependencies": { "rou3": "^0.8.1", "srvx": "^0.11.13" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg=="], | 3134 | "h3-v2": ["h3@2.0.1-rc.20", "", { "dependencies": { "rou3": "^0.8.1", "srvx": "^0.11.13" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg=="], |
| @@ -3431,7 +3477,7 @@ · 342 unmodified lines | |||
| 3431 | 3477 | ||
| 3432 | "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], | 3478 | "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], |
| 3433 | 3479 | ||
| 3434 | "lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="], | 3480 | "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], |
| 3435 | 3481 | ||
| 3436 | "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], | 3482 | "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], |
| 3437 | 3483 | ||
| @@ -4417,6 +4463,8 @@ · 979 unmodified lines | |||
| 4417 | 4463 | ||
| 4418 | "url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="], | 4464 | "url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="], |
| 4419 | 4465 | ||
| 4466 | "urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="], | ||
| 4467 | |||
| 4420 | "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], | 4468 | "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], |
| 4421 | 4469 | ||
| 4422 | "use-merge-value": ["use-merge-value@1.2.0", "", { "peerDependencies": { "react": ">= 16.x" } }, "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw=="], | 4470 | "use-merge-value": ["use-merge-value@1.2.0", "", { "peerDependencies": { "react": ">= 16.x" } }, "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw=="], |
| @@ -4645,6 +4693,12 @@ · 222 unmodified lines | |||
| 4645 | 4693 | ||
| 4646 | "@executor-js/storage-postgres/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], | 4694 | "@executor-js/storage-postgres/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], |
| 4647 | 4695 | ||
| 4696 | "@graphql-tools/executor/@graphql-tools/utils": ["@graphql-tools/utils@11.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag=="], | ||
| 4697 | |||
| 4698 | "@graphql-tools/merge/@graphql-tools/utils": ["@graphql-tools/utils@11.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag=="], | ||
| 4699 | |||
| 4700 | "@graphql-tools/schema/@graphql-tools/utils": ["@graphql-tools/utils@11.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag=="], | ||
| 4701 | |||
| 4648 | "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], | 4702 | "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], |
| 4649 | 4703 | ||
| 4650 | "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], | 4704 | "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], |
| @@ -4679,8 +4733,6 @@ · 28 unmodified lines | |||
| 4679 | 4733 | ||
| 4680 | "@modelcontextprotocol/sdk/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], | 4734 | "@modelcontextprotocol/sdk/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], |
| 4681 | 4735 | ||
| 4682 | "@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], | ||
| 4683 | |||
| 4684 | "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="], | 4736 | "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="], |
| 4685 | 4737 | ||
| 4686 | "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="], | 4738 | "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="], |
| @@ -4885,8 +4937,6 @@ · 198 unmodified lines | |||
| 4885 | 4937 | ||
| 4886 | "cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], | 4938 | "cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], |
| 4887 | 4939 | ||
| 4888 | "cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], | ||
| 4889 | |||
| 4890 | "cacache/p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], | 4940 | "cacache/p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], |
| 4891 | 4941 | ||
| 4892 | "cheerio/undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="], | 4942 | "cheerio/undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="], |
| @@ -5027,8 +5077,6 @@ · 134 unmodified lines | |||
| 5027 | 5077 | ||
| 5028 | "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], | 5078 | "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], |
| 5029 | 5079 | ||
| 5030 | "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], | ||
| 5031 | |||
| 5032 | "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], | 5080 | "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], |
| 5033 | 5081 | ||
| 5034 | "posthog-js/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], | 5082 | "posthog-js/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], |
| @@ -5101,6 +5149,8 @@ · 66 unmodified lines | |||
| 5101 | 5149 | ||
| 5102 | "unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], | 5150 | "unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], |
| 5103 | 5151 | ||
| 5152 | "unstorage/lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="], | ||
| 5153 | |||
| 5104 | "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], | 5154 | "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], |
| 5105 | 5155 | ||
| 5106 | "wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], | 5156 | "wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], |
| @@ -46,7 +46,7 @@ Testing services belong behind explicit `./testing` subpath exports: · 45 unmodified lines | |||
| 46 | 46 | ||
| 47 | The plugin packages own the protocol details. Core SDK testing owns only boring | 47 | The plugin packages own the protocol details. Core SDK testing owns only boring |
| 48 | Executor fixtures such as memory adapters, memory secrets, auto-accept | 48 | Executor fixtures such as memory adapters, memory secrets, auto-accept |
| 49 | elicitation, and fetch-shaped adapters that delegate to Effect HTTP. | 49 | elicitation, and other Effect-native test primitives. |
| 50 | 50 | ||
| 51 | Use `TestLayer` / `TestLayers` naming for realistic local test services. | 51 | Use `TestLayer` / `TestLayers` naming for realistic local test services. |
| 52 | Reserve `LiveLayer` / `LiveLayers` for production wiring. These servers are | 52 | Reserve `LiveLayer` / `LiveLayers` for production wiring. These servers are |
| @@ -124,7 +124,6 @@ Suggested file layout: · 71 unmodified lines | |||
| 124 | ```txt | 124 | ```txt |
| 125 | packages/core/sdk/src/testing.ts | 125 | packages/core/sdk/src/testing.ts |
| 126 | packages/core/sdk/src/testing/ | 126 | packages/core/sdk/src/testing/ |
| 127 | fetch.ts | ||
| 128 | memory-secrets.ts | 127 | memory-secrets.ts |
| 129 | 128 | ||
| 130 | packages/plugins/openapi/src/testing/ | 129 | packages/plugins/openapi/src/testing/ |
| @@ -191,9 +190,6 @@ Useful additions: · 60 unmodified lines | |||
| 191 | in-test secret provider definitions. | 190 | in-test secret provider definitions. |
| 192 | - `makeExecutorTestLayer(options)`, if repeated `createExecutor(makeTestConfig)` | 191 | - `makeExecutorTestLayer(options)`, if repeated `createExecutor(makeTestConfig)` |
| 193 | setup becomes noisy. | 192 | setup becomes noisy. |
| 194 | - `makeFetchFromHttpClient` or `FetchTestAdapter`, for third-party APIs that | ||
| 195 | require a fetch-shaped callback while still routing requests through Effect's | ||
| 196 | HTTP client. | ||
| 197 | - Small request/response capture primitives built on `Ref`, only if multiple | 193 | - Small request/response capture primitives built on `Ref`, only if multiple |
| 198 | plugins need the same shape. | 194 | plugins need the same shape. |
| 199 | 195 | ||
| @@ -243,8 +239,8 @@ Work: · 43 unmodified lines | |||
| 243 | - Support a fresh `McpServer` factory per session, matching the current helper. | 239 | - Support a fresh `McpServer` factory per session, matching the current helper. |
| 244 | - Keep malformed/non-MCP HTTP endpoints as explicit test layers for probe | 240 | - Keep malformed/non-MCP HTTP endpoints as explicit test layers for probe |
| 245 | behavior. | 241 | behavior. |
| 246 | - Replace fetch-injected probe tests with real local HTTP servers and the | 242 | - Replace fetch-injected probe tests with real local HTTP servers and |
| 247 | Effect-backed fetch adapter. | 243 | Effect-native HTTP boundaries. |
| 248 | 244 | ||
| 249 | Representative scenarios: | 245 | Representative scenarios: |
| 250 | 246 | ||
| @@ -261,10 +257,11 @@ Representative scenarios: · 10 unmodified lines | |||
| 261 | GraphQL should gain a real executable schema server. This is the place where a | 257 | GraphQL should gain a real executable schema server. This is the place where a |
| 262 | new dependency may be justified. | 258 | new dependency may be justified. |
| 263 | 259 | ||
| 264 | Preferred first dependency is the core `graphql` package if it is enough: | 260 | Use `graphql-yoga` for the test server. The extra dependency is justified |
| 265 | define a schema, execute introspection and operations with `graphql(...)`, and | 261 | because the fixture needs to exercise a real JSON-over-HTTP GraphQL endpoint, |
| 266 | serve the standard JSON-over-HTTP GraphQL endpoint through Effect/node HTTP. | 262 | not only direct `graphql(...)` execution. Keep Vitest configured to inline the |
| 267 | Use `graphql-yoga` only if we need a fuller server feature set later. | 263 | Yoga/GraphQL dependency chain so executable schemas and server execution share |
| 264 | one GraphQL module realm. | ||
| 268 | 265 | ||
| 269 | Work: | 266 | Work: |
| 270 | 267 | ||
| @@ -292,19 +289,19 @@ Representative scenarios: · 21 unmodified lines | |||
| 292 | ## Fetch Boundary | 289 | ## Fetch Boundary |
| 293 | 290 | ||
| 294 | The target rule is: product and plugin code should not call raw `fetch` | 291 | The target rule is: product and plugin code should not call raw `fetch` |
| 295 | directly. HTTP should go through Effect HTTP or a small approved adapter that | 292 | directly. HTTP should go through Effect HTTP. If a third-party library truly |
| 296 | turns an Effect `HttpClient` into a fetch-shaped function for libraries that | 293 | forces a fetch-shaped callback, the adapter should live at that owning package's |
| 297 | force that API. | 294 | boundary rather than in shared SDK testing. |
| 298 | 295 | ||
| 299 | Boundary files may exist, but they should be explicit and scarce. Examples: | 296 | Boundary files may exist, but they should be explicit and scarce. Examples: |
| 300 | 297 | ||
| 301 | - a SDK adapter for `oauth4webapi` custom fetch | 298 | - a core OAuth adapter for `oauth4webapi` custom fetch, if that library forces it |
| 302 | - a MCP transport adapter if the upstream MCP SDK requires fetch-shaped input | 299 | - a MCP transport adapter if the upstream MCP SDK requires fetch-shaped input |
| 303 | - platform entrypoints that must implement a `fetch(request, env, ctx)` method | 300 | - platform entrypoints that must implement a `fetch(request, env, ctx)` method |
| 304 | - test harnesses that intentionally call a Worker binding's `fetch` method | 301 | - test harnesses that intentionally call a Worker binding's `fetch` method |
| 305 | 302 | ||
| 306 | Everything else should use Effect HTTP, Executor client APIs, or the approved | 303 | Everything else should use Effect HTTP, Executor client APIs, or an explicit |
| 307 | adapter. | 304 | package-local boundary. |
| 308 | 305 | ||
| 309 | Lint plan: | 306 | Lint plan: |
| 310 | 307 | ||
| @@ -319,17 +316,17 @@ Lint plan: · 8 unmodified lines | |||
| 319 | - Start with a narrow allowlist for known boundary files and remove entries as | 316 | - Start with a narrow allowlist for known boundary files and remove entries as |
| 320 | migrations land. | 317 | migrations land. |
| 321 | - Include error messages that tell the author which Effect HTTP or approved | 318 | - Include error messages that tell the author which Effect HTTP or approved |
| 322 | adapter to use. | 319 | package-local boundary to use. |
| 323 | 320 | ||
| 324 | The lint rule should land after the first adapter exists. Otherwise every fix | 321 | The lint rule can land once the core test primitives and at least one real |
| 325 | has to invent its own local escape hatch. | 322 | protocol fixture exist. Fetch-shaped adapters should be added only at forced |
| 323 | library boundaries during migration. | ||
| 326 | 324 | ||
| 327 | ## Migration Order | 325 | ## Migration Order |
| 328 | 326 | ||
| 329 | 1. Add `./testing` exports and SDK test primitives. | 327 | 1. Add `./testing` exports and SDK test primitives. |
| 330 | - Expose `@executor-js/sdk/testing`. | 328 | - Expose `@executor-js/sdk/testing`. |
| 331 | - Add memory secrets test support. | 329 | - Add memory secrets test support. |
| 332 | - Add the Effect HTTP to fetch-shaped adapter. | ||
| 333 | - Keep `makeTestConfig` available from the root SDK. | 330 | - Keep `makeTestConfig` available from the root SDK. |
| 334 | 331 | ||
| 335 | 2. Build the GraphQL vertical slice. | 332 | 2. Build the GraphQL vertical slice. |
| @@ -344,8 +341,8 @@ has to invent its own local escape hatch. · 8 unmodified lines | |||
| 344 | - Migrate current tests to import from the new public testing subpaths. | 341 | - Migrate current tests to import from the new public testing subpaths. |
| 345 | 342 | ||
| 346 | 4. Replace global fetch patching. | 343 | 4. Replace global fetch patching. |
| 347 | - Convert core OAuth discovery/helper tests to real local servers or the | 344 | - Convert core OAuth discovery/helper tests to real local servers or |
| 348 | Effect-backed fetch adapter. | 345 | package-local Effect HTTP boundaries. |
| 349 | - Convert OpenAPI OAuth tests to the OAuth test server. | 346 | - Convert OpenAPI OAuth tests to the OAuth test server. |
| 350 | - Convert MCP probe tests to real local HTTP server layers. | 347 | - Convert MCP probe tests to real local HTTP server layers. |
| 351 | - Convert Google Discovery tests using the same boundary pattern. | 348 | - Convert Google Discovery tests using the same boundary pattern. |
| @@ -403,8 +400,8 @@ Turbo. Do not use `bun test`. · 51 unmodified lines | |||
| 403 | 400 | ||
| 404 | - Should `@executor-js/sdk/testing` be a new subpath around the existing | 401 | - Should `@executor-js/sdk/testing` be a new subpath around the existing |
| 405 | `src/testing.ts`, or should `src/testing.ts` become `src/testing/index.ts`? | 402 | `src/testing.ts`, or should `src/testing.ts` become `src/testing/index.ts`? |
| 406 | - Should GraphQL use only the `graphql` package, or should we adopt | 403 | - How much GraphQL HTTP behavior should live in the shared fixture versus |
| 407 | `graphql-yoga` once tests need more HTTP behavior? | 404 | scenario-specific schemas in individual tests? |
| 408 | - Which app-level raw fetch calls should remain approved platform boundaries | 405 | - Which app-level raw fetch calls should remain approved platform boundaries |
| 409 | versus being migrated to Effect HTTP? | 406 | versus being migrated to Effect HTTP? |
| 410 | - Should the raw fetch lint rule become repo-wide immediately with allowlists, | 407 | - Should the raw fetch lint rule become repo-wide immediately with allowlists, |
| @@ -19,7 +19,8 @@ · 18 unmodified lines | |||
| 19 | ".": "./src/index.ts", | 19 | ".": "./src/index.ts", |
| 20 | "./core": "./src/index.ts", | 20 | "./core": "./src/index.ts", |
| 21 | "./promise": "./src/promise.ts", | 21 | "./promise": "./src/promise.ts", |
| 22 | "./client": "./src/client.ts" | 22 | "./client": "./src/client.ts", |
| 23 | "./testing": "./src/testing.ts" | ||
| 23 | }, | 24 | }, |
| 24 | "publishConfig": { | 25 | "publishConfig": { |
| 25 | "access": "public", | 26 | "access": "public", |
| @@ -41,6 +42,12 @@ · 15 unmodified lines | |||
| 41 | "types": "./dist/client.d.ts", | 42 | "types": "./dist/client.d.ts", |
| 42 | "default": "./dist/client.js" | 43 | "default": "./dist/client.js" |
| 43 | } | 44 | } |
| 45 | }, | ||
| 46 | "./testing": { | ||
| 47 | "import": { | ||
| 48 | "types": "./dist/testing.d.ts", | ||
| 49 | "default": "./dist/testing.js" | ||
| 50 | } | ||
| 44 | } | 51 | } |
| 45 | } | 52 | } |
| 46 | }, | 53 | }, |
| @@ -1,11 +1,14 @@ | |||
| 1 | import { makeMemoryAdapter } from "@executor-js/storage-core/testing/memory"; | 1 | import { makeMemoryAdapter } from "@executor-js/storage-core/testing/memory"; |
| 2 | 2 | ||
| 3 | import { Effect } from "effect"; | ||
| 4 | |||
| 3 | import { makeInMemoryBlobStore } from "./blob"; | 5 | import { makeInMemoryBlobStore } from "./blob"; |
| 4 | import type { ExecutorConfig } from "./executor"; | 6 | import type { ExecutorConfig } from "./executor"; |
| 5 | import { collectSchemas } from "./executor"; | 7 | import { collectSchemas } from "./executor"; |
| 6 | import { ScopeId } from "./ids"; | 8 | import { ScopeId } from "./ids"; |
| 7 | import type { AnyPlugin } from "./plugin"; | 9 | import { definePlugin, type AnyPlugin } from "./plugin"; |
| 8 | import { Scope } from "./scope"; | 10 | import { Scope } from "./scope"; |
| 11 | import type { SecretProvider } from "./secrets"; | ||
| 9 | 12 | ||
| 10 | // --------------------------------------------------------------------------- | 13 | // --------------------------------------------------------------------------- |
| 11 | // makeTestConfig — build an ExecutorConfig backed by in-memory adapter + | 14 | // makeTestConfig — build an ExecutorConfig backed by in-memory adapter + |
| @@ -42,3 +45,31 @@ export const makeTestConfig = <const TPlugins extends readonly AnyPlugin[] = []> · 30 unmodified lines | |||
| 42 | onElicitation: "accept-all", | 45 | onElicitation: "accept-all", |
| 43 | }; | 46 | }; |
| 44 | }; | 47 | }; |
| 48 | |||
| 49 | export const memorySecretsPlugin = definePlugin(() => { | ||
| 50 | const store = new Map<string, string>(); | ||
| 51 | |||
| 52 | const provider: SecretProvider = { | ||
| 53 | key: "memory", | ||
| 54 | writable: true, | ||
| 55 | get: (id, scope) => Effect.sync(() => store.get(`${scope}\u0000${id}`) ?? null), | ||
| 56 | set: (id, value, scope) => | ||
| 57 | Effect.sync(() => { | ||
| 58 | store.set(`${scope}\u0000${id}`, value); | ||
| 59 | }), | ||
| 60 | delete: (id, scope) => Effect.sync(() => store.delete(`${scope}\u0000${id}`)), | ||
| 61 | list: () => | ||
| 62 | Effect.sync(() => | ||
| 63 | Array.from(store.keys()).map((key) => { | ||
| 64 | const name = key.split("\u0000", 2)[1] ?? key; | ||
| 65 | return { id: name, name }; | ||
| 66 | }), | ||
| 67 | ), | ||
| 68 | }; | ||
| 69 | |||
| 70 | return { | ||
| 71 | id: "memory-secrets" as const, | ||
| 72 | storage: () => ({}), | ||
| 73 | secretProviders: [provider], | ||
| 74 | }; | ||
| 75 | }); | ||
| @@ -5,6 +5,7 @@ export default defineConfig({ · 4 unmodified lines | |||
| 5 | index: "src/promise.ts", | 5 | index: "src/promise.ts", |
| 6 | core: "src/index.ts", | 6 | core: "src/index.ts", |
| 7 | client: "src/client.ts", | 7 | client: "src/client.ts", |
| 8 | testing: "src/testing.ts", | ||
| 8 | }, | 9 | }, |
| 9 | format: ["esm"], | 10 | format: ["esm"], |
| 10 | dts: false, | 11 | dts: false, |
| @@ -21,7 +21,8 @@ · 20 unmodified lines | |||
| 21 | "./api": "./src/api/index.ts", | 21 | "./api": "./src/api/index.ts", |
| 22 | "./react": "./src/react/index.ts", | 22 | "./react": "./src/react/index.ts", |
| 23 | "./presets": "./src/sdk/presets.ts", | 23 | "./presets": "./src/sdk/presets.ts", |
| 24 | "./client": "./src/react/plugin-client.tsx" | 24 | "./client": "./src/react/plugin-client.tsx", |
| 25 | "./testing": "./src/testing/index.ts" | ||
| 25 | }, | 26 | }, |
| 26 | "publishConfig": { | 27 | "publishConfig": { |
| 27 | "access": "public", | 28 | "access": "public", |
| @@ -37,6 +38,12 @@ · 9 unmodified lines | |||
| 37 | "types": "./dist/sdk/index.d.ts", | 38 | "types": "./dist/sdk/index.d.ts", |
| 38 | "default": "./dist/core.js" | 39 | "default": "./dist/core.js" |
| 39 | } | 40 | } |
| 41 | }, | ||
| 42 | "./testing": { | ||
| 43 | "import": { | ||
| 44 | "types": "./dist/testing/index.d.ts", | ||
| 45 | "default": "./dist/testing.js" | ||
| 46 | } | ||
| 40 | } | 47 | } |
| 41 | } | 48 | } |
| 42 | }, | 49 | }, |
| @@ -51,7 +58,9 @@ · 8 unmodified lines | |||
| 51 | "@effect/platform-node": "catalog:", | 58 | "@effect/platform-node": "catalog:", |
| 52 | "@executor-js/config": "workspace:*", | 59 | "@executor-js/config": "workspace:*", |
| 53 | "@executor-js/sdk": "workspace:*", | 60 | "@executor-js/sdk": "workspace:*", |
| 54 | "effect": "catalog:" | 61 | "effect": "catalog:", |
| 62 | "graphql": "^16.12.0", | ||
| 63 | "graphql-yoga": "^5.17.0" | ||
| 55 | }, | 64 | }, |
| 56 | "devDependencies": { | 65 | "devDependencies": { |
| 57 | "@effect/atom-react": "catalog:", | 66 | "@effect/atom-react": "catalog:", |
| @@ -220,8 +220,9 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* ( · 219 unmodified lines | |||
| 220 | ); | 220 | ); |
| 221 | 221 | ||
| 222 | if (response.status !== 200) { | 222 | if (response.status !== 200) { |
| 223 | const body = yield* response.text.pipe(Effect.catch(() => Effect.succeed("<unreadable>"))); | ||
| 223 | return yield* new GraphqlIntrospectionError({ | 224 | return yield* new GraphqlIntrospectionError({ |
| 224 | message: `Introspection failed with status ${response.status}`, | 225 | message: `Introspection failed with status ${response.status}: ${body.slice(0, 1_000)}`, |
| 225 | }); | 226 | }); |
| 226 | } | 227 | } |
| 227 | 228 | ||
| @@ -1,26 +1,24 @@ | |||
| 1 | import { describe, it, expect } from "@effect/vitest"; | 1 | import { describe, it, expect } from "@effect/vitest"; |
| 2 | import { Data, Effect } from "effect"; | 2 | import { Effect } from "effect"; |
| 3 | 3 | ||
| 4 | import { | 4 | import { |
| 5 | ConnectionId, | 5 | ConnectionId, |
| 6 | CreateConnectionInput, | 6 | CreateConnectionInput, |
| 7 | createExecutor, | 7 | createExecutor, |
| 8 | definePlugin, | ||
| 9 | makeTestConfig, | 8 | makeTestConfig, |
| 10 | Scope, | 9 | Scope, |
| 11 | ScopeId, | 10 | ScopeId, |
| 12 | SecretId, | 11 | SecretId, |
| 13 | type SecretProvider, | ||
| 14 | TokenMaterial, | 12 | TokenMaterial, |
| 15 | } from "@executor-js/sdk"; | 13 | } from "@executor-js/sdk"; |
| 14 | import { memorySecretsPlugin } from "@executor-js/sdk/testing"; | ||
| 16 | 15 | ||
| 17 | import { graphqlPlugin } from "./plugin"; | 16 | import { graphqlPlugin } from "./plugin"; |
| 18 | import type { IntrospectionResult } from "./introspect"; | 17 | import type { IntrospectionResult } from "./introspect"; |
| 18 | import { makeGreetingGraphqlSchema, serveGraphqlTestServer } from "../testing"; | ||
| 19 | 19 | ||
| 20 | const TEST_SCOPE = "test-scope"; | 20 | const TEST_SCOPE = "test-scope"; |
| 21 | 21 | ||
| 22 | class TestServerAddressError extends Data.TaggedError("TestServerAddressError")<{}> {} | ||
| 23 | |||
| 24 | // --------------------------------------------------------------------------- | 22 | // --------------------------------------------------------------------------- |
| 25 | // Mock introspection response | 23 | // Mock introspection response |
| 26 | // --------------------------------------------------------------------------- | 24 | // --------------------------------------------------------------------------- |
| @@ -92,31 +90,119 @@ const introspectionResult: IntrospectionResult = { · 65 unmodified lines | |||
| 92 | 90 | ||
| 93 | const introspectionJson = JSON.stringify({ data: introspectionResult }); | 91 | const introspectionJson = JSON.stringify({ data: introspectionResult }); |
| 94 | 92 | ||
| 95 | const makeMemorySecretsPlugin = () => { | 93 | describe("graphqlPlugin real protocol server", () => { |
| 96 | const store = new Map<string, string>(); | 94 | const serveGreetingServer = serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema() }); |
| 97 | const provider: SecretProvider = { | 95 | |
| 98 | key: "memory", | 96 | it.effect("adds a source by introspecting the live GraphQL endpoint", () => |
| 99 | writable: true, | 97 | Effect.gen(function* () { |
| 100 | get: (id, scope) => Effect.sync(() => store.get(`${scope}\u0000${id}`) ?? null), | 98 | const server = yield* serveGreetingServer; |
| 101 | set: (id, value, scope) => | 99 | const executor = yield* createExecutor( |
| 102 | Effect.sync(() => { | 100 | makeTestConfig({ plugins: [graphqlPlugin()] as const }), |
| 103 | store.set(`${scope}\u0000${id}`, value); | 101 | ); |
| 104 | }), | 102 | |
| 105 | delete: (id, scope) => Effect.sync(() => store.delete(`${scope}\u0000${id}`)), | 103 | const result = yield* executor.graphql.addSource({ |
| 106 | list: () => | 104 | endpoint: server.endpoint, |
| 107 | Effect.sync(() => | 105 | scope: TEST_SCOPE, |
| 108 | Array.from(store.keys()).map((key) => { | 106 | namespace: "live_graph", |
| 109 | const name = key.split("\u0000", 2)[1] ?? key; | 107 | }); |
| 110 | return { id: name, name }; | 108 | |
| 109 | expect(result).toEqual({ toolCount: 2, namespace: "live_graph" }); | ||
| 110 | |||
| 111 | const tools = yield* executor.tools.list(); | ||
| 112 | expect(tools.map((tool) => tool.id)).toEqual( | ||
| 113 | expect.arrayContaining(["live_graph.query.hello", "live_graph.mutation.setGreeting"]), | ||
| 114 | ); | ||
| 115 | |||
| 116 | const requests = yield* server.requests; | ||
| 117 | expect(requests.some((request) => request.payload.query?.includes("__schema"))).toBe(true); | ||
| 118 | }), | ||
| 119 | ); | ||
| 120 | |||
| 121 | it.effect("invokes a live query with headers and query params", () => | ||
| 122 | Effect.gen(function* () { | ||
| 123 | const server = yield* serveGreetingServer; | ||
| 124 | const executor = yield* createExecutor( | ||
| 125 | makeTestConfig({ plugins: [graphqlPlugin()] as const }), | ||
| 126 | ); | ||
| 127 | |||
| 128 | yield* executor.graphql.addSource({ | ||
| 129 | endpoint: server.endpoint, | ||
| 130 | scope: TEST_SCOPE, | ||
| 131 | namespace: "live_invoke", | ||
| 132 | headers: { "x-static": "abc" }, | ||
| 133 | queryParams: { token: "qp-token" }, | ||
| 134 | }); | ||
| 135 | yield* server.clearRequests; | ||
| 136 | |||
| 137 | const result = yield* executor.tools.invoke("live_invoke.query.hello", { | ||
| 138 | name: "Ada", | ||
| 139 | }); | ||
| 140 | |||
| 141 | expect(result).toEqual({ | ||
| 142 | status: 200, | ||
| 143 | data: { hello: "Hello Ada" }, | ||
| 144 | errors: null, | ||
| 145 | }); | ||
| 146 | |||
| 147 | const requests = yield* server.requests; | ||
| 148 | expect(requests.length).toBe(1); | ||
| 149 | expect(requests[0]?.headers["x-static"]).toBe("abc"); | ||
| 150 | expect(new URL(requests[0]!.url).searchParams.get("token")).toBe("qp-token"); | ||
| 151 | expect(requests[0]?.payload.variables).toEqual({ name: "Ada" }); | ||
| 152 | }), | ||
| 153 | ); | ||
| 154 | |||
| 155 | it.effect("invokes OAuth-backed sources with a bearer token", () => | ||
| 156 | Effect.gen(function* () { | ||
| 157 | const server = yield* serveGreetingServer; | ||
| 158 | const executor = yield* createExecutor( | ||
| 159 | makeTestConfig({ | ||
| 160 | plugins: [memorySecretsPlugin(), graphqlPlugin()] as const, | ||
| 111 | }), | 161 | }), |
| 112 | ), | 162 | ); |
| 113 | }; | 163 | |
| 114 | return definePlugin(() => ({ | 164 | const connectionId = ConnectionId.make("graphql-oauth2-test"); |
| 115 | id: "memory-secrets" as const, | 165 | yield* executor.connections.create( |
| 116 | storage: () => ({}), | 166 | new CreateConnectionInput({ |
| 117 | secretProviders: [provider], | 167 | id: connectionId, |
| 118 | })); | 168 | scope: ScopeId.make(TEST_SCOPE), |
| 119 | }; | 169 | provider: "oauth2", |
| 170 | identityLabel: "GraphQL Test", | ||
| 171 | accessToken: new TokenMaterial({ | ||
| 172 | secretId: SecretId.make(`${connectionId}.access_token`), | ||
| 173 | name: "GraphQL Access Token", | ||
| 174 | value: "secret-token", | ||
| 175 | }), | ||
| 176 | refreshToken: null, | ||
| 177 | expiresAt: null, | ||
| 178 | oauthScope: null, | ||
| 179 | providerState: null, | ||
| 180 | }), | ||
| 181 | ); | ||
| 182 | |||
| 183 | yield* executor.graphql.addSource({ | ||
| 184 | endpoint: server.endpoint, | ||
| 185 | scope: TEST_SCOPE, | ||
| 186 | namespace: "oauth_graph", | ||
| 187 | auth: { kind: "oauth2", connectionId }, | ||
| 188 | }); | ||
| 189 | yield* server.clearRequests; | ||
| 190 | |||
| 191 | const result = yield* executor.tools.invoke("oauth_graph.query.hello", { | ||
| 192 | name: "Ada", | ||
| 193 | }); | ||
| 194 | |||
| 195 | expect(result).toEqual({ | ||
| 196 | status: 200, | ||
| 197 | data: { hello: "Hello Ada" }, | ||
| 198 | errors: null, | ||
| 199 | }); | ||
| 200 | |||
| 201 | const requests = yield* server.requests; | ||
| 202 | expect(requests[0]?.headers.authorization).toBe("Bearer secret-token"); | ||
| 203 | }), | ||
| 204 | ); | ||
| 205 | }); | ||
| 120 | 206 | ||
| 121 | describe("graphqlPlugin", () => { | 207 | describe("graphqlPlugin", () => { |
| 122 | it.effect("registers tools from introspection JSON", () => | 208 | it.effect("registers tools from introspection JSON", () => |
| @@ -256,85 +342,6 @@ describe("graphqlPlugin", () => { · 133 unmodified lines | |||
| 256 | }), | 342 | }), |
| 257 | ); | 343 | ); |
| 258 | 344 | ||
| 259 | it.effect("invokes OAuth-backed sources with a bearer token", () => | ||
| 260 | Effect.gen(function* () { | ||
| 261 | const http = yield* Effect.promise(() => import("node:http")); | ||
| 262 | let authorizationHeader: string | null = null; | ||
| 263 | const server = yield* Effect.acquireRelease( | ||
| 264 | Effect.callback< | ||
| 265 | { | ||
| 266 | readonly server: ReturnType<typeof http.createServer>; | ||
| 267 | readonly port: number; | ||
| 268 | }, | ||
| 269 | Error | TestServerAddressError | ||
| 270 | >((resume) => { | ||
| 271 | const server = http.createServer((req, res) => { | ||
| 272 | authorizationHeader = req.headers.authorization ?? null; | ||
| 273 | res.setHeader("content-type", "application/json"); | ||
| 274 | res.end(JSON.stringify({ data: { hello: "Hello Ada" } })); | ||
| 275 | }); | ||
| 276 | server.listen(0, "127.0.0.1", () => { | ||
| 277 | const address = server.address(); | ||
| 278 | if (!address || typeof address === "string") { | ||
| 279 | resume(Effect.fail(new TestServerAddressError())); | ||
| 280 | return; | ||
| 281 | } | ||
| 282 | resume(Effect.succeed({ server, port: address.port })); | ||
| 283 | }); | ||
| 284 | server.once("error", (error) => resume(Effect.fail(error))); | ||
| 285 | }), | ||
| 286 | ({ server }) => | ||
| 287 | Effect.callback<void>((resume) => { | ||
| 288 | server.close(() => resume(Effect.void)); | ||
| 289 | }).pipe(Effect.ignore), | ||
| 290 | ); | ||
| 291 | |||
| 292 | const executor = yield* createExecutor( | ||
| 293 | makeTestConfig({ | ||
| 294 | plugins: [makeMemorySecretsPlugin()(), graphqlPlugin()] as const, | ||
| 295 | }), | ||
| 296 | ); | ||
| 297 | |||
| 298 | const connectionId = ConnectionId.make("graphql-oauth2-test"); | ||
| 299 | yield* executor.connections.create( | ||
| 300 | new CreateConnectionInput({ | ||
| 301 | id: connectionId, | ||
| 302 | scope: ScopeId.make(TEST_SCOPE), | ||
| 303 | provider: "oauth2", | ||
| 304 | identityLabel: "GraphQL Test", | ||
| 305 | accessToken: new TokenMaterial({ | ||
| 306 | secretId: SecretId.make(`${connectionId}.access_token`), | ||
| 307 | name: "GraphQL Access Token", | ||
| 308 | value: "secret-token", | ||
| 309 | }), | ||
| 310 | refreshToken: null, | ||
| 311 | expiresAt: null, | ||
| 312 | oauthScope: null, | ||
| 313 | providerState: null, | ||
| 314 | }), | ||
| 315 | ); | ||
| 316 | |||
| 317 | yield* executor.graphql.addSource({ | ||
| 318 | endpoint: `http://127.0.0.1:${server.port}/graphql`, | ||
| 319 | scope: TEST_SCOPE, | ||
| 320 | introspectionJson, | ||
| 321 | namespace: "oauth_graph", | ||
| 322 | auth: { kind: "oauth2", connectionId }, | ||
| 323 | }); | ||
| 324 | |||
| 325 | const result = yield* executor.tools.invoke("oauth_graph.query.hello", { | ||
| 326 | name: "Ada", | ||
| 327 | }); | ||
| 328 | |||
| 329 | expect(result).toEqual({ | ||
| 330 | status: 200, | ||
| 331 | data: { hello: "Hello Ada" }, | ||
| 332 | errors: null, | ||
| 333 | }); | ||
| 334 | expect(authorizationHeader).toBe("Bearer secret-token"); | ||
| 335 | }), | ||
| 336 | ); | ||
| 337 | |||
| 338 | it.effect("static graphql.addSource delegates to extension", () => | 345 | it.effect("static graphql.addSource delegates to extension", () => |
| 339 | Effect.gen(function* () { | 346 | Effect.gen(function* () { |
| 340 | const executor = yield* createExecutor( | 347 | const executor = yield* createExecutor( |
| @@ -506,7 +513,7 @@ describe("graphqlPlugin", () => { · 165 unmodified lines | |||
| 506 | Effect.gen(function* () { | 513 | Effect.gen(function* () { |
| 507 | const executor = yield* createExecutor( | 514 | const executor = yield* createExecutor( |
| 508 | makeTestConfig({ | 515 | makeTestConfig({ |
| 509 | plugins: [makeMemorySecretsPlugin()(), graphqlPlugin()] as const, | 516 | plugins: [memorySecretsPlugin(), graphqlPlugin()] as const, |
| 510 | }), | 517 | }), |
| 511 | ); | 518 | ); |
| 512 | 519 | ||
| @@ -543,7 +550,7 @@ describe("graphqlPlugin", () => { · 30 unmodified lines | |||
| 543 | Effect.gen(function* () { | 550 | Effect.gen(function* () { |
| 544 | const executor = yield* createExecutor( | 551 | const executor = yield* createExecutor( |
| 545 | makeTestConfig({ | 552 | makeTestConfig({ |
| 546 | plugins: [makeMemorySecretsPlugin()(), graphqlPlugin()] as const, | 553 | plugins: [memorySecretsPlugin(), graphqlPlugin()] as const, |
| 547 | }), | 554 | }), |
| 548 | ); | 555 | ); |
| 549 | 556 | ||
| @@ -579,7 +586,7 @@ describe("graphqlPlugin", () => { · 29 unmodified lines | |||
| 579 | Effect.gen(function* () { | 586 | Effect.gen(function* () { |
| 580 | const executor = yield* createExecutor( | 587 | const executor = yield* createExecutor( |
| 581 | makeTestConfig({ | 588 | makeTestConfig({ |
| 582 | plugins: [makeMemorySecretsPlugin()(), graphqlPlugin()] as const, | 589 | plugins: [memorySecretsPlugin(), graphqlPlugin()] as const, |
| 583 | }), | 590 | }), |
| 584 | ); | 591 | ); |
| 585 | 592 | ||
| @@ -0,0 +1,195 @@ | |||
| 1 | import { createServer, type IncomingHttpHeaders, type Server } from "node:http"; | ||
| 2 | |||
| 3 | import { Context, Data, Effect, Layer, Ref, Schema as EffectSchema, Scope } from "effect"; | ||
| 4 | import { GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString } from "graphql"; | ||
| 5 | import { createYoga, type GraphQLParams, type YogaInitialContext } from "graphql-yoga"; | ||
| 6 | |||
| 7 | const GraphqlRequestPayload = EffectSchema.Struct({ | ||
| 8 | query: EffectSchema.optional(EffectSchema.String), | ||
| 9 | variables: EffectSchema.optional(EffectSchema.Record(EffectSchema.String, EffectSchema.Unknown)), | ||
| 10 | operationName: EffectSchema.optional(EffectSchema.NullOr(EffectSchema.String)), | ||
| 11 | }); | ||
| 12 | |||
| 13 | type GraphqlRequestPayload = typeof GraphqlRequestPayload.Type; | ||
| 14 | |||
| 15 | export interface GraphqlTestRequest { | ||
| 16 | readonly url: string; | ||
| 17 | readonly method: string; | ||
| 18 | readonly path: string; | ||
| 19 | readonly headers: Readonly<Record<string, string>>; | ||
| 20 | readonly payload: GraphqlRequestPayload; | ||
| 21 | } | ||
| 22 | |||
| 23 | export interface GraphqlTestContext { | ||
| 24 | readonly request: GraphqlTestRequest; | ||
| 25 | } | ||
| 26 | |||
| 27 | export interface GraphqlTestServerOptions { | ||
| 28 | readonly schema: GraphQLSchema; | ||
| 29 | readonly path?: string; | ||
| 30 | } | ||
| 31 | |||
| 32 | export interface GraphqlTestServerShape { | ||
| 33 | readonly endpoint: string; | ||
| 34 | readonly schema: GraphQLSchema; | ||
| 35 | readonly requests: Effect.Effect<readonly GraphqlTestRequest[]>; | ||
| 36 | readonly clearRequests: Effect.Effect<void>; | ||
| 37 | } | ||
| 38 | |||
| 39 | class GraphqlTestServerAddressError extends Data.TaggedError("GraphqlTestServerAddressError")<{ | ||
| 40 | readonly address: unknown; | ||
| 41 | }> {} | ||
| 42 | |||
| 43 | class GraphqlTestServerListenError extends Data.TaggedError("GraphqlTestServerListenError")<{ | ||
| 44 | readonly cause: unknown; | ||
| 45 | }> {} | ||
| 46 | |||
| 47 | const headersFromRequest = ( | ||
| 48 | headers: Headers | IncomingHttpHeaders, | ||
| 49 | ): Readonly<Record<string, string>> => { | ||
| 50 | if ("entries" in headers && typeof headers.entries === "function") { | ||
| 51 | return Object.fromEntries(headers.entries()); | ||
| 52 | } | ||
| 53 | return Object.fromEntries( | ||
| 54 | Object.entries(headers).flatMap(([name, value]) => { | ||
| 55 | if (value === undefined) return []; | ||
| 56 | return [[name, Array.isArray(value) ? value.join(", ") : value]]; | ||
| 57 | }), | ||
| 58 | ); | ||
| 59 | }; | ||
| 60 | |||
| 61 | const payloadFromParams = (params: GraphQLParams): GraphqlRequestPayload => ({ | ||
| 62 | query: params.query, | ||
| 63 | variables: | ||
| 64 | typeof params.variables === "object" && params.variables !== null | ||
| 65 | ? params.variables | ||
| 66 | : undefined, | ||
| 67 | operationName: params.operationName ?? null, | ||
| 68 | }); | ||
| 69 | |||
| 70 | const captureRequest = ( | ||
| 71 | initial: YogaInitialContext, | ||
| 72 | requests: Ref.Ref<readonly GraphqlTestRequest[]>, | ||
| 73 | ) => { | ||
| 74 | const url = new URL(initial.request.url); | ||
| 75 | const captured: GraphqlTestRequest = { | ||
| 76 | url: initial.request.url, | ||
| 77 | method: initial.request.method, | ||
| 78 | path: url.pathname, | ||
| 79 | headers: headersFromRequest(initial.request.headers), | ||
| 80 | payload: payloadFromParams(initial.params), | ||
| 81 | }; | ||
| 82 | return Effect.runPromise( | ||
| 83 | Ref.update(requests, (all) => [...all, captured]).pipe(Effect.as(captured)), | ||
| 84 | ); | ||
| 85 | }; | ||
| 86 | |||
| 87 | const closeServer = (server: Server): Effect.Effect<void> => | ||
| 88 | Effect.callback<void>((resume) => { | ||
| 89 | server.close(() => resume(Effect.void)); | ||
| 90 | }); | ||
| 91 | |||
| 92 | export const serveGraphqlTestServer = ( | ||
| 93 | options: GraphqlTestServerOptions, | ||
| 94 | ): Effect.Effect< | ||
| 95 | GraphqlTestServerShape, | ||
| 96 | GraphqlTestServerAddressError | GraphqlTestServerListenError, | ||
| 97 | Scope.Scope | ||
| 98 | > => | ||
| 99 | Effect.gen(function* () { | ||
| 100 | const requests = yield* Ref.make<readonly GraphqlTestRequest[]>([]); | ||
| 101 | const path = options.path ?? "/graphql"; | ||
| 102 | |||
| 103 | const yoga = createYoga<Record<string, never>, GraphqlTestContext>({ | ||
| 104 | schema: options.schema, | ||
| 105 | graphqlEndpoint: path, | ||
| 106 | graphiql: false, | ||
| 107 | landingPage: false, | ||
| 108 | logging: false, | ||
| 109 | maskedErrors: false, | ||
| 110 | context: (initial) => | ||
| 111 | captureRequest(initial, requests).then((request) => ({ | ||
| 112 | request, | ||
| 113 | })), | ||
| 114 | }); | ||
| 115 | const server = createServer(yoga); | ||
| 116 | |||
| 117 | const port = yield* Effect.acquireRelease( | ||
| 118 | Effect.callback<number, GraphqlTestServerAddressError | GraphqlTestServerListenError>( | ||
| 119 | (resume) => { | ||
| 120 | const onError = (cause: unknown) => | ||
| 121 | resume(Effect.fail(new GraphqlTestServerListenError({ cause }))); | ||
| 122 | server.once("error", onError); | ||
| 123 | server.listen(0, "127.0.0.1", () => { | ||
| 124 | server.off("error", onError); | ||
| 125 | const address = server.address(); | ||
| 126 | if (!address || typeof address === "string") { | ||
| 127 | resume(Effect.fail(new GraphqlTestServerAddressError({ address }))); | ||
| 128 | return; | ||
| 129 | } | ||
| 130 | resume(Effect.succeed(address.port)); | ||
| 131 | }); | ||
| 132 | }, | ||
| 133 | ), | ||
| 134 | () => closeServer(server), | ||
| 135 | ); | ||
| 136 | |||
| 137 | return { | ||
| 138 | endpoint: `http://127.0.0.1:${port}${path}`, | ||
| 139 | schema: options.schema, | ||
| 140 | requests: Ref.get(requests), | ||
| 141 | clearRequests: Ref.set(requests, []), | ||
| 142 | }; | ||
| 143 | }); | ||
| 144 | |||
| 145 | export class GraphqlTestServer extends Context.Service<GraphqlTestServer, GraphqlTestServerShape>()( | ||
| 146 | "@executor-js/plugin-graphql/testing/GraphqlTestServer", | ||
| 147 | ) { | ||
| 148 | static readonly layer = (options: GraphqlTestServerOptions) => | ||
| 149 | Layer.effect(GraphqlTestServer, serveGraphqlTestServer(options)); | ||
| 150 | } | ||
| 151 | |||
| 152 | const stringArgument = ( | ||
| 153 | args: Readonly<Record<string, unknown>>, | ||
| 154 | key: string, | ||
| 155 | fallback: string, | ||
| 156 | ): string => { | ||
| 157 | const value = args[key]; | ||
| 158 | return typeof value === "string" ? value : fallback; | ||
| 159 | }; | ||
| 160 | |||
| 161 | export const makeGreetingGraphqlSchema = (): GraphQLSchema => { | ||
| 162 | const Query = new GraphQLObjectType<unknown, GraphqlTestContext>({ | ||
| 163 | name: "Query", | ||
| 164 | fields: { | ||
| 165 | hello: { | ||
| 166 | type: GraphQLString, | ||
| 167 | description: "Say hello", | ||
| 168 | args: { | ||
| 169 | name: { type: GraphQLString }, | ||
| 170 | }, | ||
| 171 | resolve: (_source, args) => `Hello ${stringArgument(args, "name", "world")}`, | ||
| 172 | }, | ||
| 173 | }, | ||
| 174 | }); | ||
| 175 | |||
| 176 | const Mutation = new GraphQLObjectType<unknown, GraphqlTestContext>({ | ||
| 177 | name: "Mutation", | ||
| 178 | fields: { | ||
| 179 | setGreeting: { | ||
| 180 | type: GraphQLString, | ||
| 181 | description: "Set greeting message", | ||
| 182 | args: { | ||
| 183 | message: { type: new GraphQLNonNull(GraphQLString) }, | ||
| 184 | }, | ||
| 185 | resolve: (_source, args) => stringArgument(args, "message", ""), | ||
| 186 | }, | ||
| 187 | }, | ||
| 188 | }); | ||
| 189 | |||
| 190 | return new GraphQLSchema({ query: Query, mutation: Mutation }); | ||
| 191 | }; | ||
| 192 | |||
| 193 | export const TestLayers = { | ||
| 194 | greeting: () => GraphqlTestServer.layer({ schema: makeGreetingGraphqlSchema() }), | ||
| 195 | }; | ||
| @@ -4,10 +4,11 @@ export default defineConfig({ · 3 unmodified lines | |||
| 4 | entry: { | 4 | entry: { |
| 5 | index: "src/promise.ts", | 5 | index: "src/promise.ts", |
| 6 | core: "src/sdk/index.ts", | 6 | core: "src/sdk/index.ts", |
| 7 | testing: "src/testing/index.ts", | ||
| 7 | }, | 8 | }, |
| 8 | format: ["esm"], | 9 | format: ["esm"], |
| 9 | dts: false, | 10 | dts: false, |
| 10 | sourcemap: true, | 11 | sourcemap: true, |
| 11 | clean: true, | 12 | clean: true, |
| 12 | external: [/^@executor-js\//, /^effect/, /^@effect\//], | 13 | external: [/^@executor-js\//, /^effect/, /^@effect\//, /^graphql/, /^graphql-yoga/], |
| 13 | }); | 14 | }); |
| @@ -3,5 +3,10 @@ import { defineConfig } from "vitest/config"; · 2 unmodified lines | |||
| 3 | export default defineConfig({ | 3 | export default defineConfig({ |
| 4 | test: { | 4 | test: { |
| 5 | include: ["src/**/*.test.ts"], | 5 | include: ["src/**/*.test.ts"], |
| 6 | server: { | ||
| 7 | deps: { | ||
| 8 | inline: ["graphql", "graphql-yoga", "@graphql-tools", "@envelop", "@whatwg-node"], | ||
| 9 | }, | ||
| 10 | }, | ||
| 6 | }, | 11 | }, |
| 7 | }); | 12 | }); |
| @@ -21,7 +21,8 @@ · 20 unmodified lines | |||
| 21 | "./api": "./src/api/index.ts", | 21 | "./api": "./src/api/index.ts", |
| 22 | "./react": "./src/react/index.ts", | 22 | "./react": "./src/react/index.ts", |
| 23 | "./presets": "./src/sdk/presets.ts", | 23 | "./presets": "./src/sdk/presets.ts", |
| 24 | "./client": "./src/react/plugin-client.tsx" | 24 | "./client": "./src/react/plugin-client.tsx", |
| 25 | "./testing": "./src/testing/index.ts" | ||
| 25 | }, | 26 | }, |
| 26 | "publishConfig": { | 27 | "publishConfig": { |
| 27 | "access": "public", | 28 | "access": "public", |
| @@ -37,6 +38,12 @@ · 9 unmodified lines | |||
| 37 | "types": "./dist/sdk/index.d.ts", | 38 | "types": "./dist/sdk/index.d.ts", |
| 38 | "default": "./dist/core.js" | 39 | "default": "./dist/core.js" |
| 39 | } | 40 | } |
| 41 | }, | ||
| 42 | "./testing": { | ||
| 43 | "import": { | ||
| 44 | "types": "./dist/testing/index.d.ts", | ||
| 45 | "default": "./dist/testing.js" | ||
| 46 | } | ||
| 40 | } | 47 | } |
| 41 | } | 48 | } |
| 42 | }, | 49 | }, |
| @@ -12,7 +12,7 @@ import { · 11 unmodified lines | |||
| 12 | } from "@executor-js/sdk"; | 12 | } from "@executor-js/sdk"; |
| 13 | 13 | ||
| 14 | import { mcpPlugin } from "./plugin"; | 14 | import { mcpPlugin } from "./plugin"; |
| 15 | import { serveMcpServer } from "./test-utils"; | 15 | import { serveMcpServer } from "../testing"; |
| 16 | 16 | ||
| 17 | // --------------------------------------------------------------------------- | 17 | // --------------------------------------------------------------------------- |
| 18 | // Test MCP server on a real HTTP port | 18 | // Test MCP server on a real HTTP port |
| @@ -18,7 +18,7 @@ import { · 17 unmodified lines | |||
| 18 | 18 | ||
| 19 | import { mcpPlugin } from "./plugin"; | 19 | import { mcpPlugin } from "./plugin"; |
| 20 | import { extractManifestFromListToolsResult, deriveMcpNamespace, joinToolPath } from "./manifest"; | 20 | import { extractManifestFromListToolsResult, deriveMcpNamespace, joinToolPath } from "./manifest"; |
| 21 | import { serveMcpServer } from "./test-utils"; | 21 | import { serveMcpServer } from "../testing"; |
| 22 | 22 | ||
| 23 | // --------------------------------------------------------------------------- | 23 | // --------------------------------------------------------------------------- |
| 24 | // Memory secrets plugin — without a writable provider in the stack, | 24 | // Memory secrets plugin — without a writable provider in the stack, |
| @@ -0,0 +1 @@ | |||
| 1 | export { serveMcpServer, type McpTestServer } from "./server"; | ||
| @@ -10,12 +10,6 @@ export type McpTestServer = { · 9 unmodified lines | |||
| 10 | readonly sessionCount: () => number; | 10 | readonly sessionCount: () => number; |
| 11 | }; | 11 | }; |
| 12 | 12 | ||
| 13 | /** | ||
| 14 | * Spin up a real HTTP server backed by a fresh `McpServer` per session, | ||
| 15 | * with proper session routing via `mcp-session-id`. The factory is invoked | ||
| 16 | * once per new session, so tools registered inside it are isolated per | ||
| 17 | * connection (matching the SDK's own server/transport coupling). | ||
| 18 | */ | ||
| 19 | export const serveMcpServer = (factory: () => McpServer) => | 13 | export const serveMcpServer = (factory: () => McpServer) => |
| 20 | Effect.acquireRelease( | 14 | Effect.acquireRelease( |
| 21 | Effect.callback<McpTestServer, Error>((resume) => { | 15 | Effect.callback<McpTestServer, Error>((resume) => { |
| @@ -4,6 +4,7 @@ export default defineConfig({ · 3 unmodified lines | |||
| 4 | entry: { | 4 | entry: { |
| 5 | index: "src/promise.ts", | 5 | index: "src/promise.ts", |
| 6 | core: "src/sdk/index.ts", | 6 | core: "src/sdk/index.ts", |
| 7 | testing: "src/testing/index.ts", | ||
| 7 | }, | 8 | }, |
| 8 | format: ["esm"], | 9 | format: ["esm"], |
| 9 | dts: false, | 10 | dts: false, |
| @@ -21,7 +21,8 @@ · 20 unmodified lines | |||
| 21 | "./api": "./src/api/index.ts", | 21 | "./api": "./src/api/index.ts", |
| 22 | "./react": "./src/react/index.ts", | 22 | "./react": "./src/react/index.ts", |
| 23 | "./client": "./src/react/plugin-client.tsx", | 23 | "./client": "./src/react/plugin-client.tsx", |
| 24 | "./presets": "./src/sdk/presets.ts" | 24 | "./presets": "./src/sdk/presets.ts", |
| 25 | "./testing": "./src/testing/index.ts" | ||
| 25 | }, | 26 | }, |
| 26 | "publishConfig": { | 27 | "publishConfig": { |
| 27 | "access": "public", | 28 | "access": "public", |
| @@ -37,6 +38,12 @@ · 9 unmodified lines | |||
| 37 | "types": "./dist/sdk/index.d.ts", | 38 | "types": "./dist/sdk/index.d.ts", |
| 38 | "default": "./dist/core.js" | 39 | "default": "./dist/core.js" |
| 39 | } | 40 | } |
| 41 | }, | ||
| 42 | "./testing": { | ||
| 43 | "import": { | ||
| 44 | "types": "./dist/testing/index.d.ts", | ||
| 45 | "default": "./dist/testing.js" | ||
| 46 | } | ||
| 40 | } | 47 | } |
| 41 | } | 48 | } |
| 42 | }, | 49 | }, |
| @@ -13,20 +13,20 @@ import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; · 12 unmodified lines | |||
| 13 | import { | 13 | import { |
| 14 | createExecutor, | 14 | createExecutor, |
| 15 | type DBAdapter, | 15 | type DBAdapter, |
| 16 | definePlugin, | ||
| 17 | makeTestConfig, | 16 | makeTestConfig, |
| 18 | Scope, | 17 | Scope, |
| 19 | ScopeId, | 18 | ScopeId, |
| 20 | SecretId, | 19 | SecretId, |
| 21 | SetSecretInput, | 20 | SetSecretInput, |
| 22 | type InvokeOptions, | 21 | type InvokeOptions, |
| 23 | type SecretProvider, | ||
| 24 | type Where, | 22 | type Where, |
| 25 | } from "@executor-js/sdk"; | 23 | } from "@executor-js/sdk"; |
| 24 | import { memorySecretsPlugin } from "@executor-js/sdk/testing"; | ||
| 26 | 25 | ||
| 27 | const TEST_SCOPE = "test-scope"; | 26 | const TEST_SCOPE = "test-scope"; |
| 28 | import { openApiPlugin } from "./plugin"; | 27 | import { openApiPlugin } from "./plugin"; |
| 29 | import { OAuth2Auth, OpenApiSourceBindingInput } from "./types"; | 28 | import { OAuth2Auth, OpenApiSourceBindingInput } from "./types"; |
| 29 | import { makeOpenApiTestServer } from "../testing"; | ||
| 30 | 30 | ||
| 31 | const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; | 31 | const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; |
| 32 | 32 | ||
| @@ -53,38 +53,6 @@ const recordFindMany = (adapter: DBAdapter, calls: FindManyCall[]): DBAdapter => · 20 unmodified lines | |||
| 53 | ), | 53 | ), |
| 54 | }); | 54 | }); |
| 55 | 55 | ||
| 56 | // --------------------------------------------------------------------------- | ||
| 57 | // In-memory secrets provider plugin — registered alongside openapi so | ||
| 58 | // executor.secrets.set/get work in tests. | ||
| 59 | // --------------------------------------------------------------------------- | ||
| 60 | |||
| 61 | const memoryProvider: SecretProvider = (() => { | ||
| 62 | const store = new Map<string, string>(); | ||
| 63 | return { | ||
| 64 | key: "memory", | ||
| 65 | writable: true, | ||
| 66 | get: (id, scope) => Effect.sync(() => store.get(`${scope}\u0000${id}`) ?? null), | ||
| 67 | set: (id, value, scope) => | ||
| 68 | Effect.sync(() => { | ||
| 69 | store.set(`${scope}\u0000${id}`, value); | ||
| 70 | }), | ||
| 71 | delete: (id, scope) => Effect.sync(() => store.delete(`${scope}\u0000${id}`)), | ||
| 72 | list: () => | ||
| 73 | Effect.sync(() => | ||
| 74 | Array.from(store.keys()).map((k) => { | ||
| 75 | const name = k.split("\u0000", 2)[1] ?? k; | ||
| 76 | return { id: name, name }; | ||
| 77 | }), | ||
| 78 | ), | ||
| 79 | }; | ||
| 80 | })(); | ||
| 81 | |||
| 82 | const memorySecretsPlugin = definePlugin(() => ({ | ||
| 83 | id: "memory-secrets" as const, | ||
| 84 | storage: () => ({}), | ||
| 85 | secretProviders: [memoryProvider], | ||
| 86 | })); | ||
| 87 | |||
| 88 | // --------------------------------------------------------------------------- | 56 | // --------------------------------------------------------------------------- |
| 89 | // Define a test API with Effect HttpApi | 57 | // Define a test API with Effect HttpApi |
| 90 | // --------------------------------------------------------------------------- | 58 | // --------------------------------------------------------------------------- |
| @@ -186,19 +154,18 @@ const TestLayer = HttpRouter.serve(ApiLive, { disableListenLog: true, disableLog · 95 unmodified lines | |||
| 186 | layer(TestLayer)("OpenAPI Plugin", (it) => { | 154 | layer(TestLayer)("OpenAPI Plugin", (it) => { |
| 187 | it.effect("previewSpec returns metadata and header presets", () => | 155 | it.effect("previewSpec returns metadata and header presets", () => |
| 188 | Effect.gen(function* () { | 156 | Effect.gen(function* () { |
| 189 | const httpClient = yield* HttpClient.HttpClient; | 157 | const server = yield* makeOpenApiTestServer({ spec }); |
| 190 | const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); | ||
| 191 | 158 | ||
| 192 | const executor = yield* createExecutor( | 159 | const executor = yield* createExecutor( |
| 193 | makeTestConfig({ | 160 | makeTestConfig({ |
| 194 | plugins: [ | 161 | plugins: [ |
| 195 | openApiPlugin({ httpClientLayer: clientLayer }), | 162 | openApiPlugin({ httpClientLayer: server.httpClientLayer }), |
| 196 | memorySecretsPlugin(), | 163 | memorySecretsPlugin(), |
| 197 | ] as const, | 164 | ] as const, |
| 198 | }), | 165 | }), |
| 199 | ); | 166 | ); |
| 200 | 167 | ||
| 201 | const preview = yield* executor.openapi.previewSpec(specJson); | 168 | const preview = yield* executor.openapi.previewSpec(server.specJson); |
| 202 | 169 | ||
| 203 | expect(preview.operationCount).toBeGreaterThanOrEqual(2); | 170 | expect(preview.operationCount).toBeGreaterThanOrEqual(2); |
| 204 | expect(preview.servers).toBeDefined(); | 171 | expect(preview.servers).toBeDefined(); |
| @@ -0,0 +1,86 @@ | |||
| 1 | import { Context, Data, Effect, Layer, Predicate, Schema } from "effect"; | ||
| 2 | import { HttpClient, HttpServer } from "effect/unstable/http"; | ||
| 3 | |||
| 4 | export class OpenApiTestServerAddressError extends Data.TaggedError( | ||
| 5 | "OpenApiTestServerAddressError", | ||
| 6 | )<{ | ||
| 7 | readonly address: unknown; | ||
| 8 | }> {} | ||
| 9 | |||
| 10 | export class OpenApiTestServerSpecError extends Data.TaggedError("OpenApiTestServerSpecError")<{ | ||
| 11 | readonly cause: unknown; | ||
| 12 | }> {} | ||
| 13 | |||
| 14 | export interface OpenApiTestServerOptions { | ||
| 15 | readonly spec: unknown; | ||
| 16 | } | ||
| 17 | |||
| 18 | export interface OpenApiTestServerShape { | ||
| 19 | readonly baseUrl: string; | ||
| 20 | readonly specJson: string; | ||
| 21 | readonly httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>; | ||
| 22 | } | ||
| 23 | |||
| 24 | const decodeJsonSpec = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown)); | ||
| 25 | |||
| 26 | const isJsonObject = (value: unknown): value is Readonly<Record<string, unknown>> => | ||
| 27 | typeof value === "object" && value !== null && !Array.isArray(value); | ||
| 28 | |||
| 29 | export const openApiSpecJsonWithServer = ( | ||
| 30 | spec: unknown, | ||
| 31 | baseUrl: string, | ||
| 32 | ): Effect.Effect<string, OpenApiTestServerSpecError> => | ||
| 33 | Effect.gen(function* () { | ||
| 34 | const parsed = | ||
| 35 | typeof spec === "string" | ||
| 36 | ? yield* decodeJsonSpec(spec).pipe( | ||
| 37 | Effect.mapError((cause) => new OpenApiTestServerSpecError({ cause })), | ||
| 38 | ) | ||
| 39 | : spec; | ||
| 40 | const withServer = isJsonObject(parsed) | ||
| 41 | ? { | ||
| 42 | ...parsed, | ||
| 43 | servers: [{ url: baseUrl }], | ||
| 44 | } | ||
| 45 | : parsed; | ||
| 46 | return yield* Effect.try({ | ||
| 47 | try: () => JSON.stringify(withServer), | ||
| 48 | catch: (cause) => new OpenApiTestServerSpecError({ cause }), | ||
| 49 | }); | ||
| 50 | }); | ||
| 51 | |||
| 52 | export const makeOpenApiTestServer = ( | ||
| 53 | options: OpenApiTestServerOptions, | ||
| 54 | ): Effect.Effect< | ||
| 55 | OpenApiTestServerShape, | ||
| 56 | OpenApiTestServerAddressError | OpenApiTestServerSpecError, | ||
| 57 | HttpClient.HttpClient | HttpServer.HttpServer | ||
| 58 | > => | ||
| 59 | Effect.gen(function* () { | ||
| 60 | const server = yield* HttpServer.HttpServer; | ||
| 61 | const address = server.address; | ||
| 62 | if (!Predicate.isTagged(address, "TcpAddress")) { | ||
| 63 | return yield* new OpenApiTestServerAddressError({ address }); | ||
| 64 | } | ||
| 65 | |||
| 66 | const client = yield* HttpClient.HttpClient; | ||
| 67 | const baseUrl = `http://127.0.0.1:${address.port}`; | ||
| 68 | const specJson = yield* openApiSpecJsonWithServer(options.spec, baseUrl); | ||
| 69 | |||
| 70 | return { | ||
| 71 | baseUrl, | ||
| 72 | specJson, | ||
| 73 | httpClientLayer: Layer.succeed(HttpClient.HttpClient, client), | ||
| 74 | }; | ||
| 75 | }); | ||
| 76 | |||
| 77 | export class OpenApiTestServer extends Context.Service<OpenApiTestServer, OpenApiTestServerShape>()( | ||
| 78 | "@executor-js/plugin-openapi/testing/OpenApiTestServer", | ||
| 79 | ) { | ||
| 80 | static readonly layer = (options: OpenApiTestServerOptions) => | ||
| 81 | Layer.effect(OpenApiTestServer, makeOpenApiTestServer(options)); | ||
| 82 | } | ||
| 83 | |||
| 84 | export const TestLayers = { | ||
| 85 | server: OpenApiTestServer.layer, | ||
| 86 | }; | ||
| @@ -4,6 +4,7 @@ export default defineConfig({ · 3 unmodified lines | |||
| 4 | entry: { | 4 | entry: { |
| 5 | index: "src/promise.ts", | 5 | index: "src/promise.ts", |
| 6 | core: "src/sdk/index.ts", | 6 | core: "src/sdk/index.ts", |
| 7 | testing: "src/testing/index.ts", | ||
| 7 | }, | 8 | }, |
| 8 | format: ["esm"], | 9 | format: ["esm"], |
| 9 | dts: false, | 10 | dts: false, |
| @@ -12,6 +12,7 @@ import noManualTagCheck from "./oxlint-plugin-executor/rules/no-manual-tag-check · 11 unmodified lines | |||
| 12 | import noPromiseCatch from "./oxlint-plugin-executor/rules/no-promise-catch.js"; | 12 | import noPromiseCatch from "./oxlint-plugin-executor/rules/no-promise-catch.js"; |
| 13 | import noPromiseClientSurface from "./oxlint-plugin-executor/rules/no-promise-client-surface.js"; | 13 | import noPromiseClientSurface from "./oxlint-plugin-executor/rules/no-promise-client-surface.js"; |
| 14 | import noPromiseReject from "./oxlint-plugin-executor/rules/no-promise-reject.js"; | 14 | import noPromiseReject from "./oxlint-plugin-executor/rules/no-promise-reject.js"; |
| 15 | import noRawFetch from "./oxlint-plugin-executor/rules/no-raw-fetch.js"; | ||
| 15 | import noRawErrorThrow from "./oxlint-plugin-executor/rules/no-raw-error-throw.js"; | 16 | import noRawErrorThrow from "./oxlint-plugin-executor/rules/no-raw-error-throw.js"; |
| 16 | import noRedundantPrimitiveCast from "./oxlint-plugin-executor/rules/no-redundant-primitive-cast.js"; | 17 | import noRedundantPrimitiveCast from "./oxlint-plugin-executor/rules/no-redundant-primitive-cast.js"; |
| 17 | import noRedundantErrorFactory from "./oxlint-plugin-executor/rules/no-redundant-error-factory.js"; | 18 | import noRedundantErrorFactory from "./oxlint-plugin-executor/rules/no-redundant-error-factory.js"; |
| @@ -48,6 +49,7 @@ export default { · 30 unmodified lines | |||
| 48 | "no-promise-catch": noPromiseCatch, | 49 | "no-promise-catch": noPromiseCatch, |
| 49 | "no-promise-client-surface": noPromiseClientSurface, | 50 | "no-promise-client-surface": noPromiseClientSurface, |
| 50 | "no-promise-reject": noPromiseReject, | 51 | "no-promise-reject": noPromiseReject, |
| 52 | "no-raw-fetch": noRawFetch, | ||
| 51 | "no-raw-error-throw": noRawErrorThrow, | 53 | "no-raw-error-throw": noRawErrorThrow, |
| 52 | "no-redundant-primitive-cast": noRedundantPrimitiveCast, | 54 | "no-redundant-primitive-cast": noRedundantPrimitiveCast, |
| 53 | "no-redundant-error-factory": noRedundantErrorFactory, | 55 | "no-redundant-error-factory": noRedundantErrorFactory, |
| @@ -0,0 +1,76 @@ | |||
| 1 | import { | ||
| 2 | getPropertyName, | ||
| 3 | isDeclarationFile, | ||
| 4 | isIdentifier, | ||
| 5 | toRepoRelative, | ||
| 6 | unwrapExpression, | ||
| 7 | } from "../utils.js"; | ||
| 8 | |||
| 9 | const message = | ||
| 10 | "Do not call raw fetch in core/plugin code. Route HTTP through Effect HttpClient or an explicit package-local boundary. Skill: wrdn-effect-raw-fetch-boundary."; | ||
| 11 | |||
| 12 | const checkedPrefixes = [ | ||
| 13 | "packages/core/sdk/src/", | ||
| 14 | "packages/plugins/graphql/src/", | ||
| 15 | "packages/plugins/mcp/src/", | ||
| 16 | "packages/plugins/openapi/src/", | ||
| 17 | ]; | ||
| 18 | |||
| 19 | const temporaryAllowedFiles = new Set([ | ||
| 20 | "packages/core/sdk/src/oauth-discovery.ts", | ||
| 21 | "packages/core/sdk/src/oauth-discovery.test.ts", | ||
| 22 | "packages/core/sdk/src/oauth-helpers.test.ts", | ||
| 23 | "packages/core/sdk/src/oauth-service.ts", | ||
| 24 | "packages/plugins/mcp/src/sdk/probe-shape.ts", | ||
| 25 | "packages/plugins/openapi/src/sdk/client-credentials-oauth.test.ts", | ||
| 26 | "packages/plugins/openapi/src/sdk/multi-scope-oauth.test.ts", | ||
| 27 | "packages/plugins/openapi/src/sdk/oauth-refresh.test.ts", | ||
| 28 | ]); | ||
| 29 | |||
| 30 | const shouldCheck = (filename) => { | ||
| 31 | const normalized = toRepoRelative(filename); | ||
| 32 | if (isDeclarationFile(filename)) return false; | ||
| 33 | if (temporaryAllowedFiles.has(normalized)) return false; | ||
| 34 | return checkedPrefixes.some((prefix) => normalized.startsWith(prefix)); | ||
| 35 | }; | ||
| 36 | |||
| 37 | const isGlobalFetchMember = (node) => { | ||
| 38 | const expression = unwrapExpression(node); | ||
| 39 | if (expression?.type !== "MemberExpression") return false; | ||
| 40 | const object = unwrapExpression(expression.object); | ||
| 41 | const property = getPropertyName(expression.property); | ||
| 42 | return ( | ||
| 43 | property === "fetch" && | ||
| 44 | (isIdentifier(object, "globalThis") || | ||
| 45 | isIdentifier(object, "window") || | ||
| 46 | isIdentifier(object, "self")) | ||
| 47 | ); | ||
| 48 | }; | ||
| 49 | |||
| 50 | const isBareFetchCall = (node) => isIdentifier(unwrapExpression(node), "fetch"); | ||
| 51 | |||
| 52 | export default { | ||
| 53 | meta: { | ||
| 54 | type: "problem", | ||
| 55 | docs: { | ||
| 56 | description: "Disallow raw fetch outside approved Effect HTTP boundaries.", | ||
| 57 | }, | ||
| 58 | }, | ||
| 59 | create(context) { | ||
| 60 | if (!shouldCheck(context.filename)) return {}; | ||
| 61 | |||
| 62 | return { | ||
| 63 | CallExpression(node) { | ||
| 64 | if (isBareFetchCall(node.callee) || isGlobalFetchMember(node.callee)) { | ||
| 65 | context.report({ node: node.callee, message }); | ||
| 66 | } | ||
| 67 | }, | ||
| 68 | MemberExpression(node) { | ||
| 69 | if (node.parent?.type === "CallExpression" && node.parent.callee === node) return; | ||
| 70 | if (isGlobalFetchMember(node)) { | ||
| 71 | context.report({ node, message }); | ||
| 72 | } | ||
| 73 | }, | ||
| 74 | }; | ||
| 75 | }, | ||
| 76 | }; | ||