/**
 * MeshRpc — the signed, recipient-bound machine-to-machine command layer for the
 * Multi-Machine Session Pool (spec §L0). A thin request/response envelope over the
 * existing authenticated HTTP/tunnel channel carrying a small command set:
 * place / claim / release / transfer / capacity-report / session-status /
 * secret-share.
 *
 * This module is PURE LOGIC — the canonical envelope, the 5-step receipt
 * verification, and the per-command RBAC gate. Crypto (Ed25519 sign/verify), the
 * nonce store, the peer registry, and the router/ownership reads are injected as
 * seams so the dangerous parts (recipient-binding, replay, authorization) are
 * unit-testable with in-memory fakes. The HTTP transport + production wiring sit
 * on top and call these functions.
 *
 * Two independent gates protect every command (spec §L0 Invariant):
 *   (a) verifyEnvelope — WHO sent it: a valid Ed25519 signature from a registered
 *       peer of the same agent, cryptographically bound to THIS recipient, fresh
 *       (unseen nonce) and timely (timestamp within tolerance).
 *   (b) checkCommandRBAC — whether they MAY issue it: the per-command role check
 *       (place/transfer → router; claim → placement-target or failover-router;
 *       release → owner or failover-router; reports/secret-share → any peer).
 * The CAS (§L3) is the final correctness fence; these gates refuse an
 * unauthorized command at the door, before any state read/write.
 */
/** The canonical bytes the signature covers — field-ordered, recipient included. */
export function canonicalizeEnvelope(e) {
    return JSON.stringify([e.sender, e.recipient, e.command, e.epoch, e.nonce, e.timestamp]);
}
/** Build + sign an outgoing command envelope naming THIS sender + the recipient. */
export function signEnvelope(parts, sign) {
    return { ...parts, signature: sign(canonicalizeEnvelope(parts)) };
}
/**
 * The 5-step receipt verification (spec §L0), evaluated IN ORDER. Pure: it does
 * NOT record the nonce — the caller records it ONLY on a fully-accepted command
 * (after RBAC), so a rejected command never burns a nonce.
 */
export function verifyEnvelope(env, deps) {
    const tol = deps.clockToleranceMs ?? 30000;
    // (1) recipient-bound: a command signed for A cannot be replayed to C.
    if (env.recipient !== deps.selfMachineId)
        return { ok: false, reason: 'wrong-recipient' };
    // (2) signature valid for the claimed sender's registered key.
    if (!deps.verify(canonicalizeEnvelope(env), env.signature, env.sender))
        return { ok: false, reason: 'signature-invalid' };
    // (3) sender is a registered peer of this agent.
    if (!deps.isRegisteredPeer(env.sender))
        return { ok: false, reason: 'unknown-sender' };
    // (4) nonce unseen (replay guard).
    if (deps.seenNonce(env.sender, env.nonce))
        return { ok: false, reason: 'replayed-nonce' };
    // (5) timestamp within tolerance.
    if (Math.abs(deps.now() - env.timestamp) > tol)
        return { ok: false, reason: 'stale-timestamp' };
    return { ok: true, reason: 'ok' };
}
/**
 * Per-command authorization gate (spec §L0). A valid signature proves WHO; this
 * proves they MAY. Runs BEFORE any state read/write. `sender` is the verified
 * envelope sender; `command` is its command.
 */
