# Capability Spec: network-intra-relay **Package**: `@suaveplan/network-plugin-intra-relay` **Status**: CANONICAL. > Every requirement below carries a `**Verified by:**` line. ## Requirements ### Requirement: RelaySecurityManager authorises inbound relay messages The package SHALL export a `RelaySecurityManager` class that runs three checks in order against every inbound relay message: trusted-peer allowlist, `ed25519` signature validation, and per-peer + global token-bucket rate limit. The class SHALL throw a `NetworkIntraRelayError` carrying one of `RELAY_PEER_UNTRUSTED`, `RELAY_SIGNATURE_INVALID`, or `RELAY_RATE_LIMIT_EXCEEDED` on failure. The class SHALL accept a `RelaySecurityManagerConfig` carrying `{ policy: RelayAuthPolicy; rateLimit: RelayRateLimit; peerKeyResolver: PeerKeyResolver; clock?: () => number; signatureVerifier?: SignatureVerifier }`. **Verified by:** `src/security/relay-security-manager.spec-conformance.test.ts` #### Scenario: admits a peer in the trusted-set allowlist - **WHEN** `policy.trustedPeers` is `Set(["peer-a"])` and `authorize` is called with `senderPeerId: "peer-a"` and a valid signature - **THEN** the call does NOT throw #### Scenario: rejects a peer not in a non-empty allowlist - **WHEN** `policy.trustedPeers` is `Set(["peer-a"])` and `authorize` is called with `senderPeerId: "peer-b"` - **THEN** the call throws `NetworkIntraRelayError(RELAY_PEER_UNTRUSTED)` #### Scenario: empty allowlist admits any peer - **WHEN** `policy.trustedPeers` is the empty set - **THEN** any peer id is admitted (subject to the signature and rate-limit checks) #### Scenario: rejects bad signature with RELAY_SIGNATURE_INVALID - **WHEN** a message arrives with a signature that fails ed25519 verification against the resolved public key - **THEN** `authorize` throws `NetworkIntraRelayError(RELAY_SIGNATURE_INVALID)` #### Scenario: missing signature when policy requires one - **WHEN** `policy.requireSignature` is `true` and the input lacks a `signature` - **THEN** `authorize` throws `NetworkIntraRelayError(RELAY_SIGNATURE_INVALID)` #### Scenario: missing signature when policy does not require one - **WHEN** `policy.requireSignature` is `false` and the input lacks a `signature` - **THEN** `authorize` does NOT throw on the signature check #### Scenario: per-peer rate-limit exhaustion - **WHEN** a single peer consumes more than `rateLimit.burstSize` tokens before the clock advances enough to refill the bucket - **THEN** the next `authorize` call from that peer throws `NetworkIntraRelayError(RELAY_RATE_LIMIT_EXCEEDED)` with `context.scope === "per-peer"` #### Scenario: global rate-limit exhaustion separate from per-peer - **WHEN** multiple peers collectively exhaust the global bucket while each peer's individual bucket still has tokens - **THEN** further `authorize` calls throw `NetworkIntraRelayError(RELAY_RATE_LIMIT_EXCEEDED)` with `context.scope === "global"` #### Scenario: isRateLimited inspection without consuming - **WHEN** `isRateLimited(peerId)` is called - **THEN** the result reflects whether `authorize` would currently fail on the rate-limit check, without consuming a token ### Requirement: error-code registry extended with three security codes The `NETWORK_INTRA_RELAY_CODES` registry SHALL include `RELAY_SIGNATURE_INVALID`, `RELAY_PEER_UNTRUSTED`, and `RELAY_RATE_LIMIT_EXCEEDED` as frozen members. **Verified by:** `src/error-codes.spec-conformance.test.ts` #### Scenario: registry exposes the three codes - **WHEN** `NETWORK_INTRA_RELAY_CODES` is inspected - **THEN** it contains `RELAY_SIGNATURE_INVALID`, `RELAY_PEER_UNTRUSTED`, `RELAY_RATE_LIMIT_EXCEEDED` mapped to `"NET_INTRA_RELAY."` values ### Requirement: IntraRelayPlugin exposes a security getter `IntraRelayPlugin` SHALL accept an optional `security?: RelaySecurityManagerConfig` field in its constructor config and SHALL expose a `security` getter returning `RelaySecurityManager | null`. Omitting the field SHALL leave security disabled (`security` returns `null`); supplying it SHALL construct a `RelaySecurityManager` wired to the supplied policy / rate-limit / resolver. **Verified by:** `src/plugin/intra-relay-plugin.spec-conformance.test.ts` #### Scenario: security getter returns null when no config supplied - **WHEN** `IntraRelayPlugin` is constructed without `security` - **THEN** the `security` getter returns `null` #### Scenario: security getter returns a manager when configured - **WHEN** `IntraRelayPlugin` is constructed with a valid `security` config - **THEN** the `security` getter returns a `RelaySecurityManager` whose `isRateLimited` and `authorize` methods are wired correctly ### Requirement: types are exported for consumers The package SHALL export `RelayAuthPolicy`, `RelayRateLimit`, and `RelaySecurityCheckInput` types from the root barrel and from a subpath export at `./security/security`. **Verified by:** `tests/dep-graph.test.ts`, package `exports` map.