Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
# 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.