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"andpull.scheme === "bunnycdn+pull", and both reference the same adapter
Scenario: factory requires storageZone
- WHEN
bunnyUriPlugin({ adapter, storageZone: "" })is called - THEN the call throws
StorageCdnBunnycdnUriErrorwithcode === "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 withstorageZone: "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
StorageCdnBunnycdnUriErrorwithcode === "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 withpullZoneHostname: "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 withoutpullZoneHostname - 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 hastokenAuthKeyconfigured - 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
metadoes NOT include atokenkey
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 withpullZoneHostname: "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.uriattribute 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_CODESappears 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
runUriPluginConformanceis 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"