# Design — @suaveplan/storage-cdn Non-normative companion to `proposal.md` and `specs/storage-cdn/spec.md`. ## 1. Role in the family ```text @suaveplan/storage AbstractStorage core (key/value, in-memory) └─ @suaveplan/storage-http Generic prefix+key reader (no purge/signing) └─ @suaveplan/storage-cdn CdnAdapter contract (THIS PACKAGE) ├─ @suaveplan/storage-cdn-rackspace Cloud Files origin + Rackspace CDN └─ @suaveplan/storage-cdn-bunnycdn BunnyCDN Storage origin + Pull Zone ``` `storage-http` and `storage-cdn` are siblings, not layers. A consumer that only needs read access to a CDN-fronted static site uses `storage-http`. A consumer that needs purge + signed URLs uses a `storage-cdn-*` provider. The two abstractions stay separable so neither pulls weight the other does not need. ## 2. The contract — three methods ```ts abstract class CdnAdapter extends AbstractStorage { abstract purge(key: string, signal?: AbortSignal): Promise; abstract signUrl(key: string, options: SignUrlOptions, signal?: AbortSignal): Promise; abstract edgeUrl(key: string): string; // pure, synchronous; just URL composition readonly capabilities: StorageCapabilities & { cdn: CdnCapabilities }; } ``` - `purge` — single-key edge invalidation. Throws `STORAGE_CDN.PURGE_NOT_SUPPORTED` if `capabilities.cdn.cdnPurge === false`. - `signUrl` — produces a short-lived public URL. Throws `STORAGE_CDN.SIGN_URL_NOT_SUPPORTED` if `capabilities.cdn.cdnSignedUrls === false`. The `method` option distinguishes GET (signed read), HEAD (signed exists), PUT (signed upload — only meaningful when the provider supports it). - `edgeUrl` — pure composition: `new URL(encodedKey, edgeBaseUrl).toString()`. Synchronous. Every provider supports this — it is just URL math. `purge` taking a prefix is a separate capability (`cdnPrefixPurge`). Rationale in proposal §"Out of scope". ## 3. Capability matrix examples | Provider | cdnPurge | cdnPrefixPurge | cdnSignedUrls | cdnOriginUploads | |---|---|---|---|---| | Rackspace Cloud Files + CDN | ✅ | ⚠️ via re-list+per-key | ✅ HMAC-SHA1 TempURL | ✅ Swift PUT | | BunnyCDN Storage + Pull Zone | ✅ | ✅ (`/purge?url=…/*`) | ✅ HMAC-SHA256 Token Auth | ✅ HTTP PUT | | Cloudflare R2 + custom domain | ✅ via Cache API | ✅ tag-based | ✅ presigned (S3-compat) | ✅ S3-compat | The matrix is a guide for provider package authors, not a normative requirement of this package. ## 4. File layout ```text src/ index.ts # explicit named re-exports error-codes.ts # defineCodes("STORAGE_CDN", [...]) + StorageCdnError types/ cdn-adapter.types.ts # CdnAdapter abstract class, CdnAdapterOptions cdn-capabilities.types.ts # CdnCapabilities interface + DEFAULT_CDN_CAPABILITIES sign-url.types.ts # SignUrlOptions interface sign-url.schemas.ts # SignUrlOptionsSchema (Zod) cdn-adapter/ cdn-adapter.ts # base class with default-not-supported helpers cdn-adapter.md cdn-adapter.test.ts cdn-adapter.spec-conformance.test.ts sign-url/ sign-url.ts # schema parse + boundary helpers sign-url.md sign-url.test.ts guards/ guards.ts # assertCdnAdapter, isCdnAdapter guards.md guards.test.ts ``` ## 5. Tier purity Universal — no `node:*`, no DOM. Pure types + small helper functions. ## 6. Trade-offs - **One abstract base vs. interface mixed into AbstractStorage.** Chose abstract base. Reason: provider packages can use `defaultPurgeNotSupported()` / `defaultSignUrlNotSupported()` helpers from a real super-class without each re-implementing them. The cost — `CdnAdapter` becomes a second extension point alongside `AbstractStorage` — is small because providers always know they are CDN providers when authoring. - **`signUrl` Promise vs. synchronous.** Some providers (Rackspace TempURL, BunnyCDN Token Auth) are HMAC-local and could be sync. Others (R2 presigned URLs via STS, signed cookies via auth API) are async. Forcing Promise everywhere keeps callers uniform; sync providers just resolve immediately. - **`edgeUrl` synchronous.** Pure URL composition is always available and never throws — there is no reason to make it async. Forces provider authors to ensure `edgeBaseUrl` is validated at construction time, which is the right invariant.