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.
|
||||
75
openspec/archive/2026-05-16-add-storage-cdn/proposal.md
Normal file
75
openspec/archive/2026-05-16-add-storage-cdn/proposal.md
Normal 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.
|
||||
@@ -0,0 +1,132 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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.
|
||||
|
||||
## ADDED 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 `CdnAdapter` is constructed
|
||||
- **THEN** the instance is `instanceof AbstractStorage` AND `instanceof CdnAdapter`
|
||||
|
||||
#### Scenario: missing abstract method is a type error at compile time
|
||||
|
||||
- **WHEN** a subclass omits `purge`, `signUrl`, or `edgeUrl`
|
||||
- **THEN** `bunx turbo typecheck` fails 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 `StorageCdnError` with `code === "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 `StorageCdnError` with `code === "STORAGE_CDN.PURGE_NOT_SUPPORTED"`
|
||||
|
||||
#### Scenario: defaultSignUrlNotSupported throws the documented error
|
||||
|
||||
- **WHEN** a caller invokes `defaultSignUrlNotSupported()`
|
||||
- **THEN** the call throws `StorageCdnError` with `code === "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 `true` AND TypeScript narrows the variable to `CdnAdapter`
|
||||
|
||||
#### 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_CODES` has 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 | undefined` and reading `undefined` does 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"`
|
||||
62
openspec/archive/2026-05-16-add-storage-cdn/tasks.md
Normal file
62
openspec/archive/2026-05-16-add-storage-cdn/tasks.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Tasks — storage-cdn
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [ ] 1.1 Read `openspec/specs/storage/spec.md` and confirm capability boundary
|
||||
- [ ] 1.2 Read `openspec/AGENTS.md` §0c, §13, §14
|
||||
- [ ] 1.3 Read sibling change `openspec/changes/add-storage-http/` for the family conventions
|
||||
- [ ] 1.4 Draft `proposal.md`, `design.md`, `specs/storage-cdn/spec.md`, `tasks.md`
|
||||
- [ ] 1.5 Numbered Requirement/Scenario checklist per §0c step 6
|
||||
|
||||
## 2. Scaffolding
|
||||
|
||||
- [ ] 2.1 Create `packages/storage/storage-cdn/` mirroring `packages/storage/storage-http/` conventions (types-only — no benchmarks)
|
||||
- [ ] 2.2 `package.json` v0.0.0, `"sideEffects": false`, subpath exports
|
||||
- [ ] 2.3 Deps: `@suaveplan/error`, `@suaveplan/storage`, `@suaveplan/types`, `zod`, `tslib` (workspace/catalog)
|
||||
- [ ] 2.4 DevDeps: `@suaveplan/testing`, `@vitest/coverage-istanbul`, `typescript`, `vite`, `vite-plugin-dts`, `vitest`
|
||||
- [ ] 2.5 `tsconfig`, `vite.config.ts`, `vitest.config.ts` via `createTestConfig`
|
||||
|
||||
## 3. Implementation
|
||||
|
||||
- [ ] 3.1 `src/error-codes.ts` — `defineCodes("STORAGE_CDN", [...])` with 8 codes, `StorageCdnError` class
|
||||
- [ ] 3.2 `src/types/cdn-capabilities.types.ts` — `CdnCapabilities` interface + `DEFAULT_CDN_CAPABILITIES`
|
||||
- [ ] 3.3 `src/types/sign-url.types.ts` + `.schemas.ts` — `SignUrlOptions` + Zod schema
|
||||
- [ ] 3.4 `src/types/cdn-adapter.types.ts` — `CdnAdapterOptions` base interface
|
||||
- [ ] 3.5 `src/cdn-adapter/cdn-adapter.ts` — `CdnAdapter` abstract class extending `AbstractStorage`, three abstract methods, `defaultPurgeNotSupported()`/`defaultSignUrlNotSupported()` helpers
|
||||
- [ ] 3.6 `src/sign-url/sign-url.ts` — schema parse + error mapping
|
||||
- [ ] 3.7 `src/guards/guards.ts` — `isCdnAdapter`, `assertCdnAdapter`
|
||||
- [ ] 3.8 `src/storage-augment.ts` — `declare module "@suaveplan/storage"` slot for `cdn?: CdnCapabilities`
|
||||
- [ ] 3.9 `src/index.ts` — explicit named re-exports
|
||||
|
||||
## 4. Tests
|
||||
|
||||
- [ ] 4.1 `src/cdn-adapter/cdn-adapter.test.ts` — defaults, helpers throw documented codes
|
||||
- [ ] 4.2 `src/cdn-adapter/cdn-adapter.spec-conformance.test.ts` — type conformance, augmentation slot optional, tier-purity grep
|
||||
- [ ] 4.3 `src/sign-url/sign-url.test.ts` — schema rejects invalid ttl/method
|
||||
- [ ] 4.4 `src/guards/guards.test.ts` — narrowing + assert throws
|
||||
- [ ] 4.5 `src/error-codes/error-codes.test.ts` — every code reachable (provider codes forward-referenced)
|
||||
- [ ] 4.6 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
- [ ] 4.7 All test imports via `@suaveplan/testing/runner` (§7)
|
||||
|
||||
## 5. Documentation
|
||||
|
||||
- [ ] 5.1 Sibling `.md` for every implementation `.ts`
|
||||
- [ ] 5.2 `README.md` documents the family and links the two provider proposals
|
||||
|
||||
## 6. Gates
|
||||
|
||||
- [ ] 6.1 `bun run check`
|
||||
- [ ] 6.2 `bunx turbo lint typecheck test --filter=@suaveplan/storage-cdn`
|
||||
- [ ] 6.3 `bunx turbo test:coverage --filter=@suaveplan/storage-cdn` (100/100/100/100)
|
||||
- [ ] 6.4 `bun scripts/lint/check-test-runner-imports.ts`
|
||||
- [ ] 6.5 Tier-purity grep returns empty
|
||||
- [ ] 6.6 `bun scripts/openspec-conformance.ts @suaveplan/storage-cdn`
|
||||
|
||||
## 7. Manifest & archival
|
||||
|
||||
- [ ] 7.1 `bun scripts/folder-reorg/generate-manifest.ts`
|
||||
- [ ] 7.2 Update `STATUS.md`
|
||||
- [ ] 7.3 Fold `specs/storage-cdn/spec.md` → `openspec/specs/storage-cdn/spec.md`
|
||||
- [ ] 7.4 Move change folder → `openspec/archive/<yyyy-mm-dd>-add-storage-cdn/`
|
||||
Reference in New Issue
Block a user