# Capability Spec: storage-uri-plugin-bundle **Package**: `@suaveplan/storage-uri-plugin-bundle` **Status**: SHIPPED > Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions. ## Purpose `@suaveplan/storage-uri-plugin-bundle` is a per-package `bundle://` URI scheme plugin for `@suaveplan/storage-uri`. It lets a consuming package ship binary assets alongside its tarball and resolve them through the same `StorageUriResolver` that handles `file:`, `https:`, `s3:`, and `drive:` URIs. The plugin is universal-tier: every read goes through an injectable `FetchLike` against `new URL(encodedRelPath, rootUrl)`, so `file:`, `http(s):`, `data:`, `blob:`, and bundler-virtual schemes all flow through the same code path. Bundles are immutable by contract — every write surface (`set`, `setPartial`, `delete`, `clear`, `list`) throws `BUNDLE_URI.READ_ONLY` so consumers and caches can rely on the URI being content-addressed by package version. ## Requirements ### Requirement: Plugin factory shape and registration The package SHALL export `bundleUriPlugin(options: BundleUriPluginOptions): UriPlugin` returning a `UriPlugin` (per `@suaveplan/storage-uri`) with `scheme === "bundle"`. The factory SHALL validate `options` through `BundleUriPluginOptionsSchema` (Zod) at construction; validation failures SHALL throw `BundleUriError` with `code === "BUNDLE_URI.INVALID_URI"` AND a `cause` carrying the Zod issue. `options.rootUrl` SHALL be normalised to a `URL` instance with a trailing-slash pathname; inputs already ending in `/` SHALL be accepted verbatim, inputs without `/` SHALL be rejected with `INVALID_URI` rather than silently amended (so caller mistakes surface). The factory SHALL default `options.fetch` to `globalThis.fetch.bind(globalThis)` when unset. #### Scenario: factory returns a UriPlugin with scheme "bundle" - **WHEN** a caller invokes `bundleUriPlugin({ packageSlug: "audio-ir-room-plugin-club", rootUrl: new URL("file:///pkgs/club/assets/") })` - **THEN** the returned object has `scheme === "bundle"`, a callable `parse`, a callable `build`, and an `adapter` that is structurally a `StorageLike` #### Scenario: invalid slug rejected at construction - **WHEN** a caller invokes `bundleUriPlugin({ packageSlug: "Invalid Slug With Spaces", rootUrl: new URL("file:///x/") })` - **THEN** the call throws `BundleUriError` with `code === "BUNDLE_URI.INVALID_URI"` #### Scenario: rootUrl missing trailing slash rejected - **WHEN** a caller invokes `bundleUriPlugin({ packageSlug: "x", rootUrl: "file:///pkgs/x/assets" })` - **THEN** the call throws `BundleUriError` with `code === "BUNDLE_URI.INVALID_URI"` and message naming the trailing-slash requirement **Verified by:** `src/factory/factory.test.ts::"returns a UriPlugin with scheme bundle"` AND `src/factory/factory.test.ts::"rejects invalid slug at construction"` AND `src/factory/factory.test.ts::"rejects rootUrl missing trailing slash"` --- ### Requirement: `bundle://` URI grammar enforced by parse The plugin's `parse(uri)` SHALL accept any URI of the form `bundle:///[?query][#fragment]` where `` matches the authority component and `` is the non-empty URL-decoded path within `rootUrl`. The scheme SHALL be matched case-insensitively per RFC 3986 §3.1. `parse` SHALL return `{ key: relPath, meta: { packageSlug } }` per the `ParsedUri` contract. Malformed URIs (missing authority, empty rel-path, scheme other than `bundle`, malformed pct-encoding) SHALL throw `BundleUriError` with `code === "BUNDLE_URI.INVALID_URI"`. #### Scenario: well-formed URI parses to key plus meta - **WHEN** `parse("bundle://audio-ir-room-plugin-club/assets/club.wav")` is called - **THEN** the result is `{ key: "assets/club.wav", meta: { packageSlug: "audio-ir-room-plugin-club" } }` #### Scenario: scheme mismatch rejected - **WHEN** `parse("https://audio-ir-room-plugin-club/assets/club.wav")` is called - **THEN** the call throws `BundleUriError` with `code === "BUNDLE_URI.INVALID_URI"` #### Scenario: empty rel-path rejected - **WHEN** `parse("bundle://audio-ir-room-plugin-club/")` or `parse("bundle://audio-ir-room-plugin-club")` is called - **THEN** the call throws `BundleUriError` with `code === "BUNDLE_URI.INVALID_URI"` #### Scenario: missing authority rejected - **WHEN** `parse("bundle:///assets/club.wav")` is called - **THEN** the call throws `BundleUriError` with `code === "BUNDLE_URI.INVALID_URI"` **Verified by:** `src/parse/parse.test.ts::"parses well-formed URI to key plus meta"` AND `src/parse/parse.test.ts::"rejects scheme mismatch with INVALID_URI"` AND `src/parse/parse.test.ts::"rejects empty rel-path with INVALID_URI"` AND `src/parse/parse.test.ts::"rejects missing authority with INVALID_URI"` --- ### Requirement: Per-package slug enforcement When the parsed authority differs from `options.packageSlug`, `parse` SHALL throw `BundleUriError` with `code === "BUNDLE_URI.PACKAGE_MISMATCH"` carrying both the expected slug (on `packageSlug`) and the offending slug (on `actual`). Slug comparison SHALL be case-sensitive (npm slug rules forbid uppercase, so a case difference is a real mismatch). #### Scenario: URI slug not equal to options.packageSlug - **WHEN** a plugin configured with `packageSlug: "audio-ir-room-plugin-club"` is asked to `parse("bundle://other-pkg/x")` - **THEN** the call throws `BundleUriError` with `code === "BUNDLE_URI.PACKAGE_MISMATCH"`, `packageSlug === "audio-ir-room-plugin-club"`, AND `actual === "other-pkg"` **Verified by:** `src/parse/parse.test.ts::"rejects slug mismatch with PACKAGE_MISMATCH carrying actual"` --- ### Requirement: Path-traversal prevention `parse` SHALL reject any rel-path containing a `..` segment, both in the raw input and after a single `decodeURIComponent` pass, throwing `BundleUriError` with `code === "BUNDLE_URI.PATH_TRAVERSAL"`. As a defence-in-depth check, the resolved URL (`new URL(encodedRelPath, rootUrl)`) SHALL have a pathname that starts with `rootUrl.pathname`; any resolved URL that escapes rootUrl SHALL also throw `PATH_TRAVERSAL`. #### Scenario: raw `..` rejected - **WHEN** `parse("bundle://x/../etc/passwd")` is called against a plugin with `packageSlug: "x"` - **THEN** the call throws `BundleUriError` with `code === "BUNDLE_URI.PATH_TRAVERSAL"` #### Scenario: pct-encoded `..` rejected after decode - **WHEN** `parse("bundle://x/%2E%2E/etc/passwd")` is called against a plugin with `packageSlug: "x"` - **THEN** the call throws `BundleUriError` with `code === "BUNDLE_URI.PATH_TRAVERSAL"` #### Scenario: URL-resolve escape check - **WHEN** a hypothetical input slips the first two passes but resolves to a pathname outside `rootUrl.pathname` - **THEN** the call throws `BundleUriError` with `code === "BUNDLE_URI.PATH_TRAVERSAL"` **Verified by:** `src/parse/parse.test.ts::"rejects raw .. segment with PATH_TRAVERSAL"` AND `src/parse/parse.test.ts::"rejects pct-encoded .. after decode with PATH_TRAVERSAL"` AND `src/parse/parse.test.ts::"rejects URL-resolve escape outside rootUrl with PATH_TRAVERSAL"` --- ### Requirement: Read-only enforcement on every write surface The plugin's `adapter` (a `StorageLike`) SHALL throw `BundleUriError` with `code === "BUNDLE_URI.READ_ONLY"` for every method that would mutate state: `set`, `setPartial`, `delete`, `clear`, and `list`. Each rejection SHALL carry the offending operation name on a discriminant `op` so operators can correlate dashboards. No write surface SHALL be conditionally enabled — the contract is immutable bundles. #### Scenario: set throws READ_ONLY carrying op - **WHEN** `adapter.set("assets/x.wav", new Uint8Array([1,2,3]))` is called - **THEN** the call rejects with `BundleUriError` whose `code === "BUNDLE_URI.READ_ONLY"` AND `op === "set"` #### Scenario: setPartial throws READ_ONLY - **WHEN** `adapter.setPartial("assets/x.wav", 0, new Uint8Array([1,2,3]))` is called - **THEN** the call rejects with `BundleUriError` whose `code === "BUNDLE_URI.READ_ONLY"` AND `op === "setPartial"` #### Scenario: delete throws READ_ONLY - **WHEN** `adapter.delete("assets/x.wav")` is called - **THEN** the call rejects with `BundleUriError` whose `code === "BUNDLE_URI.READ_ONLY"` AND `op === "delete"` #### Scenario: clear throws READ_ONLY - **WHEN** `adapter.clear()` is called - **THEN** the call rejects with `BundleUriError` whose `code === "BUNDLE_URI.READ_ONLY"` AND `op === "clear"` #### Scenario: list throws READ_ONLY - **WHEN** `adapter.list({ prefix: "assets/" })` is called - **THEN** the call rejects with `BundleUriError` whose `code === "BUNDLE_URI.READ_ONLY"` AND `op === "list"` **Verified by:** `src/storage-shim/storage-shim.test.ts::"set throws READ_ONLY with op set"` AND `src/storage-shim/storage-shim.test.ts::"setPartial throws READ_ONLY"` AND `src/storage-shim/storage-shim.test.ts::"delete throws READ_ONLY"` AND `src/storage-shim/storage-shim.test.ts::"clear throws READ_ONLY"` AND `src/storage-shim/storage-shim.test.ts::"list throws READ_ONLY"` --- ### Requirement: GET resolves through injected FetchLike `adapter.get(key, signal?)` SHALL issue an HTTP `GET` to `new URL(encodedRelPath, rootUrl)` through the injected `FetchLike` and SHALL return the response body as `Uint8Array`. On `200 OK` the body SHALL be returned. On `404 Not Found` or `410 Gone` the method SHALL throw `BundleUriError` with `code === "BUNDLE_URI.NOT_FOUND"`. On any other non-success status the method SHALL throw `BundleUriError` with `code === "BUNDLE_URI.FETCH_FAILED"` carrying `httpStatus` and the upstream `Response` summary on `cause`. #### Scenario: HTTP root returns bytes verbatim - **WHEN** the injected `FetchLike` resolves to `new Response(new Uint8Array([1,2,3]), { status: 200 })` for `http://localhost:/club.wav` - **AND** the caller invokes `await adapter.get("club.wav")` - **THEN** the result is a `Uint8Array` byte-equal to `[1,2,3]` #### Scenario: file:// root returns bytes verbatim - **WHEN** a real temp-dir `file://` `rootUrl` is configured AND `club.wav` exists at `/club.wav` with bytes `[9,8,7]` - **AND** the caller invokes `await adapter.get("club.wav")` with a `FetchLike` capable of reading `file:` URLs (the runtime's native `fetch` on Bun, OR the `nodeFileFetch` adapter shipped at the `./node-fetch` subpath on Node) - **THEN** the result is a `Uint8Array` byte-equal to `[9,8,7]` > **NOTE (corrected post-ship 2026-05-17):** Node's undici-backed global > `fetch` does NOT support `file:` URLs in any release through Node 24 > (the original `design.md §3` claim of "Node ≥ 21 native support" was > incorrect). Node consumers with a `file://` `rootUrl` MUST pass the > `nodeFileFetch` adapter from `@suaveplan/storage-uri-plugin-bundle/node-fetch` > as `options.fetch`. Bun's native `fetch` supports `file:` directly so > the default wiring works there. #### Scenario: 404 → NOT_FOUND - **WHEN** the injected `FetchLike` resolves to `new Response(null, { status: 404 })` - **THEN** the call rejects with `BundleUriError` whose `code === "BUNDLE_URI.NOT_FOUND"` #### Scenario: 500 → FETCH_FAILED with httpStatus - **WHEN** the injected `FetchLike` resolves to `new Response(null, { status: 500 })` - **THEN** the call rejects with `BundleUriError` whose `code === "BUNDLE_URI.FETCH_FAILED"` AND `httpStatus === 500` **Verified by:** `src/storage-shim/storage-shim.test.ts::"HTTP root returns bytes verbatim"` AND `src/storage-shim/storage-shim.test.ts::"file:// root returns bytes verbatim against real temp dir"` AND `src/storage-shim/storage-shim.test.ts::"404 maps to NOT_FOUND"` AND `src/storage-shim/storage-shim.test.ts::"500 maps to FETCH_FAILED with httpStatus"` --- ### Requirement: HEAD-backed exists and getMetadata `adapter.exists(key, signal?)` SHALL issue `HEAD ` and return `true` on `2xx`, `false` on `404`/`410`, and throw `FETCH_FAILED` on other non-success status. `adapter.getMetadata(key, signal?)` SHALL issue `HEAD ` and parse `content-length`, `content-type`, `last-modified`, and `etag` from the response headers into the `StatResult` shape declared by `@suaveplan/storage-uri`. Absence of `content-length` SHALL yield `size: undefined`. #### Scenario: HEAD 200 yields exists=true - **WHEN** the injected `FetchLike` resolves a `HEAD` to `new Response(null, { status: 200 })` - **THEN** `await adapter.exists("k")` is `true` #### Scenario: HEAD 404 yields exists=false - **WHEN** the injected `FetchLike` resolves a `HEAD` to `new Response(null, { status: 404 })` - **THEN** `await adapter.exists("k")` is `false` #### Scenario: getMetadata parses headers - **WHEN** the injected `FetchLike` resolves a `HEAD` to `new Response(null, { status: 200, headers: { "content-length": "42", "content-type": "audio/wav", "etag": "\"abc\"", "last-modified": "Wed, 17 May 2026 00:00:00 GMT" } })` - **THEN** `await adapter.getMetadata("k")` returns `{ size: 42, contentType: "audio/wav", etag: '"abc"', lastModified: Date(2026-05-17T00:00:00Z) }` **Verified by:** `src/storage-shim/storage-shim.test.ts::"HEAD 200 yields exists true"` AND `src/storage-shim/storage-shim.test.ts::"HEAD 404 yields exists false"` AND `src/storage-shim/storage-shim.test.ts::"getMetadata parses content-length content-type etag last-modified"` --- ### Requirement: Range reads with fallback `adapter.getPartial(key, offset, length, signal?)` SHALL validate `offset >= 0` and `length > 0` (both integers) and throw `BundleUriError` with `code === "BUNDLE_URI.RANGE_INVALID"` for any violation, without issuing a fetch. Valid ranges SHALL be requested via `Range: bytes=-`. A `206 Partial Content` response SHALL return its body verbatim. A `200 OK` response SHALL be slow-pathed (full body sliced client-side at `[offset, offset+length)`) AND SHALL increment `bundle_uri.range_fallback_total{root_kind}`. A `416 Range Not Satisfiable` response SHALL throw `BundleUriError` with `code === "BUNDLE_URI.RANGE_INVALID"`. For `data:` and `blob:` roots, every range read SHALL be slow-pathed (Range header dropped or unsupported) AND SHALL increment `range_fallback_total`. #### Scenario: HTTP root 206 returns the slice - **WHEN** the injected `FetchLike` honours `Range: bytes=10-19` with `new Response(<10 bytes>, { status: 206 })` - **THEN** `await adapter.getPartial("k", 10, 10)` returns the 10-byte body verbatim #### Scenario: HTTP root 200 fallback increments counter - **WHEN** the injected `FetchLike` ignores the `Range` header and returns `new Response(<100 bytes>, { status: 200 })` - **AND** the caller invokes `adapter.getPartial("k", 10, 10)` - **THEN** the returned bytes equal `body.slice(10, 20)` AND `bundle_uri.range_fallback_total{root_kind:"http"}` is incremented by 1 #### Scenario: 416 maps to RANGE_INVALID - **WHEN** the injected `FetchLike` resolves to `new Response(null, { status: 416 })` - **THEN** the call rejects with `BundleUriError` whose `code === "BUNDLE_URI.RANGE_INVALID"` #### Scenario: invalid offset rejected before fetch - **WHEN** `adapter.getPartial("k", -1, 10)` is called - **THEN** the call rejects with `BundleUriError` whose `code === "BUNDLE_URI.RANGE_INVALID"` AND the injected `FetchLike` is NEVER invoked #### Scenario: data: root always slow-pathed - **WHEN** the rootUrl is a `data:` URL AND `adapter.getPartial("ignored", 0, 4)` is called - **THEN** the bytes are sliced client-side AND `bundle_uri.range_fallback_total{root_kind:"data"}` is incremented by 1 **Verified by:** `src/range/range.test.ts::"HTTP root 206 returns slice verbatim"` AND `src/range/range.test.ts::"HTTP root 200 fallback slices client-side and increments counter"` AND `src/range/range.test.ts::"416 maps to RANGE_INVALID"` AND `src/range/range.test.ts::"invalid offset rejected before fetch"` AND `src/range/range.test.ts::"data root always slow-pathed and counter incremented"` --- ### Requirement: Streaming reads `adapter.createReadStream(key, signal?)` SHALL return a `ReadableStream`. When the underlying `Response.body` is non-null, the method SHALL return that stream directly without buffering. When `Response.body` is null (some adapter or polyfill paths), the method SHALL fall back to a one-chunk `ReadableStream` wrapping the buffered bytes. #### Scenario: createReadStream forwards Response.body - **WHEN** the injected `FetchLike` returns a `Response` with a non-null `body` - **THEN** the returned stream is the same reference as `response.body` (no re-wrap) #### Scenario: one-chunk fallback when body is null - **WHEN** the injected `FetchLike` returns a `Response` whose `body` is `null` AND whose `arrayBuffer()` resolves to `Uint8Array([1,2,3])` - **THEN** the returned stream yields exactly one chunk equal to `Uint8Array([1,2,3])` then closes **Verified by:** `src/streaming/streaming.test.ts::"forwards Response.body when present"` AND `src/streaming/streaming.test.ts::"one-chunk fallback when Response.body is null"` --- ### Requirement: AbortSignal propagation Every read on the adapter (`get`, `exists`, `getMetadata`, `getPartial`, `createReadStream`) SHALL accept an `AbortSignal` and SHALL thread it into the underlying `fetch` call. When the signal is already aborted at call time, the method SHALL reject with `BundleUriError` whose `code === "BUNDLE_URI.ABORTED"` WITHOUT invoking the injected `FetchLike`. Mid-flight aborts that propagate from `fetch` as an `AbortError` SHALL be rewrapped as `BundleUriError(code: "BUNDLE_URI.ABORTED")` carrying the original `AbortError` on `cause`. #### Scenario: pre-aborted signal short-circuits - **WHEN** `adapter.get("k", AbortSignal.abort())` is called - **THEN** the call rejects with `BundleUriError` whose `code === "BUNDLE_URI.ABORTED"` AND the injected `FetchLike` is NEVER invoked #### Scenario: mid-flight abort propagates on cause - **WHEN** `adapter.get("k", controller.signal)` is called AND `controller.abort()` fires before the response resolves AND `fetch` rejects with an `AbortError` - **THEN** the call rejects with `BundleUriError` whose `code === "BUNDLE_URI.ABORTED"` AND whose `cause` is the original `AbortError` **Verified by:** `src/abort/abort.test.ts::"pre-aborted signal short-circuits without fetch"` AND `src/abort/abort.test.ts::"mid-flight abort propagates AbortError on cause"` --- ### Requirement: Optional in-memory LRU cache The plugin SHALL support an optional in-memory LRU cache keyed by full URI. The cache SHALL be OFF by default; when `options.cache = { maxBytes }` is supplied, every successful `get` result SHALL be inserted into the cache AND subsequent `get`s of the same URI SHALL return the cached bytes WITHOUT invoking the injected `FetchLike`. The cache SHALL evict oldest entries first until total cached bytes ≤ `maxBytes`. Cache hits SHALL increment `bundle_uri.cache_hits_total{root_kind}`; misses SHALL increment `bundle_uri.cache_misses_total{root_kind}`. #### Scenario: cache off by default - **WHEN** the plugin is constructed without `options.cache` AND the caller invokes `get("k")` twice - **THEN** the injected `FetchLike` is invoked twice AND `bundle_uri.cache_hits_total` stays at 0 #### Scenario: cache hit returns cached bytes without fetch - **WHEN** the plugin is constructed with `options.cache = { maxBytes: 1024 }` AND the caller invokes `get("k")` twice - **THEN** the injected `FetchLike` is invoked exactly once AND `bundle_uri.cache_hits_total{root_kind}` is incremented by 1 #### Scenario: LRU evicts oldest when maxBytes exceeded - **WHEN** the plugin is constructed with `options.cache = { maxBytes: 100 }` AND three `get` calls insert entries of size 50, 50, 50 in order - **THEN** after the third insert, the first entry (50 bytes) is evicted AND a subsequent `get` for its URI misses the cache **Verified by:** `src/cache/cache.test.ts::"cache off by default does not hit"` AND `src/cache/cache.test.ts::"cache hit returns cached bytes without fetch"` AND `src/cache/cache.test.ts::"LRU evicts oldest when maxBytes exceeded"` --- ### Requirement: OpenTelemetry instrumentation Every public operation on the adapter SHALL emit a span on the `@suaveplan/storage-uri-plugin-bundle` tracer AND SHALL record `bundle_uri.op_duration_ms{op, root_kind, outcome}` on the meter. Errors SHALL increment `bundle_uri.errors_total{root_kind, code}` with the resolved `BUNDLE_URI.*` code. Spans SHALL carry `bundle.package`, `bundle.rel_path` (truncated to 256 chars; query strings stripped), and `bundle.root_kind` (`file` | `http` | `data` | `blob` | `other`) attributes. Cache and range fallback paths SHALL increment the dedicated counters declared in `design.md §6`. #### Scenario: successful read records duration histogram - **WHEN** `get("k")` succeeds against an HTTP root - **THEN** the meter records one observation on `bundle_uri.op_duration_ms{op:"get", root_kind:"http", outcome:"success"}` AND one increment of `bundle_uri.reads_total{root_kind:"http", outcome:"success"}` #### Scenario: error increments errors_total with resolved code - **WHEN** `get("k")` rejects with `NOT_FOUND` - **THEN** `bundle_uri.errors_total{root_kind:"http", code:"BUNDLE_URI.NOT_FOUND"}` is incremented by 1 #### Scenario: query string stripped from rel_path attribute - **WHEN** `get("k?token=secret")` is attempted (the caller-side rel-path includes a query) - **THEN** the emitted span's `bundle.rel_path` attribute is `"k"` (no `?token=…`) #### Scenario: root_kind classified correctly per resolution matrix - **WHEN** the plugin is constructed with `rootUrl: new URL("file:///pkgs/x/")` AND a successful `get` records a span - **THEN** the span's `bundle.root_kind` attribute is `"file"` **Verified by:** `src/otel/otel.test.ts::"records op_duration_ms on success"` AND `src/otel/otel.test.ts::"increments errors_total with resolved code"` AND `src/otel/otel.test.ts::"strips query string from rel_path attribute"` AND `src/otel/otel.test.ts::"classifies root_kind per resolution matrix"` --- ### Requirement: build() inverse for round-trip The plugin SHALL expose a `build(key, meta?)` function that re-serialises a key plus optional meta into a `bundle://` URI such that `parse(build(key, { packageSlug })) === { key, meta: { packageSlug } }` for every key the plugin's `parse` accepts. When `meta?.packageSlug` is absent, `build` SHALL default to `options.packageSlug`. #### Scenario: build round-trips through parse - **WHEN** a plugin configured with `packageSlug: "x"` calls `build("assets/y.wav")` - **THEN** the result is `"bundle://x/assets/y.wav"` AND `parse("bundle://x/assets/y.wav")` returns `{ key: "assets/y.wav", meta: { packageSlug: "x" } }` **Verified by:** `src/parse/parse.test.ts::"build round-trips through parse"` --- ### Requirement: Conformance harness pass The package SHALL include a conformance test that invokes `runUriPluginConformance(bundleUriPlugin(options), fixtures)` from `@suaveplan/storage-uri` and SHALL pass without throwing against TWO real backing roots: (a) a temp-dir `file://` root populated with fixture bytes; (b) a `http://localhost:/` root served by a real `node:http` fixture server stood up in `beforeAll` and torn down in `afterAll`. Both runs SHALL use the runtime's native `fetch` (no injected `FetchLike`) to prove the universal-tier resolution strategy works end-to-end. #### Scenario: conformance passes against file:// root - **WHEN** the harness is invoked with `validUris: ["bundle://x/a.bin"]`, `invalidUris: ["bad"]`, `buildRoundTrip: [{ key: "a.bin", uri: "bundle://x/a.bin" }]` against a `file://` rootUrl pointing at a temp dir containing `a.bin` - **THEN** all assertions pass #### Scenario: conformance passes against http:// root - **WHEN** the harness is invoked with the same fixture shape against an `http://localhost:/` rootUrl served by a real `node:http` fixture server - **THEN** all assertions pass **Verified by:** `src/conformance/conformance.test.ts::"runUriPluginConformance passes against file:// root"` AND `src/conformance/conformance.test.ts::"runUriPluginConformance passes against http:// localhost fixture"` --- ### Requirement: Error-code registry exhaustion The `BUNDLE_URI_CODES` registry exposed via `defineCodes("BUNDLE_URI", [...])` SHALL contain exactly the nine codes declared in `design.md §6`: `PACKAGE_MISMATCH`, `PATH_TRAVERSAL`, `READ_ONLY`, `NOT_FOUND`, `INVALID_URI`, `FETCH_FAILED`, `RANGE_INVALID`, `RANGE_UNSUPPORTED`, `ABORTED`. Every code SHALL be reachable from a real call site exercised by the test suite — unused codes are banned per `AGENTS.md §2c`. #### Scenario: every code is reachable - **WHEN** the test suite enumerates `Object.values(BUNDLE_URI_CODES)` AND maps each to a fixture call site - **THEN** every entry maps to at least one `it(...)` assertion that exercises the code from a real call site **Verified by:** `src/error-codes/error-codes.test.ts::"every BUNDLE_URI code is reachable from a real call site"` --- ### Requirement: Server-tier `./node-fetch` subpath for `file://` roots on Node The package SHALL expose a dedicated server-tier subpath export `./node-fetch` resolving to a `FetchLike`-compatible function `nodeFileFetch` that closes the real-world gap left by Node's undici-backed `globalThis.fetch` (which does not support `file:` URLs in any release through Node 24). The default package barrel (`./`) SHALL remain universal-tier — no `node:*` import SHALL be reachable from `src/index.ts`. `nodeFileFetch` SHALL: 1. Inspect the input URL's protocol; non-`file:` URLs SHALL delegate to `globalThis.fetch(input, init)` verbatim so a single adapter instance can serve mixed `file://` + `https://` roots. 2. For `file:` URLs, translate the URL to a filesystem path via `node:url.fileURLToPath` and return a real `Response` shaped as the universal-tier plugin code expects: `status: 200`, body as the file's bytes, headers carrying `content-length` and `last-modified` (RFC 7231 IMF-fixdate from `node:fs/promises.stat().mtime`). 3. Honour the `Range: bytes=-` request header. Both the closed form (`bytes=10-19`) AND the open form (`bytes=10-`, interpreted as "through end of file" per RFC 7233 §2.1) SHALL be supported. Successful range reads SHALL return `206 Partial Content` with `content-length` AND `content-range: bytes -/`. A range whose start is past EOF OR whose start exceeds its end SHALL return `416 Range Not Satisfiable` with `content-range: bytes */`. A malformed `Range` header SHALL fall back to a full `200` response (the plugin's range pipeline already handles 200 fallback). End values past EOF SHALL be clamped to `size - 1`. 4. Honour `HEAD` requests by returning `status: 200` with the same `content-length` and `last-modified` headers and no body. 5. Thread `init.signal` into `node:fs/promises.readFile({ signal })`. A pre-aborted signal SHALL short-circuit before the `stat` round trip and throw the signal's `reason` (if it is an `Error`) OR an `AbortError`. Mid-flight aborts that propagate from `fs.readFile` as native `AbortError` SHALL pass through unchanged so the plugin's `wrapAbortError` upstream rewrites them to `BUNDLE_URI.ABORTED`. 6. Map filesystem failures (`ENOENT`, non-file inode, permission errors) to `status: 404` so the plugin's existing `NOT_FOUND` mapping fires. The adapter SHALL NOT throw HTTP-shaped errors directly. The subpath SHALL declare `node:fs/promises` and `node:url` as runtime-only externals in the package's bundler config so the universal-tier default export never pulls them through tree-shaking. #### Scenario: `file://` URL returns 200 with file bytes and content-length - **WHEN** `nodeFileFetch(new URL("file:///club.bin"))` is invoked AND `club.bin` exists with a known 20-byte payload - **THEN** the result is a `Response` with `status === 200`, `headers.get("content-length") === "20"`, AND `arrayBuffer()` byte-equal to the on-disk payload #### Scenario: `HEAD` returns headers only - **WHEN** `nodeFileFetch(fileUrl, { method: "HEAD" })` is invoked - **THEN** the result has `status === 200`, `headers.get("content-length")` matches the file size, AND `arrayBuffer().byteLength === 0` #### Scenario: closed-form `Range` returns 206 with the slice - **WHEN** `nodeFileFetch(fileUrl, { headers: { range: "bytes=5-9" } })` is invoked against a 20-byte file - **THEN** the result has `status === 206`, `headers.get("content-range") === "bytes 5-9/20"`, AND the body is byte-equal to the file's `[5, 10)` slice #### Scenario: open-form `bytes=-` reads through EOF - **WHEN** `nodeFileFetch(fileUrl, { headers: { range: "bytes=15-" } })` is invoked against a 20-byte file - **THEN** the result has `status === 206`, `headers.get("content-range") === "bytes 15-19/20"`, AND the body is the trailing 5 bytes #### Scenario: start past EOF maps to 416 - **WHEN** `nodeFileFetch(fileUrl, { headers: { range: "bytes=20-" } })` is invoked against a 20-byte file - **THEN** the result has `status === 416` AND `headers.get("content-range") === "bytes */20"` #### Scenario: missing file maps to 404 - **WHEN** `nodeFileFetch(new URL("file:///nope.bin"))` is invoked AND the path does not exist - **THEN** the result has `status === 404` #### Scenario: directory path maps to 404 - **WHEN** `nodeFileFetch(directoryUrl)` is invoked against a `file://` URL pointing at a directory (not a regular file) - **THEN** the result has `status === 404` #### Scenario: pre-aborted signal short-circuits - **WHEN** `nodeFileFetch(fileUrl, { signal: AbortSignal.abort() })` is invoked - **THEN** the call throws synchronously with an `Error` whose `name === "AbortError"` OR with the signal's custom `reason` if one was provided #### Scenario: non-`file:` URL delegates to globalThis.fetch - **WHEN** `nodeFileFetch(new URL("/echo", "http://localhost:/"))` is invoked against a real `node:http` fixture server - **THEN** the call resolves through `globalThis.fetch` AND the response body matches the fixture's reply #### Scenario: plugin end-to-end on Node against a `file://` root - **WHEN** `bundleUriPlugin({ packageSlug: "x", rootUrl: , fetch: nodeFileFetch })` is constructed AND `adapter.get("club.wav")` is invoked - **THEN** the result is a `Uint8Array` byte-equal to the on-disk bytes (the test suite uses this exact wiring to exercise the file:// scenarios on the universal-tier shim under Node) **Verified by:** `src/node-fetch/node-fetch.test.ts::"reads file bytes verbatim"` AND `src/node-fetch/node-fetch.test.ts::"returns headers without body"` AND `src/node-fetch/node-fetch.test.ts::"honours bytes=- with 206"` AND `src/node-fetch/node-fetch.test.ts::"open-ended bytes=- reads through EOF"` AND `src/node-fetch/node-fetch.test.ts::"start past EOF returns 416"` AND `src/node-fetch/node-fetch.test.ts::"missing file returns 404"` AND `src/node-fetch/node-fetch.test.ts::"directory path returns 404"` AND `src/node-fetch/node-fetch.test.ts::"pre-aborted signal throws AbortError"` AND `src/node-fetch/node-fetch.test.ts::"delegates http:// to globalThis.fetch"` AND `src/storage-shim/storage-shim.test.ts::"file:// root returns bytes verbatim against real temp dir"` --- ### Requirement: `./node-fetch` subpath emits a runnable `.js` artifact in `dist/` The package's build SHALL emit `dist/node-fetch/node-fetch.js` (a real, non-empty ECMAScript module) alongside `dist/node-fetch/node-fetch.d.ts` so that runtime importers of `@suaveplan/storage-uri-plugin-bundle/node-fetch` resolve to a runnable file. The `vite.config.ts` SHALL declare `src/node-fetch/node-fetch.ts` as an explicit additional library entry — the universal-tier root barrel (`src/index.ts`) deliberately does NOT re-export `nodeFileFetch` (to keep `node:*` imports out of the universal surface), so rollup cannot discover the file via the root entry alone. The build SHALL preserve the existing universal-tier guarantee: `dist/index.js` SHALL still contain zero `node:*` imports. #### Scenario: build emits a non-empty `dist/node-fetch/node-fetch.js` - **WHEN** the test runs `node:fs.statSync(/dist/node-fetch/node-fetch.js)` after a clean `bunx turbo build --filter=@suaveplan/storage-uri-plugin-bundle` - **THEN** the stat succeeds, `.isFile()` is `true`, AND `.size > 0` #### Scenario: emitted `.js` is dynamically importable and exports `nodeFileFetch` - **WHEN** the test invokes `await import(/dist/node-fetch/node-fetch.js)` after a clean build - **THEN** the import resolves AND `typeof imported.nodeFileFetch === "function"` #### Scenario: universal-tier root barrel remains node-free - **WHEN** the test reads `dist/index.js` and searches for `node:` (substring match, case-sensitive) - **THEN** no match is found (the root barrel imports no `node:*` modules; only the subpath entry pulls them) **Verified by:** `tests/build/node-fetch-emit.test.ts::"build emits a non-empty dist/node-fetch/node-fetch.js"` AND `tests/build/node-fetch-runtime-import.test.ts::"emitted node-fetch.js exports nodeFileFetch as a function"` AND `tests/build/node-fetch-emit.test.ts::"dist/index.js contains no node: imports"`