14 KiB
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— schemedrive. Parsesdrive://<driveId>/<path>and forwardspathto the caller-suppliedDrive(which the consumer wires withchunksPlugin+ a local-only configuration). ThedriveIdis captured inmeta.driveIdand surfaced on spans, errors, and the rebuilt URI; the plugin enforces that the suppliedDrivematches the expecteddriveIdvia the options'driveIdfield. - ADDED:
driveMeshUriPlugin(options): UriPlugin— schemedrive+mesh. Parsesdrive+mesh://<swarmId>/<path>and forwardspathto a caller-suppliedDrivethat has been configured withchunksPlugin+ a mesh transport (drive-transport-httpsand/ordrive-webrtc) and a populatedPeerRegistry. TheswarmIdis captured inmeta.swarmIdand used only for instrumentation; the plugin does not dispatch on swarmId — that responsibility stays with theDriveand its transports. - ADDED:
driveTemplateUriPlugin(options): UriPlugin— schemedrive+template. Parsesdrive+template://<templateId>/<path>and, before delegating todrive.read(path), fetches theDriveTemplatevia an injectedtemplateProvider.fetchTemplate(templateId, signal), hydrates theDriveviadrive.hydrateFrom(template, { signal }), and caches the(templateId → hydrated)pair so subsequent reads against the same template skip the round-trip. Hydration errors fromDrive.hydrateFrom(anyDTP_*code) are re-wrapped asDRIVE_URI.TEMPLATE_INVALIDwithcausepreserved. - ADDED:
DriveStorageLikeAdapter(internal, exported for testability) — translatesDrive.read/Drive.write/Drive.delete/Drive.stat/Drive.createReadStreaminto theStorageLikeshape consumed byStorageUriResolver. The shim mapsDrive's "throw on missing" semantics intoStorageLike's "getreturnsundefined" semantics (DRIVE.IO_FAILED with aNOT_FOUND-flavoured cause →undefined; every other failure re-throws so the resolver can wrap it asSTORAGE_URI.ADAPTER_ERROR). - ADDED:
DRIVE_URI_CODESregistry viadefineCodes("DRIVE_URI", [...] as const)— 7 codes:INVALID_URI,DRIVE_ID_MISMATCH,TEMPLATE_NOT_AVAILABLE,TEMPLATE_INVALID,WRITE_UNSUPPORTED,CAPABILITY_MISSING,ABORTED. The resolver itself emitsSTORAGE_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<DriveUriErrorCode>withuri,scheme,driveId?,swarmId?,templateId?,path?discriminants. Adapter errors fromDriveretain their originalDRIVE_*/CHUNKS_*/DRIVE_MESH_*/DTP_*codes viacause. - 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-uritracer withdrive.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 parentStorageUriResolverspan context — the resolver still emits its ownstorage_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 realDrive(no mocks per CLAUDE.md rule 2).
Out of scope
- Writes via
drive*://URIs. Initial release rejectswrite/removewithDRIVE_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-upadd-drive-uri-writeschange once read paths are proven in production. - A new
DriveUriResolverclass. The three plugins satisfy the existingUriPlugincontract from@suaveplan/storage-uri; consumers register them into aStorageUriResolvernext to their storage plugins. Adding a separate resolver would duplicate dispatch logic without changing observable behaviour. - A
composeUriResolvershelper. 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.createReadStreamalready streams chunk-by-chunk via the chunks plugin; byte-range slicing on top requires a futureDrive.createReadStream({ offset, length })extension — tracked separately. - Auto-discovery of peers across swarms. Each
drive+mesh://URI carries itsswarmIdin the authority; cross-swarm peer routing is the application's job, not the resolver's. - A
DriveLikeinterface in@suaveplan/types. Adding it requires ≥2 consumers per AGENTS.md §11;drive-uriis 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
Drivereference the consumer provides.drive-uriis 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/runUriPluginConformancesurface in@suaveplan/storage-uri(openspec/specs/storage-uri/spec.md, folded post-archival). - The
Driveclass in@suaveplan/drive— exact signatures used:Drive.read(path: string, options?: ReadOptions): Promise<Uint8Array>,Drive.createReadStream(path: string, signal?: AbortSignal): ReadableStream<Uint8Array>,Drive.write(path, data, options?),Drive.delete(path, signal?),Drive.stat(path, signal?),Drive.hydrateFrom(template, options?). - The
DriveTemplateschema from@suaveplan/drive-template— fetched via the consumer-suppliedtemplateProvider; never decoded or validated insidedrive-uri(validation happens insideDrive.hydrateFromvia 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— parsedrive://<driveId>/<path>, dispatch toDrive.read(path),DRIVE_ID_MISMATCHwhen the URI's driveId differs from the boundDrive's configured id,INVALID_URIfor malformed inputs, AbortSignal propagation.src/plugins/drive/drive.streams.test.ts—createReadStreamforwarding, mid-stream abort cancels the underlyingDrive.createReadStream.src/plugins/drive-mesh/drive-mesh.test.ts— parsedrive+mesh://<swarmId>/<path>, dispatch to a mesh-configuredDrive, swarmId captured inmeta.swarmId, span attribute populated, error wrapping forDRIVE_MESH.*codes.src/plugins/drive-template/drive-template.test.ts— parsedrive+template://<templateId>/<path>,templateProvider.fetchTemplateinvoked exactly once per templateId (memoisation),drive.hydrateFromcalled before first read,TEMPLATE_NOT_AVAILABLEwhen provider rejects,TEMPLATE_INVALIDwhenhydrateFromthrows anyDTP_*code,CAPABILITY_MISSINGwhen no provider is wired.src/adapter/drive-storage-like.test.ts—getreturnsundefinedonDRIVE.IO_FAILED-with-NOT_FOUND cause, re-throws on every other failure,existsisHEAD-flavoured (does not buffer file bytes),statpopulatessize/lastModifiedfromDrive.stat.src/error-codes/error-codes.test.ts— exhaustiveDRIVE_URI_CODESregistry,DriveUriErrordiscriminant fields,causepreserved across re-wraps.src/otel/otel.test.ts—template_hydrations_totalincrements per first-use template, query string stripped fromdrive.uriattribute, 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 viaDrive.write(path, bytes)on a real local-storage Drive, reads it viaresolver.read("drive://.../path")throughStorageUriResolver, asserts equality.tests/integration/template-roundtrip.test.ts— issues aDriveTemplate, dehydrates, exposes the template via an in-processtemplateProvider, resolvesdrive+template://.../paththrough a fully wired Drive + chunks plugin pair, asserts plaintext matches source.tests/integration/compose-with-storage-uri.test.ts— singleStorageUriResolverregistered withfileUriPlugin+driveUriPlugin; resolvesfile:///tmp/fooanddrive://.../foothrough 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-urireturning 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.