# Capability Spec Delta: storage-cdn-bunnycdn (complete-storage-uri-adapters) **Package**: `@suaveplan/storage-cdn-bunnycdn` **Status**: PROPOSED > Every requirement below carries a `**Verified by:**` line at authoring time. ## ADDED Requirements ### Requirement: `bunnycdn://` and `bunnycdn+pull://` URI plugin factory The package SHALL export `bunnyUriPlugin(options)` from `./uri-plugin/uri-plugin` returning an object `{ storage: UriPlugin; pull: UriPlugin }` whose `storage.scheme` is `"bunnycdn"` and `pull.scheme` is `"bunnycdn+pull"`. Both plugins SHALL share the bound `BunnyCdnAdapter`; only the URI surface differs. #### Scenario: factory exposes two plugin instances - **WHEN** `bunnyUriPlugin({ adapter, storageZone: "uploads" })` is called - **THEN** the returned object has `storage.scheme === "bunnycdn"` and `pull.scheme === "bunnycdn+pull"`, and both reference the same adapter #### Scenario: factory requires storageZone - **WHEN** `bunnyUriPlugin({ adapter, storageZone: "" })` is called - **THEN** the call throws `StorageCdnBunnycdnUriError` with `code === "STORAGE_CDN_BUNNY_URI.INVALID_URI"` **Verified by:** `src/uri-plugin/uri-plugin.test.ts::"exposes both storage and pull plugins"` AND `src/uri-plugin/uri-plugin.test.ts::"rejects empty storageZone"` --- ### Requirement: Storage-zone parsing with mismatch detection The `storage` plugin SHALL parse `bunnycdn:///` URIs by taking the authority as the storage zone and the path (minus leading `/`) as the storage key. When the URI's zone does NOT match the factory's configured `storageZone`, the call SHALL raise `STORAGE_CDN_BUNNY_URI.ZONE_MISMATCH`. #### Scenario: matching zone parses successfully - **WHEN** `storage.parse("bunnycdn://uploads/img/a.jpg")` is called against a plugin configured with `storageZone: "uploads"` - **THEN** the returned `key === "img/a.jpg"` #### Scenario: mismatched zone raises ZONE_MISMATCH - **WHEN** `storage.parse("bunnycdn://wrong/img/a.jpg")` is called against the same plugin - **THEN** the call throws `StorageCdnBunnycdnUriError` with `code === "STORAGE_CDN_BUNNY_URI.ZONE_MISMATCH"` **Verified by:** `src/uri-plugin/uri-plugin.test.ts::"parses matching storage zone"` AND `src/uri-plugin/uri-plugin.test.ts::"raises ZONE_MISMATCH for wrong zone"` --- ### Requirement: Pull-zone read-only enforcement The `pull` plugin SHALL parse `bunnycdn+pull:///` URIs and SHALL reject `set` / `delete` calls with `STORAGE_CDN_BUNNY_URI.UNSUPPORTED_OPERATION`. Pull zones are read-only caches; writes have no semantic target. When the factory was constructed without `options.pullZoneHostname`, parsing a pull-zone URI SHALL raise `STORAGE_CDN_BUNNY_URI.PULL_ZONE_NOT_CONFIGURED`. #### Scenario: pull-zone read parses successfully - **WHEN** `pull.parse("bunnycdn+pull://cdn.example.com/img/a.jpg")` is called against a plugin with `pullZoneHostname: "cdn.example.com"` - **THEN** the returned `key === "img/a.jpg"` #### Scenario: pull-zone write is rejected - **WHEN** the resolver routes `write("bunnycdn+pull://cdn.example.com/x", bytes)` - **THEN** the rejection has `code === "STORAGE_CDN_BUNNY_URI.UNSUPPORTED_OPERATION"` #### Scenario: pull-zone URI without configured hostname is rejected - **WHEN** `pull.parse("bunnycdn+pull://cdn.example.com/x")` is called against a plugin that was constructed without `pullZoneHostname` - **THEN** the call throws with `code === "STORAGE_CDN_BUNNY_URI.PULL_ZONE_NOT_CONFIGURED"` **Verified by:** `src/uri-plugin/uri-plugin.test.ts::"parses pull-zone reads"` AND `src/uri-plugin/uri-plugin.test.ts::"rejects pull-zone writes"` AND `src/uri-plugin/uri-plugin.test.ts::"raises PULL_ZONE_NOT_CONFIGURED when missing"` --- ### Requirement: Signed-URL token passthrough When a URI carries `?token=&expires=`, the plugin SHALL forward those parameters to the existing `buildTokenAuthQuery` helper when invoking the adapter. The token bytes SHALL NEVER be persisted on `ParsedUri.meta`; they live only on the ephemeral fetch path. #### Scenario: signed token reaches buildTokenAuthQuery - **WHEN** the resolver routes `read("bunnycdn://uploads/x?token=abc&expires=99")` and the bound adapter has `tokenAuthKey` configured - **THEN** the adapter's outgoing request URL contains the token-authenticated query string produced by `buildTokenAuthQuery` #### Scenario: token is absent from ParsedUri.meta - **WHEN** `storage.parse("bunnycdn://uploads/x?token=abc&expires=99")` is called - **THEN** the returned `meta` does NOT include a `token` key **Verified by:** `src/uri-plugin/uri-plugin.test.ts::"forwards signed token to buildTokenAuthQuery"` AND `src/uri-plugin/uri-plugin.test.ts::"does not persist token on ParsedUri.meta"` --- ### Requirement: `build(key, meta?)` with surface discriminator The plugin SHALL implement `build(key, meta?)` on both surfaces. When `meta?.surface === "pull"`, the result uses the `bunnycdn+pull://` form with `pullZoneHostname`; otherwise the result uses the `bunnycdn://` form with the configured `storageZone`. #### Scenario: storage build defaults to storage zone - **WHEN** `storage.build("img/a.jpg")` is called - **THEN** the result is `"bunnycdn:///img/a.jpg"` #### Scenario: pull build uses pull-zone hostname - **WHEN** `pull.build("img/a.jpg")` is called against a plugin with `pullZoneHostname: "cdn.example.com"` - **THEN** the result is `"bunnycdn+pull://cdn.example.com/img/a.jpg"` **Verified by:** `src/uri-plugin/uri-plugin.test.ts::"storage build emits bunnycdn scheme"` AND `src/uri-plugin/uri-plugin.test.ts::"pull build emits bunnycdn+pull scheme"` --- ### Requirement: Span-attribute redaction The plugin SHALL redact `token`, `signature`, and `expires` query parameters from any URI surfaced on OTel span attributes or in raised error `.uri` discriminants. Redaction SHALL be exposed as `redactSignedUrl(uri): string`. #### Scenario: token is redacted on span attribute - **WHEN** the resolver routes `read("bunnycdn://uploads/x?token=abc&expires=99")` and emits a span - **THEN** the `cdn.uri` attribute is `"bunnycdn://uploads/x?token=&expires="` **Verified by:** `src/uri-plugin/redact-signed-url.test.ts::"redacts token, signature, and expires"` --- ### Requirement: `STORAGE_CDN_BUNNY_URI_CODES` registry The package SHALL export `STORAGE_CDN_BUNNY_URI_CODES` produced by `defineCodes("STORAGE_CDN_BUNNY_URI", [...] as const)` covering exactly 6 codes: `INVALID_URI`, `ZONE_MISMATCH`, `PULL_ZONE_NOT_CONFIGURED`, `TOKEN_INVALID`, `KEY_TOO_LONG`, `UNSUPPORTED_OPERATION`. Every code SHALL be reachable, and `StorageCdnBunnycdnUriError` SHALL extend `SuaveplanError` with `uri`/`scheme` discriminants. #### Scenario: every code is reachable - **WHEN** the test suite exercises every documented failure path - **THEN** every member of `STORAGE_CDN_BUNNY_URI_CODES` appears on at least one raised error instance **Verified by:** `src/uri-plugin/error-codes.test.ts::"exposes 6 codes all reachable"` --- ### Requirement: AbortSignal threading The plugin SHALL forward `AbortSignal` unchanged to every adapter call. A pre-aborted signal SHALL short-circuit with the adapter's existing `STORAGE_CDN_BUNNYCDN.ABORTED` code without invoking the adapter. #### Scenario: pre-aborted signal short-circuits - **WHEN** the resolver routes `read("bunnycdn://uploads/x", AbortSignal.abort())` - **THEN** the rejection has `code === "STORAGE_CDN_BUNNYCDN.ABORTED"` and the adapter is NEVER invoked **Verified by:** `src/uri-plugin/uri-plugin.test.ts::"short-circuits on pre-aborted signal"` --- ### Requirement: Universal-tier purity Every source file under `src/uri-plugin/` SHALL be universal-tier: ZERO imports from `node:*`, `bun:*`, browser-specific globals, or React. #### Scenario: tier-purity grep returns empty - **WHEN** `grep -rnE "from ['\"](node:|bun:)" packages/storage/storage-cdn-bunnycdn/src/uri-plugin/` is run - **THEN** the command returns zero matches **Verified by:** `src/uri-plugin/uri-plugin.test.ts::"uri-plugin module has no node: or bun: imports"` --- ### Requirement: Subpath exports The package SHALL declare subpath exports for `"./uri-plugin/uri-plugin"`, `"./uri-plugin/redact-signed-url"`, and `"./uri-plugin/error-codes"`. #### Scenario: subpath imports resolve - **WHEN** `import("@suaveplan/storage-cdn-bunnycdn/uri-plugin/uri-plugin")` is executed - **THEN** the module resolves and re-exports `bunnyUriPlugin` **Verified by:** `src/uri-plugin/uri-plugin.test.ts::"is importable via the documented subpath"` --- ### Requirement: Conformance harness pass Both `storage` and `pull` plugins SHALL pass `runUriPluginConformance` with fixtures covering zone-match cases, invalid zone forms, signed-URL passthrough, and non-empty `buildRoundTrip` sets per surface. #### Scenario: both plugins pass conformance - **WHEN** `runUriPluginConformance` is invoked against each plugin surface with the documented fixtures - **THEN** both calls return without throwing **Verified by:** `src/uri-plugin/uri-plugin.test.ts::"storage plugin passes runUriPluginConformance"` AND `src/uri-plugin/uri-plugin.test.ts::"pull plugin passes runUriPluginConformance"`