Initial Commit
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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"`
|
||||
Reference in New Issue
Block a user