/**
 * Session introspection service.
 *
 * Builds the `/session` / `get_session_identity` response payload from the
 * already-resolved {@link AgentIdentity} (populated by the AAuth middleware
 * and/or the MCP `initialize` handshake) plus the active
 * {@link AttributionPolicySnapshot}. Pure / read-only: this service must
 * never issue database writes, telemetry calls, or side-effectful logs.
 *
 * Consumers:
 *   - HTTP `GET /session` in {@link ../actions.ts} (preflight health check
 *     for local proxies before enabling writes).
 *   - MCP tool `get_session_identity` in {@link ../server.ts} (same shape,
 *     reachable via the MCP transport).
 *   - CLI `auth session` command.
 */
import { normaliseClientNameWithReason } from "../crypto/agent_identity.js";
import { getAttributionPolicySnapshot } from "./attribution_policy.js";
/**
 * Assemble a {@link SessionInfo} from the resolved identity + optional
 * diagnostic decision. Pure; callers must already have authenticated the
 * request and resolved the identity.
 */
export function buildSessionInfo(params) {
    const { userId, identity, middlewareDecision, rawClientInfoName, admission } = params;
    const tier = identity?.tier ?? "anonymous";
    const decision = mergeDecision({
        middlewareDecision: middlewareDecision ?? null,
        rawClientInfoName: rawClientInfoName ?? null,
        resolvedTier: tier,
    });
    const attribution = {
        tier,
        decision,
    };
    if (identity?.thumbprint)
        attribution.agent_thumbprint = identity.thumbprint;
    if (identity?.sub)
        attribution.agent_sub = identity.sub;
    if (identity?.iss)
        attribution.agent_iss = identity.iss;
    if (identity?.algorithm)
        attribution.agent_algorithm = identity.algorithm;
    if (identity?.publicKey)
        attribution.agent_public_key = identity.publicKey;
    if (identity?.clientName)
        attribution.client_name = identity.clientName;
    if (identity?.clientVersion)
        attribution.client_version = identity.clientVersion;
    if (identity?.connectionId)
        attribution.connection_id = identity.connectionId;
    const aauth = buildAAuthInfo({
        middlewareDecision: middlewareDecision ?? null,
        admission: admission ?? null,
    });
    const policy = getAttributionPolicySnapshot();
    const eligible = isEligibleForTrustedWrites(tier, policy);
    return {
        user_id: userId,
        attribution,
        aauth,
        policy,
        eligible_for_trusted_writes: eligible,
    };
}
/**
 * Compose the public `aauth` block. Verified ≠ admitted on purpose: a
 * caller that signed correctly but has no active grant in this user's
 * scope still gets `verified: true, admitted: false,
 * admission_reason: "no_match" | "no_grants_for_user"` so they can
 * differentiate a missing signature from a missing grant.
 */
function buildAAuthInfo(params) {
    const { middlewareDecision, admission } = params;
    const verified = Boolean(middlewareDecision?.signature_verified);
    if (!admission) {
        return {
            verified,
            admitted: false,
            grant_id: null,
            admission_reason: verified ? null : "not_signed",
        };
    }
    const info = {
        verified,
        admitted: Boolean(admission.admitted),
        grant_id: admission.admitted ? (admission.grant_id ?? null) : null,
        admission_reason: admission.reason ?? (admission.admitted ? "admitted" : null),
    };
    if (admission.agent_label)
        info.agent_label = admission.agent_label;
    return info;
}
/**
 * Combine the signature-side decision stashed by the AAuth middleware
 * with client-info normalisation to produce the final diagnostic record
 * surfaced to clients. Missing inputs degrade gracefully: when no
 * middleware decision is stashed (e.g. unsigned local stdio call) we
 * synthesise `signature_present: false`.
 */
function mergeDecision(params) {
    const { middlewareDecision, rawClientInfoName, resolvedTier } = params;
    const base = middlewareDecision
        ? {
            signature_present: middlewareDecision.signature_present,
            signature_verified: middlewareDecision.signature_verified,
            signature_error_code: middlewareDecision.signature_error_code,
            resolved_tier: resolvedTier,
        }
        : {
            signature_present: false,
            signature_verified: false,
            resolved_tier: resolvedTier,
        };
    if (middlewareDecision?.attestation) {
        base.attestation = middlewareDecision.attestation;
    }
    if (middlewareDecision?.operator_allowlist_source) {
        base.operator_allowlist_source = middlewareDecision.operator_allowlist_source;
    }
    if (rawClientInfoName !== null && rawClientInfoName !== undefined) {
        const { value, reason } = normaliseClientNameWithReason(rawClientInfoName);
        if (typeof rawClientInfoName === "string" && rawClientInfoName.length > 0) {
            base.client_info_raw_name = rawClientInfoName;
        }
        if (value === undefined && reason) {
            base.client_info_normalised_to_null_reason = reason;
        }
    }
    return base;
}
/**
 * Mirrors the core decision that
 * {@link import('./attribution_policy.js').enforceAttributionPolicy} makes
 * on the default write path (`observations`). Kept in sync so the
 * session endpoint is a reliable preflight.
 */
function isEligibleForTrustedWrites(tier, policy) {
    const mode = policy.per_path?.observations ?? policy.anonymous_writes;
    if (tier === "anonymous") {
        return mode !== "reject";
    }
    if (!policy.min_tier)
        return true;
    const rank = {
        anonymous: 0,
        unverified_client: 1,
        software: 2,
        operator_attested: 3,
        hardware: 4,
    };
    return rank[tier] >= rank[policy.min_tier];
}
//# sourceMappingURL=session_info.js.map