Initial Commit
This commit is contained in:
193
openspec/specs/drive-mesh/spec.md
Normal file
193
openspec/specs/drive-mesh/spec.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# @suaveplan/drive-mesh — P2P Mesh Orchestration Capability
|
||||
|
||||
P2P mesh layer over `@suaveplan/drive-plugin-chunks`: peer registry, bitset
|
||||
gossip, parallel chunk pull (multi-peer racing window), peer scoring, server
|
||||
fallback (built-in `ServerFallback` HTTPS path — see "HTTPS chunk fetch via
|
||||
ServerFallback" below), and bad-signature denylist. Consumes a pluggable list
|
||||
of peer-to-peer `DriveTransport` implementations (currently WebRTC, via
|
||||
`@suaveplan/drive-transport-webrtc`) without importing `@suaveplan/drive`
|
||||
core directly.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Mesh orchestrator surface
|
||||
|
||||
The package SHALL expose a `DriveMesh` class with:
|
||||
|
||||
- `start(signal?)` — begin gossip + listen for chunk requests.
|
||||
- `stop(reason?)` — drain in-flight, close transports. Idempotent.
|
||||
- `requestChunk(fileId, chunkId, signal?): Promise<Uint8Array>` — fetch a
|
||||
chunk from the mesh, falling back to the server if configured.
|
||||
- `advertise(fileId, bitset)` / `unadvertise(fileId)` — push our local
|
||||
availability through the gossip engine.
|
||||
- `peers(): Iterable<PeerInfo>` — snapshot of the peer registry.
|
||||
- `registry` — direct handle on the `PeerRegistry` for application glue.
|
||||
- `tickGossip()` — manual tick (testable / configurable cadence).
|
||||
- TypedEventEmitter for `chunk:*` / `peer:*` / `gossip:*` events.
|
||||
|
||||
### Requirement: Server is root of trust
|
||||
|
||||
`DriveMesh` SHALL treat the configured `serverPubKey` as the only authority
|
||||
for chunk validity:
|
||||
|
||||
- Every chunk delivered by a peer MUST be passed through
|
||||
`drive-plugin-chunks` signature verification before being returned to the
|
||||
caller.
|
||||
- A peer that delivers a chunk with an invalid signature MUST be added to
|
||||
the registry's denylist (per-session by default, persistent if configured).
|
||||
- A bad-sig peer MUST emit `peer:denied{peerId, reason: "bad-signature"}` so
|
||||
upstream `NetworkBus` consumers can take action.
|
||||
|
||||
#### Scenario: Bad-sig peer evicted
|
||||
|
||||
- **WHEN** a peer delivers bytes whose signature fails
|
||||
`verifyChunkSig(serverPubKey, …)`
|
||||
- **THEN** `peerRegistry.recordBadSig(peerId)` MUST be called and the peer
|
||||
MUST NOT be selected for subsequent chunk pulls.
|
||||
|
||||
### Requirement: PeerRegistry overlay
|
||||
|
||||
`PeerRegistry` SHALL track per-peer state including availability bitsets per
|
||||
fileId, EMA latency (α=0.3), success / failure / bad-sig counts, and a
|
||||
composite score for `rank(fileId, chunkId)`. The registry is an **overlay**
|
||||
that consumes peer-identity events from `network-plugin-registry` (per the
|
||||
`drive-network-integration` boundary spec); it MUST NOT duplicate peer-
|
||||
identity storage.
|
||||
|
||||
### Requirement: Gossip engine
|
||||
|
||||
`GossipEngine` SHALL emit periodic outbound advertisements with compressed
|
||||
bitsets per `fileId` (using `@suaveplan/drive-mesh-protocol` framed-bitset
|
||||
helpers) and SHALL ingest inbound advertisements into the `PeerRegistry`.
|
||||
|
||||
### Requirement: Parallel puller with racing window
|
||||
|
||||
`ParallelPuller` SHALL issue chunk requests to the top-K candidates (default
|
||||
3) within a configurable racing window (default 250 ms), accept the first
|
||||
verified response, and abort the losers via a shared `AbortController`.
|
||||
`maxParallelRequests` SHALL cap global concurrency (default 8). Failed or
|
||||
timed-out attempts blacklist that peer for the specific chunkId only;
|
||||
bad-signature is the only failure that escalates to a registry-wide denylist.
|
||||
|
||||
### Requirement: Server fallback
|
||||
|
||||
`ServerFallback` SHALL accept a `{ url, transport }` config and execute
|
||||
equal-jitter exponential backoff retries when all peer attempts fail.
|
||||
Server-delivered chunks MUST still pass signature verification.
|
||||
|
||||
#### Scenario: All-peers-fail with no fallback
|
||||
|
||||
- **WHEN** every peer attempt fails AND no `serverFallback` is configured
|
||||
- **THEN** `requestChunk` MUST reject with `DriveMeshError(ALL_PEERS_FAILED)`.
|
||||
|
||||
### Requirement: Wire transports as plugins
|
||||
|
||||
`DriveMesh` SHALL accept a list of `DriveTransport` implementations through
|
||||
its constructor and use them interchangeably. The structural `DriveTransport`
|
||||
interface MUST expose:
|
||||
|
||||
- `requestChunk(peerId, fileId, chunkId, signal?): AsyncIterable<Uint8Array>`
|
||||
- `advertise(peerId, fileId, bitset): Promise<void>`
|
||||
- `onAdvertisement(handler): () => void`
|
||||
- `peers(): Iterable<PeerInfo>`
|
||||
- `dispose(): Promise<void>`
|
||||
|
||||
`@suaveplan/drive-transport-webrtc` satisfies this interface. The HTTPS
|
||||
chunk-fetch path is NOT a `DriveTransport` plugin — see "HTTPS chunk fetch
|
||||
via ServerFallback" below.
|
||||
|
||||
### Requirement: No network-plugin-registry import
|
||||
|
||||
`DriveMesh` MUST NOT import `@suaveplan/network-plugin-registry` directly.
|
||||
Applications glue `peer:joined` / `peer:left` from `NetworkBus` to
|
||||
`mesh.registry.addOrUpdate` / `mesh.registry.forget`. This enforces the
|
||||
layering decision recorded in `openspec/specs/drive-network-integration/spec.md`.
|
||||
|
||||
### Requirement: Error registry
|
||||
|
||||
The package SHALL export `DriveMeshError extends SuaveplanError<…>` with 10
|
||||
stable codes covering chunk unavailable, all-peers-fail, signature failure,
|
||||
transport failure, fallback failure, invalid bitset, peer-denied paths.
|
||||
|
||||
### Requirement: AbortSignal threading
|
||||
|
||||
Every async surface (`start`, `stop`, `requestChunk`, `advertise`) SHALL
|
||||
accept an optional `AbortSignal`. Pre-aborted signals reject before any
|
||||
side effect. Mid-flight aborts cancel transport requests and racing
|
||||
attempts cleanly.
|
||||
|
||||
### Requirement: Universal-tier-clean tests
|
||||
|
||||
Tests MUST NOT use `vi.mock` or `vi.stubGlobal`. The package SHALL ship
|
||||
real-implementation fixtures (in-memory `Broker` + `FakeDriveTransport`,
|
||||
real `ed25519` keypairs via `@suaveplan/crypto-curve`, real
|
||||
`buildSignedChunk` from `@suaveplan/drive-plugin-chunks`, real
|
||||
`encodeFramedBitset` from `@suaveplan/drive-mesh-protocol`). All test
|
||||
imports come from `@suaveplan/testing/runner`.
|
||||
|
||||
### Requirement: Coverage + gate
|
||||
|
||||
`bunx turbo lint typecheck test:coverage --filter=@suaveplan/drive-mesh`
|
||||
MUST be green and istanbul coverage MUST be 100% on every touched module
|
||||
(statements / branches / functions / lines) before any release.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: HTTPS chunk fetch via ServerFallback
|
||||
|
||||
The HTTPS / server-fallback chunk-fetch path SHALL be served by the
|
||||
built-in `ServerFallback` module that ships inside `@suaveplan/drive-mesh`
|
||||
(`./server-fallback/server-fallback`) — NOT by a separate
|
||||
`DriveTransport`-shaped plugin package.
|
||||
|
||||
`ServerFallback` SHALL:
|
||||
|
||||
- Issue HTTPS `GET` to `${baseUrl}/${fileId}/${chunkId}` via an
|
||||
injectable `fetchFn` (defaults to `globalThis.fetch`).
|
||||
- Run every fetched chunk through `verifyChunkSig(serverPubKey, …)` from
|
||||
`@suaveplan/drive-plugin-chunks` before returning bytes to the
|
||||
scheduler — same root-of-trust contract as peer-served chunks.
|
||||
- Apply equal-jitter exponential backoff with a caller-configurable
|
||||
`maxRetries`, `backoffBaseMs`, and `backoffMaxMs`.
|
||||
- Thread an `AbortSignal` through every retry.
|
||||
- Surface every failure as a `DriveMeshError(FALLBACK_FAILED)` with the
|
||||
underlying HTTP status / fetch error preserved in `cause`.
|
||||
|
||||
Bearer-token or other authentication SHALL be supplied by the consumer
|
||||
through the `fetchFn` closure (e.g. wrapping `globalThis.fetch` with an
|
||||
`Authorization` header) — no transport-level auth plumbing is added.
|
||||
|
||||
`ServerFallback` deliberately does NOT implement the
|
||||
`DriveTransport` interface. Advertisement / peer-discovery / receive
|
||||
semantics do not apply to a server-driven HTTPS fetch path; modelling
|
||||
HTTPS as a degraded `DriveTransport` (with no-op `advertise`,
|
||||
`onAdvertisement`, and a single synthetic peer) was rejected because it
|
||||
adds no consumer-facing capability beyond what `ServerFallback` already
|
||||
provides while doubling the surface the scheduler has to reason about.
|
||||
|
||||
#### Scenario: Bad-sig server chunk rejected
|
||||
|
||||
- **WHEN** `ServerFallback.fetchChunk(fileId, chunkId)` returns bytes whose
|
||||
signature fails `verifyChunkSig(serverPubKey, …)`
|
||||
- **THEN** `requestChunk` MUST reject with
|
||||
`DriveMeshError(FALLBACK_FAILED)` and the chunk MUST NOT be cached.
|
||||
|
||||
#### Scenario: Auth wrapping happens in the consumer's fetchFn
|
||||
|
||||
- **WHEN** a consumer needs bearer-token authentication against the
|
||||
fallback endpoint
|
||||
- **THEN** the consumer SHALL pass an authenticated `fetchFn` to
|
||||
`ServerFallback` (e.g. `(url, init) => fetch(url, { ...init, headers: { ...init?.headers, Authorization: 'Bearer ...' } })`)
|
||||
rather than registering a separate transport.
|
||||
|
||||
### Requirement: No HTTPS DriveTransport plugin
|
||||
|
||||
There SHALL NOT be a parallel `@suaveplan/drive-transport-https` (or
|
||||
similar) package implementing the `DriveTransport` interface for the
|
||||
HTTPS path. The original `drive-transport-https-port` proposal was
|
||||
retired on 2026-05-12 because `ServerFallback` already provides every
|
||||
documented capability (HTTPS chunk fetch, signature verification,
|
||||
exponential backoff with jitter, injectable `fetchFn` for tests,
|
||||
`AbortSignal` threading) without inheriting the peer-shaped surface
|
||||
(`advertise` / `onAdvertisement` / `peers`) that would have to be
|
||||
no-ops on a unidirectional HTTPS link.
|
||||
Reference in New Issue
Block a user