export function checkCommandRBAC(command, sender, deps) {
    const isRouter = deps.routerHolder() === sender;
    switch (command.type) {
        case 'place':
        case 'transfer':
        case 'deliverMessage':
            // Router-only (the router forwards inbound messages to the session owner — §L4).
            return isRouter ? { ok: true, reason: 'ok' } : { ok: false, reason: 'not-router' };
        case 'drain':
            // WS1.2: router-only with its OWN refusal reason (spec: "a NEW mesh verb
            // with its own router-only RBAC case"). Only the transfer planner — the
            // lease-holder — may order an owner to drain; a peer (even the transfer
            // TARGET) may not.
            return isRouter ? { ok: true, reason: 'ok' } : { ok: false, reason: 'drain-unauthorized' };
        case 'claim': {
            // The router's assigned target for this session, OR the router on a failover re-place.
            if (deps.placementTargetOf(command.session) === sender)
                return { ok: true, reason: 'ok' };
            if (isRouter && command.failover === true)
                return { ok: true, reason: 'ok' };
            return { ok: false, reason: 'claim-unauthorized' };
        }
        case 'release': {
            // The current owner, OR the router during a fenced failover.
            if (deps.ownerOf(command.session) === sender)
                return { ok: true, reason: 'ok' };
            if (isRouter && command.failover === true)
                return { ok: true, reason: 'ok' };
            return { ok: false, reason: 'release-unauthorized' };
        }
        case 'slice-renew': {
            // WS5.2 R7a — OPERATOR-MANDATE-GATED, deliberately its OWN case (NOT the
            // "any registered peer" read class). verifyEnvelope already proved WHO; this
            // proves the operator's mandate AUTHORIZES this requester to borrow this
            // account's spend. DENY-BY-DEFAULT: with no `authorizeSliceRenew` seam wired,
            // or when the mandate does not authorize (sender, account, requesting fp), the
            // verb is refused. The FENCED-single-writer "only the lease holder issues"
            // check lives in the handler/SliceIssuer (a non-holder still refuses there);
            // the gate's job is the mandate authorization, before any issuance work.
            const authd = deps.authorizeSliceRenew?.({
                sender,
                mandateId: command.mandateId,
                accountId: command.accountId,
                requestingMachineFp: command.requestingMachineFp,
            }) ?? false;
            return authd ? { ok: true, reason: 'ok' } : { ok: false, reason: 'slice-renew-unauthorized' };
        }
        case 'account-follow-me-mandate-deliver': {
            // WS5.2 R4a — deliberately its OWN case (NOT the "any registered peer" read class). The
            // RBAC gate proves the sender is a registered ACTIVE peer (verifyEnvelope already did the
            // crypto) AND that follow-me is enabled with this sender bound as the operator machine.
            // DENY-BY-DEFAULT: with no `authorizeMandateDeliver` seam wired, or when the sender is not
            // the trusted operator machine / the feature is dark, the verb is refused HERE — before the
            // handler runs. The LOAD-BEARING authority is still the R4a signature re-verification in the
            // handler (this gate is the coarse, fail-closed pre-filter).
            const authd = deps.authorizeMandateDeliver?.({ sender }) ?? false;
            return authd ? { ok: true, reason: 'ok' } : { ok: false, reason: 'mandate-deliver-unauthorized' };
        }
        case 'commitment-mutate':
            // MUTATING verb, deliberately its OWN case (COMMITMENTS-COHERENCE-SPEC
            // §3.4/§5): any registered peer may issue it under the same-operator
            // posture — meaning RBAC adds NO authorization beyond verifyEnvelope,
            // WHICH IS THE INTENDED SOLE AUTHORITY (stated so no reviewer assumes
            // a role check exists). The owner re-validates every transition through
            // the unchanged commitment state machine; replay is fenced by the
            // durable owner-side opKey window.
            return { ok: true, reason: 'ok' };
        case 'capacity-report':
        case 'session-status':
        case 'pool-stream-ticket':
        case 'journal-sync':
        case 'working-set-pull':
        case 'topic-profile-pull':
        case 'commitments-sync':
        case 'preferences-sync':
        case 'state-snapshot':
        case 'pool-view-fetch':
        case 'secret-share':
            // Read/observe class (or e2e-encrypted) — any registered peer (already
            // proven a registered peer by verifyEnvelope). journal-sync joins this
            // class: it serves/applies own-stream coherence-journal deltas, which are
            // self-binding (first-hop sender binding fences forged entries in the
            // applier) — no router/owner role is required. working-set-pull joins
            // it (WORKING-SET-HANDOFF-SPEC §3.2): it serves OWN jailed working
            // files behind a fresh-manifest allowlist (disclosure accepted for
            // registered same-operator peers, §3.1 note); the handler itself stays
            // dark unless replication is explicitly enabled (§3.7 gate).
            // topic-profile-pull joins it (TOPIC-PROFILE-SPEC §5.3): it serves OWN
            // per-topic profile entries — disclosure-only for registered
            // same-operator peers; the receiver revalidates every field (§10.2)
            // before anything persists, so the verb carries reach, not authority.
            // pool-view-fetch joins it (MULTI-MACHINE-SEAMLESSNESS-SPEC §WS4.4): the
            // verb is reachable by any registered peer, but its HANDLER is the real
            // gate — it serves a private view body ONLY when the carried user-auth
            // ASSERTION verifies (audience-bound to this holder + this view + this
            // method, single-use, signed by the authenticated sender) AND the
            // holder's own per-view authorization passes. The probeOnly shape
            // discloses only existence (not the body) to a registered peer.
            // state-snapshot joins it (multi-machine-replicated-store-foundation
            // §6.1/§6.3): the HOLDER serves a SINGLE-ORIGIN snapshot of a store it
            // authored to a registered same-operator peer. It is self-binding —
            // origin === the authenticated sender end-to-end, and the recovering
            // machine re-validates every record's origin before landing (a
            // cross-origin record rejects the whole snapshot), so no router/owner role
            // is required; the handler itself stays dark unless the store is enabled.
            return { ok: true, reason: 'ok' };
        default:
            return { ok: false, reason: 'claim-unauthorized' };
    }
}
/**
 * Full acceptance gate: verify (who) THEN RBAC (may). Returns the first failure's
 * reason. The caller records the nonce ONLY when this returns ok (so a rejected
 * command never consumes a nonce). Convenience over verifyEnvelope + checkCommandRBAC.
 */
