# Add @suaveplan/drive-uri — drive, drive+mesh, drive+template URI plugins for StorageUriResolver **Package**: `@suaveplan/drive-uri` **Status**: PROPOSED **Type**: new-package (genesis-original) **Depends on**: `add-storage-uri` (archived) — provides `UriPlugin`, `StorageUriResolver`, `runUriPluginConformance`. - **Design**: ./design.md - **Spec delta**: ./specs/drive-uri/spec.md ## Why `@suaveplan/storage-uri` collapses seven storage adapters into a single `read("scheme://…")` surface, but it dispatches at the storage tier only. The drive ecosystem — `drive`, `drive-template`, `drive-mesh`, `drive-plugin-chunks`, `drive-transport-https`, `drive-webrtc` — sits **above** storage and addresses files by drive identity, swarm partition, or dehydrated template descriptor, not by a flat storage key. A consumer holding a `drive+mesh://swarm-abc/raw/clip.mov` URI today has no way to reach the bytes without instantiating a `Drive`, wiring `chunksPlugin`, a mesh transport, a peer registry, and a trust map at every call site. `drive-uri` is the bridge. It exports three `UriPlugin` factories — `driveUriPlugin`, `driveMeshUriPlugin`, `driveTemplateUriPlugin` — that each wrap a caller-provided `Drive` instance in a `StorageLike`-conformant adapter and plug into the existing `StorageUriResolver`. The resolver itself is unchanged; the consumer simply registers drive plugins alongside their `fileUriPlugin` / `httpsUriPlugin` / `s3UriPlugin` set and the same `resolver.read("drive+mesh://…")` call returns plaintext bytes via the drive's existing middleware (`chunksPlugin` reassembles chunks from mesh peers; `Drive.hydrateFrom` validates the template) — `drive-uri` does **not** re-implement any chunk codec, peer discovery, or signature verification logic. This composition is the central design choice: `storage-uri` stays drive-agnostic and `drive-uri` stays storage-agnostic; they meet in the consumer's plugin list. No new resolver class, no umbrella composition helper, no API duplication. ## What changes - ADDED: `driveUriPlugin(options): UriPlugin` — scheme `drive`. Parses `drive:///` and forwards `path` to the caller-supplied `Drive` (which the consumer wires with `chunksPlugin` + a local-only configuration). The `driveId` is captured in `meta.driveId` and surfaced on spans, errors, and the rebuilt URI; the plugin enforces that the supplied `Drive` matches the expected `driveId` via the options' `driveId` field. - ADDED: `driveMeshUriPlugin(options): UriPlugin` — scheme `drive+mesh`. Parses `drive+mesh:///` and forwards `path` to a caller-supplied `Drive` that has been configured with `chunksPlugin` + a mesh transport (`drive-transport-https` and/or `drive-webrtc`) and a populated `PeerRegistry`. The `swarmId` is captured in `meta.swarmId` and used only for instrumentation; the plugin does **not** dispatch on swarmId — that responsibility stays with the `Drive` and its transports. - ADDED: `driveTemplateUriPlugin(options): UriPlugin` — scheme `drive+template`. Parses `drive+template:///` and, before delegating to `drive.read(path)`, fetches the `DriveTemplate` via an injected `templateProvider.fetchTemplate(templateId, signal)`, hydrates the `Drive` via `drive.hydrateFrom(template, { signal })`, and caches the `(templateId → hydrated)` pair so subsequent reads against the same template skip the round-trip. Hydration errors from `Drive.hydrateFrom` (any `DTP_*` code) are re-wrapped as `DRIVE_URI.TEMPLATE_INVALID` with `cause` preserved. - ADDED: `DriveStorageLikeAdapter` (internal, exported for testability) — translates `Drive.read` / `Drive.write` / `Drive.delete` / `Drive.stat` / `Drive.createReadStream` into the `StorageLike` shape consumed by `StorageUriResolver`. The shim maps `Drive`'s "throw on missing" semantics into `StorageLike`'s "`get` returns `undefined`" semantics (DRIVE.IO_FAILED with a `NOT_FOUND`-flavoured cause → `undefined`; every other failure re-throws so the resolver can wrap it as `STORAGE_URI.ADAPTER_ERROR`). - ADDED: `DRIVE_URI_CODES` registry via `defineCodes("DRIVE_URI", [...] as const)` — 7 codes: `INVALID_URI`, `DRIVE_ID_MISMATCH`, `TEMPLATE_NOT_AVAILABLE`, `TEMPLATE_INVALID`, `WRITE_UNSUPPORTED`, `CAPABILITY_MISSING`, `ABORTED`. The resolver itself emits `STORAGE_URI.*` codes for routing concerns (`UNKNOWN_SCHEME`, `NOT_FOUND`, etc.); `DRIVE_URI.*` is reserved for drive-specific semantic failures the resolver cannot diagnose. - ADDED: `DriveUriError extends SuaveplanError` with `uri`, `scheme`, `driveId?`, `swarmId?`, `templateId?`, `path?` discriminants. Adapter errors from `Drive` retain their original `DRIVE_*` / `CHUNKS_*` / `DRIVE_MESH_*` / `DTP_*` codes via `cause`. - ADDED: OTel surface — `drive_uri.op_duration_ms{op, scheme, outcome}` histogram; `drive_uri.{reads, writes, deletes, errors, template_hydrations}_total{scheme, outcome}` counters; spans on `@suaveplan/drive-uri` tracer with `drive.uri`, `drive.uri.scheme`, `drive.uri.drive_id?`, `drive.uri.swarm_id?`, `drive.uri.template_id?` attributes (URI query strings stripped to avoid peer-key leakage). The plugins set those attributes via the parent `StorageUriResolver` span context — the resolver still emits its own `storage_uri.*` instruments. - ADDED: Conformance — each plugin's parse/build round-trip is verified via `runUriPluginConformance` (re-exported from `@suaveplan/storage-uri`); each is also covered by an integration test that wires a real `Drive` (no mocks per CLAUDE.md rule 2). ## Out of scope - **Writes via `drive*://` URIs.** Initial release rejects `write`/`remove` with `DRIVE_URI.WRITE_UNSUPPORTED`. Reason: write semantics for shared/mesh files (chunk re-upload, gossip re-advertise, template re-issue, retroactive key grants) are non-trivial; they belong in a follow-up `add-drive-uri-writes` change once read paths are proven in production. - **A new `DriveUriResolver` class.** The three plugins satisfy the existing `UriPlugin` contract from `@suaveplan/storage-uri`; consumers register them into a `StorageUriResolver` next to their storage plugins. Adding a separate resolver would duplicate dispatch logic without changing observable behaviour. - **A `composeUriResolvers` helper.** Equivalent to "register more plugins into one resolver." Out of scope unless a future use case requires two resolvers with conflicting state. - **Range reads on `drive+mesh://`.** `drive.createReadStream` already streams chunk-by-chunk via the chunks plugin; byte-range slicing on top requires a future `Drive.createReadStream({ offset, length })` extension — tracked separately. - **Auto-discovery of peers across swarms.** Each `drive+mesh://` URI carries its `swarmId` in the authority; cross-swarm peer routing is the application's job, not the resolver's. - **A `DriveLike` interface in `@suaveplan/types`.** Adding it requires ≥2 consumers per AGENTS.md §11; `drive-uri` is the only consumer today. If a second appears, hoist then. - **Reimplementing chunk codec / template validation / peer discovery.** All three live in their owning packages and are exercised through the `Drive` reference the consumer provides. `drive-uri` is a routing layer, not a drive substitute. ## Source mapping This is a **genesis-original** package; there is no upstream in `../suaveplan/`. All requirements derive from: - The `UriPlugin` / `StorageUriResolver` / `runUriPluginConformance` surface in `@suaveplan/storage-uri` (`openspec/specs/storage-uri/spec.md`, folded post-archival). - The `Drive` class in `@suaveplan/drive` — exact signatures used: `Drive.read(path: string, options?: ReadOptions): Promise`, `Drive.createReadStream(path: string, signal?: AbortSignal): ReadableStream`, `Drive.write(path, data, options?)`, `Drive.delete(path, signal?)`, `Drive.stat(path, signal?)`, `Drive.hydrateFrom(template, options?)`. - The `DriveTemplate` schema from `@suaveplan/drive-template` — fetched via the consumer-supplied `templateProvider`; never decoded or validated inside `drive-uri` (validation happens inside `Drive.hydrateFrom` via the existing 8-step pipeline). ### Requirements with no source line | Requirement | Justification | |---|---| | Three distinct schemes rather than one `drive://` with a discriminator segment | Each scheme has different authority semantics (driveId / swarmId / templateId), different consumer-side wiring (local-only Drive vs. mesh-configured Drive vs. mesh-configured Drive + templateProvider), and different failure modes. Collapsing them would force every consumer to wire every dependency. | | `templateId → hydrated` cache lives in `drive-uri`, not `Drive` | The cache is a URI-resolver concern: it prevents repeated `templateProvider.fetchTemplate` calls when the same template is referenced by many URIs. `Drive.hydrateFrom` is naturally idempotent on `(driveId, nonce)` via `TemplateNonceLru`, but it does not memoise the template object itself — `drive-uri` does, scoped to its lifetime. | | `driveId` mismatch is `DRIVE_URI.DRIVE_ID_MISMATCH`, not a `Drive`-tier error | The check happens in the plugin before any `Drive` call, so the failure has no `Drive` origin to wrap. Surfacing it as a drive-uri code keeps the error code namespace honest. | | Query strings dropped from span attributes | A future iteration may carry peer hints or seeds in the query string; redacting now establishes the contract before any sensitive value is leaked. | | Adapter shim translates `Drive`'s throw-on-missing into `StorageLike`'s undefined-on-missing | `StorageUriResolver.read` already maps `undefined` → `STORAGE_URI.NOT_FOUND`; pushing the translation up into `Drive` would change `Drive`'s contract for every consumer. Keeping it in the shim is one-line and local. | | No `composeUriResolvers` helper despite the earlier proposal mentioning one | A single `StorageUriResolver` with mixed storage + drive plugins gives the same composition without introducing a second resolver class or a fall-through dispatch. Simpler is better. | | `DRIVE_URI.CAPABILITY_MISSING` is the lazy-check code | A consumer who never resolves `drive+template://` should not have to wire a `templateProvider`. The plugin only requires the dependency on first use; absence at first use raises this code (not at construction). | ## Verification approach Every requirement in the spec delta carries a `**Verified by:**` line referencing concrete `it(...)` titles in these planned test files: - `src/plugins/drive/drive.test.ts` — parse `drive:///`, dispatch to `Drive.read(path)`, `DRIVE_ID_MISMATCH` when the URI's driveId differs from the bound `Drive`'s configured id, `INVALID_URI` for malformed inputs, AbortSignal propagation. - `src/plugins/drive/drive.streams.test.ts` — `createReadStream` forwarding, mid-stream abort cancels the underlying `Drive.createReadStream`. - `src/plugins/drive-mesh/drive-mesh.test.ts` — parse `drive+mesh:///`, dispatch to a mesh-configured `Drive`, swarmId captured in `meta.swarmId`, span attribute populated, error wrapping for `DRIVE_MESH.*` codes. - `src/plugins/drive-template/drive-template.test.ts` — parse `drive+template:///`, `templateProvider.fetchTemplate` invoked exactly once per templateId (memoisation), `drive.hydrateFrom` called before first read, `TEMPLATE_NOT_AVAILABLE` when provider rejects, `TEMPLATE_INVALID` when `hydrateFrom` throws any `DTP_*` code, `CAPABILITY_MISSING` when no provider is wired. - `src/adapter/drive-storage-like.test.ts` — `get` returns `undefined` on `DRIVE.IO_FAILED`-with-NOT_FOUND cause, re-throws on every other failure, `exists` is `HEAD`-flavoured (does not buffer file bytes), `stat` populates `size`/`lastModified` from `Drive.stat`. - `src/error-codes/error-codes.test.ts` — exhaustive `DRIVE_URI_CODES` registry, `DriveUriError` discriminant fields, `cause` preserved across re-wraps. - `src/otel/otel.test.ts` — `template_hydrations_total` increments per first-use template, query string stripped from `drive.uri` attribute, per-scheme error counters populated. - `src/conformance/conformance.test.ts` — `runUriPluginConformance` (re-exported) passes for all three plugins with the planned URI fixture sets. - `tests/integration/drive-roundtrip.test.ts` — writes a file via `Drive.write(path, bytes)` on a real local-storage Drive, reads it via `resolver.read("drive://.../path")` through `StorageUriResolver`, asserts equality. - `tests/integration/template-roundtrip.test.ts` — issues a `DriveTemplate`, dehydrates, exposes the template via an in-process `templateProvider`, resolves `drive+template://.../path` through a fully wired Drive + chunks plugin pair, asserts plaintext matches source. - `tests/integration/compose-with-storage-uri.test.ts` — single `StorageUriResolver` registered with `fileUriPlugin` + `driveUriPlugin`; resolves `file:///tmp/foo` and `drive://.../foo` through the same dispatcher without code-path divergence. ## Impact ### Affected dependents Run `bunx turbo ls --filter=...@suaveplan/drive-uri` to enumerate. The package is new — no dependents currently. After adoption, the obvious upstream beneficiaries are `enterprise-media`, audio/video pipelines that consume URI-addressed sources, and `drive-sharing` flows that surface shareable URIs. - No dependents (verified by `bunx turbo ls --filter=...@suaveplan/drive-uri` returning the package itself). ### Affected upstreams (none modified) | Package | Action | Breaking? | |---|---|---| | `@suaveplan/storage-uri` | None — consumed via `UriPlugin` + `runUriPluginConformance` export; new package registers plugins into the existing resolver | no | | `@suaveplan/drive` | None — `Drive.read` / `createReadStream` / `write` / `delete` / `stat` / `hydrateFrom` consumed as-is | no | | `@suaveplan/drive-template` | None — only the `DriveTemplate` type is consumed (passed through to `Drive.hydrateFrom`) | no | | `@suaveplan/drive-mesh` | None — only via the consumer-supplied `Drive` reference | no | | `@suaveplan/drive-plugin-chunks` | None — only via the consumer-supplied `Drive` reference | no | | `@suaveplan/drive-transport-https` / `drive-webrtc` | None — wired into `Drive` by the consumer | no | `drive-uri` MUST NOT silently grow any of these packages' contracts. If a needed capability turns out to be missing during implementation, capture it as a separate proposal against the relevant upstream package — never bend the contract here. ### Ordering This change is unblocked: `add-storage-uri` was archived in commit `f41fe490` (2026-05-16). All other dependencies (`drive`, `drive-mesh`, `drive-template`, `drive-plugin-chunks`) ship today with the consumed surface intact.