# 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://.storage.bunnycdn.com//` with `AccessKey` header) and **Pull Zones** (the edge — `https://.b-cdn.net/` 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://.storage.bunnycdn.com//` with `AccessKey: ` header. `LIST` returns the documented JSON array shape. - ADDED: `BunnyPullZoneClient` — purge a single URL via `POST https://api.bunny.net/purge?url=` with `AccessKey: `; purge by prefix supported via `url=*`. - ADDED: `BunnyTokenAuth` — HMAC-SHA256 Token Authentication per BunnyCDN docs. Computes `token = base64(SHA256(tokenAuthKey + path + expires))` with URL-safe base64; appends `?token=&expires=` (or `&token_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: - BunnyCDN Storage HTTP API: - BunnyCDN Pull Zone purge API: - BunnyCDN Token Authentication: ### 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).