79 lines
4.5 KiB
Markdown
79 lines
4.5 KiB
Markdown
# 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.
|