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

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" and runUriPluginConformance passes

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 key ends with "?X-Amz-Signature=abc"

Scenario: fragment is stripped

  • WHEN plugin.parse("https://host/p#section") is called
  • THEN the returned key does 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 exposes getPartial
  • THEN adapter.getPartial is 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 StorageHttpUriError with code === "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 and HttpStorage was configured with headers: (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 StorageHttpUriError with code === "STORAGE_HTTP_URI.REDIRECT_LOOP" and cause set

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 adapter baseUrl === "https://host"
  • THEN the result is "https://host/a/b?ver=1" and parses 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 a storage.uri attribute
  • 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_CODES appears on at least one raised StorageHttpUriError instance

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's get 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 (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 httpUriPlugin and httpsUriPlugin

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