Files
2026-05-27 23:29:53 +01:00

9.1 KiB

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://<zone>/<path> 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://<host>/<path> 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=<value>&expires=<unix>, 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://<storageZone>/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=<redacted>&expires=<redacted>"

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<StorageCdnBunnycdnUriErrorCode> 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"