7.2 KiB
Add @suaveplan/storage-cdn — CDN adapter interface over AbstractStorage
Package: @suaveplan/storage-cdn
Status: PROPOSED
Type: new-package (genesis-original)
- Design: ./design.md
- Spec delta: ./specs/storage-cdn/spec.md
Why
@suaveplan/storage-http (separate proposal) covers the generic "URL prefix + key" read surface. Real CDN providers (Rackspace Cloud Files + Rackspace CDN, BunnyCDN Storage + Pull Zones, Cloudflare R2 + custom domain, Fastly + origin, etc.) add three capabilities the generic adapter cannot express:
- Cache purge / invalidation — purge a key or prefix from edge POPs after origin updates.
- Signed / temporary URLs — produce a short-lived URL the caller can hand to a browser without exposing credentials.
- Origin vs edge split — writes go to a private origin endpoint; reads go through a public edge URL. The adapter needs both.
storage-cdn is the types-only foundation that defines a common contract (CdnAdapter extends AbstractStorage + CdnCapabilities + purge() / signUrl() / edgeUrl()) so consumers can write provider-agnostic code and provider packages (storage-cdn-rackspace, storage-cdn-bunnycdn, future entrants) implement against the same surface. The package mirrors the role @suaveplan/db plays for database adapters, or @suaveplan/network plays for transports: a small core that establishes the contract, with concrete adapters in sibling packages.
What changes
- ADDED:
CdnAdapterabstract class extendingAbstractStoragefrom@suaveplan/storage. Adds three abstract methods:purge(key: string, signal?: AbortSignal): Promise<void>,signUrl(key: string, options: SignUrlOptions, signal?: AbortSignal): Promise<string>,edgeUrl(key: string): string. - ADDED:
CdnCapabilitiesinterface extendingStorageCapabilitieswithcdnPurge: boolean,cdnSignedUrls: boolean,cdnPrefixPurge: boolean,cdnOriginUploads: boolean. Defaults allfalse. - ADDED:
SignUrlOptionsinterface —{ ttlSeconds: number; method?: "GET" | "PUT" | "HEAD"; responseContentType?: string; responseContentDisposition?: string }. Zod schema for boundary validation. - ADDED:
CdnAdapterOptionsbase interface (provider packages extend it) —originBaseUrl: string(write endpoint, must end with/),edgeBaseUrl: string(public read endpoint, must end with/),credentials(provider-specific subtype slot),fetch?: FetchLike. - ADDED:
STORAGE_CDN_CODESregistry viadefineCodes("STORAGE_CDN", [...])— 8 codes:PURGE_FAILED,SIGN_URL_NOT_SUPPORTED,PURGE_NOT_SUPPORTED,PREFIX_PURGE_NOT_SUPPORTED,INVALID_TTL,INVALID_OPTIONS,EDGE_ORIGIN_MISMATCH,CREDENTIAL_EXPIRED. - ADDED:
StorageCdnError extends SuaveplanError<StorageCdnErrorCode>withkey,provider, andpurgeIddiscriminants. - ADDED:
defaultPurgeNotSupported()anddefaultSignUrlNotSupported()reference implementations — concreteCdnAdaptersubclasses that lack a capability MUST throw via these helpers so the error surface is uniform. - ADDED:
assertCdnAdapter(value)type-guard for runtime narrowing — useful when accepting aStorageLikeand needing to opt into CDN features. - ADDED: Module-augmentation slot —
declare module "@suaveplan/storage"augmentsStorageCapabilitieswithcdn?: CdnCapabilities(optional nested capability object). Provider packages setcapabilities.cdn = { … }.
Out of scope
- Concrete provider implementations.
storage-cdn-rackspaceandstorage-cdn-bunnycdnare separate change proposals. This package is types-only foundation plus the error registry and helper functions — no provider-specific code. - Listing. Inherited from
AbstractStorage; provider packages decide whether their origin API supports it. The CDN abstraction itself adds no listing contract. - Cache-control header policy.
Cache-Control/Expiresbelong to the consumer's content strategy and are emitted by the origin server config, not the adapter. The adapter MAY pass-through headers viasetoptions but does not own the policy. - Origin shielding / multi-origin failover. A failover/shielding layer is a separate concern composed via
storage-mux(future) or application logic. - Edge-side rendering integration (image transforms, video JIT, etc.). Out of scope; provider-specific and orthogonal to storage semantics.
Source mapping
Genesis-original. No upstream in ../suaveplan/. The package contract derives from:
openspec/specs/storage/spec.md—AbstractStoragebase.- Industry conventions: AWS S3
PutObject+ CloudFrontCreateInvalidation; Rackspace Cloud Files + CDN purge; BunnyCDN Storage + Pull Zone purge; Cloudflare R2 + cache purge API. The three abstract methods (purge,signUrl,edgeUrl) are the intersection of every major CDN.
Requirements with no source line
| Requirement | Justification |
|---|---|
Extend AbstractStorage |
Every storage adapter in genesis extends AbstractStorage for StorageProviderRegistry compatibility. |
originBaseUrl and edgeBaseUrl distinct fields |
The two URLs are genuinely different on every real CDN (Rackspace storage101.<dc>.clouddrive.com vs <container>.cdn.clouddrive.com; BunnyCDN storage.bunnycdn.com/<zone> vs <zone>.b-cdn.net). Conflating them produces broken signed URLs and wasted edge cache. |
signUrl returns Promise<string> not string |
Some providers compute signed URLs via an API call (Rackspace temp URLs are HMAC-local, but R2 presigned URLs require an STS call). Returning a Promise keeps both shapes uniform. |
purge is a single-key method |
Multi-key / prefix purge is a separate capability flag (cdnPrefixPurge). Many providers rate-limit prefix purge heavily; forcing single-key as the minimum contract keeps the cheap path cheap. |
Error code EDGE_ORIGIN_MISMATCH |
A common foot-gun is configuring originBaseUrl and edgeBaseUrl against different containers/zones. The adapter validates at construction time and surfaces a clear code rather than mysterious 404s at runtime. |
Verification approach
src/cdn-adapter/cdn-adapter.test.ts— abstract class shape, capability defaults, default-not-supported helpers throw correctly.src/cdn-adapter/cdn-adapter.spec-conformance.test.ts— type-level conformance withAbstractStorage,assertCdnAdapterdiscriminates correctly.src/sign-url/sign-url.test.ts—SignUrlOptionsSchemavalidatesttlSeconds > 0, method whitelist, response-* fields optional.src/error-codes/error-codes.test.ts— everySTORAGE_CDN_*code reachable from a real call site.
Impact
Affected dependents
Run bunx turbo ls --filter=...@suaveplan/storage-cdn to enumerate. The package is new — no dependents currently. The two follow-up changes (add-storage-cdn-rackspace, add-storage-cdn-bunnycdn) declare a hard dependency on this package.
- No dependents yet (verified by
bunx turbo ls --filter=...@suaveplan/storage-cdnreturning the package itself). - Forward-declared consumers:
@suaveplan/storage-cdn-rackspace,@suaveplan/storage-cdn-bunnycdn.
Affected upstreams
@suaveplan/storage—StorageCapabilitiesmodule-augmented with optionalcdn?: CdnCapabilities. Additive; no breaking change. [x] no action needed.