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,78 @@
# 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<void>;
abstract signUrl(key: string, options: SignUrlOptions, signal?: AbortSignal): Promise<string>;
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.