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"`
|
||||
Reference in New Issue
Block a user