# 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. ## 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 `/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 //` 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=-`. 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 //` 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 `//?temp_url_sig=&temp_url_expires=` where `` is `HMAC-SHA1(tempUrlKey, "\n\n")` 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"`