Initial Commit
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: storage-cdn-bunnycdn (complete-storage-uri-adapters)
|
||||
|
||||
**Package**: `@suaveplan/storage-cdn-bunnycdn`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: `bunnycdn://` and `bunnycdn+pull://` URI plugin factory
|
||||
|
||||
The package SHALL export `bunnyUriPlugin(options)` from
|
||||
`./uri-plugin/uri-plugin` returning an object
|
||||
`{ storage: UriPlugin; pull: UriPlugin }` whose `storage.scheme` is
|
||||
`"bunnycdn"` and `pull.scheme` is `"bunnycdn+pull"`. Both plugins
|
||||
SHALL share the bound `BunnyCdnAdapter`; only the URI surface differs.
|
||||
|
||||
#### Scenario: factory exposes two plugin instances
|
||||
|
||||
- **WHEN** `bunnyUriPlugin({ adapter, storageZone: "uploads" })` is
|
||||
called
|
||||
- **THEN** the returned object has `storage.scheme === "bunnycdn"` and
|
||||
`pull.scheme === "bunnycdn+pull"`, and both reference the same adapter
|
||||
|
||||
#### Scenario: factory requires storageZone
|
||||
|
||||
- **WHEN** `bunnyUriPlugin({ adapter, storageZone: "" })` is called
|
||||
- **THEN** the call throws `StorageCdnBunnycdnUriError` with
|
||||
`code === "STORAGE_CDN_BUNNY_URI.INVALID_URI"`
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"exposes both storage and pull plugins"` AND `src/uri-plugin/uri-plugin.test.ts::"rejects empty storageZone"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Storage-zone parsing with mismatch detection
|
||||
|
||||
The `storage` plugin SHALL parse `bunnycdn://<zone>/<path>` URIs by
|
||||
taking the authority as the storage zone and the path (minus leading
|
||||
`/`) as the storage key. When the URI's zone does NOT match the
|
||||
factory's configured `storageZone`, the call SHALL raise
|
||||
`STORAGE_CDN_BUNNY_URI.ZONE_MISMATCH`.
|
||||
|
||||
#### Scenario: matching zone parses successfully
|
||||
|
||||
- **WHEN** `storage.parse("bunnycdn://uploads/img/a.jpg")` is called
|
||||
against a plugin configured with `storageZone: "uploads"`
|
||||
- **THEN** the returned `key === "img/a.jpg"`
|
||||
|
||||
#### Scenario: mismatched zone raises ZONE_MISMATCH
|
||||
|
||||
- **WHEN** `storage.parse("bunnycdn://wrong/img/a.jpg")` is called
|
||||
against the same plugin
|
||||
- **THEN** the call throws `StorageCdnBunnycdnUriError` with
|
||||
`code === "STORAGE_CDN_BUNNY_URI.ZONE_MISMATCH"`
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"parses matching storage zone"` AND `src/uri-plugin/uri-plugin.test.ts::"raises ZONE_MISMATCH for wrong zone"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Pull-zone read-only enforcement
|
||||
|
||||
The `pull` plugin SHALL parse `bunnycdn+pull://<host>/<path>` URIs and
|
||||
SHALL reject `set` / `delete` calls with
|
||||
`STORAGE_CDN_BUNNY_URI.UNSUPPORTED_OPERATION`. Pull zones are read-only
|
||||
caches; writes have no semantic target. When the factory was constructed
|
||||
without `options.pullZoneHostname`, parsing a pull-zone URI SHALL raise
|
||||
`STORAGE_CDN_BUNNY_URI.PULL_ZONE_NOT_CONFIGURED`.
|
||||
|
||||
#### Scenario: pull-zone read parses successfully
|
||||
|
||||
- **WHEN** `pull.parse("bunnycdn+pull://cdn.example.com/img/a.jpg")` is
|
||||
called against a plugin with `pullZoneHostname: "cdn.example.com"`
|
||||
- **THEN** the returned `key === "img/a.jpg"`
|
||||
|
||||
#### Scenario: pull-zone write is rejected
|
||||
|
||||
- **WHEN** the resolver routes `write("bunnycdn+pull://cdn.example.com/x", bytes)`
|
||||
- **THEN** the rejection has
|
||||
`code === "STORAGE_CDN_BUNNY_URI.UNSUPPORTED_OPERATION"`
|
||||
|
||||
#### Scenario: pull-zone URI without configured hostname is rejected
|
||||
|
||||
- **WHEN** `pull.parse("bunnycdn+pull://cdn.example.com/x")` is called
|
||||
against a plugin that was constructed without `pullZoneHostname`
|
||||
- **THEN** the call throws with
|
||||
`code === "STORAGE_CDN_BUNNY_URI.PULL_ZONE_NOT_CONFIGURED"`
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"parses pull-zone reads"` AND `src/uri-plugin/uri-plugin.test.ts::"rejects pull-zone writes"` AND `src/uri-plugin/uri-plugin.test.ts::"raises PULL_ZONE_NOT_CONFIGURED when missing"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Signed-URL token passthrough
|
||||
|
||||
When a URI carries `?token=<value>&expires=<unix>`, the plugin SHALL
|
||||
forward those parameters to the existing `buildTokenAuthQuery` helper
|
||||
when invoking the adapter. The token bytes SHALL NEVER be persisted on
|
||||
`ParsedUri.meta`; they live only on the ephemeral fetch path.
|
||||
|
||||
#### Scenario: signed token reaches buildTokenAuthQuery
|
||||
|
||||
- **WHEN** the resolver routes
|
||||
`read("bunnycdn://uploads/x?token=abc&expires=99")` and the bound
|
||||
adapter has `tokenAuthKey` configured
|
||||
- **THEN** the adapter's outgoing request URL contains the
|
||||
token-authenticated query string produced by `buildTokenAuthQuery`
|
||||
|
||||
#### Scenario: token is absent from ParsedUri.meta
|
||||
|
||||
- **WHEN** `storage.parse("bunnycdn://uploads/x?token=abc&expires=99")` is
|
||||
called
|
||||
- **THEN** the returned `meta` does NOT include a `token` key
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"forwards signed token to buildTokenAuthQuery"` AND `src/uri-plugin/uri-plugin.test.ts::"does not persist token on ParsedUri.meta"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `build(key, meta?)` with surface discriminator
|
||||
|
||||
The plugin SHALL implement `build(key, meta?)` on both surfaces. When
|
||||
`meta?.surface === "pull"`, the result uses the `bunnycdn+pull://` form
|
||||
with `pullZoneHostname`; otherwise the result uses the `bunnycdn://`
|
||||
form with the configured `storageZone`.
|
||||
|
||||
#### Scenario: storage build defaults to storage zone
|
||||
|
||||
- **WHEN** `storage.build("img/a.jpg")` is called
|
||||
- **THEN** the result is `"bunnycdn://<storageZone>/img/a.jpg"`
|
||||
|
||||
#### Scenario: pull build uses pull-zone hostname
|
||||
|
||||
- **WHEN** `pull.build("img/a.jpg")` is called against a plugin with
|
||||
`pullZoneHostname: "cdn.example.com"`
|
||||
- **THEN** the result is `"bunnycdn+pull://cdn.example.com/img/a.jpg"`
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"storage build emits bunnycdn scheme"` AND `src/uri-plugin/uri-plugin.test.ts::"pull build emits bunnycdn+pull scheme"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Span-attribute redaction
|
||||
|
||||
The plugin SHALL redact `token`, `signature`, and `expires` query
|
||||
parameters from any URI surfaced on OTel span attributes or in raised
|
||||
error `.uri` discriminants. Redaction SHALL be exposed as
|
||||
`redactSignedUrl(uri): string`.
|
||||
|
||||
#### Scenario: token is redacted on span attribute
|
||||
|
||||
- **WHEN** the resolver routes
|
||||
`read("bunnycdn://uploads/x?token=abc&expires=99")` and emits a span
|
||||
- **THEN** the `cdn.uri` attribute is
|
||||
`"bunnycdn://uploads/x?token=<redacted>&expires=<redacted>"`
|
||||
|
||||
**Verified by:** `src/uri-plugin/redact-signed-url.test.ts::"redacts token, signature, and expires"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `STORAGE_CDN_BUNNY_URI_CODES` registry
|
||||
|
||||
The package SHALL export `STORAGE_CDN_BUNNY_URI_CODES` produced by
|
||||
`defineCodes("STORAGE_CDN_BUNNY_URI", [...] as const)` covering exactly
|
||||
6 codes: `INVALID_URI`, `ZONE_MISMATCH`, `PULL_ZONE_NOT_CONFIGURED`,
|
||||
`TOKEN_INVALID`, `KEY_TOO_LONG`, `UNSUPPORTED_OPERATION`. Every code
|
||||
SHALL be reachable, and `StorageCdnBunnycdnUriError` SHALL extend
|
||||
`SuaveplanError<StorageCdnBunnycdnUriErrorCode>` with `uri`/`scheme`
|
||||
discriminants.
|
||||
|
||||
#### Scenario: every code is reachable
|
||||
|
||||
- **WHEN** the test suite exercises every documented failure path
|
||||
- **THEN** every member of `STORAGE_CDN_BUNNY_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_BUNNYCDN.ABORTED` code without invoking the adapter.
|
||||
|
||||
#### Scenario: pre-aborted signal short-circuits
|
||||
|
||||
- **WHEN** the resolver routes
|
||||
`read("bunnycdn://uploads/x", AbortSignal.abort())`
|
||||
- **THEN** the rejection has
|
||||
`code === "STORAGE_CDN_BUNNYCDN.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-bunnycdn/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-bunnycdn/uri-plugin/uri-plugin")`
|
||||
is executed
|
||||
- **THEN** the module resolves and re-exports `bunnyUriPlugin`
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"is importable via the documented subpath"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Conformance harness pass
|
||||
|
||||
Both `storage` and `pull` plugins SHALL pass
|
||||
`runUriPluginConformance` with fixtures covering zone-match cases,
|
||||
invalid zone forms, signed-URL passthrough, and non-empty
|
||||
`buildRoundTrip` sets per surface.
|
||||
|
||||
#### Scenario: both plugins pass conformance
|
||||
|
||||
- **WHEN** `runUriPluginConformance` is invoked against each plugin
|
||||
surface with the documented fixtures
|
||||
- **THEN** both calls return without throwing
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"storage plugin passes runUriPluginConformance"` AND `src/uri-plugin/uri-plugin.test.ts::"pull plugin passes runUriPluginConformance"`
|
||||
@@ -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"`
|
||||
@@ -0,0 +1,231 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: storage-file (complete-storage-uri-adapters)
|
||||
|
||||
**Package**: `@suaveplan/storage-file`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: `file://` URI plugin factory
|
||||
|
||||
The package SHALL export `fileUriPlugin(options)` from
|
||||
`./uri-plugin/uri-plugin` returning a `UriPlugin` whose `scheme` is the
|
||||
lowercase string `"file"` and whose `adapter` is the caller-supplied
|
||||
`NodeFsStorage` instance. The factory MUST validate `options.rootDir` is
|
||||
a non-empty absolute path; failure raises
|
||||
`StorageFileUriError(STORAGE_FILE_URI.INVALID_URI)` synchronously.
|
||||
|
||||
#### Scenario: factory returns a UriPlugin with scheme "file"
|
||||
|
||||
- **WHEN** `fileUriPlugin({ adapter, rootDir: "/var/data" })` is called
|
||||
- **THEN** the returned plugin's `scheme === "file"`, `adapter` is the
|
||||
same reference, and `runUriPluginConformance(plugin, fixtures)` passes
|
||||
|
||||
#### Scenario: empty rootDir is rejected
|
||||
|
||||
- **WHEN** `fileUriPlugin({ adapter, rootDir: "" })` is called
|
||||
- **THEN** the call throws `StorageFileUriError` with
|
||||
`code === "STORAGE_FILE_URI.INVALID_URI"`
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"exports a UriPlugin with scheme file"` AND `src/uri-plugin/uri-plugin.test.ts::"rejects empty rootDir"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: RFC 8089 authority handling
|
||||
|
||||
The plugin SHALL accept `file:///...` and `file://localhost/...` as
|
||||
equivalent absolute-path forms. Any other non-empty authority SHALL
|
||||
raise `STORAGE_FILE_URI.UNSUPPORTED_AUTHORITY` so network-file-system
|
||||
URIs (`file://server/share/...`) do not silently degrade to local lookups.
|
||||
|
||||
#### Scenario: empty authority resolves to local path
|
||||
|
||||
- **WHEN** `plugin.parse("file:///etc/hosts")` is called
|
||||
- **THEN** the result has `key` equal to `"etc/hosts"` (or platform
|
||||
equivalent relative to `rootDir`)
|
||||
|
||||
#### Scenario: localhost authority resolves identically to empty
|
||||
|
||||
- **WHEN** `plugin.parse("file://localhost/etc/hosts")` is called
|
||||
- **THEN** the result matches the empty-authority form exactly
|
||||
|
||||
#### Scenario: non-empty non-localhost authority is rejected
|
||||
|
||||
- **WHEN** `plugin.parse("file://nas.example.com/share/x")` is called
|
||||
- **THEN** the call throws `StorageFileUriError` with
|
||||
`code === "STORAGE_FILE_URI.UNSUPPORTED_AUTHORITY"`
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"resolves empty authority to local path"` AND `src/uri-plugin/uri-plugin.test.ts::"treats localhost authority as local"` AND `src/uri-plugin/uri-plugin.test.ts::"rejects non-empty non-localhost authority"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Windows drive-letter preservation
|
||||
|
||||
The plugin SHALL detect Windows drive-letter form
|
||||
`file:///<drive>:/path` (the leading `/` followed by `<ALPHA>:`) and
|
||||
preserve the drive letter through `parse` and `build`. Malformed
|
||||
drive-letter forms (e.g. `file:///C/path` or `file:///CC:/path`) SHALL
|
||||
raise `STORAGE_FILE_URI.WINDOWS_DRIVE_INVALID`.
|
||||
|
||||
#### Scenario: well-formed drive-letter parses
|
||||
|
||||
- **WHEN** `plugin.parse("file:///C:/Users/x.txt")` is called
|
||||
- **THEN** the parsed key preserves `C:/Users/x.txt` (verbatim, no
|
||||
percent-encoding of the colon)
|
||||
|
||||
#### Scenario: malformed drive-letter is rejected
|
||||
|
||||
- **WHEN** `plugin.parse("file:///CC:/x")` is called
|
||||
- **THEN** the call throws `StorageFileUriError` with
|
||||
`code === "STORAGE_FILE_URI.WINDOWS_DRIVE_INVALID"`
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"preserves Windows drive letters"` AND `src/uri-plugin/uri-plugin.test.ts::"rejects malformed drive-letter forms"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `..` traversal rejection
|
||||
|
||||
The plugin SHALL reject any URI whose decoded path resolves outside
|
||||
`rootDir` after `..` normalisation. Detection runs BEFORE the call
|
||||
reaches the underlying adapter.
|
||||
|
||||
#### Scenario: parent-directory escape is rejected
|
||||
|
||||
- **WHEN** `plugin.parse("file:///../../etc/passwd")` is called against
|
||||
a plugin with `rootDir: "/var/data"`
|
||||
- **THEN** the call throws `StorageFileUriError` with
|
||||
`code === "STORAGE_FILE_URI.PATH_ESCAPE"`
|
||||
|
||||
#### Scenario: same-directory descent succeeds
|
||||
|
||||
- **WHEN** `plugin.parse("file:///a/b/../c.txt")` is called with the
|
||||
same rootDir
|
||||
- **THEN** the parse succeeds with the normalised key `a/c.txt`
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"rejects path traversal escapes"` AND `src/uri-plugin/uri-plugin.test.ts::"normalises in-tree traversal"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Permission-denied passthrough
|
||||
|
||||
The plugin SHALL re-wrap permission-denied failures raised by the bound
|
||||
`NodeFsStorage` (`STORAGE_FILE.PERMISSION_DENIED` from `EACCES`/`EPERM`) as
|
||||
`StorageFileUriError(STORAGE_FILE_URI.PERMISSION_DENIED)` and MUST
|
||||
preserve the original on `cause`.
|
||||
|
||||
#### Scenario: adapter EACCES becomes STORAGE_FILE_URI.PERMISSION_DENIED
|
||||
|
||||
- **WHEN** the resolver routes `read("file:///protected/x")` and the
|
||||
underlying `adapter.get` throws `STORAGE_FILE.PERMISSION_DENIED`
|
||||
- **THEN** the rejection is `StorageFileUriError` with
|
||||
`code === "STORAGE_FILE_URI.PERMISSION_DENIED"` and `cause` set to the
|
||||
original `StorageFileError`
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"wraps EACCES as STORAGE_FILE_URI.PERMISSION_DENIED"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `build(key)` inverse with percent-encoding
|
||||
|
||||
The plugin SHALL implement `build(key)` returning a `file://`-scheme URI
|
||||
that round-trips through `parse` for every key produced by the bound
|
||||
adapter's `list` operation. Path segments SHALL be percent-encoded per
|
||||
RFC 3986 §3.3; Windows drive letters SHALL be preserved verbatim.
|
||||
|
||||
#### Scenario: POSIX key round-trips
|
||||
|
||||
- **WHEN** `plugin.build("a/b%file.txt")` is called
|
||||
- **THEN** the result `parse`s back to a `ParsedUri` whose `key === "a/b%file.txt"`
|
||||
|
||||
#### Scenario: Windows key round-trips with drive letter
|
||||
|
||||
- **WHEN** `plugin.build("C:/Users/x.txt")` is called
|
||||
- **THEN** the result is `"file:///C:/Users/x.txt"` and `parse`s back
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"build round-trips POSIX keys"` AND `src/uri-plugin/uri-plugin.test.ts::"build round-trips Windows keys"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `STORAGE_FILE_URI_CODES` registry
|
||||
|
||||
The package SHALL export `STORAGE_FILE_URI_CODES` produced by
|
||||
`defineCodes("STORAGE_FILE_URI", [...] as const)` covering exactly 6
|
||||
codes: `INVALID_URI`, `UNSUPPORTED_AUTHORITY`, `PATH_ESCAPE`,
|
||||
`PERMISSION_DENIED`, `WINDOWS_DRIVE_INVALID`, `KEY_TOO_LONG`. Every
|
||||
code SHALL be reachable from a real call site exercised by the test
|
||||
suite, and the structured error class `StorageFileUriError` SHALL extend
|
||||
`SuaveplanError<StorageFileUriErrorCode>` carrying optional
|
||||
`uri`/`scheme` discriminants.
|
||||
|
||||
#### Scenario: every code is reachable
|
||||
|
||||
- **WHEN** the test suite exercises every documented failure path
|
||||
- **THEN** every member of `STORAGE_FILE_URI_CODES` appears on at least
|
||||
one raised `StorageFileUriError` instance
|
||||
|
||||
#### Scenario: error preserves cause
|
||||
|
||||
- **WHEN** the plugin re-wraps an adapter `StorageFileError`
|
||||
- **THEN** the resulting `StorageFileUriError.cause` is the original
|
||||
adapter error instance
|
||||
|
||||
**Verified by:** `src/uri-plugin/error-codes.test.ts::"exposes 6 codes all reachable"` AND `src/uri-plugin/error-codes.test.ts::"preserves cause when re-wrapping"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: AbortSignal threading
|
||||
|
||||
The plugin SHALL forward an `AbortSignal` (received via the
|
||||
resolver's options bag) unchanged to every adapter call. A pre-aborted
|
||||
signal SHALL short-circuit with `STORAGE_FILE.ABORTED` (the adapter's
|
||||
existing code) without invoking the adapter's I/O methods.
|
||||
|
||||
#### Scenario: pre-aborted signal short-circuits
|
||||
|
||||
- **WHEN** the resolver routes `read("file:///x", AbortSignal.abort())`
|
||||
- **THEN** the rejection has `code === "STORAGE_FILE.ABORTED"` and the
|
||||
adapter's `get` is NEVER invoked
|
||||
|
||||
#### Scenario: live signal is forwarded
|
||||
|
||||
- **WHEN** `read("file:///x", signal)` is called with a live signal
|
||||
- **THEN** the adapter receives the same signal reference
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"short-circuits on pre-aborted signal"` AND `src/uri-plugin/uri-plugin.test.ts::"forwards live signal to adapter"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subpath exports
|
||||
|
||||
The package SHALL declare subpath exports for
|
||||
`"./uri-plugin/uri-plugin"` and `"./uri-plugin/error-codes"` in
|
||||
`package.json` so consumers can tree-shake the URI plugin independently.
|
||||
|
||||
#### Scenario: subpath imports resolve
|
||||
|
||||
- **WHEN** `import("@suaveplan/storage-file/uri-plugin/uri-plugin")` is
|
||||
executed
|
||||
- **THEN** the module resolves and re-exports `fileUriPlugin`
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"is importable via the documented subpath"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Conformance harness pass
|
||||
|
||||
`fileUriPlugin` SHALL pass `runUriPluginConformance` from
|
||||
`@suaveplan/storage-uri/conformance/conformance` with fixtures covering
|
||||
POSIX paths, Windows drive-letter paths, `localhost` authority,
|
||||
malformed inputs (empty path, non-localhost authority, traversal
|
||||
escape), and a non-empty `buildRoundTrip` set.
|
||||
|
||||
#### Scenario: conformance fixtures pass
|
||||
|
||||
- **WHEN** `runUriPluginConformance(fileUriPlugin(opts), fixtures)` runs
|
||||
with the documented fixture set
|
||||
- **THEN** the call returns without throwing
|
||||
|
||||
**Verified by:** `src/uri-plugin/uri-plugin.test.ts::"passes runUriPluginConformance"`
|
||||
@@ -0,0 +1,248 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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 `parse`s 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"`
|
||||
Reference in New Issue
Block a user