export function acceptEnvelope(env, verifyDeps, rbacDeps) {
    const v = verifyEnvelope(env, verifyDeps);
    if (!v.ok)
        return v;
    return checkCommandRBAC(env.command, env.sender, rbacDeps);
}
/** HTTP status for each rejection reason — auth failures 401/403, freshness 409, unimplemented 501. */
function statusForReason(reason) {
    switch (reason) {
        case 'wrong-recipient':
        case 'signature-invalid':
        case 'unknown-sender':
            return 401;
        case 'not-router':
        case 'claim-unauthorized':
        case 'release-unauthorized':
        case 'drain-unauthorized':
        case 'slice-renew-unauthorized':
        case 'mandate-deliver-unauthorized':
            return 403;
        case 'replayed-nonce':
        case 'stale-timestamp':
            return 409;
        case 'no-handler':
            return 501;
        default:
            return 400;
    }
}
/**
 * The receive side of MeshRpc — transport-agnostic (the HTTP route, or a tunnel
 * carrier, calls `dispatch(envelope)`). Runs the two gates (verify THEN rbac),
 * records the nonce ONLY on full accept (a rejected command never burns a nonce),
 * audits rejections, then routes to the registered handler. Returns a result +
 * an HTTP status so any transport can map it.
 */
export class MeshRpcDispatcher {
    d;
    constructor(deps) {
        this.d = deps;
    }
    async dispatch(env) {
        const accept = acceptEnvelope(env, this.d.verify, this.d.rbac);
        if (!accept.ok) {
            this.d.onReject?.(env, accept.reason);
            this.d.logger?.(`[mesh-rpc] rejected ${env.command?.type} from ${env.sender}: ${accept.reason}`);
            return { ok: false, reason: accept.reason, status: statusForReason(accept.reason) };
        }
        // Accepted — burn the nonce so an immediate replay is caught.
        this.d.recordNonce(env.sender, env.nonce);
        const handler = this.d.handlers[env.command.type];
        if (!handler) {
            return { ok: false, reason: 'no-handler', status: statusForReason('no-handler') };
        }
        const result = await handler(env.command, env.sender, env);
        return { ok: true, result };
    }
}
//# sourceMappingURL=MeshRpc.js.map