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

73 lines
6.3 KiB
Markdown

# 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:
- 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 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).