/**
 * PoolLinkAssertion — the cross-machine user-authentication assertion for WS4.4
 * (MULTI-MACHINE-SEAMLESSNESS-SPEC §WS4.4 "Links that survive machine
 * boundaries"). SECURITY-SENSITIVE.
 *
 * When a tunnel-fronting machine proxies a `/view/:id` (or dashboard asset)
 * request to the machine that actually HOLDS the content, it must tell the
 * holder "I, the fronting machine, validated the END USER's credential (their
 * PIN session / view token)" — WITHOUT forwarding the raw credential. Each
 * machine's PIN secret and authToken NEVER cross the boundary (spec §WS4.4 b/e):
 * the holder makes its own authorization decision, but it needs to trust that
 * the user really authenticated at the edge.
 *
 * This module is PURE LOGIC — the canonical assertion bytes, the construction,
 * and the verification gate. Crypto (Ed25519 sign/verify, reusing
 * MachineIdentity.sign/verify), the single-use jti store, the peer-key lookup,
 * and the clock are injected as seams so the dangerous parts (audience-binding,
 * replay, expiry) are unit-testable with in-memory fakes. The HTTP transport +
 * production wiring sit on top and call these functions.
 *
 * Three independent properties protect every assertion (spec §WS4.4 e):
 *   (1) AUDIENCE-BOUND — the signed bytes name the EXPECTED holder fingerprint,
 *       the SPECIFIC view id, and the HTTP method. A capture cannot be replayed
 *       against another resource, another holder, or another verb.
 *   (2) SIGNED by the fronting machine's mesh key — the holder verifies the
 *       signature against the EXPECTED fronting machine's REGISTERED public key,
 *       so only a registered peer can mint a valid assertion.
 *   (3) SINGLE-USE within a short TTL — the jti is recorded by the holder on
 *       first accept; a replay inside the window is rejected, and the TTL bounds
 *       the window beyond which the assertion is stale regardless.
 *
 * The fronting machine is a DUMB RELAY: it validates the user credential locally
 * (its own concern), mints THIS assertion, and forwards it. It substitutes NO
 * machine/mesh credential for the user credential at the holder — the assertion
 * only attests "a user authenticated at the edge for this exact resource", and
 * the holder still applies its own per-view authorization on top.
 */
/** The canonical bytes the signature covers — field-ordered, audience included.
 *  Excludes `signature` itself. Stable ordering so sign/verify agree. */
export function canonicalizePoolLinkAssertion(a) {
    return JSON.stringify([
        a.iss,
        a.aud.holderFingerprint,
        a.aud.viewId,
        a.aud.method,
        a.jti,
        a.iat,
        a.exp,
        a.userAuth,
    ]);
}
/** Default assertion lifetime (ms). Short by design — the proxy hop is sub-second. */
export const DEFAULT_POOL_LINK_TTL_MS = 30_000;
/**
 * Hard ceiling (ms) on the TTL SPAN (`exp - iat`) a holder will honor, regardless
 * of what the issuer minted. A misbehaving (but registered) peer could otherwise
 * mint a far-future `exp` and pin its jti in the holder's single-use store for
 * that whole span — an unbounded-retention DoS. The holder rejects any span over
 * this ceiling as `ttl-too-long` BEFORE the jti is ever recorded. 5 min is far
 * above the sub-second proxy hop + the default 30s mint, so it never rejects a
 * legitimate assertion. (P19 — bounded by construction.)
 */
export const DEFAULT_POOL_LINK_MAX_TTL_MS = 5 * 60_000;
/**
 * Build + sign a pool-link assertion naming THIS fronting machine as issuer and
 * binding it to the (holder, view, method) audience. The method is upper-cased
 * so the audience-binding is case-insensitive on the verb (HTTP methods are
 * case-insensitive by spec, but we canonicalize to avoid an accidental mismatch).
 */
