7.5 KiB
Capability Spec Delta: storage-cdn (storage-cdn)
Package: @suaveplan/storage-cdn
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time. No exceptions.
Requirements
Requirement: CdnAdapter abstract class extends AbstractStorage
The package SHALL provide a CdnAdapter abstract class extending AbstractStorage from @suaveplan/storage. The class SHALL declare three abstract methods: purge(key: string, signal?: AbortSignal): Promise<void>, signUrl(key: string, options: SignUrlOptions, signal?: AbortSignal): Promise<string>, and edgeUrl(key: string): string. Every concrete subclass MUST implement all three. The class SHALL declare a readonly capabilities property of type StorageCapabilities & { cdn: CdnCapabilities }.
Scenario: subclass is instance of both AbstractStorage and CdnAdapter
- WHEN a concrete subclass of
CdnAdapteris constructed - THEN the instance is
instanceof AbstractStorageANDinstanceof CdnAdapter
Scenario: missing abstract method is a type error at compile time
- WHEN a subclass omits
purge,signUrl, oredgeUrl - THEN
bunx turbo typecheckfails with TS2515 ("Non-abstract class does not implement inherited abstract member")
Verified by: src/cdn-adapter/cdn-adapter.spec-conformance.test.ts::"subclass is instance of AbstractStorage and CdnAdapter" AND src/cdn-adapter/cdn-adapter.spec-conformance.test.ts::"missing abstract method fails typecheck"
Requirement: CdnCapabilities interface with four boolean flags
The package SHALL declare CdnCapabilities with exactly these four boolean fields: cdnPurge, cdnPrefixPurge, cdnSignedUrls, cdnOriginUploads. The package SHALL export DEFAULT_CDN_CAPABILITIES with every flag false. Concrete subclasses MUST override per native support.
Scenario: defaults are all false
- WHEN a caller reads
DEFAULT_CDN_CAPABILITIES - THEN every field is
false
Verified by: src/cdn-adapter/cdn-adapter.test.ts::"DEFAULT_CDN_CAPABILITIES has every flag false"
Requirement: SignUrlOptions validated via Zod schema
The package SHALL declare SignUrlOptions interface with required ttlSeconds: number AND optional method?: "GET" | "PUT" | "HEAD", responseContentType?: string, responseContentDisposition?: string. The package SHALL export SignUrlOptionsSchema (Zod) enforcing ttlSeconds integer > 0 and method whitelist. Subclasses MUST parse options through the schema before signing and throw StorageCdnError with code STORAGE_CDN.INVALID_TTL on ttlSeconds <= 0 and STORAGE_CDN.INVALID_OPTIONS on other schema failures.
Scenario: zero or negative ttl is rejected with INVALID_TTL
- WHEN a caller calls
SignUrlOptionsSchema.parse({ ttlSeconds: 0 })or({ ttlSeconds: -5 }) - THEN the parse throws AND a wrapping helper maps the error to
StorageCdnErrorwithcode === "STORAGE_CDN.INVALID_TTL"
Scenario: method outside whitelist is rejected
- WHEN a caller calls
SignUrlOptionsSchema.parse({ ttlSeconds: 60, method: "DELETE" }) - THEN the parse throws
Verified by: src/sign-url/sign-url.test.ts::"rejects zero or negative ttlSeconds with INVALID_TTL" AND src/sign-url/sign-url.test.ts::"rejects method outside GET/PUT/HEAD whitelist"
Requirement: Default-not-supported helpers
The package SHALL export defaultPurgeNotSupported(): never and defaultSignUrlNotSupported(): never helpers. Each SHALL throw StorageCdnError with codes STORAGE_CDN.PURGE_NOT_SUPPORTED and STORAGE_CDN.SIGN_URL_NOT_SUPPORTED respectively. Concrete CdnAdapter subclasses that lack a capability MUST invoke these helpers from their method bodies so the error surface is uniform across providers.
Scenario: defaultPurgeNotSupported throws the documented error
- WHEN a caller invokes
defaultPurgeNotSupported() - THEN the call throws
StorageCdnErrorwithcode === "STORAGE_CDN.PURGE_NOT_SUPPORTED"
Scenario: defaultSignUrlNotSupported throws the documented error
- WHEN a caller invokes
defaultSignUrlNotSupported() - THEN the call throws
StorageCdnErrorwithcode === "STORAGE_CDN.SIGN_URL_NOT_SUPPORTED"
Verified by: src/cdn-adapter/cdn-adapter.test.ts::"defaultPurgeNotSupported throws PURGE_NOT_SUPPORTED" AND src/cdn-adapter/cdn-adapter.test.ts::"defaultSignUrlNotSupported throws SIGN_URL_NOT_SUPPORTED"
Requirement: assertCdnAdapter type guard
The package SHALL export isCdnAdapter(value: unknown): value is CdnAdapter and assertCdnAdapter(value: unknown): asserts value is CdnAdapter. The guards SHALL narrow on instanceof CdnAdapter. The assert form SHALL throw StorageCdnError with code STORAGE_CDN.INVALID_OPTIONS when the value is not a CdnAdapter.
Scenario: isCdnAdapter narrows when given a subclass instance
- WHEN a caller invokes
isCdnAdapter(concreteCdnInstance) - THEN the result is
trueAND TypeScript narrows the variable toCdnAdapter
Scenario: assertCdnAdapter throws on non-CdnAdapter
- WHEN a caller invokes
assertCdnAdapter(new MemoryStorage()) - THEN the call throws
StorageCdnError
Verified by: src/guards/guards.test.ts::"isCdnAdapter narrows on instanceof" AND src/guards/guards.test.ts::"assertCdnAdapter throws on non-CdnAdapter"
Requirement: STORAGE_CDN error registry
The package SHALL declare STORAGE_CDN_CODES via defineCodes("STORAGE_CDN", [...]) with exactly these 8 codes: PURGE_FAILED, SIGN_URL_NOT_SUPPORTED, PURGE_NOT_SUPPORTED, PREFIX_PURGE_NOT_SUPPORTED, INVALID_TTL, INVALID_OPTIONS, EDGE_ORIGIN_MISMATCH, CREDENTIAL_EXPIRED. The package SHALL declare StorageCdnError extends SuaveplanError<StorageCdnErrorCode> with optional key, provider, and purgeId discriminants. Every code SHALL be reachable from a real call site exercised by tests in this package (or in a downstream provider test, with the reachability annotated).
Scenario: every declared code is reachable
- WHEN the test suite is run
- THEN every code in
STORAGE_CDN_CODEShas at least one test in this package or an annotated forward reference to a provider package test
Verified by: src/error-codes/error-codes.test.ts::"every STORAGE_CDN code is reachable from a real call site"
Requirement: Module augmentation adds cdn capability slot
The package SHALL declare declare module "@suaveplan/storage" augmenting StorageCapabilities with cdn?: CdnCapabilities. The augmentation SHALL be additive — base storage adapters that do not augment SHALL continue to typecheck unchanged.
Scenario: capabilities.cdn is optional on the base type
- WHEN a caller reads
someAbstractStorage.capabilities.cdn - THEN the type is
CdnCapabilities | undefinedand readingundefineddoes not throw at runtime
Verified by: src/cdn-adapter/cdn-adapter.spec-conformance.test.ts::"capabilities.cdn slot is optional on base AbstractStorage"
Requirement: Tier purity — universal types-only
The package SHALL NOT import from node:*, bun:*, or any DOM-only API. The package SHALL be types-and-helpers-only — no I/O, no network, no filesystem. The package SHALL run unmodified under Node 18+, Bun, Deno, modern browsers, and Cloudflare Workers.
Scenario: tier-purity grep finds no Node or DOM imports
- WHEN
grep -rnE "from ['\"](node:|bun:)" packages/storage/storage-cdn/src/is run - THEN the output is empty
Verified by: src/cdn-adapter/cdn-adapter.spec-conformance.test.ts::"src tree contains no node: or bun: imports"