This commit is contained in:
Raven Scott
2026-07-18 16:22:56 -04:00
parent 015d92a257
commit f747ffbd25
36 changed files with 1975 additions and 873 deletions
+104 -131
View File
@@ -1,158 +1,131 @@
# Protocol
## Constants
Wire contract for PearData P2P RPC. Source of truth: `shared/protocol.js`, `shared/schema.js`, `shared/metrics.js`.
## Identity
| Constant | Value |
|----------|--------|
| `PROTOCOL` | `peardata/rpc` |
| `PROTOCOL_VERSION` | `1` |
| `APP_NAME` | `peardata` |
| `APP_VERSION` | `0.1.0` (keep in sync with package where useful) |
| Encoding | compact-encoding JSON (`shared/encodings.js`) |
| Schema | lightweight validators (`shared/schema.js`) `SCHEMA_VERSION=1` |
|----------|-------|
| Protocol id | `peardata/rpc` |
| Protocol version | `1` |
| Invite prefix | `pd1.` |
| Default role | `viewer` |
Bump `PROTOCOL_VERSION` on breaking request/response shapes. Additive methods may land without a bump if clients ignore unknown methods.
After `npm run rename`, `PROTOCOL` becomes `<slug>/rpc` and invite prefix is regenerated.
## Transport
1. Client opens HyperDHT secret stream to server public key (Noise, mutual key auth).
2. `ProtomuxRPC` is attached with `protocol: PROTOCOL` and shared encodings.
3. Client calls `handshake` before other RPCs (connection helper does this automatically).
4. Server may `push` events on named channels.
Bump `PROTOCOL_VERSION` on breaking argument/result shapes.
## Roles
| Role | Intent |
|------|--------|
| `viewer` | Read-only |
| `operator` | Mutate domain data |
| `admin` | Invite mint, clear, revoke, config |
| Role | Rank | Typical access |
|------|------|----------------|
| `viewer` | 1 | Read metrics, subscribe, list alerts |
| `operator` | 2 | Ack/silence alerts, run jobs, set alert config |
| `admin` | 3 | Mint invites, revoke peers, export snapshot |
Hierarchy: `admin > operator > viewer` (`roleAllows`).
Unknown methods default to **admin** required in `assertAllowed` if missing from `MethodRoles` — always register new methods.
Auth modes at handshake: public key (viewer), capability token / `pd1.` invite, admin seed proof, allowlist.
## Methods
| Method | Min role | Request args | Response (summary) |
|--------|----------|--------------|--------------------|
| `handshake` | viewer | `clientName`, `clientVersion`, optional `capability`, `adminProof` | role, auth, versions, features |
| `ping` | viewer | `{}` | `{ ok, pong, peerId }` |
| `getServerInfo` | viewer | `{}` | app, versions, host, peer count |
| `getAuthStatus` | viewer | `{}` | peerId, role, authMode, displayName |
| `listMessages` | viewer | `{ limit? }` | `{ messages: [...] }` |
| `getPresence` | viewer | `{}` | `{ peers: [...] }` |
| `postMessage` | operator | `{ text }` (12000 chars) | created message |
| `setDisplayName` | viewer | `{ name }` (140 chars) | updated label |
| `clearMessages` | admin | `{}` | success + system push |
| `mintInvite` | admin | `{ role?, ttlMs?, peerId?, alias? }` | `invite` (`pd1.…`), jti, exp |
| `listPeers` | admin | `{}` | live + policy peers |
| `revokePeer` | admin | `{ peerId }` (64 hex) | success; target dropped |
### Session
### Handshake request
| Method | Role | Notes |
|--------|------|-------|
| `handshake` | viewer | Negotiate version + elevate role |
| `ping` | viewer | Hot |
| `getServerInfo` | viewer | Agent metadata |
| `getAuthStatus` | viewer | Peer role / auth mode |
| `setDisplayName` | viewer | Label for audit / UI |
```json
{
"clientName": "peardata",
"clientVersion": "0.1.0",
"capability": "<optional HMAC token>",
"adminProof": { "nonce": "<hex>", "mac": "<hex>" }
}
```
### Node
### Handshake response
| Method | Role | Result |
|--------|------|--------|
| `getNodeInfo` | viewer | Hostname, CPUs, charts, sample interval |
| `getHealth` | viewer | `{ status, score, checks }` |
```json
{
"success": true,
"protocol": "peardata/rpc",
"protocolVersion": 1,
"schemaVersion": 1,
"role": "operator",
"peerId": "<64 hex>",
"serverTime": 0,
"auth": { "mode": "capability", "role": "operator" },
"features": { "hmacAuth": true, "invites": true, "room": true }
}
```
### Metrics discovery & query
### Auth modes (`auth.mode` / `session.authMode`)
| Method | Role | Args | Result |
|--------|------|------|--------|
| `listContexts` | viewer | — | Context catalog |
| `getContext` | viewer | `{ id }` | Charts in context |
| `listCharts` | viewer | — | Netdata-ish chart map |
| `getChart` | viewer | `{ id }` | Chart summary |
| `queryData` | viewer | `{ chart, after, before, points, group, tier }` | Time series |
| `getAllMetrics` | viewer | `{ format: json\|prometheus\|shell }` | Latest export |
| Mode | How obtained |
|------|----------------|
| `viewer` | Default after connect with no grant |
| `capability` | Valid capability / invite |
| `seed` | Valid admin proof from `SERVER_SEED` |
| `allowlist` / registered | Elevated via policy / admin keys (implementation in ACL + policy) |
`after` / `before`: absolute unix seconds, or relative (negative = relative to `before`/`now`), Netdata-style.
Exact labels depend on handshake path; UI shows `authMode` from `getAuthStatus`.
### Live subscriptions
## Pushes (server → client events)
| Method | Role | Args |
|--------|------|------|
| `subscribeMetrics` | viewer | `{ charts: string[]\|['*'], intervalMs }` |
| `unsubscribeMetrics` | viewer | — |
| `subscribeAnomalies` | viewer | — |
| `unsubscribeAnomalies` | viewer | — |
| Push | Payload |
### Anomalies & alerts
| Method | Role | Notes |
|--------|------|-------|
| `listAnomalies` | viewer | Recent events |
| `listAlerts` / `getAlert` | viewer | State + config |
| `setAlertConfig` | operator | Upsert threshold |
| `ackAlert` | operator | Clear until next breach |
| `silenceAlert` | operator | Disable temporarily |
### Jobs
| Method | Role | Known jobs |
|--------|------|------------|
| `listJobs` | viewer | — |
| `runJob` | operator | `collectOnce`, `snapshot`, `gcBuffers` |
| `cancelJob` | operator | By job id |
### Admin
| Method | Role |
|--------|------|
| `mintInvite` | admin |
| `listPeers` | admin |
| `revokePeer` | admin |
| `exportSnapshot` | admin |
## Pushes (server → client)
| Event | Payload |
|-------|---------|
| `push:metrics` | `{ samples: MetricSample[] }` |
| `push:anomaly` | `AnomalyEvent` |
| `push:alert` | Alert transition |
| `push:health` | `HealthSnapshot` |
| `push:job` | `JobRecord` |
| `push:system` | Generic notices |
## Errors
Handlers throw `Error` with `.code`:
| Code | Meaning |
|------|---------|
| `push:message` | `{ id, peerId, displayName, text, ts }` |
| `push:presence` | `{ peers: [...] }` |
| `push:system` | `{ type, ... }` e.g. `{ type: "cleared" }` |
Registered via `rpc.event` / `session.push`. Client `connection.js` binds all `Pushes` values.
## Invites
Envelope: `pd1.` + base64url(JSON):
```json
{
"v": 1,
"publicKeyHex": "<server>",
"capability": "<token>",
"role": "operator",
"jti": "...",
"expiresAt": null
}
```
Capability token: `base64url(payload).base64url(HMAC-SHA256)`.
Payload fields (canonical order for MAC): `v`, `role`, `peerId`, `exp`, `jti`, `iat`.
### Connection input classification
`classifyConnectionInput(string)` accepts:
| Input | Kind |
|-------|------|
| 64 hex chars | `publicKey` |
| `pd1.…` | `invite` (extracts key + capability) |
| other | error |
## Error codes
| Code | When |
|------|------|
| `RATE_LIMIT_EXCEEDED` | Peer over RPM budget |
| `PERMISSION_DENIED` | Role too low for method |
| `INVALID_ARGS` | Schema validation failed |
| `CONNECTION_TIMEOUT` | Client dial timeout |
| `RPC_ERROR` | Generic client-normalized failure |
| `UNKNOWN_ERROR` | Unclassified server handler error |
Clients should read `error.code` when present (`client/errors.js` preserves codes).
| `PERMISSION_DENIED` | Role too low |
| `RATE_LIMIT_EXCEEDED` | RPM exceeded |
| `INVALID_ARGS` | Schema failure |
| `NOT_CONNECTED` | Client-side |
| `CAPABILITY_*` | Invite/token issues |
## Versioning policy
1. Document every method in this file.
2. Add `MethodRoles` entry before implementing handlers.
3. Add `validateMethodArgs` case for mutating methods.
4. Add brittle tests for pure helpers; integration test for critical paths.
5. Bump `PROTOCOL_VERSION` when existing response shapes break.
6. Bump `SCHEMA_VERSION` when validation semantics change meaningfully.
1. Additive methods/fields: no version bump if old clients ignore unknowns.
2. Rename/remove/change meaning: bump `PROTOCOL_VERSION`; reject or compat-negotiate in handshake.
3. REST API versions (`v1`/`v2`/`v3`) are independent of RPC version but share the store.
## Related
## Example session
- [ARCHITECTURE.md](./ARCHITECTURE.md)
- [SECURITY.md](./SECURITY.md)
- [EXTENDING.md](./EXTENDING.md)
- [TESTING.md](./TESTING.md)
```text
client → handshake { clientName, clientVersion, capability? }
server → { role, protocolVersion, auth }
client → subscribeMetrics { charts: ['*'], intervalMs: 1000 }
server → push:metrics { samples: [...] } # ~1 Hz
client → queryData { chart: 'system.cpu', after: -300, points: 300 }
```