Files
suaveplan/openspec/changes/complete-storage-uri-adapters/specs/storage-cdn-rackspace/spec.md
2026-05-27 23:29:53 +01:00

9.5 KiB

Capability Spec Delta: storage-cdn-rackspace (complete-storage-uri-adapters)

Package: @suaveplan/storage-cdn-rackspace Status: PROPOSED

Every requirement below carries a **Verified by:** line at authoring time.

ADDED Requirements

Requirement: rackspace:// URI plugin factory

The package SHALL export rackspaceUriPlugin(options) from ./uri-plugin/uri-plugin returning a UriPlugin whose scheme is "rackspace" and whose adapter is the supplied RackspaceCdnAdapter. The factory SHALL validate options.container is a non-empty string; failure raises StorageCdnRackspaceUriError(STORAGE_CDN_RACKSPACE_URI.INVALID_URI) synchronously.

Scenario: factory returns a UriPlugin with scheme rackspace

  • WHEN rackspaceUriPlugin({ adapter, container: "assets" }) is called
  • THEN the returned plugin's scheme === "rackspace", adapter is the same reference, and runUriPluginConformance passes

Scenario: empty container is rejected

  • WHEN rackspaceUriPlugin({ adapter, container: "" }) is called
  • THEN the call throws with code === "STORAGE_CDN_RACKSPACE_URI.INVALID_URI"

Verified by: src/uri-plugin/uri-plugin.test.ts::"exports a UriPlugin with scheme rackspace" AND src/uri-plugin/uri-plugin.test.ts::"rejects empty container"


Requirement: Container + object-key parsing

The plugin SHALL parse rackspace://<container>/<object-key> URIs by taking the authority as the container and the path (minus leading /) as the object key. The literal authority _ (or empty) SHALL rebind to the factory's configured container. A mismatched non-_ container SHALL raise STORAGE_CDN_RACKSPACE_URI.CONTAINER_MISMATCH.

Scenario: matching container parses successfully

  • WHEN plugin.parse("rackspace://assets/img/a.jpg") is called against a plugin with container: "assets"
  • THEN the returned key === "img/a.jpg" and meta.container === "assets"

Scenario: underscore authority rebinds to configured container

  • WHEN plugin.parse("rackspace://_/img/a.jpg") is called against the same plugin
  • THEN the returned key === "img/a.jpg" and meta.container === "assets"

Scenario: mismatched container raises CONTAINER_MISMATCH

  • WHEN plugin.parse("rackspace://wrong/img/a.jpg") is called
  • THEN the call throws with code === "STORAGE_CDN_RACKSPACE_URI.CONTAINER_MISMATCH"

Verified by: src/uri-plugin/uri-plugin.test.ts::"parses matching container" AND src/uri-plugin/uri-plugin.test.ts::"underscore rebinds to configured container" AND src/uri-plugin/uri-plugin.test.ts::"raises CONTAINER_MISMATCH for wrong container"


Requirement: Auth-token threading without URI exposure

The plugin SHALL NOT carry the Rackspace auth token in URIs. Authentication lives entirely on the bound RackspaceAuthClient / RackspaceCdnAdapter. When the adapter's token has expired and refresh fails, the plugin SHALL re-wrap the failure as STORAGE_CDN_RACKSPACE_URI.AUTH_TOKEN_EXPIRED preserving the original on cause.

Scenario: token expiry on adapter becomes AUTH_TOKEN_EXPIRED

  • WHEN the bound adapter raises a token-expired error during a routed read
  • THEN the resolver's caller sees StorageCdnRackspaceUriError with code === "STORAGE_CDN_RACKSPACE_URI.AUTH_TOKEN_EXPIRED" and cause set

Scenario: no auth token appears in parsed URI

  • WHEN plugin.parse("rackspace://assets/img/a.jpg") is called
  • THEN the returned meta does NOT include X-Auth-Token or any auth key

Verified by: src/uri-plugin/uri-plugin.test.ts::"wraps token expiry as AUTH_TOKEN_EXPIRED" AND src/uri-plugin/uri-plugin.test.ts::"never exposes auth token on parsed meta"


Requirement: TempURL signed-URL verification

The plugin SHALL verify ?temp_url_sig=<sig>&temp_url_expires=<unix> query parameters when present, using the existing buildTempUrlSignature helper against the configured tempUrlKey on the bound adapter. Mismatched signatures SHALL raise STORAGE_CDN_RACKSPACE_URI.TEMP_URL_INVALID.

Scenario: valid temp-URL passes verification

  • WHEN plugin.parse("rackspace://assets/x?temp_url_sig=<valid>&temp_url_expires=99") is called against a plugin whose adapter's tempUrlKey produces the same signature
  • THEN the parse succeeds and the resolver routes the read normally

Scenario: invalid temp-URL raises TEMP_URL_INVALID

  • WHEN plugin.parse("rackspace://assets/x?temp_url_sig=bogus&temp_url_expires=99") is called
  • THEN the call throws with code === "STORAGE_CDN_RACKSPACE_URI.TEMP_URL_INVALID"

Verified by: src/uri-plugin/uri-plugin.test.ts::"accepts a valid temp-URL signature" AND src/uri-plugin/uri-plugin.test.ts::"raises TEMP_URL_INVALID on bad signature"


