first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
# @suaveplan/network-gundb-server - Server-tier GunDB relay daemon for @suaveplan/network-discovery-gundb clients — Node-only HTTP/WebSocket relay with structural Zod wire-payload validation, AbortSignal lifecycle, health/ready/live endpoints, OTel observability, and injectable Gun factory for real-fake integration tests.
## Summary
Server-tier GunDB relay daemon. Hosts a Gun relay peer plus standard
`/health` / `/ready` / `/live` probes on a single Node `http.Server`.
Browser-tier clients running `@suaveplan/network-discovery-gundb` pass
`http(s)://<host>:<port>/gun` in their `peers[]` list and gossip
signed peer-announces through the relay.
<!-- begin:auto-docs -->
## Documentation tree
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [listen-http](./src/server/listen-http.md) — Free function that binds a `node:http` server to `(port, host)` and translates the resulting Node EventEmitter signals…
- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_ - [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_ - [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_ - [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_
## Why this package
- **Single daemon, no orchestrator coupling.** Boot the relay,
publish the endpoint, watch the health probes.
- **Defensive by default.** Every inbound announce is structurally
validated against the shared Zod schema; malformed records are
dropped silently and counted on
`gundb_server.invalid_messages_total{reason}`.
- **Injection-friendly.** The Gun runtime is a peer dep — production
wires real `Gun`, tests bind a deterministic in-memory fake.
- **Lifecycle-correct.** `start({ signal? })` resolves with the bound
`{ address, port }`. `stop()` is idempotent.
## Run the daemon
```ts
import { GunDBServer } from "@suaveplan/network-gundb-server";
import Gun from "gun";
const server = new GunDBServer({
port: 8765,
host: "0.0.0.0",
storagePath: "./data/gun",
radisk: true,
maxConnections: 256,
gunFactory: (cfg) => Gun(cfg),
});
const { address, port } = await server.start();
console.log(`relay listening on ${address}:${port}/gun`);
process.on("SIGTERM", async () => {
await server.stop();
process.exit(0);
});
```
## Configuration reference
See [`src/types/types.md`](./src/types/types.md) for the full strict
`GunDBServerConfig`. All fields except `port` and `gunFactory` are
optional and have spec-documented defaults.
## Health endpoints
| Verb | 200 OK when | 503 when |
|----------|--------------------------------------|--------------------------------------|
| `/live` | process alive (always, until stop) | after `stop()` |
| `/ready` | started AND below `maxConnections` | starting, at-cap, or stopped |
| `/health`| started AND below `maxConnections` | starting, at-cap, or stopped |
See [`src/health/health.md`](./src/health/health.md).
## Observability
OpenTelemetry instrument names emitted by the daemon:
- `gundb_server.start` / `gundb_server.stop` — spans.
- `gundb_server.connections_total{outcome}` — counter.
- `gundb_server.messages_relayed_total{outcome}` — counter.
- `gundb_server.invalid_messages_total{reason}` — security counter.
- `gundb_server.active_connections` — up-down gauge.
- `gundb_server.start_duration_ms` — histogram.
See [`src/otel/otel.md`](./src/otel/otel.md).
## Security model
The relay does **not** verify Ed25519 signatures on inbound
announces — signature verification is the client's responsibility per
the gossip security model. The relay structurally validates the
presence and shape of every signature field so a malformed announce
cannot poison the namespace, then forwards the signed payload
unchanged.
See [`src/validation/validation.md`](./src/validation/validation.md).
## Companion package
[`@suaveplan/network-discovery-gundb`](../network-discovery-gundb) is
the browser-tier client this daemon serves.

View File

@@ -0,0 +1,84 @@
# `network-gundb-server/errors`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the frozen error-code registry and `NetGunDBServerError` class
used by every public boundary in `@suaveplan/network-gundb-server`. The
registry is built via `defineCodes("NET_GUNDB_SERVER", [...] as const)`
so codes are stable string literals, type-narrowable at the call site,
and surfaced as labels on the structural counters in `./otel`.
The relay must never crash on a malformed inbound payload — wire-payload
rejection codes are surfaced via counters only, not thrown. Lifecycle
codes (`START_FAILED` / `PORT_IN_USE` / `DISPOSED` / `NOT_RUNNING` /
`INVALID_OPTIONS`) ARE thrown out of `start` / `stop`.
## Features
- 6-code frozen registry: `INVALID_OPTIONS`, `START_FAILED`,
`PORT_IN_USE`, `DISPOSED`, `NOT_RUNNING`, `INVALID_WIRE_PAYLOAD`.
- `NetGunDBServerError extends SuaveplanError<NetGunDBServerErrorCode>`
preserves the canonical `code` / `messageKey` / `cause` / `context`
surface from `@suaveplan/error`.
- Codes also serve as low-cardinality labels for the OTel counter
surface (`reason` attribute on
`gundb_server.invalid_messages_total`).
## Usage: Basic Example
```ts
import {
NET_GUNDB_SERVER_CODES,
NetGunDBServerError,
} from "@suaveplan/network-gundb-server";
throw new NetGunDBServerError(
NET_GUNDB_SERVER_CODES.NOT_RUNNING,
"gundb_server.start-required"
);
```
## Usage: Advanced Example
```ts
import {
NET_GUNDB_SERVER_CODES,
NetGunDBServerError,
} from "@suaveplan/network-gundb-server";
try {
await server.start();
} catch (err) {
if (
err instanceof NetGunDBServerError &&
err.code === NET_GUNDB_SERVER_CODES.PORT_IN_USE
) {
// Fallback to a random free port.
return retryWithFreshPort();
}
throw err;
}
```
## API Reference
- `NET_GUNDB_SERVER_CODES` — frozen `Record<UpperSnake, string>` of
registered codes.
- `NetGunDBServerErrorCode` — union of every registered code literal.
- `class NetGunDBServerError` — constructor
`(code, messageKey, options?)` — throws on every public lifecycle
boundary.
## Implementation Notes
- `INVALID_WIRE_PAYLOAD` is **registered** but **not thrown** out of
the inbound message handler — the relay drops malformed payloads
silently and increments the
`gundb_server.invalid_messages_total{reason}` counter so dashboards
can spot spikes from misbehaving clients without taking the node
down.
- `defineCodes` produces an `as const` frozen object so every code is
type-narrowable in `switch` / `if` chains downstream.

View File

@@ -0,0 +1,89 @@
# `network-gundb-server/health`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Inline health/readiness/liveness endpoints for the `GunDBServer`
daemon. The package surfaces three standard verbs at `/health`,
`/ready`, and `/live` on the same HTTP server the Gun relay listens
on — orchestrators (Kubernetes probes, AWS ALB, Docker healthchecks)
hit them to decide whether to route traffic, restart the container, or
mark the instance terminated. The module ships as a pure function set
(no `http.Server` import) so the daemon can keep its single HTTP
listener and dispatch health routes before the Gun relay sees them.
Once `@suaveplan/health-check` lands as a dedicated package this module
will become a thin shim over that surface; today it owns the
implementation directly so the relay is not blocked by an
out-of-package dependency.
## Features
- Three standard verbs: `/health`, `/ready`, `/live`.
- `live` returns 200 unless `stop()` has been called.
- `ready` returns 200 only when started AND has spare capacity.
- `health` returns 200 when running and not at-cap, 503 when degraded.
- JSON-only response body with structured status fields.
- Query-string and trailing-slash agnostic path classifier.
- Pure functions — every input is captured at call-time so the same
response is byte-deterministic for any given snapshot.
## Usage: Basic Example
```ts
import {
classifyHealthRequest,
handleHealth,
} from "@suaveplan/network-gundb-server";
if (handleHealth(req, res, snapshot)) {
return; // Health route handled; do not fall through to Gun.
}
```
## Usage: Advanced Example
```ts
import {
buildHealthResponse,
type HealthSnapshot,
} from "@suaveplan/network-gundb-server";
const snap: HealthSnapshot = {
running: true,
disposed: false,
activeConnections: 42,
maxConnections: 100,
startedAt: Date.now() - 5_000,
};
const { status, body } = buildHealthResponse("ready", snap);
console.log(status, body.status); // 200 "ok"
```
## API Reference
- `HEALTH_PATHS` — frozen `{ health, ready, live }` path constants.
- `HealthKind` — `"health"|"ready"|"live"|null`.
- `classifyHealthRequest(url)` — match URL path → verb.
- `HealthSnapshot` — daemon state record (running, disposed, active,
max, startedAt).
- `HealthResponse` — JSON body shape.
- `buildHealthResponse(verb, snap)` — pure decision function.
- `handleHealth(req, res, snap)` — dispatch + write. Returns `true`
when the request was handled.
## Implementation Notes
- The classifier strips a single trailing slash and the entire
query-string portion so probes that hit `/health?ts=12345` or
`/health/` both match — the strict match would otherwise leak 404s
into production telemetry.
- The `live` verb is decoupled from `ready` so a node that has hit
`maxConnections` keeps reporting live (the process is fine) while
reporting not-ready (do not send more traffic) — the standard
K8s pattern.
- Returning `false` from `handleHealth` keeps the dispatcher composable:
the daemon's HTTP listener can fall through to the Gun relay when
the URL is not a health route.

View File

@@ -0,0 +1,95 @@
# `network-gundb-server/otel`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Owns the full OpenTelemetry instrumentation surface for the
`GunDBServer` daemon. Every counter, histogram, and gauge consumed by
the daemon is defined here and accessed through small typed helper
functions — direct `metrics.getMeter()` calls outside this module are
forbidden so dashboards never need to reference more than one set of
instrument names. Tracer spans wrap the public `start()` / `stop()`
boundaries so downstream OTel exporters can correlate lifecycle latency
with relay traffic.
The instruments here are also the **security-observability lever** —
the `gundb_server.invalid_messages_total{reason}` counter is the
primary signal a relay operator uses to detect a misbehaving client
flooding the namespace with malformed announces.
## Features
- `gundb_server.start` / `gundb_server.stop` span helpers.
- `gundb_server.connections_total{outcome}` counter (lifecycle).
- `gundb_server.messages_relayed_total{outcome}` counter (data plane).
- `gundb_server.invalid_messages_total{reason}` counter (security).
- `gundb_server.active_connections` up-down gauge.
- `gundb_server.start_duration_ms` histogram (lifecycle latency).
- Tiny adapter helpers (`recordConnection`, `recordMessageRelayed`,
`recordInvalidMessage`, `adjustActiveConnections`,
`recordStartDuration`).
## Usage: Basic Example
```ts
import {
recordConnection,
withStartSpan,
} from "@suaveplan/network-gundb-server/otel";
await withStartSpan(async () => {
// start logic
recordConnection("connected");
});
```
## Usage: Advanced Example
```ts
import {
adjustActiveConnections,
recordInvalidMessage,
recordMessageRelayed,
withStopSpan,
} from "@suaveplan/network-gundb-server/otel";
// Per-frame inbound:
recordInvalidMessage("malformed");
// Live peer-count tracking:
adjustActiveConnections(+1);
// ... peer detaches ...
adjustActiveConnections(-1);
// Per-relayed-frame outbound:
recordMessageRelayed("ok");
// Stop wrap:
await withStopSpan(async () => {
/* drain + close */
});
```
## API Reference
- `withStartSpan(fn)` / `withStopSpan(fn)` — async-span wrappers.
- `recordConnection(outcome)` — `"connected"|"disconnected"|"rejected"`.
- `recordMessageRelayed(outcome)` — `"ok"|"error"`.
- `recordInvalidMessage(reason)` — `InvalidWirePayloadReason`.
- `adjustActiveConnections(delta)` — `+1` on connect, `-1` on detach.
- `recordStartDuration(ms)` — observation in milliseconds.
## Implementation Notes
- The `reason` attribute on `gundb_server.invalid_messages_total` is
drawn from a closed enum (`InvalidWirePayloadReason`) so the metric
remains low-cardinality and safe for production telemetry.
- `adjustActiveConnections` uses `UpDownCounter` rather than a polled
`ObservableGauge` because the relay already knows the exact delta at
every transition — there is no underlying sampling concern.
- The package never registers an exporter — that is the consumer's
responsibility. We rely on the global `@opentelemetry/api` no-op
provider when no SDK is wired up, which keeps unit tests free of
exporter setup.

View File

@@ -0,0 +1,54 @@
# listen-http
## Purpose
Free function that binds a `node:http` server to `(port, host)` and translates the resulting Node EventEmitter signals into a Promise-rejecting error envelope keyed off `NET_GUNDB_SERVER_CODES`. Extracted out of `GunDBServer.#listen` so the generic-errno branch can be exercised in a unit test — Bun's `http.Server.listen` coerces every listen failure to `EADDRINUSE` through the public surface, making the non-EADDRINUSE branch otherwise unreachable from `GunDBServer.start()` under Bun.
## Features
- Maps `EADDRINUSE` to `NET_GUNDB_SERVER.PORT_IN_USE`.
- Maps every other errno to a generic `NET_GUNDB_SERVER.START_FAILED`.
- Maps a fired `AbortSignal` to `START_FAILED` with the dedicated `gundb_server.aborted` message key.
- Cleans up listeners on every resolution path (success, error, abort).
## Usage: Basic
```ts
import { createServer } from "node:http";
import { listenHttp } from "@suaveplan/network-gundb-server/server/listen-http";
const http = createServer();
await listenHttp(http, 8765, "127.0.0.1", undefined);
```
## Usage: Advanced
```ts
import { createServer } from "node:http";
import { listenHttp } from "@suaveplan/network-gundb-server/server/listen-http";
const http = createServer();
const ctrl = new AbortController();
queueMicrotask(() => ctrl.abort());
try {
await listenHttp(http, 8765, "127.0.0.1", ctrl.signal);
} catch (err) {
// err.code === NET_GUNDB_SERVER_CODES.START_FAILED
// err.messageKey === "gundb_server.aborted"
}
```
## API Reference
### `listenHttp(http, port, host, signal): Promise<void>`
- `http` — A `node:http.Server` instance.
- `port` — Port number (use `0` for an ephemeral port).
- `host` — Host name or IP address to bind.
- `signal` — Optional `AbortSignal` that, when fired, rejects the promise and `close()`s the server.
Resolves once the underlying `listening` event fires. Rejects with a `NetGunDBServerError` keyed appropriately.
## Implementation Notes
The non-EADDRINUSE branch is intentionally distinct from `EADDRINUSE` so callers (operators, dashboards) can distinguish "port collision" from "real fault" — the former is recoverable by retrying on another port, the latter usually indicates a misconfigured host or permission problem. The `gundb_server.aborted` message key on the abort path is separate from the listen-failure key so observability can count abort-driven rollbacks separately from listen failures.

View File

@@ -0,0 +1,114 @@
# `network-gundb-server/server`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`GunDBServer` is the server-tier daemon class that hosts a GunDB relay
peer plus inline health/readiness/liveness endpoints on a Node
`http.Server`. Browser-tier clients running
`@suaveplan/network-discovery-gundb` pass `http(s)://<host>:<port>/gun`
in their `peers[]` list and gossip signed peer-announces through the
relay. The daemon owns the HTTP listener, attaches a Gun instance via
an injected factory (production wires real `Gun`, tests bind a
deterministic fake), subscribes to the announce room, validates every
inbound record against the shared Zod schema, and tracks per-peer
connection state for observability.
The daemon is single-purpose — it does not handle authentication,
TLS termination, or rate-limiting. Those concerns live in front of the
daemon (reverse proxy / API gateway) or in the security-layer plugins
on the client side. The relay's sole defensive responsibility is to
drop structurally-invalid wire payloads so a misbehaving client cannot
poison the namespace.
## Features
- `start({ signal? })` and `stop()` lifecycle, idempotent on stop.
- Injectable `gunFactory` — never resolves `gun` at module-eval time.
- Inline `/health` / `/ready` / `/live` JSON probes on the same HTTP
listener as the relay.
- Structural Zod wire-payload validation on every inbound announce,
with counter-only rejections.
- Per-peer tracking + `peer-connected` / `peer-disconnected` events on
the optional `EventBusLike`.
- `maxConnections` cap with `connected/disconnected/rejected` counter
outcomes.
- AbortSignal threaded through `start()` for graceful cancellation.
- OTel spans + counters + gauge + histogram.
## Usage: Basic Example
```ts
import { GunDBServer } from "@suaveplan/network-gundb-server";
import Gun from "gun";
const server = new GunDBServer({
port: 8765,
gunFactory: (cfg) => Gun(cfg),
});
const { address, port } = await server.start();
console.log(`relay listening on ${address}:${port}`);
// ... later
await server.stop();
```
## Usage: Advanced Example
```ts
import { EventBus } from "@suaveplan/events";
import { GunDBServer, type GunDBServerEventMap } from "@suaveplan/network-gundb-server";
import Gun from "gun";
const bus = new EventBus<GunDBServerEventMap>();
bus.on("gundb_server:peer-connected", (e) =>
console.log(`peer joined: ${e.peerId}`)
);
const controller = new AbortController();
const server = new GunDBServer({
port: 8765,
host: "0.0.0.0",
peers: ["https://upstream.example.com/gun"],
storagePath: "./data/gun",
radisk: true,
maxConnections: 256,
room: "suaveplan-discovery",
bus,
gunFactory: (cfg) => Gun(cfg),
});
process.on("SIGTERM", () => controller.abort());
await server.start({ signal: controller.signal });
```
## API Reference
- `new GunDBServer(config)` — constructor, validates via Zod;
throws `INVALID_OPTIONS`.
- `start({ signal? })` — `Promise<{ address, port }>`. Throws
`START_FAILED` / `PORT_IN_USE` / `DISPOSED`.
- `stop()` — `Promise<void>`. Idempotent.
- `peers()` — readonly snapshot of every observed peer.
- `activeConnections` — getter, live count.
- `snapshot()` — `HealthSnapshot` for probe responses.
- `config` — `ResolvedGunDBServerConfig`.
## Implementation Notes
- The HTTP listener is wired with `handleHealth` first so probes never
reach Gun's WebSocket upgrade handler — this matters under load
because Gun's handler is synchronous-heavy.
- `stop()` flips `disposed` before draining so any in-flight inbound
handler call sees `running=false` and skips state mutation.
- `AbortSignal` is honoured _before_ the `listen()` syscall fires
(pre-aborted signal short-circuits) and _during_ listen (mid-flight
abort calls `close()` on the listener and rejects the start promise).
- The relay does not verify Ed25519 signatures on inbound announces —
that is the client's responsibility per the gossip security model.
Structural validation (presence + length of signature fields) is
enforced to prevent shape-poisoning attacks.
- `peer-connected` is emitted on the _first_ valid announce seen for
a `peerId`; subsequent announces refresh `lastSeen` and bump
`messagesRelayed`. There is no explicit "leave" semantics — peers
age out via the client's `peerTtlMs` mechanism.

View File

@@ -0,0 +1,100 @@
# `network-gundb-server/types`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Public type surface for `@suaveplan/network-gundb-server`. Owns the
`GunDBServerConfig` strict-options struct, the `GunFactory` /
`GunNodeLike` adapter interfaces injected by callers, the
`GunDBServerEventMap` of typed lifecycle events surfaced on the optional
`EventBusLike`, and the supporting per-peer / start-result / invalid-
payload-reason records. Every other module in this package consumes this
file — keeping it framework- and runtime-agnostic ensures the daemon
remains injection-friendly and the test fakes never reach for the real
`gun` module.
## Features
- Strict `GunDBServerConfig` with REQUIRED `gunFactory` (no implicit
default — production wires real `Gun`, tests bind a fake).
- `GunNodeLike` minimal structural surface (`get` / `map` / `on` /
`off`) covering every operation the relay performs.
- `GunFactoryConfig` enumerates exactly which constructor options the
daemon forwards (the rest must be wired inside the caller-supplied
factory).
- Typed `GunDBServerEventMap` for lifecycle pub-sub via
`@suaveplan/types` `EventBusLike`.
- `ResolvedGunDBServerConfig` mirrors the public struct with every
optional field defaulted — what the daemon actually carries
internally.
- `InvalidWirePayloadReason` enumerates the reason labels attached to
the `gundb_server.invalid_messages_total{reason}` counter.
## Usage: Basic Example
```ts
import type { GunDBServerConfig } from "@suaveplan/network-gundb-server";
import Gun from "gun";
const config: GunDBServerConfig = {
port: 8765,
gunFactory: (cfg) => Gun(cfg),
};
```
## Usage: Advanced Example
```ts
import type {
GunDBServerConfig,
GunDBServerEventMap,
} from "@suaveplan/network-gundb-server";
import { EventBus } from "@suaveplan/events";
import Gun from "gun";
const bus = new EventBus<GunDBServerEventMap>();
bus.on("gundb_server:started", (e) =>
console.log(`relay listening on ${e.address}:${e.port}`)
);
const config: GunDBServerConfig = {
port: 8765,
host: "127.0.0.1",
peers: ["https://upstream.example.com/gun"],
storagePath: "./data/gun",
radisk: true,
maxConnections: 256,
room: "suaveplan-discovery",
gunFactory: (cfg) => Gun(cfg),
bus,
};
```
## API Reference
- `GunDBServerConfig` — public, strict options struct.
- `ResolvedGunDBServerConfig` — internal, every optional field
defaulted.
- `GunFactory` / `GunFactoryConfig` / `GunNodeLike` — adapter triple.
- `GunDBServerStartResult` — `{ address, port }` returned by `start()`.
- `GunDBServerPeerInfo` — per-client connection record.
- `GunDBServerEventMap` — typed lifecycle event topics.
- `InvalidWirePayloadReason` — `"malformed" | "wrong-kind" |
"missing-signature" | "missing-public-key"`.
## Implementation Notes
- The `gunFactory` is a hard requirement — even the fake `Gun` used by
tests passes through it. This keeps the runtime dependency optional
(`peerDependenciesMeta.gun.optional = true`) and makes the daemon
trivially substitutable for in-process integration tests.
- The relay does **not** verify Ed25519 signatures on incoming
announces — signature verification is the client's responsibility per
the gossip security model. The relay validates the _structural_
presence of `signature` + `signaturePublicKey` so a malformed
announce cannot poison the namespace, but the signature itself is
passed through unchanged.
- `GunDBServerEventMap` is shaped to interoperate with both
`@suaveplan/events` and any other `EventBusLike` adapter.

View File

@@ -0,0 +1,85 @@
# `network-gundb-server/validation`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Wire-payload validation for inbound `peer-announce` records arriving on
the GunDB relay namespace. The relay enforces the same structural
contract the browser-tier `@suaveplan/network-discovery-gundb` client
enforces on outbound frames — Zod-validated discriminated union with
mandatory `kind`, `signature`, and `signaturePublicKey` fields — so a
single misbehaving client cannot poison the namespace with malformed
records that every other client would then have to filter on read.
The relay deliberately does **not** verify the Ed25519 signature
itself: cryptographic verification is a client responsibility per the
gossip security model. The relay only confirms that the payload is
shape-correct and carries the signature material, then passes it
through unchanged so authorized clients can verify it locally.
## Features
- Structural Zod validation against the shared
`GunDBPeerAnnounceSchema` exported by the companion client package
(single source of truth — server and client cannot drift).
- Distinct rejection reasons for low-cardinality OTel labelling:
`"malformed"`, `"wrong-kind"`, `"missing-signature"`,
`"missing-public-key"`.
- Silent rejection: every rejection increments the
`gundb_server.invalid_messages_total{reason}` counter; the function
returns a discriminated union and never throws.
- Strips Gun's internal `_` metadata key before validation so the
payload shape is invariant under the relay's wire envelope.
## Usage: Basic Example
```ts
import { validateInboundAnnounce } from "@suaveplan/network-gundb-server";
const result = validateInboundAnnounce(rawRecord);
if (result.ok) {
relayForward(result.payload);
}
```
## Usage: Advanced Example
```ts
import { validateInboundAnnounce } from "@suaveplan/network-gundb-server";
gun.get("suaveplan-discovery")
.map()
.on((data) => {
const result = validateInboundAnnounce(data);
if (!result.ok) {
// Counter already incremented in the helper.
log.debug("dropped malformed announce", { reason: result.reason });
return;
}
forwardToSubscribers(result.payload);
});
```
## API Reference
- `WirePayloadValidation` — discriminated union
`{ ok: true; payload } | { ok: false; reason }`.
- `validateInboundAnnounce(raw)` — structural validator. Side-effect:
increments `gundb_server.invalid_messages_total{reason}` on
rejection. Never throws.
## Implementation Notes
- The function performs a fast structural pre-check before invoking
the full Zod schema so the `reason` label is informative — a Zod
failure alone cannot distinguish "missing signature" from
"malformed JSON".
- Pulling the Zod schema from the companion client package guarantees
the server and client agree on wire format — a breaking change to
the schema breaks both packages' tests simultaneously, which is the
correct failure mode.
- Returning a discriminated union (rather than throwing) is
load-bearing: the relay's inbound handler must remain crash-proof
even when an adversarial peer floods garbage records.