first commit
This commit is contained in:
20
docs/networking/network/network-plugin-auth/README.md
Normal file
20
docs/networking/network/network-plugin-auth/README.md
Normal 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…
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user