diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..71b2081 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +## Unreleased + +### Breaking changes + +- Name the supported root exports explicitly. Remove `normalizeClientAddress`, + `ConnectNext` and `NodeWebSocketOptions`; use the peer header or derive nested + types from `NodeHandler` and `ListenOptions` as described in + [the complete API inventory](docs/0.5.0-api.md). + +### Fixed + +- Dispose startup abort hooks and WebSocket ownership when native `listen` throws. +- Release failed response writers and bounded WebSocket rejections without waiting + for application cancellation promises; cancel stalled or late upgrade bodies. +- Cancel late HTTP responses and bodies rejected by Node's header validation; + clear partial application headers before writing the minimal error response. +- Allocate the bounded MCP line buffer before installing abort hooks, so a setup allocation error leaves no hooks behind. +- Prevent MCP dispatch after shutdown or cancellation while authentication waits. +- Contain asynchronous diagnostic and protocol-output stream failures, including writes pending when + the MCP connection closes. +- Correct the documented default WebSocket origin policy and the README router example. + +### Validation + +- Add real-socket ownership regressions, MCP framing/recovery cases, V8 source + coverage and strict installed-package checks with TypeScript 6 and 7. +- Ship migration and hardening evidence with the package. Align qualification + types to Node 24 and update compatible development tooling advisories. diff --git a/README.md b/README.md index e5fffe6..b58c4b0 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,13 @@ npm install @askrjs/server @askrjs/node ```ts import { createServer } from "node:http"; -import { createServerApp, json } from "@askrjs/server"; +import { createServerApp } from "@askrjs/server"; +import { createRouter } from "@askrjs/server/router"; +import { json } from "@askrjs/server/http"; import { createNodeHandler } from "@askrjs/node"; const app = createServerApp({ - routes: [{ path: "/health", handler: () => json({ status: "ok" }) }], + router: createRouter().get("/health", () => json({ status: "ok" })), }); createServer(createNodeHandler(app, { baseUrl: "http://localhost:3000" })).listen(3000); @@ -140,3 +142,8 @@ Protocol messages use stdin/stdout; diagnostics remain isolated on stderr. Authe provided directly or resolved from the process environment for each message. Closing stdin, calling `connection.close()`, or aborting its signal detaches the transport, cancels active requests, terminates the MCP session, and prevents late protocol output. + +## 0.5.0 preparation + +See [API decisions and migration](docs/0.5.0-api.md) and +[transport hardening evidence](docs/0.5.0-hardening.md). diff --git a/benches/http.bench.ts b/benches/http.bench.ts index f1db7b4..2cc368d 100644 --- a/benches/http.bench.ts +++ b/benches/http.bench.ts @@ -1,4 +1,5 @@ import { EventEmitter } from "node:events"; +import assert from "node:assert/strict"; import type { IncomingMessage, ServerResponse } from "node:http"; import { bench, describe, type BenchOptions } from "vitest"; import type { ServedApplication } from "../src/contracts.js"; @@ -145,6 +146,7 @@ describe("Node HTTP server", () => { "should serve an empty application response over HTTP", async () => { const response = await fetch(served.url); + assert.equal(response.status, 204); await response.arrayBuffer(); }, { @@ -165,6 +167,7 @@ describe("Node HTTP server", () => { "should serve a dynamic route with an asset root over HTTP", async () => { const response = await fetch(`${served.url}/route`); + assert.equal(response.status, 204); await response.arrayBuffer(); }, { @@ -184,15 +187,16 @@ describe("Node HTTP server", () => { bench( "should serve a small static asset over HTTP", async () => { - const response = await fetch(`${served.url}/package.json`); - await response.arrayBuffer(); + const response = await fetch(`${served.url}/bench-asset.txt`); + assert.equal(response.status, 200); + assert.equal((await response.arrayBuffer()).byteLength, 1024); }, { ...BENCH_OPTIONS, setup: async () => { served = await serve( { fetch: async () => new Response(null, { status: 500 }) }, - { assets: { root: "." }, signals: false }, + { assets: { root: "tests/fixtures" }, signals: false }, ); }, teardown: async () => { @@ -205,6 +209,7 @@ describe("Node HTTP server", () => { "should reject a missing static asset over HTTP", async () => { const response = await fetch(`${served.url}/missing.js`); + assert.equal(response.status, 404); await response.arrayBuffer(); }, { @@ -228,6 +233,7 @@ describe("Node HTTP server", () => { body: JSON_BODY, method: "POST", }); + assert.equal(response.status, 204); await response.arrayBuffer(); }, { diff --git a/docs/0.5.0-api.md b/docs/0.5.0-api.md new file mode 100644 index 0000000..c21eb2f --- /dev/null +++ b/docs/0.5.0-api.md @@ -0,0 +1,65 @@ +# Node adapter API decisions for 0.5.0 + +The root now names its supported contracts explicitly: **16 → 13 entrypoint/name +pairs** (13 → 10 at the root, all three MCP contracts retained). No deprecated +aliases remain. Package versions and dependency ranges remain on 0.4.x until the +coordinated candidate is frozen; this document describes the intended 0.5 break. + +## Entrypoints + +| Export key | Decision | Consumer contract | +| ---------------- | -------- | --------------------------------------------------------------- | +| `.` | KEEP | Node HTTP adapters and their configuration/lifecycle contracts. | +| `./mcp` | KEEP | Optional Node stdio transport, isolated from HTTP imports. | +| `./package.json` | KEEP | Tooling reads package identity and supported runtime/exports. | + +## Complete named export inventory + +| Entrypoint | Name | Decision | Reason or migration | +| ---------- | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| root | `CLIENT_ADDRESS_HEADER` | KEEP | Destroyer reads the adapter-written peer identity; applications need this key to distinguish the TCP peer from untrusted forwarded headers. | +| root | `createNodeHandler` | KEEP | Vite integrates the fetch application with its existing Node/Connect listener. | +| root | `listen` | KEEP | API-only and MCP examples own the HTTP server and its shutdown. | +| root | `serve` | KEEP | CLI full-stack/SSR templates, examples and Destroyer serve assets plus application shutdown. | +| root | `NodeHandler` | KEEP | Consumers type a Node/Connect handler, including its optional error callback. | +| root | `NodeHandlerOptions` | KEEP | Integrators name the trusted URL/host configuration passed to `createNodeHandler`. | +| root | `ListenOptions` | KEEP | Applications share typed binding, timeout, abort and WebSocket configuration. | +| root | `ListeningServer` | KEEP | Applications name the listening handle returned by `listen` and use native HTTP lifecycle operations. | +| root | `ServeOptions` | KEEP | Applications share typed static-asset and process-signal configuration for `serve`. | +| root | `ServedApplication` | KEEP | Applications store the returned URL and idempotent application shutdown handle. | +| root | `normalizeClientAddress` | REMOVE | Internal normalization of socket peers has no external imports. Read `request.headers.get(CLIENT_ADDRESS_HEADER)` for adapter-supplied identity; normalize unrelated addresses in their owning layer. | +| root | `ConnectNext` | REMOVE | Redundant nested handler type with no consumers. Use `NonNullable[2]>`. | +| root | `NodeWebSocketOptions` | REMOVE | Redundant nested configuration with no consumers. Derive the object member of `ListenOptions["websocket"]` as shown below. | +| mcp | `connectMcpStdio` | KEEP | The MCP example bridges a server to stdin/stdout without an HTTP listener. | +| mcp | `McpStdioOptions` | KEEP | Embedders supply typed dependencies, streams, authentication, cancellation and framing bounds. | +| mcp | `McpStdioConnection` | KEEP | Embedders own explicit `close()` and await `closed` independently of process exit. | + +```ts +type WebSocketOptions = Exclude; +``` + +The nested names remain implementation declarations where needed, but are not +importable package contracts. There is no deep-import replacement. + +## Consumer audit and migration + +The release's TypeScript 6 compiler inventory and source searches cover sibling +packages, CLI templates, examples, Destroyer, docs and tests. The three removed +names have no external source imports; only Node's internal address tests use the +normalizer directly. The authored website has no import of those names. Its 0.4 +API snapshot still lists them and must be regenerated for the final candidate. + +Node's own router tests and README now import `createRouter` from +`@askrjs/server/router`; README response helpers come from `@askrjs/server/http`. +The README's obsolete `routes` array has been replaced by an actual router. +`serve` uses its private content-type parser for its HTML cache policy instead of +probing the Server HTTP namespace for an implementation helper. Mixed-case media +types with parameters are exercised by the packed real-HTTP check. + +## Package verification + +`npm run check` includes a normal install of the actual tarball, exact export-key +and declaration-name inventories, all three removed-import failures, fourteen +private-path failures, strict TypeScript 6.0.2 and 7.0.2 checks with Node 24 types, +and real HTTP/MCP requests. No forced peer installation is used. Both type +replacements above are compiled in that installed consumer. diff --git a/docs/0.5.0-hardening.md b/docs/0.5.0-hardening.md new file mode 100644 index 0000000..0a5a853 --- /dev/null +++ b/docs/0.5.0-hardening.md @@ -0,0 +1,99 @@ +# Node transport hardening for 0.5.0 + +## Scope and baseline + +Reviewed from develop `1ccc49d700a698d4339561d351f9f2f4f663c0de` on Node +24.21.0. The baseline `npm run check` passed 71 cases. Source and assertion bodies +were inspected for URL/address trust, HTTP conversion/writing, assets, startup, +shutdown, WebSockets and MCP stdio. This pass prepares a candidate; it does not +publish a version or replace the final cross-package release qualification. + +## Confirmed defects and regressions + +| Boundary | Reproduction and result | Fix | +| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Synchronous native bind failure | Eight invalid-port variants, with/without WebSockets, preserve `ERR_SOCKET_BAD_PORT` but retain the signal's abort listener. | Catch native startup throws, detach startup/abort hooks and close WebSocket ownership before rejecting with the original error. | +| Failed HTTP write | An invalid stream chunk fails a real Node write; a never-settling cancel hook prevents socket destruction and reader release. Throwing cancellation is the passing control. | Start best-effort cancellation without awaiting it, destroy the failed transport and release its reader. | +| Oversized upgrade rejection | Declared and actual five-byte rejections with a four-byte limit hang when cancellation never settles. | Enforce the bound without awaiting application cleanup; send the bounded 500 fallback and release the reader. | +| Stalled upgrade rejection | A disconnected socket leaves a stalled rejection body locked and uncancelled. | Race the reader owner against the upgrade signal, cancel with that reason, and release ownership. Late application responses are also disposed. | +| Authentication/shutdown ordering | Closing or cancelling while asynchronous stdio authentication waits still starts `mcp.handle` after authentication resolves. | Recheck connection/request lifetime before dispatch and ignore authentication rejection after cancellation. | +| Asynchronous diagnostics failure | A real Writable fails its callback after the MCP connection closes; an unhandled error event terminates an isolated process. | Own diagnostic error events until pending writes settle. Share one listener across concurrent writes and preserve caller listeners. | +| Late HTTP response | A handler returns a stalled body after the client has disconnected; its cancel hook is never called. | Reject response ownership at an already-destroyed/ended target and cancel the returned body without waiting. | +| Rejected response headers | Fetch permits a header value Node rejects. The body leaks, and a partial application Content-Length/cookie contaminates the 500 fallback. | Cancel the unconsumed body on header failure and clear partial headers before writing the minimal error. | + +The initial focused run had 14 failures among 16 cases (two passing controls). +Additional isolated-process and HTTP probes reproduced diagnostics and response +ownership failures before their fixes. Tests retain real native failure codes, +original cancellation ownership and healthy-endpoint recovery. + +Protocol output has the same pending-write hazard: a stdout callback failing +after explicit close also crashed an isolated process. Both stdout and diagnostics +now use one private write owner; connection hooks can detach while the write's +error listener remains until completion. The packed consumer retains both crash +regressions. + +A fault-injected line-buffer allocation failure also reproduced a retained abort +hook during partial MCP setup. Allocation now completes before any abort hook is +installed. This is a controlled allocation-error regression, not an OS memory +exhaustion qualification. Twelve malformed framing/concurrency configurations +are rejected before acquiring hooks. + +## Executed contract matrix + +| Contract | Evidence and status | +| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Client identity and URL trust | Covered: TCP peer overwrites spoofed adapter identity; forwarded headers remain untrusted; IPv4/IPv6/mapped addresses, explicit host/base URL, absolute/network targets, fragments and backslashes are asserted. | +| Partial requests, timeout and backpressure | Covered: real-socket shared adapter conformance cancels an infinite response and expires an incomplete request; native malformed/oversized headers fail before dispatch; writer drain/early-close and repeated cookies/HEAD are asserted. | +| Startup and shutdown | Covered: invalid native ports and repeated occupied-port failure, hook removal, original error retention, application close once, concurrent close, in-flight completion and external abort. | +| WebSocket transport | Covered: text/binary echo, origins, bounds, duplicated cookies, sync/async handler/listener failure, disconnects, close-handshake timeout and shutdown. Oversized/stalled rejection cleanup is exercised over actual sockets. | +| Static assets | Covered: reserved missing paths, MIME/cache policy, source-map exclusion, decoded traversal, dotted exclusions, early/mid-stream disconnect and recovery. Symlink escape assertions run on non-Windows platforms; Windows symlink behavior is not claimed. | +| MCP framing and lifecycle | Covered: byte-at-a-time multibyte UTF-8, split CRLF, exact byte boundary, oversized-line discard/recovery, malformed UTF-8/JSON recovery, non-object messages, unterminated EOF, concurrency saturation, cancellation at capacity, pending authentication and EOF/abort cleanup. | +| Diagnostic cleanup | Covered: successful and failed writes, 32 queued reports with one owned listener, preservation of caller listeners, synchronous throw, destroyed streams and asynchronous failure after shutdown in an isolated packed consumer. | +| Installed package | Normal tarball install; all 13 declaration names, three removed names, fourteen private subpaths, strict TypeScript 6/7 with Node 24 types, HTTP/HTML cache behavior and real MCP dispatch. | + +## Coverage and limits + +`npm run check` runs V8 against every production source file, with whole-source +floors of 89% statements, 82% branches, 82% functions and 93% lines. Diagnostic +ownership is held at 100%; changed response/startup owners have additional floors. +The final local 106-case run measured 90.46% statements, 84.09% branches, +83.11% functions and 93.77% lines. Coverage is partial: defensive drain-error, +request-normalization, MCP write/error callbacks and some WebSocket cleanup +branches are not all reached. Counts are supplementary to the assertions above. + +Application callbacks that never resolve cannot be forcibly terminated. Cancelling +an application stream does not await arbitrary user cleanup. This pass does not +claim trusted-proxy inference, TLS, third-party reverse proxies, OS descriptor +exhaustion or a production capacity limit. No optimization or capacity improvement +is claimed. + +## Commands and dependency qualification + +Run all commands on Node 24.21.0: + +- `npm ci` +- `npm run fmt -- --check` +- `npm run check` +- `npm run bench:http -- --outputJson ` +- `npm audit --json` + +The development audit's four advisories are resolved through compatible tooling +updates. Production runtime dependency records are unchanged; Node type definitions +and their `undici-types` dependency are aligned from Node 26 to Node 24 for the +package's supported runtime. Production dependency ranges are unchanged. Final +source hashes, artifact integrity, hosted platform results and raw benchmark +samples are retained in the coordinated release evidence. + +The HTTP benchmark asserts expected status/body size and uses a fixed 1 KiB +fixture instead of a changing package manifest. The immutable baseline and +candidate use the same fixtures and toolchain. Measurements are diagnostic; +no budget was raised and no benchmark success is treated as hardening proof. + +## Paired HTTP diagnostics + +All eleven baseline/candidate cases produced finite positive means, rates and +sample counts. Real HTTP means were 43.923 → 44.364 µs (empty), +50.266 → 49.413 µs (dynamic with assets), 129.962 → 131.324 µs (fixed static +asset), 61.296 → 62.248 µs (missing asset) and 66.829 → 65.687 µs (small POST). +Microbench changes range from -0.4% to +4.0%; real HTTP changes range from -1.7% +to +1.6%. These single paired runs are diagnostics, not a significance claim. diff --git a/package-lock.json b/package-lock.json index 7608018..1e28e98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,11 @@ }, "devDependencies": { "@askrjs/schema": ">=0.4.0 <0.5.0", - "@types/node": "^26.3.0", + "@types/node": "24.19.2", + "@typescript/typescript6": "npm:typescript@6.0.2", + "@vitest/coverage-v8": "4.1.11", "publint": "^0.3.24", - "typescript": "^7.0.2", + "typescript": "7.0.2", "vite-plus": "^0.3.1", "vitest": "^4.1.11" }, @@ -85,6 +87,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", @@ -95,6 +107,22 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", @@ -105,6 +133,30 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@blazediff/core": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", @@ -112,6 +164,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -119,10 +181,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@oxc-project/runtime": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.148.0.tgz", - "integrity": "sha512-0ExYgJZv8+nFuoV0T/xzX5ZyXFXwkHJFJg9LWTwhqgRLpe+IzBLirvyYKSdS8mPofTBKAjDYIA2o84xh5BHXew==", + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.150.0.tgz", + "integrity": "sha512-vC4tN4n2c+gVq69Oi6CoZxGk/1mYjLiN4hVEYwvtuns/S4QwUBiJYViwLGLxPaLQnsY5FQDgGs+h6moAmSedvA==", "dev": true, "license": "MIT", "engines": { @@ -140,9 +213,9 @@ } }, "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.66.0.tgz", - "integrity": "sha512-2Me9eoptv6ERdEuI2P8AOlYdHHraXebJaM6SC0kc2Dfb+mLrep2db+fedBPKaYn673h/vBgvP4tkOdAbaudX6w==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.68.0.tgz", + "integrity": "sha512-dhfYPbzv/h9JgHjNkl2R6sOjUfxDyLGOZVb3g8/ScaTNwwJcYgmHh8kcYFDUhinuy1QAoANCWUvw1jlk+z6gAg==", "cpu": [ "arm" ], @@ -157,9 +230,9 @@ } }, "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.66.0.tgz", - "integrity": "sha512-u7O+bSSF0HGsDKkQQxBqvLGVepu93RA+JKu+ONqvfh4sCnCEbj31wZj4iG5gk3XfRwrmYj0/8catkO2LcblQKQ==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.68.0.tgz", + "integrity": "sha512-v3Njdi6qY0O/5eGfg01ww2w6gTn2mUvZ72Bnx1/UN53A9wruh3Nk6otc3WkgJLkXD4Qgz1SOcVQieH1oD03V9Q==", "cpu": [ "arm64" ], @@ -174,9 +247,9 @@ } }, "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.66.0.tgz", - "integrity": "sha512-/ikyMIVjX/sdo7KtjxoEsSUosfPzveVhT9RWMx9yGqFDKFJ89JAEKuEeLBmurDjrkb4w8tOnAdSO3SBaplY3bw==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.68.0.tgz", + "integrity": "sha512-ei4MCMzHFREmZwPJ7KuWUB4kBuHdsgDrnXGJVcEAopU7fj7S42I8BChdFILWdHvhFqR08FLJtOfbZIr2CDw0cA==", "cpu": [ "arm64" ], @@ -191,9 +264,9 @@ } }, "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.66.0.tgz", - "integrity": "sha512-q5xUsKeFqawa9NXa6ZGXWimFV19m8MogKPdTaSVDAAk2EQKBmBZRDeluwcl1p8ty/OFc9s9888OKEh3xfPVH0g==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.68.0.tgz", + "integrity": "sha512-UrKgzZxYhwB9DSvTX+vdgl9M32wLUNKJcAKIoiyx/Kzn/zveqi7W6kYVcRroFMhS3Kwz0KhTk3WBeSuQn4YCTg==", "cpu": [ "x64" ], @@ -208,9 +281,9 @@ } }, "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.66.0.tgz", - "integrity": "sha512-CR+x4VzMY0pRXLK/xFQ/RzsSFkP5t2Z2mef0QY6OP/rTRcMUoMLCOM62/3Fp/t0K+UDoBKxvMyeb6D0zPMjleA==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.68.0.tgz", + "integrity": "sha512-6jrEKgpJbilM1QaRv7hEtKXr4p4AK4jvvyOtajwyhu0kOz3e0O7OLnSTk6tBotRqCcUC4ehZRJ1Zx+Y99wieLw==", "cpu": [ "x64" ], @@ -225,9 +298,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.66.0.tgz", - "integrity": "sha512-ZEYmO/LbH9tTQCADILHGZE4GeOXOAj2VzedHkASNwjmwlwtutJCLpCJbIs37wRGTFgWRoEcD72jpMX+IBJUGjQ==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.68.0.tgz", + "integrity": "sha512-YOIVnKOBaLeGullskS179N12hjSAdFYnzLjOaKiLhAKNWgnShq9w4xRdtmUm6BlnP65l2/EA9Aw/KlftNxDM7Q==", "cpu": [ "arm" ], @@ -242,9 +315,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.66.0.tgz", - "integrity": "sha512-hNtR9/oU0CeTkq7JnRkmBQwqe17v2ZaAMLC4VcN7IIOWeRyWDk0knSPWS9iiLmtbZ2RRBBtsG01jQgkZmKCJeQ==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.68.0.tgz", + "integrity": "sha512-xW5XoEHVNqydPBv2KXvk9lmEzyAlOQHVEazKoXUuAacqekjya+OdiaFjjEBl0oJD02raG8g3TRl9OVCh9PDIHA==", "cpu": [ "arm" ], @@ -259,9 +332,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.66.0.tgz", - "integrity": "sha512-uwOVQ8i6I1LT/+eDzfsgrrcZp8Fn6NPVUPn8fF5gdFGekFf0PddF+LEuwsD0/pbNUcKZhDj2rQ5UpITh9gF4iQ==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.68.0.tgz", + "integrity": "sha512-QCvYwVVQieu6oyJglAgV9vH/YMDxZyR4cwVSYtoq9oOXd5N+D3TDUBjNwxFrLn5AdcJZOcvn/7IB17vJGp+2Og==", "cpu": [ "arm64" ], @@ -279,9 +352,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.66.0.tgz", - "integrity": "sha512-tTkF2Dmx4nGAjmBlZb+UtTGqR/EK4ZrW9qBfzte07a9XWqzoGGKzpFFlyNDhQe+Uwql94+ReCTeNbhOXscw1Dg==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.68.0.tgz", + "integrity": "sha512-4TVz5iFQ8ndrHnhX50UXiz9BIWAtUSOHJ6Nus4qWFfJBXq/Ed/krXbF/ehJu42BXM5tIvBs99jNIM22s9agY5A==", "cpu": [ "arm64" ], @@ -299,9 +372,9 @@ } }, "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.66.0.tgz", - "integrity": "sha512-F3cKHUav4yXOHn6GFnwpBhSYsJOYKKf9eqO/9jlEuqPxNw9zb98E9ZFct79gcg8pibUGkbveEu9WDlmXJpDzKw==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.68.0.tgz", + "integrity": "sha512-qLe3ao0RP84bnPxBvRI+GnlK/jybo538NWu0Xrm+zYeTmJtpzqLhnnd5BH21NafqKnplbGZjtN1cnrOlWF73lw==", "cpu": [ "ppc64" ], @@ -319,9 +392,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.66.0.tgz", - "integrity": "sha512-K5fDaNZfDyQMYA/3qL21bqyN0X9T15LLwwbFPt2aHc94+ZG7bh0vZEsy2y7NlRnjjHFSwN+Hzg6ldJtbOriH4Q==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.68.0.tgz", + "integrity": "sha512-Yvyl7a6gbb0vM6r925KW2dO+/CmXySO5TVbcX7o/uZJ+d108HFOG0TxIyHApmn5USuj5mhDPxzhiVwOlGP7uSA==", "cpu": [ "riscv64" ], @@ -339,9 +412,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.66.0.tgz", - "integrity": "sha512-44Yc+I+qOmTElRcEhm5hUKIUJEQIOugymz4ua4tB0Wox7tGAfIbjzmXz/HDAtw1Ij6gmBwZlzh4hc9679RhWeA==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.68.0.tgz", + "integrity": "sha512-mJlFuFVCxzrYM5sFStN433D/s/mb6Wq4aCQAM02vs/OudHywnaSAd2rb1vlYUJtqdYIciJtiasuxvfbYkv5fLg==", "cpu": [ "riscv64" ], @@ -359,9 +432,9 @@ } }, "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.66.0.tgz", - "integrity": "sha512-1e29Eg9hEj2kRBB19M0seIehPbbXHCk35GvImjDvb79rjjYjXCRmtbUNHJcgoktZAMIzXrTbxDBKmTc1V4bg3A==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.68.0.tgz", + "integrity": "sha512-RlfSg++qs1hbKltRR6lYvV9EoI3MdlfSQD9w1hdHVYjHqjIn1tkH4FWOpMSmjKGN20zr+nI+W9o4ARogCDudGQ==", "cpu": [ "s390x" ], @@ -379,9 +452,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.66.0.tgz", - "integrity": "sha512-vODY1UQo10gngn0+D4xHKU84F1Twm1LqrzV4SqPXvmQKSd87paehvZ6jqA5wKs6XQrlWul9clYMDVHcoW9CPMA==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.68.0.tgz", + "integrity": "sha512-nyzRB9U+dlYUKu3pMo3afHzZBUv/oTHZMG36ZfJViNVfOIzp70Q4GS8FFRGgYJ/p0zcyDCgpBvYISOdJOMh+jQ==", "cpu": [ "x64" ], @@ -399,9 +472,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.66.0.tgz", - "integrity": "sha512-YDzXx2JsT4+HL4MdkVrYjO55NS5lUKNm8rLC4ZPou8+seu0v0jhecSh+ufoO6+xEa8gccEezMlI2WHJi4ApUgw==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.68.0.tgz", + "integrity": "sha512-iCx3sbZRIvGrL1RafphEiUKBaW1lc0/tAjKOIB/Wjw2+STRBEdu5+fH1Gc1faEWEmc2k5Ks4iUUV54Zd5C9a1A==", "cpu": [ "x64" ], @@ -419,9 +492,9 @@ } }, "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.66.0.tgz", - "integrity": "sha512-mJjUYd8lj0+j4JkYyEM+5qKBf1Rnrpgjn/SVYKJhicVDqLz566ooa7Fs8zflPqt+dnZDV7X054rVIQX6ZcQNlQ==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.68.0.tgz", + "integrity": "sha512-x2X5AZez7OgyLLFpwIgItoXBUqudDM7yiaTsxv8R8vKQ6e81l0jVw0NFeUCXzcl1sAJq8h+tC8N4mY8EiMeL4w==", "cpu": [ "arm64" ], @@ -436,9 +509,9 @@ } }, "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.66.0.tgz", - "integrity": "sha512-soV+0vESv7e5ntCHWC61x4gg8OSak6IHHnWsZmHrJFlvMj2AK+kmldErCNkVkrvc1Ts2/++rJXn+IuAb2WMXhw==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.68.0.tgz", + "integrity": "sha512-AHVPjXkenLPQUh6kB8zSC8pX2ct9r4T1Edk9r/RNJyov6wPsS5uAfYtipwG4chn6+3bPFG5rI/3DxEeH8vib1w==", "cpu": [ "arm64" ], @@ -453,9 +526,9 @@ } }, "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.66.0.tgz", - "integrity": "sha512-YCPi23uRIEYuIKTZohAkKbPFpujQ5QBuUM5iDv+UqbCmTPAkaFsxjsSuB8xlBpRT0G7eP/4HMF+cPDSqHtOD9A==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.68.0.tgz", + "integrity": "sha512-n09SjEk5VH7z8Hl4WVP7hho+cCwGViENkQFiM45vbW85dJd7kEhWaHRUobaPzEmrWyu6uumd4EuNfNyDKLtzDA==", "cpu": [ "ia32" ], @@ -470,9 +543,9 @@ } }, "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.66.0.tgz", - "integrity": "sha512-bwTQcv/JVRPkOqQtMF0X7vpvpncDQiBcXHxZ9S2hR12Hlo8bvBdUR5x5XnxzDZ3kM0qoZw1rv7KaD66Ly+pFWA==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.68.0.tgz", + "integrity": "sha512-gPe+dJLXaPuWPWqlpklDAJp0k+K9KhQPYiQLHfb+i2rmFuUGfJ/5Qlj6tr1mO6of5g0DiLjG/XCFHIaPhotqqA==", "cpu": [ "x64" ], @@ -571,9 +644,9 @@ ] }, "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.81.0.tgz", - "integrity": "sha512-IcCRsXiedJoJopY6mpZUBEeVFsUrutmrG7dZ87zMuKJlhg70Ora9bBl1WcCxZQtyI10YpnVdEso5oCg7YcfSHw==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.83.0.tgz", + "integrity": "sha512-0yGY24EwsLk5YDe6F+VkmZyRHSwJDALa3nIrPpq7FXmp2lV2d0TzvBCGeZk+wgiULRGr5blhyr4QMp5KCXJUqA==", "cpu": [ "arm" ], @@ -588,9 +661,9 @@ } }, "node_modules/@oxlint/binding-android-arm64": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.81.0.tgz", - "integrity": "sha512-GRrIPyTGVhx3L3h+0T5xT2A0jFAcdPv4+IfuXpGDLIdl6XeYhgg/zw72A5ILZoUgRqZuM8F1y+V/gfDriXSxzQ==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.83.0.tgz", + "integrity": "sha512-hHfJ0vc17A4iUjH5p9BsTUPYbYRNxGpvD2lbu1aBRk54bzNIx9o5TtYF39QPZcV95DagZd+4DEAw2RH3G2ZsMg==", "cpu": [ "arm64" ], @@ -605,9 +678,9 @@ } }, "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.81.0.tgz", - "integrity": "sha512-qNQ9tXRgLuKbqSV1S2h9h4KPHjbovO7RRR2/enUOtHzTkFZ7B9X5zqqHJua8dRyc7dBy7Aoyq5pqTSLFVcAzGQ==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.83.0.tgz", + "integrity": "sha512-hsOjYjszLb/3zym/TkzUMPAoQlTJcuzSyEPOAyA+skXJIX9M0o+4JfOtqopX/Vf4hSLrJ98j0nvFo23gzk8auQ==", "cpu": [ "arm64" ], @@ -622,9 +695,9 @@ } }, "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.81.0.tgz", - "integrity": "sha512-q0QTm32jWga2Gv4j7IaVZN0jYMi9UV73sWVgFtDA4iIfqwMCLLZ3ve+9KwfYtsaKZSgQhmPaogeZWqDZpcY1Pw==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.83.0.tgz", + "integrity": "sha512-mjh5oH2EA+wl5yRJYT9K9G61O2zFlpuv+yf2JwZOi0+dq2FnTUtm1h8i+5Ik0fXPWIu/k84I1psZR9aQsLAnyA==", "cpu": [ "x64" ], @@ -639,9 +712,9 @@ } }, "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.81.0.tgz", - "integrity": "sha512-/+8wVWDXEC7wHVAhOc59Fw/SkMc1arLkFD8iQCaSsmzenK1X4doFqquL9H1wrtGUzaiycVqkf/sSpcILK6W1UA==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.83.0.tgz", + "integrity": "sha512-fNHr64/YaO8YssuoDVC8+F4Uk5enR86q5uxfHkQrjAPs1dbAILOrD2uaud+J7MO8Fx774g44ERLD0IGIvZE48w==", "cpu": [ "x64" ], @@ -656,9 +729,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.81.0.tgz", - "integrity": "sha512-4xt422FEgioRq9hAL4Tq7fujGUWnc8z1BJ+Oi8RN8vB8axaP+sdK6a2xdlcQCCYnJg9QMuMFS0AucuIFx/EacA==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.83.0.tgz", + "integrity": "sha512-Qpwy3zzAwMj+8/lyYItHmkSMwbkprFNWTK7jPYDOxSyxEhaSLOWYUTCMkjF334J8/WD0nznCCsoBbIH6hpsuIw==", "cpu": [ "arm" ], @@ -673,9 +746,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.81.0.tgz", - "integrity": "sha512-u3vna8KdGplH4DRCW9K54D68fcMo7IxVrkCJWwXnIhwtBdnDnYrmzOUA/XjmBlPpcLsgw9Z5BNdY4za9+Dj+MQ==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.83.0.tgz", + "integrity": "sha512-s+BirYLFq7JL2k9sP0XI3ZXJ9dYvJ8sX3jLCLoag7tt+zrSHpZxP0jqznfL+Gdgwu7ay0dYgGYJXrQvq3iWloA==", "cpu": [ "arm" ], @@ -690,9 +763,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.81.0.tgz", - "integrity": "sha512-3j9k+gsYsE7nv71GWotXsqsa2l9/aJenD7dVHNt/CBvsb0SgRjSMnHFeP59IXUAl1wvVFhqGl2wJNMwWU3UBlA==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.83.0.tgz", + "integrity": "sha512-7lihXt3vKr+GIyapNbHrnFHm/biiW30le6Zv/DExbAFPF6YwCQXVFlONPFehxs0CpGO4CBfYPM9rdDT+XMoIlg==", "cpu": [ "arm64" ], @@ -710,9 +783,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.81.0.tgz", - "integrity": "sha512-k5iAp3dNxW0/uDCBY+WSm8jKB2szu7SkEQZdgRRpDXvuDd69vvDcqhB3A/pWCfCwXyenjNjFn9Td1fVoyAc+Yg==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.83.0.tgz", + "integrity": "sha512-q63JalLYVkZiZvls1z3PPUnpmQluOMXp0khqQMznCeAPLGydfNY8JhvuA4WlK57JfrvikU8wB5lPVveqpIXvew==", "cpu": [ "arm64" ], @@ -730,9 +803,9 @@ } }, "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.81.0.tgz", - "integrity": "sha512-TFqLja3uYmVSte6nof9GWrex9Z8WgdZrNiLC6Te5rXGDqXB2y4j/26iFhwosXiAFqDhE9JJVuuCkDKLwptTn1g==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.83.0.tgz", + "integrity": "sha512-krQmDF+dRbxvdqVPV88ZuOoPPu8X5BuqDA8Hd+qcS4YMRQCb+nexA57DazgGsc/rGdKBe3QmV0mnv0bdpW/p5g==", "cpu": [ "ppc64" ], @@ -750,9 +823,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.81.0.tgz", - "integrity": "sha512-UEcySvGS0NOVo7h7n7CYyJL9+6gFAh7Zc/ToDXVScFvzHSTIxtzkMVU30rmQ6+nQ1LF+UdiRDdJajpDu+OylLg==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.83.0.tgz", + "integrity": "sha512-MmOl8Y6txEAXZU1RG8Rr264jQ6D7VPmqFsU/45x/FeWsGe32hklTqGrLE6UxHzp5Rjt0wP+20tY8YXKgSFB3mw==", "cpu": [ "riscv64" ], @@ -770,9 +843,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.81.0.tgz", - "integrity": "sha512-H+diDbhD00+wI1IRP8Kz88x/lat+DgtoBJzoTthS16xkTJGNaEkfb8gzmd1rzc/2uDQQMl7GNl+JFUacVeWxIA==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.83.0.tgz", + "integrity": "sha512-u1rMymh0W3JZkq370kzQsYPULGWqhE09pZRqnZvUSoYaI9pVO5yVX+iYIslmWuEgwuzH9YAaOsScJiobWCHoOw==", "cpu": [ "riscv64" ], @@ -790,9 +863,9 @@ } }, "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.81.0.tgz", - "integrity": "sha512-8znJ/5TekjOKg1j1Acho4PJMdiAHLtlcXuWEiipOhAMV6rQcXdmDdXCbheyDczN6TjBwiNfjcP81k4AthrKRzw==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.83.0.tgz", + "integrity": "sha512-y0zK3HNwGysu7rqtE+BQG/d0bx5gh/KwlOtghN8oWeK1KcWzeaLqtZrbm8owqdma1lFyrce/hTO5ismuNu+INQ==", "cpu": [ "s390x" ], @@ -810,9 +883,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.81.0.tgz", - "integrity": "sha512-Q2Wj70yFsvn5QjlmifFzbj4H+kJy53bwqc41o1fzoM7MpLV1NIbhg/LpWXRfC6KOkSAdUx1Wd8VJsdPmhp/HRA==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.83.0.tgz", + "integrity": "sha512-rS5gM0NgD7ngmuJmbIehsidtrOwKkLFwCQbKEeb9KuyQrrWNq5Zkn0uV6AYdXOMJ0grrWEiLwBuvMxt8w5vsNw==", "cpu": [ "x64" ], @@ -830,9 +903,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.81.0.tgz", - "integrity": "sha512-cPInHp/ddEe5qkyK2IiyQ8Q3Mp2oLLEhhsGgTK2oZx4L6+llGam1H1yBvJZ7qHfOXj8N3hxBS8sj4tO+gtFlIg==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.83.0.tgz", + "integrity": "sha512-W2IH4EtpcPaWcvNGCA95YoDg4vxqE/ZiPCi3arrxEEpsK7+JQN9WYwrlYFx9pcdP6KPXqRqkv3zdQPHcx7b6YQ==", "cpu": [ "x64" ], @@ -850,9 +923,9 @@ } }, "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.81.0.tgz", - "integrity": "sha512-0CQxSX4ajqm07AHBf5U33qQzXKdd7wtq/oTL/7vpY6RNNuxrRi8W4bqUV1Jyu/vj+9KmxQyDhxfeVX1nQL6kfg==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.83.0.tgz", + "integrity": "sha512-6LyKkUyoajssTPLlZmDbZIbu4IZ5B4bGuRUnBgCGpEvHP3FQMaYITncHA/unPUo7q+Z+pIu2HhdkQ+8d1SG7iA==", "cpu": [ "arm64" ], @@ -867,9 +940,9 @@ } }, "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.81.0.tgz", - "integrity": "sha512-l0hbeISm9673hVrrQU8j/p2M7YH9Ouoj7p7E/QM55NTrKVLP+P3PF8hLu+OY+x0VtGRW+ggiQKZqmdYps9H+TA==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.83.0.tgz", + "integrity": "sha512-Uz/fObEtF0jmNJQJ8CGRBKfefYstS0/wjD3s6IGzP8nUwsJykHQJBiN3npHwKiGRGn/vvBEgNr4B3cCzmmatvg==", "cpu": [ "arm64" ], @@ -884,9 +957,9 @@ } }, "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.81.0.tgz", - "integrity": "sha512-ksqPP5jbFXcYreEQ7zdJh06rJQBymCTyGRCdaXjfcf2aG4f8KxUWY5wcgYHmaTK+FJ4bPG5sUAdOX+6trnH1JA==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.83.0.tgz", + "integrity": "sha512-u7XcvPW6Bk58tY5iWs2ESb0vJjoE/kuSpHxopbwp/p3ZtWVQXZ6wor5w3ssVTHOqd/v8b+QdhSFWQ4grEUNWpA==", "cpu": [ "ia32" ], @@ -901,9 +974,9 @@ } }, "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.81.0.tgz", - "integrity": "sha512-IZuUCwGw9emG5JtCp+fYGB+Z4OWEoeEcM8R5BA1pYw63/ieYFVdcU2ylxTpHbVHSenZnsYE+ZZ20uHAJszQ4cA==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.83.0.tgz", + "integrity": "sha512-LZRubd7ph13QmAg4fFecTYVZkiYbROR2Htaxh/ufWRkDhPOm2wrwaEYR89e0YpPFD3dqBrPoxS7myBw5hmYA7Q==", "cpu": [ "x64" ], @@ -1307,12 +1380,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.3.0.tgz", - "integrity": "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw==", + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", "license": "MIT", "dependencies": { - "undici-types": "~8.3.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/ws": { @@ -1664,6 +1737,21 @@ "node": ">=16.20.0" } }, + "node_modules/@typescript/typescript6": { + "name": "typescript", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@vitest/browser": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.11.tgz", @@ -1705,6 +1793,37 @@ "vitest": "4.1.11" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.11", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", @@ -1819,9 +1938,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-darwin-arm64": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-darwin-arm64/-/vite-plus-darwin-arm64-0.3.1.tgz", - "integrity": "sha512-3xK+G6zJHa4Q60NgAnnZ38ML5NSf5n3+DBHSINqCgsPpQ7XtMWkpEsXArWU7u3nlW3f1F2XLAvuAk54TmQXptw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-darwin-arm64/-/vite-plus-darwin-arm64-0.3.3.tgz", + "integrity": "sha512-1hwUfQGqvzlO+qQ7r1i5XQWkKaWq/JcdAAZOPloMQsZq7P8rgiY1pq/0q4stzcNxFF74k4aEFOlDUKi6D149YA==", "cpu": [ "arm64" ], @@ -1836,9 +1955,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-darwin-x64": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-darwin-x64/-/vite-plus-darwin-x64-0.3.1.tgz", - "integrity": "sha512-GbJ2RWZezmpr/erPlJ4Kw9T3y2EP3cwZSkh+Ph8IfcJMj3MRqTw1lfxCl81x8Opyct6wYU6kDRU3zH95Tl341Q==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-darwin-x64/-/vite-plus-darwin-x64-0.3.3.tgz", + "integrity": "sha512-nM2+EC1BPochRhtf48FWZX3wPnrsy8aBSI5TPqjxr0OS0bydvPavrhdSUmgUGZG9/hp0NTeXhhqY9A7yuvZLZQ==", "cpu": [ "x64" ], @@ -1853,9 +1972,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-linux-arm64-gnu": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-arm64-gnu/-/vite-plus-linux-arm64-gnu-0.3.1.tgz", - "integrity": "sha512-vYXxMRxE7DAa8QIh3k5IkfTeBbA/8JHYHL14rsodq4er4FNOd4AAM07eAczin7Pdf4Aie/2BPkymSF8hFyjZDg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-arm64-gnu/-/vite-plus-linux-arm64-gnu-0.3.3.tgz", + "integrity": "sha512-WC8/kT0/btxnwgYR5QvvelbgUaLQ6lbxs8aMQpjc63p1QdMTNZEbp3vnTV2l9KgoUYeyHnALJVBW9l1CJAM+fA==", "cpu": [ "arm64" ], @@ -1873,9 +1992,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-linux-arm64-musl": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-arm64-musl/-/vite-plus-linux-arm64-musl-0.3.1.tgz", - "integrity": "sha512-LAnyEqhZ2Fji6bXDnigSWbEjS/B7u/Al01N4JERSR6scr4PbZpfMW8/1EUHfi+9pIdxwoDazIRZeh5DvEDX8eQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-arm64-musl/-/vite-plus-linux-arm64-musl-0.3.3.tgz", + "integrity": "sha512-iy3AgJK9wwOGjlEzq0Sm1bqK5pDSWF4nz29Ey5BmK7bGciyfufeKRqRASKJdMhRMEQ7WVUQz5luC5GddLEDwXg==", "cpu": [ "arm64" ], @@ -1893,9 +2012,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-linux-x64-gnu": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-x64-gnu/-/vite-plus-linux-x64-gnu-0.3.1.tgz", - "integrity": "sha512-F9Ek2p+ZnDB2FrO6ugZU2ZnuOO/Q9GR9jwUcnK2oUHwl6AZUvg671L7vU57rrtY4f9pS+AcL9lWRoS4wedXm3A==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-x64-gnu/-/vite-plus-linux-x64-gnu-0.3.3.tgz", + "integrity": "sha512-ZHN3RCA422XrGYmSDE9IZqx/eM5PsFCXHn8hUttQEwcSVXsuKBArDyb/uxonn1n0bPNW8c4G4HvNmIS3ckLXzA==", "cpu": [ "x64" ], @@ -1913,9 +2032,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-linux-x64-musl": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-x64-musl/-/vite-plus-linux-x64-musl-0.3.1.tgz", - "integrity": "sha512-3Q+hJ19iw1k2NOCoop/gK+etMPnPYbgdqXBS9mgUuuDUA142v1PoYH+gk3NMV+KyDvSZaLkgDuGrdNav0pJtqw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-x64-musl/-/vite-plus-linux-x64-musl-0.3.3.tgz", + "integrity": "sha512-lDmmjrgh6qNvBlzapzKMjxiqRigkyPjIimkxrk8CSz+/AyQp6I/wuIAyjBmNfeh14SQoDNVgf03CTJzmJycrpg==", "cpu": [ "x64" ], @@ -1933,9 +2052,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-win32-arm64-msvc": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-win32-arm64-msvc/-/vite-plus-win32-arm64-msvc-0.3.1.tgz", - "integrity": "sha512-aZAfZdQBFDzktOF/0OJDYrkOP551IxmoMRGfU3KdJm25Gpj8WHl5QTNx/mL8216rbZ7p4QIdBc4gajoRCAtsuA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-win32-arm64-msvc/-/vite-plus-win32-arm64-msvc-0.3.3.tgz", + "integrity": "sha512-/5/YdLItQH9KC+jL49uw9CidC7MeO3Q8ojRcuOPcH5ujU5tU4P4lO2qJ9RbM8ckC1lfiZYTtVtJUSkbPn1R/YQ==", "cpu": [ "arm64" ], @@ -1950,9 +2069,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-win32-x64-msvc": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-win32-x64-msvc/-/vite-plus-win32-x64-msvc-0.3.1.tgz", - "integrity": "sha512-8rxCvdTpd1v/+g/ECjqKnHxBlxmk8O9kNl4wwDcfWszbL/O/pNNSktKHq6rR3A6m6p0NCoZsenbGLuJsHE+q9A==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-win32-x64-msvc/-/vite-plus-win32-x64-msvc-0.3.3.tgz", + "integrity": "sha512-npbLQAAKdWHEB0qOFkmF/2OUzdraNBe1w7gY1LjXQFvqORAKZlHyQ2rgF5nBtG5+LKvbKWL6XpvyP+cQEh7MZg==", "cpu": [ "x64" ], @@ -1991,6 +2110,7 @@ "cpu": [ "arm64" ], + "deprecated": "yuku-codegen is pure JavaScript since 0.14", "dev": true, "license": "MIT", "optional": true, @@ -2005,6 +2125,7 @@ "cpu": [ "arm64" ], + "deprecated": "yuku-codegen is pure JavaScript since 0.14", "dev": true, "license": "MIT", "optional": true, @@ -2019,6 +2140,7 @@ "cpu": [ "x64" ], + "deprecated": "yuku-codegen is pure JavaScript since 0.14", "dev": true, "license": "MIT", "optional": true, @@ -2033,6 +2155,7 @@ "cpu": [ "x64" ], + "deprecated": "yuku-codegen is pure JavaScript since 0.14", "dev": true, "license": "MIT", "optional": true, @@ -2047,6 +2170,7 @@ "cpu": [ "arm" ], + "deprecated": "yuku-codegen is pure JavaScript since 0.14", "dev": true, "libc": [ "glibc" @@ -2064,6 +2188,7 @@ "cpu": [ "arm" ], + "deprecated": "yuku-codegen is pure JavaScript since 0.14", "dev": true, "libc": [ "musl" @@ -2081,6 +2206,7 @@ "cpu": [ "arm64" ], + "deprecated": "yuku-codegen is pure JavaScript since 0.14", "dev": true, "libc": [ "glibc" @@ -2098,6 +2224,7 @@ "cpu": [ "arm64" ], + "deprecated": "yuku-codegen is pure JavaScript since 0.14", "dev": true, "libc": [ "musl" @@ -2115,6 +2242,7 @@ "cpu": [ "x64" ], + "deprecated": "yuku-codegen is pure JavaScript since 0.14", "dev": true, "libc": [ "glibc" @@ -2132,6 +2260,7 @@ "cpu": [ "x64" ], + "deprecated": "yuku-codegen is pure JavaScript since 0.14", "dev": true, "libc": [ "musl" @@ -2149,6 +2278,7 @@ "cpu": [ "arm64" ], + "deprecated": "yuku-codegen is pure JavaScript since 0.14", "dev": true, "license": "MIT", "optional": true, @@ -2163,6 +2293,7 @@ "cpu": [ "x64" ], + "deprecated": "yuku-codegen is pure JavaScript since 0.14", "dev": true, "license": "MIT", "optional": true, @@ -2177,6 +2308,7 @@ "cpu": [ "arm64" ], + "deprecated": "yuku-parser runs on yuku-core since 0.14", "dev": true, "license": "MIT", "optional": true, @@ -2191,6 +2323,7 @@ "cpu": [ "arm64" ], + "deprecated": "yuku-parser runs on yuku-core since 0.14", "dev": true, "license": "MIT", "optional": true, @@ -2205,6 +2338,7 @@ "cpu": [ "x64" ], + "deprecated": "yuku-parser runs on yuku-core since 0.14", "dev": true, "license": "MIT", "optional": true, @@ -2219,6 +2353,7 @@ "cpu": [ "x64" ], + "deprecated": "yuku-parser runs on yuku-core since 0.14", "dev": true, "license": "MIT", "optional": true, @@ -2233,6 +2368,7 @@ "cpu": [ "arm" ], + "deprecated": "yuku-parser runs on yuku-core since 0.14", "dev": true, "libc": [ "glibc" @@ -2250,6 +2386,7 @@ "cpu": [ "arm" ], + "deprecated": "yuku-parser runs on yuku-core since 0.14", "dev": true, "libc": [ "musl" @@ -2267,6 +2404,7 @@ "cpu": [ "arm64" ], + "deprecated": "yuku-parser runs on yuku-core since 0.14", "dev": true, "libc": [ "glibc" @@ -2284,6 +2422,7 @@ "cpu": [ "arm64" ], + "deprecated": "yuku-parser runs on yuku-core since 0.14", "dev": true, "libc": [ "musl" @@ -2301,6 +2440,7 @@ "cpu": [ "x64" ], + "deprecated": "yuku-parser runs on yuku-core since 0.14", "dev": true, "libc": [ "glibc" @@ -2318,6 +2458,7 @@ "cpu": [ "x64" ], + "deprecated": "yuku-parser runs on yuku-core since 0.14", "dev": true, "libc": [ "musl" @@ -2335,6 +2476,7 @@ "cpu": [ "arm64" ], + "deprecated": "yuku-parser runs on yuku-core since 0.14", "dev": true, "license": "MIT", "optional": true, @@ -2349,6 +2491,7 @@ "cpu": [ "x64" ], + "deprecated": "yuku-parser runs on yuku-core since 0.14", "dev": true, "license": "MIT", "optional": true, @@ -2406,6 +2549,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.7.tgz", + "integrity": "sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2516,6 +2678,62 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2816,6 +3034,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -2870,13 +3116,13 @@ } }, "node_modules/oxfmt": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.66.0.tgz", - "integrity": "sha512-FfvqR8RFtV6JJpRrpkfqyVCQ7HDvZ/VriWFx7veftCgL1B5ZO9qNr+1rvPieycMQnNfVG0PWyJQiy7p0hq1I5w==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.68.0.tgz", + "integrity": "sha512-Z0XMofcXCGUXbcpBHnWyUiX93BGiw1B+lcHNbQDWEtOhX06ewoFfu4zXkyiLhRrNnMq0twqXRHUcJetf+GsiQQ==", "dev": true, "license": "MIT", "dependencies": { - "tinypool": "2.1.0" + "tinypool": "2.1.2" }, "bin": { "oxfmt": "bin/oxfmt" @@ -2888,25 +3134,25 @@ "url": "https://github.com/sponsors/oxc-project" }, "optionalDependencies": { - "@oxfmt/binding-android-arm-eabi": "0.66.0", - "@oxfmt/binding-android-arm64": "0.66.0", - "@oxfmt/binding-darwin-arm64": "0.66.0", - "@oxfmt/binding-darwin-x64": "0.66.0", - "@oxfmt/binding-freebsd-x64": "0.66.0", - "@oxfmt/binding-linux-arm-gnueabihf": "0.66.0", - "@oxfmt/binding-linux-arm-musleabihf": "0.66.0", - "@oxfmt/binding-linux-arm64-gnu": "0.66.0", - "@oxfmt/binding-linux-arm64-musl": "0.66.0", - "@oxfmt/binding-linux-ppc64-gnu": "0.66.0", - "@oxfmt/binding-linux-riscv64-gnu": "0.66.0", - "@oxfmt/binding-linux-riscv64-musl": "0.66.0", - "@oxfmt/binding-linux-s390x-gnu": "0.66.0", - "@oxfmt/binding-linux-x64-gnu": "0.66.0", - "@oxfmt/binding-linux-x64-musl": "0.66.0", - "@oxfmt/binding-openharmony-arm64": "0.66.0", - "@oxfmt/binding-win32-arm64-msvc": "0.66.0", - "@oxfmt/binding-win32-ia32-msvc": "0.66.0", - "@oxfmt/binding-win32-x64-msvc": "0.66.0" + "@oxfmt/binding-android-arm-eabi": "0.68.0", + "@oxfmt/binding-android-arm64": "0.68.0", + "@oxfmt/binding-darwin-arm64": "0.68.0", + "@oxfmt/binding-darwin-x64": "0.68.0", + "@oxfmt/binding-freebsd-x64": "0.68.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.68.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.68.0", + "@oxfmt/binding-linux-arm64-gnu": "0.68.0", + "@oxfmt/binding-linux-arm64-musl": "0.68.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.68.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.68.0", + "@oxfmt/binding-linux-riscv64-musl": "0.68.0", + "@oxfmt/binding-linux-s390x-gnu": "0.68.0", + "@oxfmt/binding-linux-x64-gnu": "0.68.0", + "@oxfmt/binding-linux-x64-musl": "0.68.0", + "@oxfmt/binding-openharmony-arm64": "0.68.0", + "@oxfmt/binding-win32-arm64-msvc": "0.68.0", + "@oxfmt/binding-win32-ia32-msvc": "0.68.0", + "@oxfmt/binding-win32-x64-msvc": "0.68.0" }, "peerDependencies": { "svelte": "^5.0.0", @@ -2922,9 +3168,9 @@ } }, "node_modules/oxlint": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.81.0.tgz", - "integrity": "sha512-HyrJYqeoOCL0iqaLEzGewGT48ZX99P3hxYh8udAF9RGGIghSamkXE4ClUyBpEDNqasamThgmlPbuMOe7SAZmHg==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.83.0.tgz", + "integrity": "sha512-cyDzSzaw3uzP0TeCeq3lLRPPoaUxkbB4ZOXj+kn+5r+BX9V+4bNVGk9lxer+WrgcpebH4JxLlJ3KQjveVztOLQ==", "dev": true, "license": "MIT", "bin": { @@ -2937,25 +3183,25 @@ "url": "https://github.com/sponsors/oxc-project" }, "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.81.0", - "@oxlint/binding-android-arm64": "1.81.0", - "@oxlint/binding-darwin-arm64": "1.81.0", - "@oxlint/binding-darwin-x64": "1.81.0", - "@oxlint/binding-freebsd-x64": "1.81.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.81.0", - "@oxlint/binding-linux-arm-musleabihf": "1.81.0", - "@oxlint/binding-linux-arm64-gnu": "1.81.0", - "@oxlint/binding-linux-arm64-musl": "1.81.0", - "@oxlint/binding-linux-ppc64-gnu": "1.81.0", - "@oxlint/binding-linux-riscv64-gnu": "1.81.0", - "@oxlint/binding-linux-riscv64-musl": "1.81.0", - "@oxlint/binding-linux-s390x-gnu": "1.81.0", - "@oxlint/binding-linux-x64-gnu": "1.81.0", - "@oxlint/binding-linux-x64-musl": "1.81.0", - "@oxlint/binding-openharmony-arm64": "1.81.0", - "@oxlint/binding-win32-arm64-msvc": "1.81.0", - "@oxlint/binding-win32-ia32-msvc": "1.81.0", - "@oxlint/binding-win32-x64-msvc": "1.81.0" + "@oxlint/binding-android-arm-eabi": "1.83.0", + "@oxlint/binding-android-arm64": "1.83.0", + "@oxlint/binding-darwin-arm64": "1.83.0", + "@oxlint/binding-darwin-x64": "1.83.0", + "@oxlint/binding-freebsd-x64": "1.83.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.83.0", + "@oxlint/binding-linux-arm-musleabihf": "1.83.0", + "@oxlint/binding-linux-arm64-gnu": "1.83.0", + "@oxlint/binding-linux-arm64-musl": "1.83.0", + "@oxlint/binding-linux-ppc64-gnu": "1.83.0", + "@oxlint/binding-linux-riscv64-gnu": "1.83.0", + "@oxlint/binding-linux-riscv64-musl": "1.83.0", + "@oxlint/binding-linux-s390x-gnu": "1.83.0", + "@oxlint/binding-linux-x64-gnu": "1.83.0", + "@oxlint/binding-linux-x64-musl": "1.83.0", + "@oxlint/binding-openharmony-arm64": "1.83.0", + "@oxlint/binding-win32-arm64-msvc": "1.83.0", + "@oxlint/binding-win32-ia32-msvc": "1.83.0", + "@oxlint/binding-win32-x64-msvc": "1.83.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", @@ -3152,6 +3398,19 @@ "node": ">=6" } }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -3175,9 +3434,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -3198,6 +3457,19 @@ "dev": true, "license": "MIT" }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3233,9 +3505,9 @@ } }, "node_modules/tinypool": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", - "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.2.tgz", + "integrity": "sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww==", "dev": true, "license": "MIT", "engines": { @@ -3298,9 +3570,9 @@ } }, "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, "node_modules/vite": { @@ -3382,13 +3654,13 @@ } }, "node_modules/vite-plus": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/vite-plus/-/vite-plus-0.3.1.tgz", - "integrity": "sha512-U8KZ3c3mbX0qLfigx9rW2W9J73w9wjdf4s/s91H77ff5afqSYEdZd8Or41Jl99kD42a+UCL3LFXzb5b+bIBCpQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/vite-plus/-/vite-plus-0.3.3.tgz", + "integrity": "sha512-gr3t8ZIsytJnpPE0YPxtkouf+asMsc+TWlKXYzEdY6rRXUdCGYxgaPmzRePx1Gyip2gmARRLogAwoMMW9BvSUA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.148.0", + "@oxc-project/types": "=0.150.0", "@oxlint/plugins": "=1.79.0", "@vitest/browser": "4.1.11", "@vitest/browser-preview": "4.1.11", @@ -3399,10 +3671,10 @@ "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", - "oxfmt": "=0.66.0", - "oxlint": "=1.81.0", + "oxfmt": "=0.68.0", + "oxlint": "=1.83.0", "oxlint-tsgolint": "=7.0.2001", - "vite": "npm:@voidzero-dev/vite-plus-core@0.3.1", + "vite": "npm:@voidzero-dev/vite-plus-core@0.3.3", "vitest": "4.1.11" }, "bin": { @@ -3415,14 +3687,14 @@ "node": "^20.19.0 || ^22.18.0 || >=24.11.0" }, "optionalDependencies": { - "@voidzero-dev/vite-plus-darwin-arm64": "0.3.1", - "@voidzero-dev/vite-plus-darwin-x64": "0.3.1", - "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.3.1", - "@voidzero-dev/vite-plus-linux-arm64-musl": "0.3.1", - "@voidzero-dev/vite-plus-linux-x64-gnu": "0.3.1", - "@voidzero-dev/vite-plus-linux-x64-musl": "0.3.1", - "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.3.1", - "@voidzero-dev/vite-plus-win32-x64-msvc": "0.3.1" + "@voidzero-dev/vite-plus-darwin-arm64": "0.3.3", + "@voidzero-dev/vite-plus-darwin-x64": "0.3.3", + "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.3.3", + "@voidzero-dev/vite-plus-linux-arm64-musl": "0.3.3", + "@voidzero-dev/vite-plus-linux-x64-gnu": "0.3.3", + "@voidzero-dev/vite-plus-linux-x64-musl": "0.3.3", + "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.3.3", + "@voidzero-dev/vite-plus-win32-x64-msvc": "0.3.3" }, "peerDependencies": { "@vitest/browser-playwright": "4.1.11", @@ -3438,9 +3710,9 @@ } }, "node_modules/vite-plus/node_modules/@oxc-project/types": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.148.0.tgz", - "integrity": "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==", + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", + "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", "dev": true, "license": "MIT", "funding": { @@ -3449,14 +3721,14 @@ }, "node_modules/vite-plus/node_modules/vite": { "name": "@voidzero-dev/vite-plus-core", - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-core/-/vite-plus-core-0.3.1.tgz", - "integrity": "sha512-3uVTyZzrLWKV3aIjRPuqkPwzG0iNQIncJlCDIT/gXFk0FQOW5US9bSHf3jsDqD+NjgTP1vAfI99MZuYTUjEDbg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-core/-/vite-plus-core-0.3.3.tgz", + "integrity": "sha512-Ui92VENIZG+p0nMJuVGMsHBHrQqgUNxXObdo5PlSDEBOdRmD8AHKr8tOClZTVU1U5blLQKtHKplxtTun0yZEmw==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/runtime": "=0.148.0", - "@oxc-project/types": "=0.148.0", + "@oxc-project/runtime": "=0.150.0", + "@oxc-project/types": "=0.150.0", "lightningcss": "^1.33.0", "postcss": "^8.5.6", "yuku-codegen": "^0.9.3", @@ -3466,20 +3738,20 @@ "node": "^20.19.0 || ^22.18.0 || >=24.11.0" }, "optionalDependencies": { - "@voidzero-dev/vite-plus-darwin-arm64": "0.3.1", - "@voidzero-dev/vite-plus-darwin-x64": "0.3.1", - "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.3.1", - "@voidzero-dev/vite-plus-linux-arm64-musl": "0.3.1", - "@voidzero-dev/vite-plus-linux-x64-gnu": "0.3.1", - "@voidzero-dev/vite-plus-linux-x64-musl": "0.3.1", - "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.3.1", - "@voidzero-dev/vite-plus-win32-x64-msvc": "0.3.1", + "@voidzero-dev/vite-plus-darwin-arm64": "0.3.3", + "@voidzero-dev/vite-plus-darwin-x64": "0.3.3", + "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.3.3", + "@voidzero-dev/vite-plus-linux-arm64-musl": "0.3.3", + "@voidzero-dev/vite-plus-linux-x64-gnu": "0.3.3", + "@voidzero-dev/vite-plus-linux-x64-musl": "0.3.3", + "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.3.3", + "@voidzero-dev/vite-plus-win32-x64-msvc": "0.3.3", "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/package.json b/package.json index f780c1a..34a1a7c 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ }, "files": [ "dist/**/*.js", - "dist/**/*.d.ts" + "dist/**/*.d.ts", + "docs/**/*.md", + "CHANGELOG.md" ], "type": "module", "main": "./dist/index.js", @@ -52,9 +54,11 @@ "test": "vp test run", "test:publint": "publint", "pack:check": "node tests/package-artifacts.js", - "check": "npm run lint && npm run typecheck && npm test && npm run build && npm run test:publint && npm run pack:check", + "check": "npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm run test:publint && npm run pack:check && npm run test:packed", "prepack": "npm run build", - "prepublishOnly": "npm run check" + "prepublishOnly": "npm run check", + "test:coverage": "vp test run --coverage", + "test:packed": "node tests/package-consumers.js" }, "dependencies": { "@askrjs/auth": ">=0.4.0 <0.5.0", @@ -64,9 +68,11 @@ }, "devDependencies": { "@askrjs/schema": ">=0.4.0 <0.5.0", - "@types/node": "^26.3.0", + "@types/node": "24.19.2", + "@typescript/typescript6": "npm:typescript@6.0.2", + "@vitest/coverage-v8": "4.1.11", "publint": "^0.3.24", - "typescript": "^7.0.2", + "typescript": "7.0.2", "vite-plus": "^0.3.1", "vitest": "^4.1.11" }, diff --git a/src/contracts.ts b/src/contracts.ts index 499b8fe..9694fbe 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -11,7 +11,7 @@ export interface NodeWebSocketOptions { readonly maxRejectionBodyBytes?: number; /** Enables or configures the permessage-deflate WebSocket extension. */ readonly perMessageDeflate?: boolean | PerMessageDeflateOptions; - /** Origins allowed to open a WebSocket connection; when omitted, all origins are allowed. */ + /** Origins allowed to open a WebSocket connection; defaults to the request's origin. */ readonly allowedOrigins?: readonly string[]; } diff --git a/src/diagnostics.ts b/src/diagnostics.ts new file mode 100644 index 0000000..d6be947 --- /dev/null +++ b/src/diagnostics.ts @@ -0,0 +1,17 @@ +import type { Writable } from "node:stream"; +import { streamWriter } from "./stream-writer.js"; + +/** Own error events until the associated diagnostic writes settle, even after shutdown. */ +export function diagnosticReporter(stream: Writable): (error: unknown) => void { + const write = streamWriter(stream); + return (error) => { + if (stream.destroyed) return; + try { + void write( + `MCP stdio error: ${error instanceof Error ? error.message : String(error)}\n`, + ).catch(() => undefined); + } catch { + // Formatting an arbitrary thrown value is also best-effort. + } + }; +} diff --git a/src/handler.ts b/src/handler.ts index c48b83b..265bc54 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -10,6 +10,7 @@ function minimalError(response: ServerResponse, error: unknown): void { return; } try { + for (const name of response.getHeaderNames()) response.removeHeader(name); const clientError = error instanceof NodeRequestError; response.statusCode = clientError ? 400 : 500; response.statusMessage = clientError ? "Bad Request" : "Internal Server Error"; diff --git a/src/index.ts b/src/index.ts index 186ccfc..375286f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,9 @@ -export * from "./client-address.js"; -export * from "./contracts.js"; -export * from "./handler.js"; -export * from "./listen.js"; -export * from "./serve.js"; +export { CLIENT_ADDRESS_HEADER } from "./client-address.js"; +export type { ListenOptions } from "./contracts.js"; +export type { NodeHandler } from "./contracts.js"; +export type { NodeHandlerOptions } from "./contracts.js"; +export type { ServeOptions } from "./contracts.js"; +export type { ServedApplication } from "./contracts.js"; +export { createNodeHandler } from "./handler.js"; +export { listen, type ListeningServer } from "./listen.js"; +export { serve } from "./serve.js"; diff --git a/src/listen.ts b/src/listen.ts index 7ed7afc..ac4e81d 100644 --- a/src/listen.ts +++ b/src/listen.ts @@ -69,17 +69,24 @@ export function listen(app: ServerApp, options: ListenOptions = {}): Promise { + const onError = (error: unknown) => { + server.off("error", onError); server.off("close", onClose); disposeAbortListener(); - reject(error); + void Promise.resolve(webSockets?.close()) + .catch(() => undefined) + .then(() => reject(error)); }; server.once("error", onError); server.once("close", onClose); - server.listen(options.port ?? 0, host, options.backlog, () => { - server.off("error", onError); - server.off("close", onClose); - resolve(server as ListeningServer); - }); + try { + server.listen(options.port ?? 0, host, options.backlog, () => { + server.off("error", onError); + server.off("close", onClose); + resolve(server as ListeningServer); + }); + } catch (error) { + onError(error); + } }); } diff --git a/src/mcp.ts b/src/mcp.ts index 2de1cd9..f63a708 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -3,6 +3,8 @@ import type { McpServer } from "@askrjs/server/mcp"; import { randomUUID } from "node:crypto"; import { addAbortListener } from "node:events"; import type { Readable, Writable } from "node:stream"; +import { diagnosticReporter } from "./diagnostics.js"; +import { streamWriter } from "./stream-writer.js"; /** Options for {@link connectMcpStdio}. */ export interface McpStdioOptions { @@ -74,23 +76,15 @@ export function connectMcpStdio( throw new TypeError("MCP maxLineBytes must be a positive integer."); if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency <= 0) throw new TypeError("MCP maxConcurrency must be a positive integer."); + const lineBuffer = Buffer.allocUnsafe(maxLineBytes); let active = 0; let abortListener: ReturnType | undefined; let cleanupTransport = () => undefined; - const report = (error: unknown) => { - try { - diagnostics.write( - `MCP stdio error: ${error instanceof Error ? error.message : String(error)}\n`, - ); - } catch { - // The diagnostic stream is best-effort and must not affect protocol shutdown. - } - }; - const write = (message: unknown) => { - if (ended) return Promise.reject(new Error("MCP stdio connection is closed.")); - return new Promise((resolve, reject) => { - output.write(`${JSON.stringify(message)}\n`, (error) => (error ? reject(error) : resolve())); - }); + const report = diagnosticReporter(diagnostics); + const writeOutput = streamWriter(output); + const write = async (message: unknown) => { + if (ended) throw new Error("MCP stdio connection is closed."); + return writeOutput(`${JSON.stringify(message)}\n`); }; const close = async () => { if (ended) return closed; @@ -164,6 +158,7 @@ export function connectMcpStdio( typeof options.auth === "function" ? await options.auth(environment) : (options.auth ?? anonymous); + if (ended || controller?.signal.aborted) return; const result = await mcp.handle(message, { dependencies: options.dependencies, auth, @@ -175,7 +170,7 @@ export function connectMcpStdio( }); if (result !== undefined && !ended) await write(result); } catch (error) { - if (!ended) { + if (!ended && !controller?.signal.aborted) { report(error); if (typeof id === "string" || typeof id === "number") { await write({ @@ -200,7 +195,6 @@ export function connectMcpStdio( active -= 1; }); }; - const lineBuffer = Buffer.allocUnsafe(maxLineBytes); const decoder = new TextDecoder("utf-8", { fatal: true }); let lineBytes = 0; let discardingOversizedLine = false; diff --git a/src/response.ts b/src/response.ts index 322a986..6840fdf 100644 --- a/src/response.ts +++ b/src/response.ts @@ -45,9 +45,18 @@ export async function writeNodeResponse( target: ServerResponse, method?: string, ): Promise { - target.statusCode = response.status; - target.statusMessage = response.statusText; - writeHeaders(response, target); + if (target.destroyed || target.writableEnded) { + void response.body?.cancel(disconnectError()).catch(() => undefined); + return; + } + try { + target.statusCode = response.status; + target.statusMessage = response.statusText; + writeHeaders(response, target); + } catch (error) { + void response.body?.cancel(error).catch(() => undefined); + throw error; + } if (method === "HEAD") { void response.body?.cancel("HEAD responses do not include a body").catch(() => undefined); target.end(); @@ -74,7 +83,8 @@ export async function writeNodeResponse( if (!disconnected) target.end(); } catch (error) { if (disconnected) return; - await reader.cancel(error).catch(() => undefined); + // Application cleanup must not delay destroying a failed transport or releasing its reader. + void reader.cancel(error).catch(() => undefined); target.destroy(error instanceof Error ? error : undefined); } finally { target.off("close", onClose); diff --git a/src/serve.ts b/src/serve.ts index 5920f7f..b8df4da 100644 --- a/src/serve.ts +++ b/src/serve.ts @@ -5,7 +5,6 @@ import { createServer } from "node:http"; import { extname, resolve, sep } from "node:path"; import { pipeline } from "node:stream"; import type { ServerApp } from "@askrjs/server"; -import * as serverHttp from "@askrjs/server/http"; import type { ServeOptions, ServedApplication } from "./contracts.js"; import { formatHostForUrl, handlerOptionsForHost, resolveBindHost } from "./bind.js"; import { createNodeHandler } from "./handler.js"; @@ -37,14 +36,12 @@ const mimeTypes: Readonly> = { ".woff2": "font/woff2", }; -const parseContentType = - (serverHttp as { contentType?: (value: string | null) => string | undefined }).contentType ?? - ((value: string | null): string | undefined => { - if (!value) return undefined; - const separator = value.indexOf(";"); - const type = (separator === -1 ? value : value.slice(0, separator)).trim(); - return type ? type.toLowerCase() : undefined; - }); +function parseContentType(value: string | null): string | undefined { + if (!value) return undefined; + const separator = value.indexOf(";"); + const type = (separator === -1 ? value : value.slice(0, separator)).trim(); + return type ? type.toLowerCase() : undefined; +} function isAssetPath(pathname: string): boolean { return extname(pathname) !== ""; diff --git a/src/stream-writer.ts b/src/stream-writer.ts new file mode 100644 index 0000000..cadd81d --- /dev/null +++ b/src/stream-writer.ts @@ -0,0 +1,30 @@ +import type { Writable } from "node:stream"; + +/** Own error events until writes settle, independently of the connection lifecycle. */ +export function streamWriter(stream: Writable): (chunk: string) => Promise { + let pending = 0; + const failed = () => { + pending = 0; + }; + const completed = () => { + if (pending > 0 && --pending === 0) stream.off("error", failed); + }; + return (chunk) => + new Promise((resolve, reject) => { + if (pending++ === 0) stream.once("error", failed); + try { + stream.write(chunk, (error) => { + if (error) { + // Node emits the error after calling the failed write's callback. + reject(error); + } else { + completed(); + resolve(); + } + }); + } catch (error) { + completed(); + reject(error); + } + }); +} diff --git a/src/websocket-rejection.ts b/src/websocket-rejection.ts new file mode 100644 index 0000000..9d57286 --- /dev/null +++ b/src/websocket-rejection.ts @@ -0,0 +1,61 @@ +import { addAbortListener } from "node:events"; + +/** Buffer a bounded rejection while retaining ownership until completion or disconnect. */ +export async function readRejectionBody( + response: Response, + maxBytes: number, + signal: AbortSignal, +): Promise { + if (signal.aborted) { + void response.body?.cancel(signal.reason).catch(() => undefined); + signal.throwIfAborted(); + } + const lengthHeader = response.headers.get("content-length"); + const declaredLength = lengthHeader === null ? undefined : Number(lengthHeader); + if ( + declaredLength !== undefined && + Number.isFinite(declaredLength) && + declaredLength > maxBytes + ) { + void response.body + ?.cancel("WebSocket rejection body exceeded maxRejectionBodyBytes") + .catch(() => undefined); + return undefined; + } + if (!response.body) return Buffer.alloc(0); + const reader = response.body.getReader(); + let rejectAbort!: (reason: unknown) => void; + const aborted = new Promise((_resolve, reject) => { + rejectAbort = reject; + }); + const listener = addAbortListener(signal, () => { + void reader.cancel(signal.reason).catch(() => undefined); + rejectAbort(signal.reason); + }); + const consume = async () => { + const chunks: Buffer[] = []; + let length = 0; + for (;;) { + const part = await reader.read(); + signal.throwIfAborted(); + if (part.done) return Buffer.concat(chunks, length); + length += part.value.byteLength; + if (length > maxBytes) { + void reader + .cancel("WebSocket rejection body exceeded maxRejectionBodyBytes") + .catch(() => undefined); + return undefined; + } + chunks.push(Buffer.from(part.value)); + } + }; + try { + return await Promise.race([consume(), aborted]); + } catch (error) { + void reader.cancel(error).catch(() => undefined); + throw error; + } finally { + listener[Symbol.dispose](); + reader.releaseLock(); + } +} diff --git a/src/websocket.ts b/src/websocket.ts index 34efc8b..d549fda 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -4,6 +4,7 @@ import type { ServerApp, ServerContext, WebSocketHandler, WebSocketLike } from " import { WebSocket, WebSocketServer } from "ws"; import type { NodeHandlerOptions, NodeWebSocketOptions } from "./contracts.js"; import { prepareNodeHandlerOptions, requestFromNode } from "./request.js"; +import { readRejectionBody } from "./websocket-rejection.js"; function subscribe( socket: WebSocket, @@ -68,50 +69,13 @@ function normalizeHttpOrigin(value: string): string { return parsed.origin; } -async function readRejectionBody( - response: Response, - maxBytes: number, -): Promise { - const lengthHeader = response.headers.get("content-length"); - const declaredLength = lengthHeader === null ? undefined : Number(lengthHeader); - if ( - declaredLength !== undefined && - Number.isFinite(declaredLength) && - declaredLength > maxBytes - ) { - await response.body - ?.cancel("WebSocket rejection body exceeded maxRejectionBodyBytes") - .catch(() => undefined); - return undefined; - } - if (!response.body) return Buffer.alloc(0); - const reader = response.body.getReader(); - const chunks: Buffer[] = []; - let length = 0; - try { - while (true) { - const part = await reader.read(); - if (part.done) return Buffer.concat(chunks, length); - length += part.value.byteLength; - if (length > maxBytes) { - await reader - .cancel("WebSocket rejection body exceeded maxRejectionBodyBytes") - .catch(() => undefined); - return undefined; - } - chunks.push(Buffer.from(part.value)); - } - } finally { - reader.releaseLock(); - } -} - async function rejectUpgrade( socket: Duplex, response: Response, maxBodyBytes: number, + signal: AbortSignal, ): Promise { - const buffered = await readRejectionBody(response, maxBodyBytes); + const buffered = await readRejectionBody(response, maxBodyBytes, signal); if (!buffered) { response = new Response("WebSocket rejection body exceeded configured limit", { status: 500 }); } @@ -204,6 +168,7 @@ export function installWebSockets( socket, new Response("Forbidden", { status: 403 }), maxRejectionBodyBytes, + controller.signal, ); return; } @@ -217,8 +182,12 @@ export function installWebSockets( }, }, }); + if (controller.signal.aborted) { + void response.body?.cancel(controller.signal.reason).catch(() => undefined); + return; + } if (!accepted || response !== accepted.response) { - await rejectUpgrade(socket, response, maxRejectionBodyBytes); + await rejectUpgrade(socket, response, maxRejectionBodyBytes, controller.signal); return; } webSockets.handleUpgrade(request, socket, head, (webSocket) => { diff --git a/tests/diagnostics.test.ts b/tests/diagnostics.test.ts new file mode 100644 index 0000000..760d80f --- /dev/null +++ b/tests/diagnostics.test.ts @@ -0,0 +1,61 @@ +import { Writable } from "node:stream"; +import { setImmediate } from "node:timers/promises"; +import { describe, expect, it, vi } from "vitest"; +import { diagnosticReporter } from "../src/diagnostics.js"; + +describe("best-effort MCP diagnostics", () => { + it("handles asynchronous failures once, preserves caller listeners and skips destroyed diagnostics", async () => { + const failure = new Error("async write failed"); + const stream = new Writable({ + write(_chunk, _encoding, callback) { + setImmediate().then(() => callback(failure)); + }, + }); + const existing = vi.fn(); + stream.on("error", existing); + const report = diagnosticReporter(stream); + for (let index = 0; index < 32; index++) report("failure"); + expect(stream.listenerCount("error")).toBe(2); + await setImmediate(); + await setImmediate(); + expect(existing).toHaveBeenCalledExactlyOnceWith(failure); + expect(stream.listenerCount("error")).toBe(1); + report("after destruction"); + expect(stream.listenerCount("error")).toBe(1); + }); + it("shares one error listener across pending writes and releases it on successful completion", async () => { + const callbacks: Array<(error?: Error | null) => void> = []; + const chunks: string[] = []; + const stream = new Writable({ + write(chunk, _encoding, callback) { + chunks.push(chunk.toString()); + callbacks.push(callback); + }, + }); + const existing = vi.fn(); + stream.on("error", existing); + const report = diagnosticReporter(stream); + for (let index = 0; index < 32; index++) report(new Error(`failure ${index}`)); + expect(stream.listenerCount("error")).toBe(2); + for (let index = 0; index < 32; index++) callbacks[index](); + await setImmediate(); + expect(chunks).toHaveLength(32); + expect(chunks[0]).toBe("MCP stdio error: failure 0\n"); + expect(stream.listenerCount("error")).toBe(1); + expect(existing).not.toHaveBeenCalled(); + stream.destroy(); + }); + + it("ignores synchronous diagnostic failures without removing caller listeners", () => { + const stream = new Writable({ + write() { + throw new Error("write failed"); + }, + }); + const existing = vi.fn(); + stream.on("error", existing); + expect(() => diagnosticReporter(stream)("report")).not.toThrow(); + expect(stream.listenerCount("error")).toBe(1); + stream.destroy(); + }); +}); diff --git a/tests/fixtures/bench-asset.txt b/tests/fixtures/bench-asset.txt new file mode 100644 index 0000000..c090a98 --- /dev/null +++ b/tests/fixtures/bench-asset.txt @@ -0,0 +1 @@ +0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \ No newline at end of file diff --git a/tests/mcp-framing.test.ts b/tests/mcp-framing.test.ts new file mode 100644 index 0000000..421b175 --- /dev/null +++ b/tests/mcp-framing.test.ts @@ -0,0 +1,81 @@ +import type { McpServer } from "@askrjs/server/mcp"; +import { PassThrough } from "node:stream"; +import { setImmediate } from "node:timers/promises"; +import { describe, expect, it, vi } from "vitest"; +import { connectMcpStdio } from "../src/mcp.js"; + +async function framing(chunks: Array, maxLineBytes = 1024) { + const input = new PassThrough(), + output = new PassThrough(), + diagnostics = new PassThrough(); + const messages: unknown[] = [], + lines: unknown[] = []; + const terminateSession = vi.fn(); + const server = { + handle: async (message: unknown) => { + messages.push(message); + return { jsonrpc: "2.0", id: 1, result: {} }; + }, + terminateSession, + } as unknown as McpServer; + output.on("data", (chunk) => lines.push(JSON.parse(chunk.toString()))); + const connection = connectMcpStdio(server, { + dependencies: undefined, + input, + output, + diagnostics, + maxLineBytes, + }); + try { + for (const chunk of chunks) input.write(chunk); + await setImmediate(); + input.end(); + await connection.closed; + expect(terminateSession).toHaveBeenCalledOnce(); + expect(input.listenerCount("data")).toBe(0); + expect(diagnostics.read()).toBeNull(); + return { messages, lines }; + } finally { + await connection.close(); + input.destroy(); + output.destroy(); + diagnostics.destroy(); + } +} +const request = { jsonrpc: "2.0", id: 1, method: "echo", params: { value: "日本語 🚀" } }; +const valid = JSON.stringify(request); + +describe("bounded MCP framing", () => { + it("preserves multibyte UTF-8 across one-byte chunks and split CRLF", async () => { + const chunks = [...Buffer.from(`${valid}\r\n`)].map((byte) => Buffer.from([byte])); + expect((await framing(chunks)).messages).toEqual([request]); + }); + it("accepts a line at the exact byte limit", async () => { + expect((await framing([`${valid}\n`], Buffer.byteLength(valid))).messages).toEqual([request]); + }); + it("discards an entire oversized line then recovers within the same chunk", async () => { + const result = await framing(["x".repeat(100), `ignored\n${valid}\n`], 100); + expect(result.messages).toEqual([request]); + expect(result.lines).toMatchObject([{ error: { code: -32600 } }, { id: 1, result: {} }]); + }); + it("recovers after invalid UTF-8 and malformed JSON", async () => { + const result = await framing([Buffer.from([0xff, 0x0a]), `bad-json\n${valid}\n`]); + expect(result.messages).toEqual([request]); + expect(result.lines).toMatchObject([ + { error: { code: -32700 } }, + { error: { code: -32700 } }, + { id: 1 }, + ]); + }); + it.each(["[]", "true", "1", '"text"'])( + "rejects %s without poisoning the next message", + async (invalid) => { + const result = await framing([`${invalid}\n${valid}\n`]); + expect(result.messages).toEqual([request]); + expect(result.lines).toMatchObject([{ error: { code: -32600 } }, { id: 1 }]); + }, + ); + it("drops an unterminated line at EOF", async () => { + expect(await framing([valid])).toEqual({ messages: [], lines: [] }); + }); +}); diff --git a/tests/mcp-lifetime.test.ts b/tests/mcp-lifetime.test.ts new file mode 100644 index 0000000..f28cfbd --- /dev/null +++ b/tests/mcp-lifetime.test.ts @@ -0,0 +1,106 @@ +import type { McpServer } from "@askrjs/server/mcp"; +import { PassThrough } from "node:stream"; +import { setImmediate } from "node:timers/promises"; +import { describe, expect, it, vi } from "vitest"; +import { connectMcpStdio } from "../src/mcp.js"; + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((yes) => { + resolve = yes; + }); + return { promise, resolve }; +} +const anonymous = { authenticated: false, principal: null, session: null, tenant: null } as const; + +describe("MCP dispatch lifetime", () => { + it.each(["close", "cancel"])("ignores authentication rejection after %s", async (mode) => { + const entered = deferred(); + let reject!: (error: Error) => void; + const auth = new Promise((_yes, no) => { + reject = no; + }); + const input = new PassThrough(), + output = new PassThrough(), + diagnostics = new PassThrough(); + const handle = vi.fn(), + terminateSession = vi.fn(); + const connection = connectMcpStdio( + { handle, terminateSession } as unknown as McpServer, + { + dependencies: undefined, + input, + output, + diagnostics, + auth: () => { + entered.resolve(); + return auth; + }, + }, + ); + try { + input.write(`${JSON.stringify({ jsonrpc: "2.0", id: 7, method: "ping" })}\n`); + await entered.promise; + if (mode === "close") await connection.close(); + else + input.write( + `${JSON.stringify({ jsonrpc: "2.0", method: "notifications/cancelled", params: { requestId: 7 } })}\n`, + ); + reject(new Error("late authentication failure")); + await setImmediate(); + expect(handle).not.toHaveBeenCalled(); + expect(output.read()).toBeNull(); + expect(diagnostics.read()).toBeNull(); + } finally { + await connection.close(); + input.destroy(); + output.destroy(); + diagnostics.destroy(); + } + expect(terminateSession).toHaveBeenCalledOnce(); + }); + it.each(["close", "cancel"])( + "does not start a handler after %s while authentication is pending", + async (mode) => { + const auth = deferred(), + entered = deferred(); + const input = new PassThrough(), + output = new PassThrough(), + diagnostics = new PassThrough(); + const handle = vi.fn(async () => ({ jsonrpc: "2.0", id: 7, result: {} })); + const terminateSession = vi.fn(); + const server = { handle, terminateSession } as unknown as McpServer; + const connection = connectMcpStdio(server, { + dependencies: undefined, + input, + output, + diagnostics, + auth: () => { + entered.resolve(); + return auth.promise; + }, + }); + try { + input.write(`${JSON.stringify({ jsonrpc: "2.0", id: 7, method: "ping" })}\n`); + await entered.promise; + if (mode === "close") await connection.close(); + else + input.write( + `${JSON.stringify({ jsonrpc: "2.0", method: "notifications/cancelled", params: { requestId: 7 } })}\n`, + ); + auth.resolve(anonymous); + await setImmediate(); + expect(handle).not.toHaveBeenCalled(); + expect(output.read()).toBeNull(); + } finally { + auth.resolve(anonymous); + await connection.close(); + input.destroy(); + output.destroy(); + diagnostics.destroy(); + } + expect(terminateSession).toHaveBeenCalledOnce(); + expect(input.listenerCount("data")).toBe(0); + }, + ); +}); diff --git a/tests/mcp-setup.test.ts b/tests/mcp-setup.test.ts new file mode 100644 index 0000000..5d3d3bd --- /dev/null +++ b/tests/mcp-setup.test.ts @@ -0,0 +1,66 @@ +import type { McpServer } from "@askrjs/server/mcp"; +import { getEventListeners } from "node:events"; +import { PassThrough } from "node:stream"; +import { describe, expect, it, vi } from "vitest"; +import { connectMcpStdio } from "../src/mcp.js"; + +describe("MCP setup ownership", () => { + it.each(["maxLineBytes", "maxConcurrency"] as const)( + "rejects malformed %s before acquiring hooks", + (key) => { + for (const value of [0, -1, 1.5, Infinity, NaN, Number.MAX_SAFE_INTEGER + 1]) { + const input = new PassThrough(), + output = new PassThrough(), + signal = new AbortController().signal; + const terminateSession = vi.fn(); + try { + expect(() => + connectMcpStdio({ terminateSession } as unknown as McpServer, { + dependencies: undefined, + input, + output, + signal, + [key]: value, + }), + ).toThrow(TypeError); + expect(getEventListeners(signal, "abort")).toHaveLength(0); + expect(input.listenerCount("data")).toBe(0); + expect(output.listenerCount("error")).toBe(0); + expect(terminateSession).not.toHaveBeenCalled(); + } finally { + input.destroy(); + output.destroy(); + } + } + }, + ); + + it("does not retain abort hooks when allocating the bounded line buffer fails", () => { + const input = new PassThrough(), + output = new PassThrough(), + signal = new AbortController().signal; + const reason = new RangeError("injected buffer allocation failure"); + const terminateSession = vi.fn(); + const allocate = vi.spyOn(Buffer, "allocUnsafe").mockImplementationOnce(() => { + throw reason; + }); + let failure: unknown; + try { + connectMcpStdio({ terminateSession } as unknown as McpServer, { + dependencies: undefined, + input, + output, + signal, + }); + } catch (error) { + failure = error; + } finally { + allocate.mockRestore(); + input.destroy(); + output.destroy(); + } + expect(failure).toBe(reason); + expect(getEventListeners(signal, "abort")).toHaveLength(0); + expect(terminateSession).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/node.test.ts b/tests/node.test.ts index 86eb2a9..ae23fc6 100644 --- a/tests/node.test.ts +++ b/tests/node.test.ts @@ -4,7 +4,8 @@ import { get, request as nodeRequest, type ServerResponse } from "node:http"; import { createConnection } from "node:net"; import { tmpdir } from "node:os"; import { basename, join } from "node:path"; -import { createRouter, createServerApp } from "@askrjs/server"; +import { createServerApp } from "@askrjs/server"; +import { createRouter } from "@askrjs/server/router"; import { runAdapterConformance } from "@askrjs/server/testing"; import { describe, expect, it } from "vitest"; import WebSocket from "ws"; diff --git a/tests/package-artifacts.js b/tests/package-artifacts.js index 0543995..cfff27e 100644 --- a/tests/package-artifacts.js +++ b/tests/package-artifacts.js @@ -27,6 +27,11 @@ for (const required of [ "package.json", "dist/index.js", "dist/index.d.ts", + "dist/mcp.js", + "dist/mcp.d.ts", + "docs/0.5.0-api.md", + "docs/0.5.0-hardening.md", + "CHANGELOG.md", ]) { if (!packedFiles.has(normalize(required))) throw new Error(`Packed artifact is missing ${required}.`); @@ -36,6 +41,9 @@ for (const file of packedFiles) { file !== normalize("LICENSE") && file !== normalize("README.md") && file !== normalize("package.json") && + file !== normalize("CHANGELOG.md") && + !file.startsWith(`${normalize("docs")}/`) && + !file.startsWith(`${normalize("docs")}\\`) && !file.startsWith(`${normalize("dist")}\\`) && !file.startsWith(`${normalize("dist")}/`) ) { diff --git a/tests/package-consumers.js b/tests/package-consumers.js new file mode 100644 index 0000000..3baba53 --- /dev/null +++ b/tests/package-consumers.js @@ -0,0 +1,148 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import ts from "@typescript/typescript6"; +import { readPackRecord } from "./pack-result.js"; + +const npmCli = process.env.npm_execpath; +if (!npmCli) throw new Error("npm_execpath is unavailable; run this check through npm"); +const root = process.cwd(); +const npm = (args, options) => execFileSync(process.execPath, [npmCli, ...args], options); +const contract = JSON.parse(await readFile("tests/public-contract.json", "utf8")); +const directory = await mkdtemp(join(tmpdir(), "askr-node-consumer-")); +try { + const packed = readPackRecord( + JSON.parse( + npm(["pack", "--ignore-scripts", "--json", "--pack-destination", directory], { + encoding: "utf8", + }), + ), + ); + await writeFile( + join(directory, "package.json"), + JSON.stringify({ private: true, type: "module" }), + ); + npm( + [ + "install", + "--ignore-scripts", + "--no-audit", + "--no-fund", + "--no-package-lock", + join(directory, packed.filename), + "@types/node@24.19.2", + ], + { cwd: directory, stdio: "pipe" }, + ); + const manifest = JSON.parse( + await readFile(join(directory, "node_modules/@askrjs/node/package.json"), "utf8"), + ); + assert.deepEqual(Object.keys(manifest.exports).sort(), contract.exportKeys); + const fixture = join(directory, "contract.ts"); + const surfaces = Object.keys(contract.entrypoints); + await writeFile( + fixture, + `${await readFile("tests/types/public-contract.ts", "utf8")}\n${surfaces.map((name, index) => `import * as Surface${index} from ${JSON.stringify(name)}; void Surface${index};`).join("\n")}\n`, + ); + await writeFile( + join(directory, "tsconfig.json"), + JSON.stringify({ + compilerOptions: { + target: "ES2022", + module: "NodeNext", + moduleResolution: "NodeNext", + strict: true, + noEmit: true, + types: ["node"], + lib: ["ES2022", "DOM", "DOM.Iterable"], + }, + files: ["contract.ts"], + }), + ); + execFileSync( + process.execPath, + [join(root, "node_modules/typescript/bin/tsc"), "-p", "tsconfig.json"], + { cwd: directory, stdio: "pipe" }, + ); + const program = ts.createProgram([fixture], { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.NodeNext, + moduleResolution: ts.ModuleResolutionKind.NodeNext, + strict: true, + noEmit: true, + types: ["node"], + typeRoots: [join(directory, "node_modules/@types")], + lib: ["lib.es2022.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts"], + }); + const diagnostics = ts.getPreEmitDiagnostics(program); + assert.equal( + diagnostics.length, + 0, + ts.formatDiagnosticsWithColorAndContext(diagnostics, { + getCanonicalFileName: (file) => file, + getCurrentDirectory: () => directory, + getNewLine: () => "\n", + }), + ); + const checker = program.getTypeChecker(); + for (const [name, surface] of Object.entries(contract.entrypoints)) { + const declaration = program + .getSourceFile(fixture) + .statements.find( + (statement) => ts.isImportDeclaration(statement) && statement.moduleSpecifier.text === name, + ); + const names = checker + .getExportsOfModule(checker.getSymbolAtLocation(declaration.moduleSpecifier)) + .map((symbol) => symbol.name) + .sort(); + assert.deepEqual(names, [...surface.values, ...surface.types].sort(), name); + } + await writeFile( + join(directory, "runtime.js"), + `${await readFile("tests/packed-runtime.js", "utf8")}\nfor (const path of ${JSON.stringify(contract.privateSubpaths)}) await assert.rejects(import('@askrjs/node/' + path), { code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' });\n`, + ); + execFileSync(process.execPath, [join(directory, "runtime.js")], { + cwd: directory, + stdio: "pipe", + timeout: 20_000, + }); + await writeFile( + join(directory, "diagnostics.js"), + await readFile("tests/packed-diagnostics.js", "utf8"), + ); + execFileSync(process.execPath, [join(directory, "diagnostics.js")], { + cwd: directory, + stdio: "pipe", + timeout: 20_000, + }); + await writeFile( + join(directory, "output-failure.js"), + await readFile("tests/packed-output-failure.js", "utf8"), + ); + execFileSync(process.execPath, [join(directory, "output-failure.js")], { + cwd: directory, + stdio: "pipe", + timeout: 20_000, + }); + console.log( + JSON.stringify({ + normalInstall: true, + declarationNames: 13, + removedNames: contract.removed.length, + privateSubpaths: contract.privateSubpaths.length, + compilers: ["6.0.2", "7.0.2"], + runtime: [ + "real HTTP", + "HTML cache policy", + "real MCP", + "cleanup", + "asynchronous diagnostics failure after close", + "asynchronous protocol output failure after close", + ], + }), + ); +} finally { + await rm(directory, { recursive: true, force: true }); +} diff --git a/tests/packed-diagnostics.js b/tests/packed-diagnostics.js new file mode 100644 index 0000000..f11cba0 --- /dev/null +++ b/tests/packed-diagnostics.js @@ -0,0 +1,43 @@ +import assert from "node:assert/strict"; +import { PassThrough, Writable } from "node:stream"; +import { setImmediate } from "node:timers/promises"; +import { connectMcpStdio } from "@askrjs/node/mcp"; + +const input = new PassThrough(), + output = new PassThrough(); +let failed = false, + terminated = 0; +const diagnostics = new Writable({ + write(_chunk, _encoding, callback) { + setImmediate().then(() => { + failed = true; + callback(new Error("diagnostics unavailable")); + }); + }, +}); +const connection = connectMcpStdio( + { + handle() { + throw new Error("request failed"); + }, + terminateSession() { + terminated++; + }, + }, + { + dependencies: undefined, + input, + output, + diagnostics, + }, +); +input.write(`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" })}\n`); +await connection.close(); +await setImmediate(); +await setImmediate(); +assert.equal(failed, true); +assert.equal(terminated, 1); +assert.equal(diagnostics.listenerCount("error"), 0); +input.destroy(); +output.destroy(); +diagnostics.destroy(); diff --git a/tests/packed-output-failure.js b/tests/packed-output-failure.js new file mode 100644 index 0000000..e4d13a8 --- /dev/null +++ b/tests/packed-output-failure.js @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import { PassThrough, Writable } from "node:stream"; +import { setImmediate } from "node:timers/promises"; +import { connectMcpStdio } from "@askrjs/node/mcp"; + +const input = new PassThrough(), + diagnostics = new PassThrough(); +let failed = false, + terminated = 0; +const output = new Writable({ + write(_chunk, _encoding, callback) { + setImmediate().then(() => { + failed = true; + callback(new Error("output unavailable")); + }); + }, +}); +const connection = connectMcpStdio( + { + handle: async () => ({ jsonrpc: "2.0", id: 1, result: {} }), + terminateSession() { + terminated++; + }, + }, + { + dependencies: undefined, + input, + output, + diagnostics, + }, +); +input.write(`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" })}\n`); +await Promise.resolve(); +await connection.close(); +await setImmediate(); +await setImmediate(); +assert.equal(failed, true); +assert.equal(terminated, 1); +assert.equal(output.listenerCount("error"), 0); +input.destroy(); +output.destroy(); +diagnostics.destroy(); diff --git a/tests/packed-runtime.js b/tests/packed-runtime.js new file mode 100644 index 0000000..49061fb --- /dev/null +++ b/tests/packed-runtime.js @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import { PassThrough } from "node:stream"; +import { setImmediate } from "node:timers/promises"; +import * as api from "@askrjs/node"; +import * as stdio from "@askrjs/node/mcp"; +import { createServerApp } from "@askrjs/server"; +import { createRouter } from "@askrjs/server/router"; +import { createMcpServer } from "@askrjs/server/mcp"; + +assert.deepEqual(Object.keys(api).sort(), [ + "CLIENT_ADDRESS_HEADER", + "createNodeHandler", + "listen", + "serve", +]); +assert.deepEqual(Object.keys(stdio), ["connectMcpStdio"]); +const running = await api.serve( + createServerApp({ + router: createRouter().get( + "/", + () => new Response("ok", { headers: { "content-type": " Text/HTML; charset=utf-8" } }), + ), + }), + { signals: false }, +); +try { + const response = await fetch(running.url); + assert.equal(await response.text(), "ok"); + assert.equal(response.headers.get("cache-control"), "no-cache"); +} finally { + await running.close(); +} + +const input = new PassThrough(), + output = new PassThrough(), + diagnostics = new PassThrough(); +const lines = []; +output.on("data", (chunk) => lines.push(JSON.parse(chunk.toString()))); +const connection = stdio.connectMcpStdio(createMcpServer({ name: "packed", version: "1" }), { + dependencies: undefined, + input, + output, + diagnostics, +}); +try { + input.write( + `${JSON.stringify({ jsonrpc: "2.0", id: 0, method: "initialize", params: { protocolVersion: "2025-11-25", capabilities: {}, clientInfo: { name: "packed", version: "1" } } })}\n`, + ); + await setImmediate(); + assert.equal(lines[0]?.result?.protocolVersion, "2025-11-25"); + input.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}\n`); + input.write(`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" })}\n`); + await setImmediate(); + assert.deepEqual(lines.slice(1), [{ jsonrpc: "2.0", id: 1, result: {} }]); + assert.equal(diagnostics.read(), null); +} finally { + await connection.close(); + input.destroy(); + output.destroy(); + diagnostics.destroy(); +} +assert.equal(input.listenerCount("data"), 0); diff --git a/tests/public-contract.json b/tests/public-contract.json new file mode 100644 index 0000000..38bc512 --- /dev/null +++ b/tests/public-contract.json @@ -0,0 +1,37 @@ +{ + "exportKeys": [".", "./mcp", "./package.json"], + "entrypoints": { + "@askrjs/node": { + "values": ["CLIENT_ADDRESS_HEADER", "createNodeHandler", "listen", "serve"], + "types": [ + "ListenOptions", + "ListeningServer", + "NodeHandler", + "NodeHandlerOptions", + "ServeOptions", + "ServedApplication" + ] + }, + "@askrjs/node/mcp": { + "values": ["connectMcpStdio"], + "types": ["McpStdioConnection", "McpStdioOptions"] + } + }, + "removed": ["ConnectNext", "NodeWebSocketOptions", "normalizeClientAddress"], + "privateSubpaths": [ + "bind", + "client-address", + "diagnostics", + "contracts", + "handler", + "listen", + "request", + "response", + "serve", + "server-options", + "websocket", + "websocket-rejection", + "stream-writer", + "dist/index.js" + ] +} diff --git a/tests/startup-hardening.test.ts b/tests/startup-hardening.test.ts new file mode 100644 index 0000000..12b1263 --- /dev/null +++ b/tests/startup-hardening.test.ts @@ -0,0 +1,66 @@ +import { getEventListeners } from "node:events"; +import { setImmediate } from "node:timers/promises"; +import { describe, expect, it } from "vitest"; +import { listen, serve } from "../src/index.js"; + +describe("startup resource ownership", () => { + it.each( + [-1, 65_536, 1.5, Number.NaN].flatMap((port) => + [false, true].map((websocket) => ({ port, websocket })), + ), + )( + "removes abort hooks after invalid port $port with WebSockets $websocket", + async ({ port, websocket }) => { + const controller = new AbortController(); + try { + await expect( + listen( + { fetch: async () => new Response() }, + { port, websocket, signal: controller.signal }, + ), + ).rejects.toMatchObject({ code: "ERR_SOCKET_BAD_PORT" }); + expect(getEventListeners(controller.signal, "abort")).toHaveLength(0); + } finally { + controller.abort(); + await setImmediate(); + } + }, + ); + + it("cleans up a failed bind exactly once while preserving the original error and listener", async () => { + const occupied = await listen({ fetch: async () => new Response("occupied") }); + const address = occupied.address(); + if (!address || typeof address === "string") throw new Error("Expected TCP address"); + const signalCount = process.listenerCount("SIGTERM"); + const controller = new AbortController(); + let closes = 0; + try { + for (let attempt = 0; attempt < 3; attempt++) { + await expect( + serve( + { + fetch: async () => new Response(), + close: async () => { + closes++; + }, + }, + { + port: address.port, + websocket: true, + signals: ["SIGTERM"], + signal: controller.signal, + }, + ), + ).rejects.toMatchObject({ code: "EADDRINUSE" }); + expect(closes).toBe(attempt + 1); + expect(process.listenerCount("SIGTERM")).toBe(signalCount); + expect(getEventListeners(controller.signal, "abort")).toHaveLength(0); + } + expect(await (await fetch(`http://127.0.0.1:${address.port}`)).text()).toBe("occupied"); + } finally { + controller.abort(); + occupied.closeAllConnections(); + await new Promise((resolve) => occupied.close(() => resolve())); + } + }); +}); diff --git a/tests/stream-hardening.test.ts b/tests/stream-hardening.test.ts new file mode 100644 index 0000000..5cf9438 --- /dev/null +++ b/tests/stream-hardening.test.ts @@ -0,0 +1,230 @@ +import { once } from "node:events"; +import { request } from "node:http"; +import { createConnection } from "node:net"; +import { describe, expect, it } from "vitest"; +import WebSocket from "ws"; +import { listen } from "../src/index.js"; + +function deferred() { + let resolve!: () => void; + const promise = new Promise((yes) => { + resolve = yes; + }); + return { promise, resolve }; +} +async function close(server: Awaited>) { + server.closeAllConnections(); + await new Promise((resolve) => server.close(() => resolve())); +} + +describe("transport stream ownership", () => { + it("disposes a response returned after the HTTP client disconnected", async () => { + const entered = deferred(), + aborted = deferred(), + release = deferred(), + cancel = deferred(); + let cancelled = 0; + let source!: ReadableStreamDefaultController; + const body = new ReadableStream({ + start(controller) { + source = controller; + }, + cancel() { + cancelled++; + return cancel.promise; + }, + }); + const server = await listen({ + fetch: async (input) => { + input.signal.addEventListener("abort", () => aborted.resolve(), { once: true }); + entered.resolve(); + await release.promise; + return new Response(body); + }, + }); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Expected TCP address"); + const client = request(`http://127.0.0.1:${address.port}`); + client.on("error", () => undefined); + client.end(); + try { + await entered.promise; + client.destroy(); + await aborted.promise; + release.resolve(); + await expect.poll(() => cancelled).toBe(1); + expect(body.locked).toBe(false); + } finally { + client.destroy(); + release.resolve(); + cancel.resolve(); + if (!cancelled) source.close(); + await close(server); + } + }); + + it("disposes an application body when Node rejects its response headers", async () => { + let cancelled = 0; + const body = new ReadableStream({ + cancel() { + cancelled++; + }, + }); + const server = await listen({ + fetch: async () => + new Response(body, { + headers: { "x-value": "\u0001", "content-length": "99999", "set-cookie": "failed=1" }, + }), + }); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Expected TCP address"); + try { + const response = await fetch(`http://127.0.0.1:${address.port}`); + expect(response.status).toBe(500); + expect(response.headers.get("content-length")).not.toBe("99999"); + expect(response.headers.get("set-cookie")).toBeNull(); + expect(await response.text()).toBe("Internal Server Error"); + expect(cancelled).toBe(1); + expect(body.locked).toBe(false); + } finally { + await close(server); + } + }); + + it.each(["throws", "never settles"])( + "closes failed response writes when cancellation %s", + async (mode) => { + const cancel = deferred(); + let cancelled = 0, + disconnected = false; + const body = new ReadableStream({ + start(controller) { + controller.enqueue({ invalid: true } as unknown as Uint8Array); + }, + cancel() { + cancelled++; + if (mode === "throws") throw new Error("cancel failed"); + return cancel.promise; + }, + }); + const server = await listen({ + fetch: async (input) => + new URL(input.url).pathname === "/healthy" + ? new Response(null, { status: 204 }) + : new Response(body), + }); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Expected TCP address"); + const origin = `http://127.0.0.1:${address.port}`; + const client = request(origin); + client.once("error", () => { + disconnected = true; + }); + client.end(); + try { + await expect.poll(() => cancelled).toBe(1); + await expect.poll(() => disconnected).toBe(true); + expect(body.locked).toBe(false); + expect((await fetch(`${origin}/healthy`)).status).toBe(204); + } finally { + client.destroy(); + cancel.resolve(); + await close(server); + } + }, + ); + + it.each([false, true])( + "bounds upgrade rejection without awaiting cancellation; Content-Length %s", + async (declared) => { + const cancel = deferred(); + let cancelled = 0, + status: number | undefined; + const body = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(5)); + }, + cancel() { + cancelled++; + return cancel.promise; + }, + }); + const server = await listen( + { + fetch: async () => + new Response(body, { status: 401, headers: declared ? { "content-length": "5" } : {} }), + }, + { websocket: { maxRejectionBodyBytes: 4 } }, + ); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Expected TCP address"); + const origin = `http://127.0.0.1:${address.port}`; + const client = new WebSocket(`ws://127.0.0.1:${address.port}/`, { origin }); + client.on("error", () => undefined); + client.once("unexpected-response", (_request, response) => { + status = response.statusCode; + response.resume(); + }); + try { + await expect.poll(() => status).toBe(500); + expect(cancelled).toBe(1); + expect(body.locked).toBe(false); + } finally { + cancel.resolve(); + client.terminate(); + await close(server); + } + }, + ); + + it("cancels a stalled upgrade rejection when its socket disconnects", async () => { + const entered = deferred(); + let cancelled = 0; + let source!: ReadableStreamDefaultController; + const body = new ReadableStream({ + start(controller) { + source = controller; + }, + pull() { + entered.resolve(); + }, + cancel() { + cancelled++; + }, + }); + const server = await listen( + { fetch: async () => new Response(body, { status: 401 }) }, + { websocket: true }, + ); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Expected TCP address"); + const client = createConnection({ host: "127.0.0.1", port: address.port }); + client.on("error", () => undefined); + try { + await once(client, "connect"); + client.write( + [ + "GET / HTTP/1.1", + `Host: 127.0.0.1:${address.port}`, + `Origin: http://127.0.0.1:${address.port}`, + "Connection: Upgrade", + "Upgrade: websocket", + "Sec-WebSocket-Version: 13", + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==", + "", + "", + ].join("\r\n"), + ); + await entered.promise; + // The stream's eager pull is not enough; wait until the adapter owns its reader. + await expect.poll(() => body.locked).toBe(true); + client.destroy(); + await expect.poll(() => cancelled).toBe(1); + expect(body.locked).toBe(false); + } finally { + client.destroy(); + if (!cancelled) source.close(); + await close(server); + } + }); +}); diff --git a/tests/types/public-contract.ts b/tests/types/public-contract.ts new file mode 100644 index 0000000..b1c0f01 --- /dev/null +++ b/tests/types/public-contract.ts @@ -0,0 +1,46 @@ +import { + CLIENT_ADDRESS_HEADER, + createNodeHandler, + listen, + serve, + type ListenOptions, + type ListeningServer, + type NodeHandler, + type NodeHandlerOptions, + type ServedApplication, + type ServeOptions, +} from "@askrjs/node"; +import { connectMcpStdio, type McpStdioConnection, type McpStdioOptions } from "@askrjs/node/mcp"; +import type { McpServer } from "@askrjs/server/mcp"; + +const app = { fetch: async () => new Response() }; +const handlerOptions: NodeHandlerOptions = { baseUrl: "https://askr.test" }; +const handler: NodeHandler = createNodeHandler(app, handlerOptions); +const next: NonNullable[2]> = (_error?: unknown) => {}; +const websocket: Exclude = { maxPayload: 1024 }; +const listenOptions: ListenOptions = { websocket, requestTimeout: 1000 }; +const server: Promise = listen(app, listenOptions); +const serveOptions: ServeOptions = { ...listenOptions, signals: false, assets: { root: "public" } }; +const running: Promise = serve(app, serveOptions); +declare const mcp: McpServer<{ value: number }>; +const stdioOptions: McpStdioOptions<{ value: number }> = { + dependencies: { value: 1 }, + maxConcurrency: 1, +}; +const connection: McpStdioConnection = connectMcpStdio(mcp, stdioOptions); +void [CLIENT_ADDRESS_HEADER, handler, next, server, running, connection]; + +// @ts-expect-error Removed low-level address helper. +import { normalizeClientAddress } from "@askrjs/node"; +// @ts-expect-error Derive the optional callback from NodeHandler instead. +import type { ConnectNext } from "@askrjs/node"; +// @ts-expect-error Derive nested configuration from ListenOptions instead. +import type { NodeWebSocketOptions } from "@askrjs/node"; +void normalizeClientAddress; +export type RemovedNames = ConnectNext | NodeWebSocketOptions; + +// @ts-expect-error Handler options require a trusted URL boundary, not a port. +const invalidHandler: NodeHandlerOptions = { port: 3000 }; +// @ts-expect-error Application dependencies are required by the MCP server's type. +const invalidMcp: McpStdioOptions<{ value: number }> = { dependencies: undefined }; +void [invalidHandler, invalidMcp]; diff --git a/tsconfig.test.json b/tsconfig.test.json index 736874d..5b8dac3 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -5,5 +5,12 @@ "noEmit": true, "rootDir": "." }, - "include": ["benches/**/*.ts", "src/**/*.ts", "tests/**/*.ts", "vite.config.ts"] + "include": [ + "benches/**/*.ts", + "src/**/*.ts", + "tests/**/*.ts", + "vite.config.ts", + "vitest.config.ts" + ], + "exclude": ["tests/types"] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..e8de2c7 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["tests/**/*.test.ts"], + coverage: { + provider: "v8", + include: ["src/**/*.ts"], + reporter: ["text", "json-summary"], + thresholds: { + statements: 89, + branches: 82, + functions: 82, + lines: 93, + "src/diagnostics.ts": { statements: 100, branches: 100, functions: 100, lines: 100 }, + "src/stream-writer.ts": { statements: 100, branches: 100, functions: 100, lines: 100 }, + "src/response.ts": { statements: 89, branches: 84, functions: 68, lines: 96 }, + "src/listen.ts": { statements: 97, branches: 93, functions: 91, lines: 97 }, + }, + }, + }, +});