9.1 KiB
Capability Spec Delta: storage-http (complete-storage-uri-adapters)
Package: @suaveplan/storage-http
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time.
ADDED Requirements
Requirement: http:// and https:// URI plugin factories
The package SHALL export httpUriPlugin(options) and
httpsUriPlugin(options) from ./uri-plugin/uri-plugin. Each factory
returns a UriPlugin whose scheme is the corresponding lowercase
literal ("http" or "https") and whose adapter is the supplied
HttpStorage instance. The two factories SHALL share their parsing
logic; only the scheme field SHALL differ.
Scenario: https factory returns scheme "https"
- WHEN
httpsUriPlugin({ adapter })is called - THEN the returned plugin's
scheme === "https"andrunUriPluginConformancepasses
Scenario: http factory returns scheme "http"
- WHEN
httpUriPlugin({ adapter })is called - THEN the returned plugin's
scheme === "http"and the same parser is shared with the https factory (verified by structural equivalence of parse results for matching authority/path)
Verified by: src/uri-plugin/uri-plugin.test.ts::"https factory exposes scheme https" AND src/uri-plugin/uri-plugin.test.ts::"http factory exposes scheme http"
Requirement: Query-string preservation on key
The plugin SHALL include the URI's query string on the parsed key so
presigned-URL signatures (e.g. ?X-Amz-Signature=...) survive the
round-trip to HttpStorage.get. Fragments (#...) SHALL be dropped.
Scenario: presigned query survives parse
- WHEN
plugin.parse("https://host/p?X-Amz-Signature=abc")is called - THEN the returned
keyends with"?X-Amz-Signature=abc"
Scenario: fragment is stripped
- WHEN
plugin.parse("https://host/p#section")is called - THEN the returned
keydoes NOT include#section
Verified by: src/uri-plugin/uri-plugin.test.ts::"preserves presigned query on key" AND src/uri-plugin/uri-plugin.test.ts::"strips fragment from key"
Requirement: Range request support via query channel
The plugin SHALL parse a ?range=bytes=<offset>-<end> query channel
(or an equivalent offset/length pair) into an { offset, length }
bag and SHALL route the resolver's read call to adapter.getPartial
when present. Malformed range values SHALL raise
STORAGE_HTTP_URI.RANGE_INVALID.
Scenario: well-formed range routes to getPartial
- WHEN the resolver routes
read("https://host/p?range=bytes=0-1023")and the plugin's adapter exposesgetPartial - THEN
adapter.getPartialis invoked with{ offset: 0, length: 1024 }and the bytes returned propagate unchanged
Scenario: malformed range raises RANGE_INVALID
- WHEN
plugin.parse("https://host/p?range=bytes=abc-")is called - THEN the call throws
StorageHttpUriErrorwithcode === "STORAGE_HTTP_URI.RANGE_INVALID"
Verified by: src/uri-plugin/uri-plugin.test.ts::"routes well-formed range to getPartial" AND src/uri-plugin/uri-plugin.test.ts::"raises RANGE_INVALID on malformed range"
Requirement: Header injection via FetchLike
The plugin SHALL surface header injection through the bound
HttpStorage.options.headers resolver. The plugin SHALL NOT introduce
its own per-request header surface; consumers wanting per-URI headers
configure HttpStorage with a HttpHeadersResolver function that
inspects the key.
Scenario: configured header resolver receives the URI key
- WHEN
read("https://host/p")is routed andHttpStoragewas configured withheaders: (key, op) => ({ "x-test": key }) - THEN the outgoing fetch sees the header
"x-test": "<computed key>"
Verified by: src/uri-plugin/uri-plugin.test.ts::"forwards URI key to header resolver"
Requirement: Redirect policy passthrough
The plugin SHALL delegate redirect handling to the bound FetchLike.
Redirect loops raised by the adapter SHALL be re-wrapped as
StorageHttpUriError(STORAGE_HTTP_URI.REDIRECT_LOOP) preserving the
original on cause.
Scenario: adapter redirect loop becomes REDIRECT_LOOP
- WHEN the bound adapter raises a redirect-loop error
- THEN the resolver's caller sees
StorageHttpUriErrorwithcode === "STORAGE_HTTP_URI.REDIRECT_LOOP"andcauseset
Verified by: src/uri-plugin/uri-plugin.test.ts::"re-wraps redirect loop as REDIRECT_LOOP"
Requirement: build(key, meta?) inverse
The plugin SHALL implement build(key, meta?) returning a URI whose
scheme matches the factory (http/https), host derived from
adapter.options.baseUrl, and path/query taken from key. The result
SHALL round-trip through parse for every key produced by list.
Scenario: build round-trips a list-produced key
- WHEN
plugin.build("a/b?ver=1")is called against a plugin whose adapterbaseUrl === "https://host" - THEN the result is
"https://host/a/b?ver=1"andparses back
Verified by: src/uri-plugin/uri-plugin.test.ts::"build round-trips list-produced keys"
Requirement: Signed-URL redaction
The plugin SHALL redact a documented set of sensitive query parameters
from any URI surfaced on OTel span attributes or in the uri
discriminant of raised errors. The deny-list SHALL be exposed as
redactSignedUrl(uri): string for reuse and SHALL match
case-insensitively against the keys X-Amz-Signature,
X-Amz-Security-Token, token, signature, sig, key, auth,
password.
Scenario: presigned signature is redacted on the span attribute
- WHEN
read("https://host/p?X-Amz-Signature=abc")is routed and the OTel span carries astorage.uriattribute - THEN the attribute value is
"https://host/p?X-Amz-Signature=<redacted>"
Scenario: redactSignedUrl is exported and idempotent
- WHEN
redactSignedUrl(redactSignedUrl(uri))is computed for any input - THEN the result equals the single-application result
Verified by: src/uri-plugin/redact-signed-url.test.ts::"redacts the documented deny-list" AND src/uri-plugin/redact-signed-url.test.ts::"is idempotent"
Requirement: STORAGE_HTTP_URI_CODES registry
The package SHALL export STORAGE_HTTP_URI_CODES produced by
defineCodes("STORAGE_HTTP_URI", [...] as const) covering exactly 6
codes: INVALID_URI, UNSUPPORTED_SCHEME, RANGE_INVALID,
REDIRECT_LOOP, HOST_MISMATCH, KEY_TOO_LONG. Every code SHALL be
reachable from a real call site exercised by the test suite, and the
structured error class StorageHttpUriError SHALL extend
SuaveplanError<StorageHttpUriErrorCode> carrying optional
uri/scheme discriminants.
Scenario: every code is reachable
- WHEN the test suite exercises every documented failure path
- THEN every member of
STORAGE_HTTP_URI_CODESappears on at least one raisedStorageHttpUriErrorinstance
Verified by: src/uri-plugin/error-codes.test.ts::"exposes 6 codes all reachable"
Requirement: AbortSignal threading
The plugin SHALL forward an AbortSignal unchanged to every adapter
call. A pre-aborted signal SHALL short-circuit with
STORAGE_HTTP.ABORTED without invoking the adapter.
Scenario: pre-aborted signal short-circuits
- WHEN the resolver routes
read("https://x/y", AbortSignal.abort()) - THEN the rejection has
code === "STORAGE_HTTP.ABORTED"and the adapter'sgetis 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
(window, document), or React. The plugin runs identically in
workers, edge runtimes, and Node.
Scenario: tier-purity grep returns empty
- WHEN
grep -rnE "from ['\"](node:|bun:)" packages/storage/storage-http/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-http/uri-plugin/uri-plugin")is executed - THEN the module resolves and re-exports
httpUriPluginandhttpsUriPlugin
Verified by: src/uri-plugin/uri-plugin.test.ts::"is importable via the documented subpath"
Requirement: Conformance harness pass
Both httpUriPlugin and httpsUriPlugin SHALL pass
runUriPluginConformance with fixtures covering authority + path +
query forms, range query variants, malformed inputs, and a non-empty
buildRoundTrip set per scheme.
Scenario: both factories pass conformance
- WHEN
runUriPluginConformanceis invoked against each factory's output with the documented fixtures - THEN both calls return without throwing
Verified by: src/uri-plugin/uri-plugin.test.ts::"httpUriPlugin passes runUriPluginConformance" AND src/uri-plugin/uri-plugin.test.ts::"httpsUriPlugin passes runUriPluginConformance"