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,20 @@
# @suaveplan/network-plugin-auth - Mesh membership authentication and cross-mesh relay authorization: join verifier, capability-token issuer, cross-mesh trust validator
## Summary
Mesh membership authentication and cross-mesh relay authorization. Bridges `@suaveplan/auth` into the mesh: peer join verification, capability-tier-bound tokens, cross-mesh trust store.
<!-- begin:auto-docs -->
## Documentation tree
- [issuer](./src/issuer/capability-token-issuer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [capability-token-issuer](./src/issuer/capability-token-issuer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [trust](./src/trust/cross-mesh-trust-validator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [cross-mesh-trust-validator](./src/trust/cross-mesh-trust-validator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [verifier](./src/verifier/mesh-membership-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [mesh-membership-verifier](./src/verifier/mesh-membership-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the mesh-auth plugin. Five codes cover the full surface: `JOIN_REJECTED` when a peer…
<!-- end:auto-docs --> - [capability-token-issuer](./src/issuer/capability-token-issuer.md) — <!-- begin:auto-docs --> _(no submodules)_- [trust](./src/trust/cross-mesh-trust-validator.md) — <!-- begin:auto-docs --> _(no submodules)_- [cross-mesh-trust-validator](./src/trust/cross-mesh-trust-validator.md) — <!-- begin:auto-docs --> _(no submodules)_- [verifier](./src/verifier/mesh-membership-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [mesh-membership-verifier](./src/verifier/mesh-membership-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the mesh-auth plugin. Five codes cover the full surface: `JOIN_REJECTED` when a peer…
- [capability-token-issuer](./src/issuer/capability-token-issuer.md) — <!-- begin:auto-docs --> _(no submodules)_- [trust](./src/trust/cross-mesh-trust-validator.md) — <!-- begin:auto-docs --> _(no submodules)_- [cross-mesh-trust-validator](./src/trust/cross-mesh-trust-validator.md) — <!-- begin:auto-docs --> _(no submodules)_- [verifier](./src/verifier/mesh-membership-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [mesh-membership-verifier](./src/verifier/mesh-membership-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the mesh-auth plugin. Five codes cover the full surface: `JOIN_REJECTED` when a peer…
- [capability-token-issuer](./src/issuer/capability-token-issuer.md) — <!-- begin:auto-docs --> _(no submodules)_- [trust](./src/trust/cross-mesh-trust-validator.md) — <!-- begin:auto-docs --> _(no submodules)_- [cross-mesh-trust-validator](./src/trust/cross-mesh-trust-validator.md) — <!-- begin:auto-docs --> _(no submodules)_- [verifier](./src/verifier/mesh-membership-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [mesh-membership-verifier](./src/verifier/mesh-membership-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the mesh-auth plugin. Five codes cover the full surface: `JOIN_REJECTED` when a peer…
- [capability-token-issuer](./src/issuer/capability-token-issuer.md) — <!-- begin:auto-docs --> _(no submodules)_- [trust](./src/trust/cross-mesh-trust-validator.md) — <!-- begin:auto-docs --> _(no submodules)_ - [cross-mesh-trust-validator](./src/trust/cross-mesh-trust-validator.md) — <!-- begin:auto-docs --> _(no submodules)_- [verifier](./src/verifier/mesh-membership-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_ - [mesh-membership-verifier](./src/verifier/mesh-membership-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the mesh-auth plugin. Five codes cover the full surface: `JOIN_REJECTED` when a peer…

View File

@@ -0,0 +1,61 @@
# MESH_AUTH_CODES
## Purpose
Frozen registry of error codes for the mesh-auth plugin. Five codes cover the full surface: `JOIN_REJECTED` when a peer fails membership verification, `TOKEN_EXPIRED` when a capability token's `expiresAt` is past, `TOKEN_INVALID_SIGNATURE` when the server signature does not verify, `CAPABILITY_DENIED` when a peer requests a capability outside its issued set, and `TRUST_NOT_ESTABLISHED` when cross-mesh relay traffic crosses an unknown boundary. The companion `MeshAuthError` class extends the cross-package `SuaveplanError` so consumers can pattern-match on `code` rather than parse messages.
## Features
- Five frozen codes registered under the `NET_AUTH` namespace.
- `MeshAuthError extends SuaveplanError<MeshAuthErrorCode>`.
- `name: "MeshAuthError"` on every instance — useful for log filters and dashboard alerts.
- `cause` and `context` propagation via `SuaveplanErrorOptions`.
- Codes are stable across versions — once registered they never disappear, only new codes are appended.
## Usage: Basic Example
```ts
import {
MESH_AUTH_CODES,
MeshAuthError,
} from "@suaveplan/network-plugin-auth";
throw new MeshAuthError(
MESH_AUTH_CODES.JOIN_REJECTED,
"auth.join-rejected",
{ context: { peerId: "alice", meshId: "m" } }
);
```
## Usage: Advanced Example
```ts
import {
MESH_AUTH_CODES,
MeshAuthError,
} from "@suaveplan/network-plugin-auth";
try {
await issuer.validate(token);
} catch (err) {
if (err instanceof MeshAuthError) {
if (err.code === MESH_AUTH_CODES.TOKEN_EXPIRED) {
return await issuer.renew(token);
}
if (err.code === MESH_AUTH_CODES.TOKEN_INVALID_SIGNATURE) {
await reportSecurityIncident(err.context);
}
}
throw err;
}
```
## API Reference
- `MESH_AUTH_CODES` — frozen `{ JOIN_REJECTED, TOKEN_EXPIRED, TOKEN_INVALID_SIGNATURE, CAPABILITY_DENIED, TRUST_NOT_ESTABLISHED }`.
- `type MeshAuthErrorCode` — union of registered code values.
- `class MeshAuthError extends SuaveplanError<MeshAuthErrorCode>` — typed throwable for every plugin failure mode.
## Implementation Notes
`TOKEN_INVALID_SIGNATURE` is treated as a security-relevant signal — callers SHOULD log it loudly because a valid client never produces it. The other four codes are recoverable: `JOIN_REJECTED` is a clean refusal, `TOKEN_EXPIRED` is fixable by renewal, `CAPABILITY_DENIED` is a permission boundary, and `TRUST_NOT_ESTABLISHED` should trigger an out-of-band trust handshake. The package barrel side-effect-imports `./error-codes.js` so subpath consumers register the codes too.

View File

@@ -0,0 +1,79 @@
# CapabilityTokenIssuer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Issues server-signed capability tokens for mesh peers and validates incoming tokens. The token shape pins the peer's `tier`, allowed `capabilities`, and a `serverSignatureHex` covering a deterministic JSON canonicalisation. Decoupling the signer via `TokenSigner` lets consumers wire Ed25519 (production), HMAC (tests), or a hardware-backed signer without changes to the issuer surface.
## Features
- Deterministic JSON canonicalisation with sorted `capabilities`.
- Configurable `defaultTtlMs` (default 1 hour); per-call `ttlMs` overrides.
- Injectable `clock` for deterministic tests.
- `validate` throws `TOKEN_EXPIRED` then `TOKEN_INVALID_SIGNATURE` in that order so callers can pattern-match.
- `renew` re-issues without re-verifying the previous signature — callers that want pre-renewal checks must call `validate` first.
## Usage: Basic Example
```ts
import {
CapabilityTokenIssuer,
type TokenSigner,
} from "@suaveplan/network-plugin-auth";
const signer: TokenSigner = {
sign: async (bytes) => "deadbeef",
verify: async () => true,
};
const issuer = new CapabilityTokenIssuer({ signer });
const token = await issuer.issue("alice", "mesh-A", 0, ["host"]);
await issuer.validate(token);
```
## Usage: Advanced Example
```ts
import {
CapabilityTokenIssuer,
MESH_AUTH_CODES,
MeshAuthError,
} from "@suaveplan/network-plugin-auth";
let now = 1_000_000;
const issuer = new CapabilityTokenIssuer({
signer,
defaultTtlMs: 60_000,
clock: () => now,
});
const token = await issuer.issue("alice", "mesh-A", 1, ["subhost"]);
now += 70_000; // past expiry
try {
await issuer.validate(token);
} catch (err) {
if (
err instanceof MeshAuthError &&
err.code === MESH_AUTH_CODES.TOKEN_EXPIRED
) {
const renewed = await issuer.renew(token);
// renewed.expiresAt > now
}
}
```
## API Reference
- `interface CapabilityToken { peerId, meshId, tier, capabilities, issuedAt, expiresAt, serverSignatureHex }`.
- `interface TokenSigner { sign, verify }`.
- `interface CapabilityTokenIssuerConfig { signer, defaultTtlMs?, clock? }`.
- `issue(peerId, meshId, tier, capabilities, ttlMs?): Promise<CapabilityToken>`.
- `validate(token): Promise<void>` — throws `TOKEN_EXPIRED` / `TOKEN_INVALID_SIGNATURE`.
- `renew(token, ttlMs?): Promise<CapabilityToken>`.
## Implementation Notes
The canonicalisation sorts `capabilities` alphabetically before JSON-stringify so a permutation of the same set produces the same signature input. We use JSON rather than a custom binary frame because (a) the token is small, (b) consumers can debug it visually, and (c) `JSON.stringify` is fast and well-specified for our scalar/string-only field set. Renewal explicitly does NOT verify the prior signature — that would require the renewer to also be the verifier, which is not always true (e.g. a hardware HSM signs but the issuer doesn't have the verify key locally). Callers that want defense-in-depth should call `validate` before `renew`.

View File

@@ -0,0 +1,83 @@
# CrossMeshTrustValidator
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Tracks bidirectional or directional trust between mesh ids and rejects cross-mesh relay/backbone traffic that would cross an unknown boundary. Centralising trust state in a single validator means relay and backbone pipelines query one source of truth, eliminating the class of bugs where one path enforces trust and another path bypasses it.
## Features
- `TrustRelation` shape pins `fromMeshId`, `toMeshId`, allowed `directions` (`relay` / `backbone`), `establishedAt`, optional `expiresAt`.
- `add` / `remove` for explicit lifecycle.
- `assertTrusts(fromMeshId, toMeshId, direction)` throws `TRUST_NOT_ESTABLISHED` for unknown, expired, or direction-disallowed pairs.
- Initial set seedable via constructor `initial` array.
- Injectable `clock` for deterministic tests.
- `list()` exposed for observability dashboards.
## Usage: Basic Example
```ts
import { CrossMeshTrustValidator } from "@suaveplan/network-plugin-auth";
const trust = new CrossMeshTrustValidator({
initial: [
{
fromMeshId: "mesh-A",
toMeshId: "mesh-B",
directions: ["relay", "backbone"],
establishedAt: Date.now(),
},
],
});
trust.assertTrusts("mesh-A", "mesh-B", "relay");
```
## Usage: Advanced Example
```ts
import {
CrossMeshTrustValidator,
MESH_AUTH_CODES,
MeshAuthError,
} from "@suaveplan/network-plugin-auth";
const trust = new CrossMeshTrustValidator({
clock: () => Date.now(),
});
trust.add({
fromMeshId: "mesh-A",
toMeshId: "mesh-C",
directions: ["relay"], // backbone explicitly disallowed
establishedAt: Date.now(),
expiresAt: Date.now() + 86_400_000,
});
try {
trust.assertTrusts("mesh-A", "mesh-C", "backbone");
} catch (err) {
if (
err instanceof MeshAuthError &&
err.code === MESH_AUTH_CODES.TRUST_NOT_ESTABLISHED
) {
// operator must establish backbone trust before this works
}
}
```
## API Reference
- `interface TrustRelation { fromMeshId, toMeshId, directions, establishedAt, expiresAt? }`.
- `type TrustDirection = "relay" | "backbone"`.
- `interface CrossMeshTrustValidatorConfig { clock?, initial? }`.
- `add(relation)`, `remove(fromMeshId, toMeshId): boolean`.
- `assertTrusts(fromMeshId, toMeshId, direction)` — throws `TRUST_NOT_ESTABLISHED` on any failure.
- `list(): readonly TrustRelation[]`.
- `clear()`.
## Implementation Notes
Trust is intentionally directional — `(A, B)` and `(B, A)` are distinct keys. Bidirectional trust requires two `add` calls. This avoids the class of bugs where one mesh trusts another but the reverse direction was assumed without being explicit. The `directions` field further restricts what kinds of cross-mesh traffic are allowed; a relay-only pairing cannot host backbone traffic until an operator explicitly upgrades the relation. `assertTrusts` checks expiry before direction so an expired relation surfaces as expired regardless of which direction was requested.

View File

@@ -0,0 +1,86 @@
# MeshMembershipVerifier
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Walks a `JoinRequest` through a four-step verification: token validation via the injected `AuthProvider`, public-key match, mesh id match, capability allow-list. Returns a `JoinResult` with the assigned tier on success or throws `MeshAuthError` on the first failure. Decoupling auth via the `AuthProvider` interface keeps this plugin from depending directly on `@suaveplan/auth` while still being easy to wire up.
## Features
- Lowest-tier-wins capability mapping: `host` → 0, `subhost`/`bridge` → 1, others → 2.
- Mesh-id mismatch raises `JOIN_REJECTED`.
- Public-key mismatch raises `JOIN_REJECTED` (when the auth provider returns a public key).
- Disallowed capability raises `CAPABILITY_DENIED` with the offending name in `context`.
- Empty `claimedCapabilities` defaults to tier 2 (most restrictive) so a join with no capabilities still produces a deterministic result.
- `AuthProvider` is structural — any object with `verifyToken(token)` can plug in.
## Usage: Basic Example
```ts
import {
MeshMembershipVerifier,
type AuthProvider,
} from "@suaveplan/network-plugin-auth";
const authProvider: AuthProvider = {
verifyToken: async (t) => ({ identity: "alice" }),
};
const verifier = new MeshMembershipVerifier({
authProvider,
meshId: "mesh-A",
allowedCapabilities: ["host", "subhost", "relay"],
});
const result = await verifier.verifyJoinRequest({
peerId: "alice",
meshId: "mesh-A",
authToken: "...",
publicKeyHex: "...",
claimedCapabilities: ["host"],
});
```
## Usage: Advanced Example
```ts
import {
MESH_AUTH_CODES,
MeshAuthError,
MeshMembershipVerifier,
} from "@suaveplan/network-plugin-auth";
const verifier = new MeshMembershipVerifier({
authProvider,
meshId: "mesh-A",
allowedCapabilities: ["relay", "audit"],
});
try {
const result = await verifier.verifyJoinRequest(request);
promotePeer(result);
} catch (err) {
if (
err instanceof MeshAuthError &&
err.code === MESH_AUTH_CODES.CAPABILITY_DENIED
) {
await retryWithReducedCapabilities(request);
} else {
throw err;
}
}
```
## API Reference
- `type Capability = "host" | "subhost" | "relay" | "bridge" | "audit" | "create_session" | "join_session"`.
- `interface AuthProvider { verifyToken(token): Promise<{ identity, publicKeyHex? }> }`.
- `interface JoinRequest { peerId, meshId, authToken, publicKeyHex, claimedCapabilities }`.
- `interface JoinResult { accepted, peerId, assignedTier, capabilities, reason? }`.
- `interface MeshMembershipVerifierConfig { authProvider, meshId, allowedCapabilities }`.
- `verifyJoinRequest(request): Promise<JoinResult>`.
## Implementation Notes
The verifier intentionally treats every failure as a thrown exception rather than a `{ accepted: false, reason }` result so callers cannot accidentally proceed past a security check by ignoring a return value. The cost is one branch per call site, which is the correct shape for security-critical paths. The `publicKeyHex` mismatch check is gated on the auth provider returning a key; providers that only verify identity (no key binding) skip the check, matching the principle that we don't fabricate a verification we don't actually perform.