first commit
This commit is contained in:
108
docs/networking/network/network-gundb-server/README.md
Normal file
108
docs/networking/network/network-gundb-server/README.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
100
docs/networking/network/network-gundb-server/src/types/types.md
Normal file
100
docs/networking/network/network-gundb-server/src/types/types.md
Normal 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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user