Files
2026-05-27 23:29:53 +01:00

14 KiB

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 <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"