Files
2026-05-27 23:29:53 +01:00

194 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# @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.