Requirement: build(key, meta?) with optional region echo

The plugin SHALL implement build(key, meta?) returning rackspace://<container>/<key>. When meta?.region is set, the result SHALL append ?region=<region> so downstream resolvers can re-bind regions per URI. Unsupported region values SHALL raise STORAGE_CDN_RACKSPACE_URI.REGION_UNSUPPORTED.

Scenario: build without region omits query string

  • WHEN plugin.build("img/a.jpg") is called against a plugin with container: "assets"
  • THEN the result is "rackspace://assets/img/a.jpg"

Scenario: build with valid region appends query

  • WHEN plugin.build("img/a.jpg", { region: "iad" }) is called
  • THEN the result is "rackspace://assets/img/a.jpg?region=iad"

Scenario: unsupported region raises REGION_UNSUPPORTED

  • WHEN plugin.build("x", { region: "mars" }) is called
  • THEN the call throws with code === "STORAGE_CDN_RACKSPACE_URI.REGION_UNSUPPORTED"

Verified by: src/uri-plugin/uri-plugin.test.ts::"build omits region when absent" AND src/uri-plugin/uri-plugin.test.ts::"build appends valid region" AND src/uri-plugin/uri-plugin.test.ts::"raises REGION_UNSUPPORTED for invalid region"


Requirement: Span-attribute redaction

The plugin SHALL redact temp_url_sig, temp_url_expires, and X-Auth-Token from any URI surfaced on OTel span attributes or in raised error .uri discriminants. Redaction SHALL be exposed as redactSignedUrl(uri): string.

Scenario: temp-URL parameters are redacted on span attribute

  • WHEN the resolver routes read("rackspace://assets/x?temp_url_sig=abc&temp_url_expires=99") and emits a span
  • THEN the cdn.uri attribute is "rackspace://assets/x?temp_url_sig=<redacted>&temp_url_expires=<redacted>"

Verified by: src/uri-plugin/redact-signed-url.test.ts::"redacts temp_url and auth-token parameters"


Requirement: STORAGE_CDN_RACKSPACE_URI_CODES registry

The package SHALL export STORAGE_CDN_RACKSPACE_URI_CODES produced by defineCodes("STORAGE_CDN_RACKSPACE_URI", [...] as const) covering exactly 6 codes: INVALID_URI, CONTAINER_MISMATCH, REGION_UNSUPPORTED, TEMP_URL_INVALID, AUTH_TOKEN_EXPIRED, KEY_TOO_LONG. Every code SHALL be reachable, and StorageCdnRackspaceUriError SHALL extend SuaveplanError<StorageCdnRackspaceUriErrorCode> with uri/scheme discriminants.

Scenario: every code is reachable

  • WHEN the test suite exercises every documented failure path
  • THEN every member of STORAGE_CDN_RACKSPACE_URI_CODES appears on at least one raised error instance

Verified by: src/uri-plugin/error-codes.test.ts::"exposes 6 codes all reachable"


Requirement: AbortSignal threading

The plugin SHALL forward AbortSignal unchanged to every adapter call. A pre-aborted signal SHALL short-circuit with the adapter's existing STORAGE_CDN_RACKSPACE.ABORTED code without invoking the adapter.

Scenario: pre-aborted signal short-circuits

  • WHEN the resolver routes read("rackspace://assets/x", AbortSignal.abort())
  • THEN the rejection has code === "STORAGE_CDN_RACKSPACE.ABORTED" and the adapter is NEVER invoked

Verified by: src/uri-plugin/uri-plugin.test.ts::"short-circuits on pre-aborted signal"


Requirement: Universal-tier purity

Every source file under src/uri-plugin/ SHALL be universal-tier: ZERO imports from node:*, bun:*, browser-specific globals, or React.

Scenario: tier-purity grep returns empty

  • WHEN grep -rnE "from ['\"](node:|bun:)" packages/storage/storage-cdn-rackspace/src/uri-plugin/ is run
  • THEN the command returns zero matches

Verified by: src/uri-plugin/uri-plugin.test.ts::"uri-plugin module has no node: or bun: imports"


Requirement: Subpath exports

The package SHALL declare subpath exports for "./uri-plugin/uri-plugin", "./uri-plugin/redact-signed-url", and "./uri-plugin/error-codes".

Scenario: subpath imports resolve

  • WHEN import("@suaveplan/storage-cdn-rackspace/uri-plugin/uri-plugin") is executed
  • THEN the module resolves and re-exports rackspaceUriPlugin

Verified by: src/uri-plugin/uri-plugin.test.ts::"is importable via the documented subpath"


Requirement: Conformance harness pass

rackspaceUriPlugin SHALL pass runUriPluginConformance with fixtures covering container-match cases, underscore-rebind cases, container-mismatch failures, temp-URL forms, and a non-empty buildRoundTrip set.

Scenario: conformance fixtures pass

  • WHEN runUriPluginConformance(rackspaceUriPlugin(opts), fixtures) is invoked with the documented fixture set
  • THEN the call returns without throwing

Verified by: src/uri-plugin/uri-plugin.test.ts::"passes runUriPluginConformance"