/**
 * Attribution policy module.
 *
 * Central seam for gating durable writes based on the resolved
 * {@link AttributionTier} stamped onto the request. Call
 * {@link enforceAttributionPolicy} at the top of each write-path service
 * so all transports (HTTP, MCP stdio, MCP HTTP, CLI backup) honour the
 * same rules uniformly.
 *
 * Sources of configuration (resolved in priority order, most specific
 * wins):
 *   1. `NEOTOMA_ATTRIBUTION_POLICY=allow|warn|reject` environment variable.
 *   2. `NEOTOMA_MIN_ATTRIBUTION_TIER=hardware|software|unverified_client`.
 *   3. Per-write-path overrides supplied via
 *      `NEOTOMA_ATTRIBUTION_POLICY_JSON` (JSON object keyed by write path).
 *
 * Defaults preserve historical behaviour: everything is allowed, no
 * minimum tier.
 */
import { logger } from "../utils/logger.js";
const DEFAULT_POLICY = {
    anonymous_writes: "allow",
};
function parseAnonymousWriteMode(raw) {
    if (!raw)
        return undefined;
    const normalised = raw.trim().toLowerCase();
    if (normalised === "allow" || normalised === "warn" || normalised === "reject") {
        return normalised;
    }
    return undefined;
}
function parseMinTier(raw) {
    if (!raw)
        return undefined;
    const normalised = raw.trim().toLowerCase();
    if (normalised === "hardware" ||
        normalised === "software" ||
        normalised === "unverified_client") {
        return normalised;
    }
    return undefined;
}
function parsePerPath(raw) {
    if (!raw)
        return undefined;
    try {
        const parsed = JSON.parse(raw);
        if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
            return undefined;
        }
        const out = {};
        const allowedPaths = new Set([
            "observations",
            "relationships",
            "sources",
            "interpretations",
            "timeline_events",
            "corrections",
        ]);
        for (const [key, value] of Object.entries(parsed)) {
            if (!allowedPaths.has(key))
                continue;
            const mode = parseAnonymousWriteMode(typeof value === "string" ? value : undefined);
            if (mode)
                out[key] = mode;
        }
        return Object.keys(out).length > 0 ? out : undefined;
    }
    catch {
        return undefined;
    }
}
/**
 * Resolve the current policy snapshot from environment variables. Always
 * returns a fresh object; callers may surface this to clients without
 * worrying about mutation.
 */
export function getAttributionPolicySnapshot() {
    const anonymousWrites = parseAnonymousWriteMode(process.env.NEOTOMA_ATTRIBUTION_POLICY) ??
        DEFAULT_POLICY.anonymous_writes;
    const minTier = parseMinTier(process.env.NEOTOMA_MIN_ATTRIBUTION_TIER);
    const perPath = parsePerPath(process.env.NEOTOMA_ATTRIBUTION_POLICY_JSON);
    const snapshot = {
        anonymous_writes: anonymousWrites,
    };
    if (minTier)
        snapshot.min_tier = minTier;
    if (perPath)
        snapshot.per_path = perPath;
    return snapshot;
}
/**
 * Effective mode for a specific write path after applying per-path
 * overrides. Shared helper so {@link enforceAttributionPolicy} and the
 * `/session` eligibility flag agree on the resolved mode.
 */
export function effectiveAnonymousWriteMode(policy, path) {
    return policy.per_path?.[path] ?? policy.anonymous_writes;
}
const TIER_RANK = {
    anonymous: 0,
    unverified_client: 1,
    software: 2,
    operator_attested: 3,
    hardware: 4,
};
/**
 * Structured error thrown by {@link enforceAttributionPolicy} when the
 * caller is below the required tier. HTTP handlers surface this as a 403
 * with `code: "ATTRIBUTION_REQUIRED"` (see `src/actions.ts`).
 */
export class AttributionPolicyError extends Error {
    code = "ATTRIBUTION_REQUIRED";
    statusCode = 403;
    writePath;
    currentTier;
    minTier;
    hint;
    /** True for transient causes (e.g. AAuth JWKS outage); callers may retry. */
    retryable;
    constructor(params) {
        super(`Attribution policy rejected write to "${params.writePath}": ` +
            `current tier "${params.currentTier}" is below required "${params.minTier}"`);
        this.name = "AttributionPolicyError";
        this.writePath = params.writePath;
        this.currentTier = params.currentTier;
        this.minTier = params.minTier;
        this.hint = params.hint;
        this.retryable = params.retryable ?? false;
    }
    toErrorEnvelope() {
        return {
            code: this.code,
            message: this.message,
            min_tier: this.minTier,
            current_tier: this.currentTier,
            write_path: this.writePath,
            hint: this.hint,
            retryable: this.retryable,
        };
    }
}
/**
 * Enforce the active policy for `writePath` against `identity`. Throws an
 * {@link AttributionPolicyError} on rejection; returns the outcome otherwise.
 * Callers MUST invoke this at the top of each public write method so
 * all transports (HTTP, MCP stdio, MCP HTTP, CLI backup) share one seam.
 *
 * This function is synchronous and cheap: it reads `process.env` once per
 * call. If a future profile caches snapshots, keep this signature stable
 * so call sites don't change.
 */
export function enforceAttributionPolicy(writePath, identity) {
    const policy = getAttributionPolicySnapshot();
    const tier = identity?.tier ?? "anonymous";
    const mode = effectiveAnonymousWriteMode(policy, writePath);
    // Anonymous tier is gated by the per-path mode.
    if (tier === "anonymous") {
        if (mode === "reject") {
            throw new AttributionPolicyError({
                writePath,
                currentTier: tier,
                minTier: policy.min_tier ?? "unverified_client",
                hint: "Sign this request with AAuth (preferred), or supply an MCP " +
                    "clientInfo.name on initialize so the request is attributable.",
            });
        }
        if (mode === "warn") {
            const warning = `write to ${writePath} accepted with anonymous attribution (policy=warn)`;
            logger.warn(JSON.stringify({
                event: "attribution_policy_warn",
                write_path: writePath,
                resolved_tier: tier,
            }));
            return { action: "warn", warningMessage: warning };
        }
        // mode === "allow"
        return { action: "allow" };
    }
    // Non-anonymous tiers honour the configured floor when set.
    if (policy.min_tier && TIER_RANK[tier] < TIER_RANK[policy.min_tier]) {
        throw new AttributionPolicyError({
            writePath,
            currentTier: tier,
            minTier: policy.min_tier,
            hint: `Upgrade attribution to at least "${policy.min_tier}" for this write path.`,
        });
    }
    return { action: "allow" };
}
//# sourceMappingURL=attribution_policy.js.map