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

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:

  1. Cache purge / invalidation — purge a key or prefix from edge POPs after origin updates.
  2. Signed / temporary URLs — produce a short-lived URL the caller can hand to a browser without exposing credentials.
  3. 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: CdnAdapter abstract class extending AbstractStorage from @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: CdnCapabilities interface extending StorageCapabilities with cdnPurge: boolean, cdnSignedUrls: boolean, cdnPrefixPurge: boolean, cdnOriginUploads: boolean. Defaults all false.
  • ADDED: SignUrlOptions interface — { ttlSeconds: number; method?: "GET" | "PUT" | "HEAD"; responseContentType?: string; responseContentDisposition?: string }. Zod schema for boundary validation.
  • ADDED: CdnAdapterOptions base 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_CODES registry via defineCodes("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> with key, provider, and purgeId discriminants.
  • ADDED: defaultPurgeNotSupported() and defaultSignUrlNotSupported() reference implementations — concrete CdnAdapter subclasses 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 a StorageLike and needing to opt into CDN features.
  • ADDED: Module-augmentation slot — declare module "@suaveplan/storage" augments StorageCapabilities with cdn?: CdnCapabilities (optional nested capability object). Provider packages set capabilities.cdn = { … }.

Out of scope

  • Concrete provider implementations. storage-cdn-rackspace and storage-cdn-bunnycdn are 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 / Expires belong to the consumer's content strategy and are emitted by the origin server config, not the adapter. The adapter MAY pass-through headers via set options 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 — AbstractStorage base.
  • Industry conventions: AWS S3 PutObject + CloudFront CreateInvalidation; 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 with AbstractStorage, assertCdnAdapter discriminates correctly.
  • src/sign-url/sign-url.test.ts — SignUrlOptionsSchema validates ttlSeconds > 0, method whitelist, response-* fields optional.
  • src/error-codes/error-codes.test.ts — every STORAGE_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-cdn returning the package itself).
  • Forward-declared consumers: @suaveplan/storage-cdn-rackspace, @suaveplan/storage-cdn-bunnycdn.

Affected upstreams

  • @suaveplan/storage — StorageCapabilities module-augmented with optional cdn?: CdnCapabilities. Additive; no breaking change. [x] no action needed.