Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,273 @@
# Complete `uriPlugin` exports on four storage adapter packages
- **Change ID**: complete-storage-uri-adapters
- **Packages**: `@suaveplan/storage-file`, `@suaveplan/storage-http`, `@suaveplan/storage-cdn-bunnycdn`, `@suaveplan/storage-cdn-rackspace` (modify — additive)
- **Status**: PROPOSED
- **Date**: 2026-05-17
- **Type**: capability-completion (multi-package, one change)
- **Design**: ./design.md
- **Spec deltas**:
- ./specs/storage-file/spec.md
- ./specs/storage-http/spec.md
- ./specs/storage-cdn-bunnycdn/spec.md
- ./specs/storage-cdn-rackspace/spec.md
- **Depends on**: `add-storage-uri` (archived `openspec/archive/2026-05-16-add-storage-uri/`)
- **Sibling in-flight**: `complete-storage-s3` ships the `s3UriPlugin` — explicitly out of scope here.
## Why
`@suaveplan/storage-uri` shipped on 2026-05-16 as a scheme-routed dispatch
layer over `StorageLike` adapters. Its contract requires every adapter
package to contribute a `UriPlugin` so the resolver stays scheme-agnostic.
The original change's task list (`openspec/archive/2026-05-16-add-storage-uri/tasks.md`
§5) enumerated the per-adapter plugin work; the file-, http-, bunnycdn-,
and rackspace adapters never shipped theirs. Today consumers cannot
register any of these four schemes with a `StorageUriResolver` without
hand-rolling a plugin per adapter — defeating the routing layer's purpose.
A single consolidated change covers all four because the pattern is
identical: parse the scheme's URI grammar into `{ key, meta? }`, bind to
the adapter's existing read/write/delete/stat/list/exists ops, redact
sensitive query parameters in span attributes, thread `AbortSignal`, raise
a per-adapter `STORAGE_<NAME>_URI.*` code registry, and pass
`runUriPluginConformance`. Four separate changes would multiply
ceremony — four proposals, four design docs, four archive cycles — for
work that shares a verbatim integration playbook. `storage-s3`'s plugin
is *excluded* and lives in `complete-storage-s3` because the S3 surface
needs presigned-URL, SSE, and multipart wiring that none of these four
schemes share.
## What changes
### `@suaveplan/storage-file` — `file://` URI plugin
- ADDED: `src/uri-plugin/uri-plugin.ts` exporting `fileUriPlugin(options)`
where `options: { adapter: NodeFsStorage; rootDir: string }`. Plugin
parses `file:///abs/path`, `file://localhost/abs/path`,
and `file:///C:/Windows/Path` (Windows drive-letter form) per RFC 8089.
Empty / `localhost` authorities are normalised to the local filesystem.
Non-empty non-`localhost` authorities raise
`STORAGE_FILE_URI.UNSUPPORTED_AUTHORITY` (no network mounts).
- ADDED: `build(key)` inverse that re-emits `file:///${rootDir}/${key}`
with proper percent-encoding (RFC 3986 path segments) and Windows
drive-letter preservation.
- ADDED: relative-vs-absolute resolution: the plugin REJECTS any URI
whose decoded path escapes `rootDir` via `..` traversal — raised as
`STORAGE_FILE_URI.PATH_ESCAPE`.
- ADDED: permission-denied passthrough — `EACCES` / `EPERM` from
`adapter` re-classified as `STORAGE_FILE_URI.PERMISSION_DENIED` so URI
consumers can branch without depending on `STORAGE_FILE` directly.
- ADDED: `STORAGE_FILE_URI_CODES` via `defineCodes("STORAGE_FILE_URI", ...)`
— 6 codes: `INVALID_URI`, `UNSUPPORTED_AUTHORITY`, `PATH_ESCAPE`,
`PERMISSION_DENIED`, `WINDOWS_DRIVE_INVALID`, `KEY_TOO_LONG`.
- ADDED: sibling `uri-plugin.md` + `uri-plugin.test.ts` (uses
`runUriPluginConformance` plus cross-platform path fixtures).
- ADDED: subpath export `"./uri-plugin/uri-plugin"` in `package.json`.
### `@suaveplan/storage-http` — `http://` + `https://` URI plugins
- ADDED: `src/uri-plugin/uri-plugin.ts` exporting `httpUriPlugin(options)`
AND `httpsUriPlugin(options)` where `options: { adapter: HttpStorage }`.
The two factories share an internal builder; the only difference is the
registered `scheme` field.
- ADDED: URI grammar: full RFC 3986 hierarchy
`https://user:pass@host:port/path?query#fragment`. The plugin parses
authority + path + query and forwards them to `HttpStorage` via the
`composeUrl` helper. Query strings reach the adapter as part of the key
to preserve presigned-URL `?X-Amz-Signature=...` semantics.
- ADDED: range-request support — `getPartial(key, { offset, length })`
reachable via the URI plugin when the URI bears an explicit
`?range=bytes=<offset>-<end>` meta channel; otherwise the plugin
passes through to `adapter.get`. Range parsing is REJECTED with
`STORAGE_HTTP_URI.RANGE_INVALID` for malformed forms.
- ADDED: header injection — the plugin reads
`HttpStorage.options.headers` via dependency injection, but URI
consumers MAY override per-request headers through the resolver's
AbortSignal-bearing options bag (no new resolver surface; the plugin
routes them through `adapter.options.headers`).
- ADDED: redirect policy — the adapter's `FetchLike` is responsible for
redirects; the plugin documents that opaque redirects expose the final
URL via the `Response.url` returned to `HttpStorage` and surfaces a
`STORAGE_HTTP_URI.REDIRECT_LOOP` when the adapter raises one.
- ADDED: `build(key, meta?)` inverse — re-emits the canonical URL with
the scheme set per factory. Authority/path encoding handled via
`new URL(key, baseUrl)`.
- ADDED: query-string redaction policy — span attributes drop any
query parameter whose key matches the case-insensitive deny-list
`X-Amz-Signature`, `X-Amz-Security-Token`, `token`, `signature`,
`sig`, `key`, `auth`, `password`. Redaction implemented as
`redactSignedUrl(uri)`; exported for reuse.
- ADDED: `STORAGE_HTTP_URI_CODES` via
`defineCodes("STORAGE_HTTP_URI", ...)` — 6 codes: `INVALID_URI`,
`UNSUPPORTED_SCHEME`, `RANGE_INVALID`, `REDIRECT_LOOP`, `HOST_MISMATCH`,
`KEY_TOO_LONG`.
- ADDED: subpath export `"./uri-plugin/uri-plugin"`.
- ADDED: sibling `uri-plugin.md` + `uri-plugin.test.ts`.
### `@suaveplan/storage-cdn-bunnycdn` — `bunnycdn://` URI plugin
- ADDED: `src/uri-plugin/uri-plugin.ts` exporting `bunnyUriPlugin(options)`
where `options: { adapter: BunnyCdnAdapter; storageZone: string; pullZoneHostname?: string }`.
- ADDED: URI grammar: `bunnycdn://<storage-zone>/<path>` for the
storage-zone (write) surface; pull-zone reads route through
`bunnycdn+pull://<pull-zone>/<path>` (a sibling sub-scheme registered
under the same plugin). The plugin discriminates on the second sub-form
via `scheme === "bunnycdn+pull"`; storage-zone authority is normalised
against the configured `storageZone` and a mismatch raises
`STORAGE_CDN_BUNNY_URI.ZONE_MISMATCH`.
- ADDED: signed-URL support — when the URI carries
`?token=<bunny-token>&expires=<unix>` query parameters, the plugin
consumes them via the existing `buildTokenAuthQuery` helper. The token
bytes are NEVER persisted on `ParsedUri.meta`; they live only on the
ephemeral fetch path.
- ADDED: `build(key, meta?)` inverse — emits
`bunnycdn://<storage-zone>/<key>` or
`bunnycdn+pull://<pull-zone>/<key>` based on `meta.surface ∈ {"storage", "pull"}`
(defaults to `"storage"`).
- ADDED: redaction policy — span `cdn.uri` attribute strips
`token`, `signature`, `expires` query parameters; only path + zone
attribute survive.
- ADDED: `STORAGE_CDN_BUNNY_URI_CODES` via
`defineCodes("STORAGE_CDN_BUNNY_URI", ...)` — 6 codes:
`INVALID_URI`, `ZONE_MISMATCH`, `PULL_ZONE_NOT_CONFIGURED`,
`TOKEN_INVALID`, `KEY_TOO_LONG`, `UNSUPPORTED_OPERATION`
(raised when a pull-zone URI is `set`/`delete`'d — pull zones are
read-only).
- ADDED: subpath export `"./uri-plugin/uri-plugin"`.
- ADDED: sibling `uri-plugin.md` + `uri-plugin.test.ts`.
### `@suaveplan/storage-cdn-rackspace` — `rackspace://` URI plugin
- ADDED: `src/uri-plugin/uri-plugin.ts` exporting `rackspaceUriPlugin(options)`
where `options: { adapter: RackspaceCdnAdapter; container: string; region?: RackspaceRegion }`.
- ADDED: URI grammar: `rackspace://<container>/<object-key>`. The
container appears as the URI authority; the object key is the path.
Authority `_` or empty re-binds to the plugin's configured `container`.
- ADDED: auth-token threading — the plugin does NOT carry the auth
token in URIs (auth lives on `RackspaceAuthClient`). It threads the
ambient signal into `RackspaceCdnAdapter` calls and propagates
`RackspaceCdnAdapter`'s token-expiry/refresh behaviour transparently.
- ADDED: temp-URL signed-URL passthrough — when the URI carries
`?temp_url_sig=<sig>&temp_url_expires=<unix>`, the plugin forwards them
to `buildTempUrlSignature` for verification; on mismatch raises
`STORAGE_CDN_RACKSPACE_URI.TEMP_URL_INVALID`.
- ADDED: `build(key, meta?)` inverse — emits
`rackspace://<container>/<key>`; `meta.region` (when set) is encoded as
`?region=<region>` for downstream resolvers that re-bind regions per
URI.
- ADDED: redaction policy — span attributes strip
`temp_url_sig`, `temp_url_expires`, `X-Auth-Token`.
- ADDED: `STORAGE_CDN_RACKSPACE_URI_CODES` via
`defineCodes("STORAGE_CDN_RACKSPACE_URI", ...)` — 6 codes:
`INVALID_URI`, `CONTAINER_MISMATCH`, `REGION_UNSUPPORTED`,
`TEMP_URL_INVALID`, `AUTH_TOKEN_EXPIRED`, `KEY_TOO_LONG`.
- ADDED: subpath export `"./uri-plugin/uri-plugin"`.
- ADDED: sibling `uri-plugin.md` + `uri-plugin.test.ts`.
### Shared scaffolding
- Each plugin uses the existing `parseUri` helper from `@suaveplan/storage-uri`
for RFC 3986 split; per-scheme validation layers on top.
- Each plugin THREADS `AbortSignal` through every delegated adapter
call. Pre-aborted signals raise the per-package `*.ABORTED`-equivalent
(each adapter already has an `ABORTED` code; the URI plugin re-uses it
rather than adding a duplicate).
- Each plugin passes `runUriPluginConformance` with valid + invalid +
buildRoundTrip fixtures.
- Each plugin gets sibling `.md` per CLAUDE.md §4 and an updated
`README.md` `## Modules` link.
## Out of scope
- **`@suaveplan/storage-s3` `s3UriPlugin`** — covered by the in-flight
`complete-storage-s3` change which lands the plugin alongside the full
S3 multipart / SSE / presign surface. Including it here would double-write
the spec for the same surface.
## Source mapping
This is a **capability-completion** change. Surface anchors:
- `openspec/archive/2026-05-16-add-storage-uri/tasks.md` §5 — original
per-adapter task list; items 5.1–5.4 deferred.
- `packages/storage/storage-uri/src/types/plugin.types.ts:29-34` —
`UriPlugin` contract.
- `packages/storage/storage-uri/src/conformance/conformance.ts:39-115` —
`runUriPluginConformance` harness.
- `packages/storage/storage-file/src/node-fs-storage/` — adapter surface
consumed verbatim.
- `packages/storage/storage-http/src/http-storage/http-storage.helpers.ts:52-58` —
`composeUrl` helper consumed.
- `packages/storage/storage-cdn-bunnycdn/src/token-auth/token-auth.ts` —
`buildTokenAuthQuery` consumed for signed-URL paths.
- `packages/storage/storage-cdn-rackspace/src/temp-url/temp-url.ts` —
`buildTempUrlSignature` consumed for signed-URL paths.
## Verification approach
Every requirement in every spec delta carries a `**Verified by:**` line
referencing concrete `it(...)` titles in planned test files. The
pre-archive gate (`§13` of `AGENTS.md`) re-runs the full conformance
suite per adapter. No mocks (per CLAUDE.md rule 2) — fixture adapters
are constructed against real `NodeFsStorage`, real `HttpStorage`
(against `testing-integration` fixtures), and real `BunnyCdnAdapter` /
`RackspaceCdnAdapter` instances wired to in-process fetch fixtures.
## Impact
### Affected dependents
For each of the four packages, run
`bunx turbo ls --filter=...@suaveplan/<pkg>` to enumerate. All additions
are purely additive named exports — no existing surface changes.
- `@suaveplan/storage-uri` — [x] tests run / [x] docs reviewed / [ ] no action needed
(consumes the four plugins via the existing `UriPlugin` interface; the
cross-scheme integration tests under
`packages/storage/storage-uri/tests/integration/` gain the four new
schemes).
- `@suaveplan/storage-file` consumers — [ ] tests run / [ ] docs reviewed
/ [x] no action needed (purely additive `fileUriPlugin` export).
- `@suaveplan/storage-http` consumers — [ ] tests run / [ ] docs reviewed
/ [x] no action needed (purely additive `httpUriPlugin` /
`httpsUriPlugin` exports).
- `@suaveplan/storage-cdn-bunnycdn` consumers — [ ] tests run / [ ] docs
reviewed / [x] no action needed.
- `@suaveplan/storage-cdn-rackspace` consumers — [ ] tests run / [ ] docs
reviewed / [x] no action needed.
None of the four adapter packages are foundation packages per §14a; the
dependent sweep is informational only.
### Affected upstreams
| Package | Action | Breaking? |
|---|---|---|
| `@suaveplan/storage-file` | ADD `fileUriPlugin` + `STORAGE_FILE_URI_CODES` | no |
| `@suaveplan/storage-http` | ADD `httpUriPlugin` + `httpsUriPlugin` + `STORAGE_HTTP_URI_CODES` | no |
| `@suaveplan/storage-cdn-bunnycdn` | ADD `bunnyUriPlugin` + `STORAGE_CDN_BUNNY_URI_CODES` | no |
| `@suaveplan/storage-cdn-rackspace` | ADD `rackspaceUriPlugin` + `STORAGE_CDN_RACKSPACE_URI_CODES` | no |
| `@suaveplan/storage-uri` | No surface change — gains test coverage via the cross-scheme integration suite | no |
## Migration
The change is purely additive — no behaviour change for existing
consumers. After landing, consumers compose a multi-scheme resolver via:
```ts
import { createStorageUriResolver } from "@suaveplan/storage-uri";
import { fileUriPlugin } from "@suaveplan/storage-file/uri-plugin/uri-plugin";
import { httpsUriPlugin } from "@suaveplan/storage-http/uri-plugin/uri-plugin";
import { bunnyUriPlugin } from "@suaveplan/storage-cdn-bunnycdn/uri-plugin/uri-plugin";
import { rackspaceUriPlugin } from "@suaveplan/storage-cdn-rackspace/uri-plugin/uri-plugin";
const resolver = createStorageUriResolver([
fileUriPlugin({ adapter: fileAdapter, rootDir: "/var/data" }),
httpsUriPlugin({ adapter: httpAdapter }),
bunnyUriPlugin({ adapter: bunnyAdapter, storageZone: "uploads" }),
rackspaceUriPlugin({ adapter: rackspaceAdapter, container: "assets" }),
]);
```
No grep search needed — there is no prior public surface for these
plugins.