Files
suaveplan/openspec/archive/2026-05-16-add-storage-cdn-bunnycdn/proposal.md
2026-05-27 23:29:53 +01:00

6.3 KiB

Add @suaveplan/storage-cdn-bunnycdn — BunnyCDN Storage + Pull Zone adapter

Package: @suaveplan/storage-cdn-bunnycdn Status: PROPOSED Type: new-package (genesis-original)

  • Design: ./design.md
  • Spec delta: ./specs/storage-cdn-bunnycdn/spec.md

Why

The user operates production traffic from BunnyCDN. BunnyCDN's architecture is two services: Storage Zones (the origin — HTTP PUT/GET/DELETE against https://<region>.storage.bunnycdn.com/<zone>/<key> with AccessKey header) and Pull Zones (the edge — https://<zone>.b-cdn.net/<key> plus optional Token Authentication for signed URLs and a per-Pull-Zone purge endpoint). This package wraps both behind the CdnAdapter contract so application code can use it identically to Rackspace.

BunnyCDN's API is simpler than Rackspace's (no service catalog, no Swift TempURL middleware — just AccessKey headers and HMAC-SHA256 token auth on Pull Zones). The package mirrors the Rackspace structure for consistency.

What changes

  • ADDED: BunnyCdnAdapter class extending CdnAdapter from @suaveplan/storage-cdn.
  • ADDED: createBunnyCdnAdapter(options) factory.
  • ADDED: BunnyCdnAdapterOptions interface + Zod schema. Required: storageZone (zone name), storageAccessKey, pullZoneHostname (e.g. mycdn.b-cdn.net). Optional: storageRegion (default de — Falkenstein; supported: de, ny, la, sg, syd, uk), tokenAuthKey (for signed URLs), purgeApiKey (account-level API key for the Bunny.net management API), fetch, retry, headers.
  • ADDED: BunnyStorageClient — PUT/GET/DELETE/HEAD/LIST against https://<region>.storage.bunnycdn.com/<zone>/<key> with AccessKey: <storageAccessKey> header. LIST returns the documented JSON array shape.
  • ADDED: BunnyPullZoneClient — purge a single URL via POST https://api.bunny.net/purge?url=<edgeUrl> with AccessKey: <purgeApiKey>; purge by prefix supported via url=<prefix>*.
  • ADDED: BunnyTokenAuth — HMAC-SHA256 Token Authentication per BunnyCDN docs. Computes token = base64(SHA256(tokenAuthKey + path + expires)) with URL-safe base64; appends ?token=<token>&expires=<unix> (or &token_path=<path> for path-scoped tokens).
  • ADDED: STORAGE_CDN_BUNNYCDN_CODES — 8 codes: STORAGE_AUTH_FAILED, PURGE_AUTH_FAILED, ZONE_NOT_FOUND, TOKEN_AUTH_KEY_MISSING, PURGE_API_KEY_MISSING, INVALID_REGION, INVALID_OPTIONS, UPSTREAM_ERROR. StorageCdnBunnycdnError class with zone/region/httpStatus discriminants.
  • ADDED: Capabilities — partialReads: true (Storage supports Range), nativeStreams: true, partialWrites: false, atomicWrites: false, contentAddressable: false; cdn: { cdnPurge: true, cdnPrefixPurge: true, cdnSignedUrls: true, cdnOriginUploads: true }.
  • ADDED: Module-augmentation registry — declare module "@suaveplan/storage" adds bunnycdn: BunnyCdnAdapter slot; registerBunnyCdnProvider() idempotent.
  • ADDED: @suaveplan/retry on transient (429, 5xx, network) with Retry-After honour.
  • ADDED: OTel surface — same shape as Rackspace adapter, with provider="bunnycdn" and storage_region attributes.

Out of scope

  • Bunny.net Stream / Optimizer / Shield. Edge-side video, image optimization, and origin shielding are separate Bunny.net products with separate APIs. Out of scope.
  • Pull Zone management API. Creating/configuring Pull Zones via the management API is deployment tooling, not the runtime adapter. The adapter assumes the Pull Zone is already configured.
  • Edge-side logging / analytics. Bunny.net log delivery is operations tooling.
  • Per-region replication strategy. Bunny Storage replicates automatically to selected regions; the adapter uses storageRegion only to pick the write endpoint. Replication policy is configured in the Bunny.net dashboard.

Source mapping

Genesis-original. References:

Requirements with no source line

Requirement Justification
Two distinct access keys (storageAccessKey, purgeApiKey) Storage zones use a per-zone storage password; purge uses the account-level API key. They are different secrets with different scopes; conflating them is a security mistake.
cdnPrefixPurge: true BunnyCDN's purge endpoint accepts URLs ending in * for prefix purge. This is genuinely supported (unlike Rackspace where it requires per-key calls).
tokenAuthKey separate from storage key Token Authentication signing key is configured on the Pull Zone, not on the Storage Zone. Caller provides it explicitly.
Region in storage URL but Pull Zone is global Storage writes go to the regional endpoint; Pull Zone reads come from the global CDN. The split matches Bunny's architecture and avoids the "edge URL with region in it" anti-pattern.

Verification approach

  • src/bunny-cdn-adapter/bunny-cdn-adapter.test.ts — CRUD via injected FetchLike, range reads, edgeUrl composition, purge happy path + prefix purge, signed-URL round-trip, AbortSignal threading, retry on 429/503.
  • src/bunny-cdn-adapter/bunny-cdn-adapter.spec-conformance.test.ts — instanceof CdnAdapter, capabilities matrix, every STORAGE_CDN_BUNNYCDN code reachable, tier-purity grep.
  • src/storage-client/storage-client.test.ts — PUT/GET/DELETE/HEAD/LIST round-trips, regional endpoint selection, AccessKey header attached.
  • src/pull-zone-client/pull-zone-client.test.ts — single URL purge, prefix purge (URL ending *), purge auth failure → PURGE_AUTH_FAILED.
  • src/token-auth/token-auth.test.ts — HMAC-SHA256 signature matches Bunny reference vector, path-scoped tokens, expired token rejected.
  • src/registry/registry.test.ts — createStorage({ type: "bunnycdn", config }) returns BunnyCdnAdapter, idempotent re-registration.

Impact

Affected dependents

  • No dependents yet (bunx turbo ls --filter=...@suaveplan/storage-cdn-bunnycdn).

Affected upstreams

  • @suaveplan/storage-cdn — StorageProviderRegistry augmented with bunnycdn slot. Additive. [x] no action needed.
  • Hard dependency on @suaveplan/storage-cdn (must be archived first).