Initial Commit
This commit is contained in:
78
openspec/archive/2026-05-16-add-storage-cdn/design.md
Normal file
78
openspec/archive/2026-05-16-add-storage-cdn/design.md
Normal 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.
|
||||
Reference in New Issue
Block a user