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,73 @@
# Add @suaveplan/storage-cdn-rackspace — Rackspace Cloud Files + Rackspace CDN adapter
**Package**: `@suaveplan/storage-cdn-rackspace`
**Status**: PROPOSED
**Type**: new-package (genesis-original)
- **Design**: ./design.md
- **Spec delta**: ./specs/storage-cdn-rackspace/spec.md
## Why
The user operates production traffic from Rackspace CDN (LON region) with Cloud Files as origin. `storage-http` covers generic prefix-based reads but knows nothing about Cloud Files auth, container CDN-enablement, purge, or TempURL signing. This package provides a first-class `CdnAdapter` so application code can write `storage.set(key, bytes)` / `storage.get(key)` / `storage.purge(key)` / `storage.signUrl(key, { ttlSeconds })` against Rackspace without leaking provider knowledge into business logic.
Rackspace Cloud Files is OpenStack Swift; the API is well-documented and stable. The CDN side adds container CDN enablement, `X-Cdn-Uri` discovery, and a per-account TempURL HMAC key. This package implements both layers via injected `FetchLike` so it can be tested without live API access.
## What changes
- ADDED: `RackspaceCdnAdapter` class extending `CdnAdapter` from `@suaveplan/storage-cdn`.
- ADDED: `createRackspaceCdnAdapter(options)` factory.
- ADDED: `RackspaceCdnAdapterOptions` interface + Zod schema. Required: `authUrl` (default `https://lon.identity.api.rackspacecloud.com/v2.0`), `username`, `apiKey`, `region` (default `LON`), `container`. Optional: `tempUrlKey`, `cdnEnabled` (default `true`), `fetch`, `tokenCache`, `headers`, `retry`.
- ADDED: `RackspaceAuthClient` — handles v2 identity auth (`POST /v2.0/tokens`), token cache with expiry, automatic re-auth on `401`. Exposes `getStorageEndpoint()` and `getCdnEndpoint()` resolved from the service catalog for the configured region.
- ADDED: `RackspaceCdnClient` — `enableCdn(container)`, `getCdnMetadata(container)` (parses `X-Cdn-Uri`, `X-Cdn-Ssl-Uri`, `X-Ttl`, `X-Cdn-Enabled`), `purgeObject(container, key)` (`DELETE` on `/v1/services/cdn/<container>/<key>` with `X-Purge-Email` honoured if provided).
- ADDED: `RackspaceTempUrl` — HMAC-SHA1 TempURL signer per the Swift TempURL middleware spec: signed string `<METHOD>\n<EXPIRES>\n<PATH>` with the account `temp-url-key` (or container-scoped key). Optional `temp-url-prefix` mode.
- ADDED: `STORAGE_CDN_RACKSPACE_CODES` — 9 codes: `AUTH_FAILED`, `TOKEN_EXPIRED`, `CONTAINER_NOT_FOUND`, `CDN_NOT_ENABLED`, `TEMP_URL_KEY_MISSING`, `PURGE_RATE_LIMITED`, `REGION_NOT_IN_CATALOG`, `INVALID_OPTIONS`, `UPSTREAM_ERROR`. `StorageCdnRackspaceError` class with `container`/`region`/`httpStatus` discriminants.
- ADDED: Capabilities — `partialReads: true` (Swift supports `Range`), `nativeStreams: true`, `partialWrites: false`, `atomicWrites: false`, `contentAddressable: false`; `cdn: { cdnPurge: true, cdnPrefixPurge: false, cdnSignedUrls: true, cdnOriginUploads: true }`.
- ADDED: Module-augmentation registry — `declare module "@suaveplan/storage"` adds `rackspace-cdn: RackspaceCdnAdapter` slot; `registerRackspaceCdnProvider()` factory hook (idempotent).
- ADDED: `@suaveplan/retry` integration on transient (`429`, `5xx`, network), with `Retry-After` honoured.
- ADDED: OTel surface — same shape as `storage-http` (`storage_cdn_rackspace.op_duration_ms`, `…io_bytes`, `…reads_total`, `…writes_total`, `…purges_total`, `…errors_total`), with extra `provider="rackspace"` attribute.
## Out of scope
- **Non-LON regions.** The package supports the full region catalog (LON, IAD, DFW, ORD, SYD, HKG) — `region` is just a config field — but the test fixture is LON-only because that is the user's deployment. Other regions verified by unit tests against a mocked service catalog response.
- **v1 auth.** Rackspace v1 auth (`X-Auth-User`/`X-Auth-Key` against `auth.api.rackspacecloud.com`) is deprecated; this package targets v2 identity exclusively.
- **CDN log delivery.** Cloud Files CDN log retrieval API is operations tooling; out of scope.
- **Bulk operations.** Swift `bulk-delete` and `bulk-upload` are not exposed via `AbstractStorage`. Callers loop.
- **Static Web hosting headers** (`X-Container-Meta-Web-Index`, `…-Error`). Belong in deployment tooling, not the adapter.
## Source mapping
Genesis-original. References:
- OpenStack Swift API v1: <https://docs.openstack.org/api-ref/object-store/>
- Rackspace Cloud Files developer guide (TempURL, CDN enablement)
- Rackspace identity v2: <https://docs.rackspace.com/docs/cloud-identity-v2>
### Requirements with no source line
| Requirement | Justification |
|---|---|
| Token cache with proactive refresh | Auth tokens expire (24h default). Without a cache, every request re-auths. Proactive refresh (5 min before expiry) avoids the cliff where in-flight requests start 401-ing. |
| Service-catalog lookup vs. hard-coded endpoints | Rackspace returns the canonical region endpoint in the auth response. Hard-coding LON URLs would break for IAD/DFW/etc. and silently misroute if Rackspace renumbers. |
| TempURL key stored in adapter options, not fetched per-call | Reading the container or account meta on every `signUrl` would be wasteful. Caller provides the key (rotated via Rackspace control panel) at construction. |
| `cdnPrefixPurge: false` | Rackspace CDN purge API is per-object. Prefix purge requires listing + per-key calls (rate-limited at ~25/min). Honest declaration. |
## Verification approach
- `src/rackspace-cdn-adapter/rackspace-cdn-adapter.test.ts` — CRUD via injected `FetchLike`, range reads, signed-URL round-trip, purge happy path, READ_ONLY on `list` if container too large.
- `src/rackspace-cdn-adapter/rackspace-cdn-adapter.spec-conformance.test.ts` — `instanceof CdnAdapter` + `AbstractStorage`, capabilities matrix, every `STORAGE_CDN_RACKSPACE` code reachable.
- `src/auth-client/auth-client.test.ts` — v2 auth round-trip, token cache hit/miss, proactive refresh under `FakeClock`, service-catalog lookup for LON / IAD.
- `src/cdn-client/cdn-client.test.ts` — `enableCdn`, `getCdnMetadata` parsing, `purgeObject` happy path, 429 retry with `Retry-After`.
- `src/temp-url/temp-url.test.ts` — HMAC-SHA1 signature byte-equal to Swift reference vectors, `temp-url-prefix` mode, expired-signature rejected.
- `src/registry/registry.test.ts` — `createStorage({ type: "rackspace-cdn", config })` returns `RackspaceCdnAdapter`, idempotent re-registration.
## Impact
### Affected dependents
- No dependents yet (`bunx turbo ls --filter=...@suaveplan/storage-cdn-rackspace`).
### Affected upstreams
- `@suaveplan/storage-cdn` — `StorageProviderRegistry` module-augmented with `rackspace-cdn` slot. Additive. [x] no action needed.
- Hard dependency on `@suaveplan/storage-cdn` (must be archived first).

