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 CdnAdapterANDinstanceof 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.cdnequals{ 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
expiresis 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
401AND 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
FakeClockadvances toexpires - 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
cloudFilesandcloudFilesCDNentries for LON - AND
region: "LON"is configured - THEN
storageEndpointandcdnEndpointreflect 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
StorageCdnRackspaceErrorwithcode === "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
FetchLikeresolvesPUTto201 CreatedandGETto200with the same body andContent-Type - THEN
await storage.set("k", bytes, { contentType: "image/png" })succeeds ANDawait storage.get("k")returns those bytes ANDawait storage.getMetadata("k")reportscontentType: "image/png"
Scenario: 404 from GET returns undefined
- WHEN
GETresolves to404 Not Found - THEN
await storage.get("k")returnsundefined
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
GETwithRange: bytes=1-2resolves to206with[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
getCdnMetadatareturnedX-Cdn-Ssl-Uri: https://abc.r88.cf6.rackcdn.com/ - THEN
storage.edgeUrl("a/b/file.png")ishttps://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
DELETEresolves to204 - 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
StorageCdnRackspaceErrorwhosecode === "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
tempUrlKeyAND a caller invokessignUrl(…) - THEN the call throws
StorageCdnRackspaceErrorwithcode === "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
StorageCdnRackspaceErrorAND 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
503withRetry-After: 1then the second resolves to200 - THEN the caller observes one wait of at least 1000 ms under
FakeClockAND the final result is the200body
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_CODESis 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
setsucceeds against an in-memoryMeterProvider - 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"