export function mintPoolLinkAssertion(audience, userAuth, deps) {
    const iat = deps.now();
    const exp = iat + (deps.ttlMs ?? DEFAULT_POOL_LINK_TTL_MS);
    const aud = {
        holderFingerprint: audience.holderFingerprint,
        viewId: audience.viewId,
        method: audience.method.toUpperCase(),
    };
    const unsigned = {
        iss: deps.selfFingerprint,
        aud,
        jti: deps.mintJti(),
        iat,
        exp,
        userAuth,
    };
    return { ...unsigned, signature: deps.sign(canonicalizePoolLinkAssertion(unsigned)) };
}
/**
 * Verify an assertion at the HOLDER, evaluated IN ORDER (spec §WS4.4 e). PURE:
 * it does NOT record the jti — the caller records it ONLY on a fully-accepted
 * assertion (so a rejected one never burns a jti, and a replay is caught by the
 * EXPECTED `requested` audience matching the stored jti). The `requested`
 * argument is the audience derived from the ACTUAL incoming request (this
 * machine's own fingerprint, the view id in the URL, the request method) — the
 * assertion's `aud` must match it exactly, which is what makes the binding real:
 * a captured assertion presented for a different view/method is rejected.
 */
export function verifyPoolLinkAssertion(assertion, requested, deps) {
    // (0) shape
    if (!assertion ||
        typeof assertion.iss !== 'string' ||
        !assertion.aud ||
        typeof assertion.aud.holderFingerprint !== 'string' ||
        typeof assertion.aud.viewId !== 'string' ||
        typeof assertion.aud.method !== 'string' ||
        typeof assertion.jti !== 'string' ||
        typeof assertion.iat !== 'number' ||
        typeof assertion.exp !== 'number' ||
        typeof assertion.signature !== 'string') {
        return { ok: false, reason: 'malformed' };
    }
    // (1) audience-bound: holder fingerprint must be THIS machine.
    if (assertion.aud.holderFingerprint !== deps.selfFingerprint) {
        return { ok: false, reason: 'wrong-holder' };
    }
    // (2) audience-bound: the assertion's view must match the REQUESTED view,
    //     and both must match the incoming URL — no wildcard, no cross-resource.
    if (assertion.aud.viewId !== requested.viewId) {
        return { ok: false, reason: 'wrong-view' };
    }
    // (3) audience-bound: method match (case-insensitive via canonicalization).
    if (assertion.aud.method !== requested.method.toUpperCase()) {
        return { ok: false, reason: 'wrong-method' };
    }
    // (4) the authenticated transport sender must equal the claimed issuer — a
    //     captured assertion cannot be presented by a different machine.
    if (deps.expectedIssuer !== null && assertion.iss !== deps.expectedIssuer) {
        return { ok: false, reason: 'unexpected-issuer' };
    }
    // (5) signature valid for the claimed issuer's REGISTERED key.
    const pem = deps.resolveIssuerPublicKeyPem(assertion.iss);
    if (!pem)
        return { ok: false, reason: 'unknown-issuer' };
    const canonical = canonicalizePoolLinkAssertion({
        iss: assertion.iss,
        aud: assertion.aud,
        jti: assertion.jti,
        iat: assertion.iat,
        exp: assertion.exp,
        userAuth: assertion.userAuth,
    });
    if (!deps.verify(canonical, assertion.signature, pem)) {
        return { ok: false, reason: 'signature-invalid' };
    }
    // (6) freshness: TTL-span ceiling + expiry + not-yet-valid (small skew tolerance).
    const now = deps.now();
    const skew = deps.clockSkewToleranceMs ?? 5000;
    // (6a) span ceiling FIRST — reject an absurd exp regardless of the current
    //      clock, so a far-future-exp assertion can never reach the jti store and
    //      pin a record there (unbounded-retention DoS). P19 — bounded span.
    const maxTtl = deps.maxTtlMs ?? DEFAULT_POOL_LINK_MAX_TTL_MS;
    if (assertion.exp - assertion.iat > maxTtl)
        return { ok: false, reason: 'ttl-too-long' };
    if (now >= assertion.exp)
        return { ok: false, reason: 'expired' };
    if (assertion.iat - now > skew)
        return { ok: false, reason: 'not-yet-valid' };
    // (7) single-use: jti unseen within the window.
    if (deps.seenJti(assertion.jti))
        return { ok: false, reason: 'replayed' };
    return { ok: true, reason: 'ok' };
}
/** HTTP status for each verify reason — auth failures 401/403, freshness/replay 409. */
export function statusForPoolLinkReason(reason) {
    switch (reason) {
        case 'malformed':
            return 400;
        case 'wrong-holder':
        case 'wrong-view':
        case 'wrong-method':
        case 'unexpected-issuer':
        case 'unknown-issuer':
        case 'signature-invalid':
            return 401;
        case 'expired':
        case 'not-yet-valid':
        case 'ttl-too-long':
        case 'replayed':
            return 409;
        default:
            return 400;
    }
}
//# sourceMappingURL=PoolLinkAssertion.js.map