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:
BunnyCdnAdapterclass extendingCdnAdapterfrom@suaveplan/storage-cdn. - ADDED:
createBunnyCdnAdapter(options)factory. - ADDED:
BunnyCdnAdapterOptionsinterface + Zod schema. Required:storageZone(zone name),storageAccessKey,pullZoneHostname(e.g.mycdn.b-cdn.net). Optional:storageRegion(defaultde— 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/LISTagainsthttps://<region>.storage.bunnycdn.com/<zone>/<key>withAccessKey: <storageAccessKey>header.LISTreturns the documented JSON array shape. - ADDED:
BunnyPullZoneClient— purge a single URL viaPOST https://api.bunny.net/purge?url=<edgeUrl>withAccessKey: <purgeApiKey>; purge by prefix supported viaurl=<prefix>*. - ADDED:
BunnyTokenAuth— HMAC-SHA256 Token Authentication per BunnyCDN docs. Computestoken = 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.StorageCdnBunnycdnErrorclass withzone/region/httpStatusdiscriminants. - ADDED: Capabilities —
partialReads: true(Storage supportsRange),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"addsbunnycdn: BunnyCdnAdapterslot;registerBunnyCdnProvider()idempotent. - ADDED:
@suaveplan/retryon transient (429,5xx, network) withRetry-Afterhonour. - ADDED: OTel surface — same shape as Rackspace adapter, with
provider="bunnycdn"andstorage_regionattributes.
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
storageRegiononly to pick the write endpoint. Replication policy is configured in the Bunny.net dashboard.
Source mapping
Genesis-original. References:
- BunnyCDN Storage HTTP API: https://docs.bunny.net/reference/put_-storagezonename-path-filename-
- BunnyCDN Pull Zone purge API: https://docs.bunny.net/reference/purgepublic_indexpost
- BunnyCDN Token Authentication: https://docs.bunny.net/docs/cdn-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 injectedFetchLike, 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, everySTORAGE_CDN_BUNNYCDNcode reachable, tier-purity grep.src/storage-client/storage-client.test.ts— PUT/GET/DELETE/HEAD/LIST round-trips, regional endpoint selection,AccessKeyheader 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 })returnsBunnyCdnAdapter, idempotent re-registration.
Impact
Affected dependents
- No dependents yet (
bunx turbo ls --filter=...@suaveplan/storage-cdn-bunnycdn).
Affected upstreams
@suaveplan/storage-cdn—StorageProviderRegistryaugmented withbunnycdnslot. Additive. [x] no action needed.- Hard dependency on
@suaveplan/storage-cdn(must be archived first).