View File

@@ -0,0 +1,241 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: storage-cdn-rackspace (storage-cdn-rackspace)
**Package**: `@suaveplan/storage-cdn-rackspace`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
## ADDED Requirements
### Requirement: RackspaceCdnAdapter extends CdnAdapter
The package SHALL provide a `RackspaceCdnAdapter` class extending `CdnAdapter` from `@suaveplan/storage-cdn`. The class SHALL implement every abstract method on `AbstractStorage` AND the three `CdnAdapter` methods (`purge`, `signUrl`, `edgeUrl`).
#### Scenario: instance is CdnAdapter and AbstractStorage
- **WHEN** a caller constructs `new RackspaceCdnAdapter({ … })`
- **THEN** the instance is `instanceof CdnAdapter` AND `instanceof AbstractStorage`
#### Scenario: capabilities declare CDN purge, signed URLs, native streams, partial reads
- **WHEN** a caller reads `storage.capabilities`
- **THEN** `partialReads === true`, `nativeStreams === true`, `partialWrites === false`, `atomicWrites === false`, `contentAddressable === false`
- **AND** `storage.capabilities.cdn` equals `{ cdnPurge: true, cdnPrefixPurge: false, cdnSignedUrls: true, cdnOriginUploads: true }`
**Verified by:** `src/rackspace-cdn-adapter/rackspace-cdn-adapter.spec-conformance.test.ts::"instance is CdnAdapter and AbstractStorage"` AND `src/rackspace-cdn-adapter/rackspace-cdn-adapter.spec-conformance.test.ts::"declares the documented capabilities matrix"`
---
### Requirement: v2 identity auth with proactive token refresh
The adapter SHALL authenticate against Rackspace identity v2 by POSTing `{ auth: { "RAX-KSKEY:apiKeyCredentials": { username, apiKey } } }` to `<authUrl>/tokens`. The response token AND service-catalog SHALL be cached in `tokenCache`. The cache entry SHALL be refreshed proactively at least 5 minutes before the documented `expires` timestamp. On `401` from any subsequent call, the adapter SHALL invalidate the cache, re-auth once, AND retry the original call exactly once.
#### Scenario: cached token is reused within validity window
- **WHEN** the auth client has a cached token whose `expires` is more than 5 minutes ahead AND the caller invokes a storage op
- **THEN** no auth request is issued AND the cached token is sent
#### Scenario: 401 triggers single re-auth and retry
- **WHEN** an op returns `401` AND a fresh auth round-trip succeeds
- **THEN** the op is retried exactly once with the new token AND the second outcome is the operation's final result
#### Scenario: proactive refresh fires before cliff
- **WHEN** the injected `FakeClock` advances to `expires - 4 minutes`
- **THEN** a refresh is in flight before any user op blocks on it
**Verified by:** `src/auth-client/auth-client.test.ts::"reuses cached token within validity window"` AND `src/auth-client/auth-client.test.ts::"re-auths exactly once on 401 and retries"` AND `src/auth-client/auth-client.test.ts::"refreshes proactively five minutes before expiry"`
---
### Requirement: Service-catalog lookup resolves region endpoints
The adapter SHALL resolve `storageEndpoint` and `cdnEndpoint` from the auth-response service catalog filtered by `region`. When the configured region is absent from the catalog, the adapter SHALL throw `StorageCdnRackspaceError` with code `STORAGE_CDN_RACKSPACE.REGION_NOT_IN_CATALOG`.
#### Scenario: LON region resolves to LON endpoints
- **WHEN** the auth response contains `cloudFiles` and `cloudFilesCDN` entries for LON
- **AND** `region: "LON"` is configured
- **THEN** `storageEndpoint` and `cdnEndpoint` reflect the LON URLs from the catalog
#### Scenario: missing region throws REGION_NOT_IN_CATALOG
- **WHEN** the auth response does NOT contain an entry for the configured region
- **THEN** the next op throws `StorageCdnRackspaceError` with `code === "STORAGE_CDN_RACKSPACE.REGION_NOT_IN_CATALOG"`
**Verified by:** `src/auth-client/auth-client.test.ts::"resolves LON region from service catalog"` AND `src/auth-client/auth-client.test.ts::"throws REGION_NOT_IN_CATALOG when region absent"`
---
### Requirement: CRUD against Cloud Files origin
`set(key, bytes)` SHALL `PUT <storageEndpoint>/<container>/<encodedKey>` with the auth token, `Content-Length`, and `Content-Type` (from `options.contentType` or `application/octet-stream` default). `get(key)` SHALL `GET …`. `delete(key)` SHALL `DELETE …`. `exists(key)` SHALL `HEAD …`. `getMetadata` SHALL parse `X-Object-Meta-*`, `Content-Length`, `Content-Type`, `Last-Modified`, `ETag`.
#### Scenario: round-trip PUT/GET preserves bytes and content-type
- **WHEN** the injected `FetchLike` resolves `PUT` to `201 Created` and `GET` to `200` with the same body and `Content-Type`
- **THEN** `await storage.set("k", bytes, { contentType: "image/png" })` succeeds AND `await storage.get("k")` returns those bytes AND `await storage.getMetadata("k")` reports `contentType: "image/png"`
#### Scenario: 404 from GET returns undefined
- **WHEN** `GET` resolves to `404 Not Found`
- **THEN** `await storage.get("k")` returns `undefined`
**Verified by:** `src/rackspace-cdn-adapter/rackspace-cdn-adapter.test.ts::"PUT then GET round-trips bytes and contentType"` AND `src/rackspace-cdn-adapter/rackspace-cdn-adapter.test.ts::"GET 404 returns undefined"`
---
### Requirement: Range reads via getPartial
`getPartial(key, offset, length, signal?)` SHALL issue `GET` with header `Range: bytes=<offset>-<offset+length-1>`. On `206`, the body SHALL be returned verbatim. On `200`, the body SHALL be sliced client-side AND a counter `storage_cdn_rackspace.range_fallback_total` SHALL increment. `capabilities.partialReads` SHALL be `true`.
#### Scenario: Swift 206 returns partial body verbatim
- **WHEN** `GET` with `Range: bytes=1-2` resolves to `206` with `[2,3]`
- **THEN** `getPartial("k", 1, 2)` returns `[2,3]`
**Verified by:** `src/rackspace-cdn-adapter/rackspace-cdn-adapter.test.ts::"Range request returns 206 body verbatim"`
---
### Requirement: edgeUrl composes from CDN container URL
`edgeUrl(key)` SHALL return `new URL(encodedKey, cdnContainerBaseUrl).toString()` where `cdnContainerBaseUrl` is the SSL CDN URL fetched from `getCdnMetadata(container)` (cached at construction). The method SHALL be synchronous AND SHALL never throw.
#### Scenario: edgeUrl returns the SSL CDN URL for the key
- **WHEN** `getCdnMetadata` returned `X-Cdn-Ssl-Uri: https://abc.r88.cf6.rackcdn.com/`
- **THEN** `storage.edgeUrl("a/b/file.png")` is `https://abc.r88.cf6.rackcdn.com/a/b/file.png`
**Verified by:** `src/rackspace-cdn-adapter/rackspace-cdn-adapter.test.ts::"edgeUrl returns SSL CDN URL composed with encoded key"`
---
### Requirement: Purge via Rackspace CDN DELETE
`purge(key, signal?)` SHALL issue `DELETE <cdnEndpoint>/<container>/<encodedKey>` with the auth token. On `2xx` the call SHALL resolve. On `429` the call SHALL throw `StorageCdnRackspaceError` with code `STORAGE_CDN_RACKSPACE.PURGE_RATE_LIMITED` after retry exhaustion. On `404` the call SHALL resolve without error (already purged or never CDN-served).
#### Scenario: 2xx purge resolves
- **WHEN** `DELETE` resolves to `204`
- **THEN** `await storage.purge("k")` resolves
#### Scenario: 429 surfaces PURGE_RATE_LIMITED after retry exhaustion
- **WHEN** every retry attempt resolves to `429`
- **THEN** the call rejects with `StorageCdnRackspaceError` whose `code === "STORAGE_CDN_RACKSPACE.PURGE_RATE_LIMITED"`
**Verified by:** `src/cdn-client/cdn-client.test.ts::"purge resolves on 204"` AND `src/cdn-client/cdn-client.test.ts::"purge surfaces PURGE_RATE_LIMITED on persistent 429"`
---
### Requirement: signUrl produces Swift TempURL HMAC-SHA1 signature
`signUrl(key, { ttlSeconds, method })` SHALL produce a URL of the form `<storageEndpoint>/<container>/<encodedKey>?temp_url_sig=<hex>&temp_url_expires=<unix>` where `<hex>` is `HMAC-SHA1(tempUrlKey, "<METHOD>\n<EXPIRES>\n<PATH>")` per the Swift TempURL middleware spec. The method SHALL throw `STORAGE_CDN_RACKSPACE.TEMP_URL_KEY_MISSING` when no `tempUrlKey` was configured. `ttlSeconds <= 0` SHALL throw `STORAGE_CDN.INVALID_TTL`.
#### Scenario: signed URL matches Swift reference vector
- **WHEN** `tempUrlKey: "b3968d0207b54ece87cccc06515a89d4"`, `method: "GET"`, `path: "/v1/AUTH_account/container/object"`, `expires: 1323479485`
- **THEN** the signature equals the reference vector documented by Swift TempURL middleware (`da39a3ee…` — actual value asserted in the test)
#### Scenario: missing tempUrlKey throws TEMP_URL_KEY_MISSING
- **WHEN** the adapter was constructed without `tempUrlKey` AND a caller invokes `signUrl(…)`
- **THEN** the call throws `StorageCdnRackspaceError` with `code === "STORAGE_CDN_RACKSPACE.TEMP_URL_KEY_MISSING"`
**Verified by:** `src/temp-url/temp-url.test.ts::"signature matches Swift TempURL reference vector"` AND `src/temp-url/temp-url.test.ts::"throws TEMP_URL_KEY_MISSING when no key configured"`
---
### Requirement: Strict key sanitisation at the boundary
Every method that accepts a key SHALL pass it through `sanitizeKey` (shared with `storage-http` semantics — empty, `..`, leading `/`, null bytes, control chars, >2048 → `STORAGE_CDN_RACKSPACE.INVALID_OPTIONS`). Per-segment `encodeURIComponent` SHALL be applied for URL composition, preserving `/`.
#### Scenario: path traversal is rejected before fetch
- **WHEN** a caller calls `storage.get("../../etc/passwd")`
- **THEN** the call rejects with `StorageCdnRackspaceError` AND no fetch is issued
**Verified by:** `src/rackspace-cdn-adapter/rackspace-cdn-adapter.test.ts::"rejects path-traversal keys before fetch"`
---
### Requirement: Retries on transient status and Retry-After honour
The adapter SHALL classify status `408`, `425`, `429`, and `5xx` as transient. Network rejection SHALL be classified as transient. Retries SHALL go through `@suaveplan/retry.retry` with injectable `TimersLike`. `Retry-After` SHALL be honoured when present. Exhausted retries SHALL surface as `STORAGE_CDN_RACKSPACE.UPSTREAM_ERROR` (HTTP) or `STORAGE_CDN_RACKSPACE.PURGE_RATE_LIMITED` (purge-only 429).
#### Scenario: 503 with Retry-After is retried
- **WHEN** the first call resolves to `503` with `Retry-After: 1` then the second resolves to `200`
- **THEN** the caller observes one wait of at least 1000 ms under `FakeClock` AND the final result is the `200` body
**Verified by:** `src/rackspace-cdn-adapter/rackspace-cdn-adapter.test.ts::"retries 503 honouring Retry-After"`
---
### Requirement: AbortSignal threaded into every fetch
Every public async method SHALL accept `signal?: AbortSignal` and thread it into the underlying fetch. Pre-aborted signal SHALL short-circuit with `STORAGE_CDN_RACKSPACE.UPSTREAM_ERROR` whose cause is `AbortError`. Mid-flight abort SHALL propagate to the in-flight fetch.
#### Scenario: pre-aborted signal short-circuits
- **WHEN** a caller passes an already-aborted signal to any method
- **THEN** the call rejects before any fetch is issued
**Verified by:** `src/rackspace-cdn-adapter/rackspace-cdn-adapter.test.ts::"pre-aborted signal short-circuits all methods"`
---
### Requirement: STORAGE_CDN_RACKSPACE error registry
The package SHALL declare `STORAGE_CDN_RACKSPACE_CODES` via `defineCodes("STORAGE_CDN_RACKSPACE", [...])` with exactly these 9 codes: `AUTH_FAILED`, `TOKEN_EXPIRED`, `CONTAINER_NOT_FOUND`, `CDN_NOT_ENABLED`, `TEMP_URL_KEY_MISSING`, `PURGE_RATE_LIMITED`, `REGION_NOT_IN_CATALOG`, `INVALID_OPTIONS`, `UPSTREAM_ERROR`. `StorageCdnRackspaceError` SHALL include `container`, `region`, and `httpStatus` discriminants. Every code SHALL be reachable from a real call site exercised by tests.
#### Scenario: every code is reachable
- **WHEN** the test suite runs
- **THEN** every code in `STORAGE_CDN_RACKSPACE_CODES` is produced by at least one test
**Verified by:** `src/rackspace-cdn-adapter/rackspace-cdn-adapter.spec-conformance.test.ts::"every STORAGE_CDN_RACKSPACE code is reachable"`
---
### Requirement: Module-augmentation registry slot
The package SHALL declare `declare module "@suaveplan/storage"` augmenting the registry with `rackspace-cdn: RackspaceCdnAdapter` and the config map with the adapter options. The package SHALL export `registerRackspaceCdnProvider(): void` that registers the constructor idempotently.
#### Scenario: createStorage with type "rackspace-cdn" yields RackspaceCdnAdapter
- **WHEN** `registerRackspaceCdnProvider()` has been called
- **AND** the caller invokes `createStorage({ type: "rackspace-cdn", config: { … } })`
- **THEN** the returned instance is `instanceof RackspaceCdnAdapter`
**Verified by:** `src/registry/registry.test.ts::"createStorage with type rackspace-cdn returns RackspaceCdnAdapter"`
---
### Requirement: OpenTelemetry instrumentation with provider attribute
The package SHALL emit `storage_cdn_rackspace.op_duration_ms` and `…io_bytes` histograms; `…reads_total`, `…writes_total`, `…deletes_total`, `…purges_total`, `…signed_urls_total`, `…errors_total`, `…range_fallback_total` counters. Every measurement SHALL carry attribute `provider="rackspace"` AND `region` from the configuration. Spans SHALL be opened on the `@suaveplan/storage-cdn-rackspace` tracer with `http.url`, `http.method`, `http.status_code`, `storage.key`, `storage.container` attributes.
#### Scenario: successful PUT emits writes_total and duration
- **WHEN** a `set` succeeds against an in-memory `MeterProvider`
- **THEN** `storage_cdn_rackspace.writes_total{outcome="ok",provider="rackspace",region="LON"}` increments by 1
**Verified by:** `src/rackspace-cdn-adapter/rackspace-cdn-adapter.test.ts::"PUT emits writes_total with provider and region attrs"`
---
### Requirement: Tier purity — universal, fetch-only
The package SHALL NOT import from `node:*`, `bun:*`, or any DOM-only API. HMAC computation SHALL use `globalThis.crypto.subtle` (WebCrypto), available in Node 18+, Bun, Deno, browsers, and Workers.
#### Scenario: tier-purity grep finds no Node or DOM imports
- **WHEN** `grep -rnE "from ['\"](node:|bun:)" packages/storage/storage-cdn-rackspace/src/` is run
- **THEN** the output is empty
**Verified by:** `src/rackspace-cdn-adapter/rackspace-cdn-adapter.spec-conformance.test.ts::"src tree contains no node: or bun: imports"`

View File

@@ -0,0 +1,69 @@
# Tasks — storage-cdn-rackspace
> Every checkbox MUST reflect work actually done in this change.
> Hard dependency: `@suaveplan/storage-cdn` (change `add-storage-cdn`) MUST be archived first.
## 1. Spec authoring
- [ ] 1.1 Read `openspec/changes/add-storage-cdn/specs/storage-cdn/spec.md` for the contract
- [ ] 1.2 Read `openspec/changes/add-storage-http/` for retry/AbortSignal/OTel patterns
- [ ] 1.3 Read OpenStack Swift API reference + Rackspace identity v2 docs + Swift TempURL middleware spec
- [ ] 1.4 Draft `proposal.md`, `design.md`, `specs/storage-cdn-rackspace/spec.md`, `tasks.md`
- [ ] 1.5 Numbered Requirement/Scenario checklist per §0c step 6
## 2. Scaffolding
- [ ] 2.1 Create `packages/storage/storage-cdn-rackspace/`
- [ ] 2.2 `package.json` v0.0.0, `"sideEffects": false`, subpath exports
- [ ] 2.3 Deps: `@suaveplan/error`, `@suaveplan/storage`, `@suaveplan/storage-cdn`, `@suaveplan/retry`, `@suaveplan/types`, `@opentelemetry/api`, `zod`, `tslib`
- [ ] 2.4 DevDeps: `@suaveplan/testing`, `@vitest/coverage-istanbul`, `fast-check`, `mitata`, `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_RACKSPACE", [...])` with 9 codes, `StorageCdnRackspaceError`
- [ ] 3.2 `src/types/*.ts` + `.schemas.ts` — `RackspaceCdnAdapterOptions` + Zod schema
- [ ] 3.3 `src/auth-client/auth-client.ts` — v2 identity auth, token cache, service-catalog lookup, proactive refresh
- [ ] 3.4 `src/cdn-client/cdn-client.ts` — `enableCdn`, `getCdnMetadata`, `purgeObject` with retry
- [ ] 3.5 `src/temp-url/temp-url.ts` — HMAC-SHA1 TempURL signer via WebCrypto
- [ ] 3.6 `src/key-sanitizer/key-sanitizer.ts` — strict boundary + per-segment encoder (or import from storage-http if shared)
- [ ] 3.7 `src/retry-helper/retry-helper.ts` — transient classifier + Retry-After honour
- [ ] 3.8 `src/otel/otel.ts` — meter/tracer with `provider="rackspace"` + `region` attrs
- [ ] 3.9 `src/rackspace-cdn-adapter/rackspace-cdn-adapter.ts` — `RackspaceCdnAdapter extends CdnAdapter`
- [ ] 3.10 `src/registry/registry.ts` — `registerRackspaceCdnProvider()` idempotent
- [ ] 3.11 `src/index.ts` — explicit named re-exports
## 4. Tests
- [ ] 4.1 `src/auth-client/auth-client.test.ts` — auth round-trip, cache reuse, 401 re-auth, proactive refresh, region resolution, missing region
- [ ] 4.2 `src/cdn-client/cdn-client.test.ts` — enable, metadata parse, purge 204, purge 429 → PURGE_RATE_LIMITED
- [ ] 4.3 `src/temp-url/temp-url.test.ts` — reference vector byte-equality, `temp-url-prefix` mode, missing key error
- [ ] 4.4 `src/key-sanitizer/key-sanitizer.test.ts` — traversal, absolute, overlong, per-segment encoding
- [ ] 4.5 `src/retry-helper/retry-helper.test.ts` — transient classifier, `Retry-After` honour, non-retry on 401
- [ ] 4.6 `src/otel/otel.test.ts` — measurement attrs include `provider` and `region`
- [ ] 4.7 `src/rackspace-cdn-adapter/rackspace-cdn-adapter.test.ts` — full CRUD, range, edgeUrl, purge, signUrl, AbortSignal, retry
- [ ] 4.8 `src/rackspace-cdn-adapter/rackspace-cdn-adapter.spec-conformance.test.ts` — type conformance, capabilities, every code reachable, tier-purity grep
- [ ] 4.9 `src/registry/registry.test.ts` — factory dispatch, idempotency
- [ ] 4.10 Every `it(...)` carries `{ timeout: 5000 }` (or 30000 for genuinely async)
- [ ] 4.11 All test imports via `@suaveplan/testing/runner` (§7)
## 5. Documentation
- [ ] 5.1 Sibling `.md` for every implementation `.ts`
- [ ] 5.2 `README.md` documents LON region default, TempURL key rotation guidance, purge rate-limit guidance
## 6. Gates
- [ ] 6.1 `bun run check`
- [ ] 6.2 `bunx turbo lint typecheck test --filter=@suaveplan/storage-cdn-rackspace`
- [ ] 6.3 `bunx turbo test:coverage --filter=@suaveplan/storage-cdn-rackspace` (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-rackspace`
## 7. Manifest & archival
- [ ] 7.1 `bun scripts/folder-reorg/generate-manifest.ts`
- [ ] 7.2 Update `STATUS.md`
- [ ] 7.3 Fold `specs/storage-cdn-rackspace/spec.md` → `openspec/specs/storage-cdn-rackspace/spec.md`
- [ ] 7.4 Move change folder → `openspec/archive/<yyyy-mm-dd>-add-storage-cdn-rackspace/`