import { createHash, generateKeyPairSync, randomBytes, timingSafeEqual } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Router } from "express";
import { and, eq, isNull, desc } from "drizzle-orm";
import { agentApiKeys, authUsers, companies, invites, joinRequests } from "@zmeel/db";
import { acceptInviteSchema, createCliAuthChallengeSchema, claimJoinRequestApiKeySchema, createCompanyInviteSchema, createOpenClawInvitePromptSchema, listJoinRequestsQuerySchema, resolveCliAuthChallengeSchema, updateMemberPermissionsSchema, updateUserCompanyAccessSchema, PERMISSION_KEYS } from "@zmeel/shared";
import { forbidden, conflict, notFound, unauthorized, badRequest } from "../errors.js";
import { logger } from "../middleware/logger.js";
import { validate } from "../middleware/validate.js";
import { accessService, agentService, boardAuthService, deduplicateAgentName, logActivity, notifyHireApproved } from "../services/index.js";
import { assertCompanyAccess } from "./authz.js";
import { claimBoardOwnership, inspectBoardClaimChallenge } from "../board-claim.js";
function hashToken(token) {
    return createHash("sha256").update(token).digest("hex");
}
const INVITE_TOKEN_PREFIX = "pcp_invite_";
const INVITE_TOKEN_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
const INVITE_TOKEN_SUFFIX_LENGTH = 8;
const INVITE_TOKEN_MAX_RETRIES = 5;
const COMPANY_INVITE_TTL_MS = 10 * 60 * 1000;
function createInviteToken() {
    const bytes = randomBytes(INVITE_TOKEN_SUFFIX_LENGTH);
    let suffix = "";
    for (let idx = 0; idx < INVITE_TOKEN_SUFFIX_LENGTH; idx += 1) {
        suffix += INVITE_TOKEN_ALPHABET[bytes[idx] % INVITE_TOKEN_ALPHABET.length];
    }
    return `${INVITE_TOKEN_PREFIX}${suffix}`;
}
function createClaimSecret() {
    return `pcp_claim_${randomBytes(24).toString("hex")}`;
}
export function companyInviteExpiresAt(nowMs = Date.now()) {
    return new Date(nowMs + COMPANY_INVITE_TTL_MS);
}
function tokenHashesMatch(left, right) {
    const leftBytes = Buffer.from(left, "utf8");
    const rightBytes = Buffer.from(right, "utf8");
    return (leftBytes.length === rightBytes.length &&
        timingSafeEqual(leftBytes, rightBytes));
}
function requestBaseUrl(req) {
    const forwardedProto = req.header("x-forwarded-proto");
    const proto = forwardedProto?.split(",")[0]?.trim() || req.protocol || "http";
    const host = req.header("x-forwarded-host")?.split(",")[0]?.trim() || req.header("host");
    if (!host)
        return "";
    return `${proto}://${host}`;
}
function buildCliAuthApprovalPath(challengeId, token) {
    return `/cli-auth/${challengeId}?token=${encodeURIComponent(token)}`;
}
function readSkillMarkdown(skillName) {
    const normalized = skillName.trim().toLowerCase();
    if (normalized !== "zmeel" &&
        normalized !== "zmeel-create-agent" &&
        normalized !== "zmeel-create-plugin" &&
        normalized !== "para-memory-files")
        return null;
    const moduleDir = path.dirname(fileURLToPath(import.meta.url));
    const candidates = [
        path.resolve(moduleDir, "../../skills", normalized, "SKILL.md"), // published: dist/routes/ -> <pkg>/skills/
        path.resolve(process.cwd(), "skills", normalized, "SKILL.md"), // cwd (e.g. monorepo root)
        path.resolve(moduleDir, "../../../skills", normalized, "SKILL.md") // dev: src/routes/ -> repo root/skills/
    ];
    for (const skillPath of candidates) {
        try {
            return fs.readFileSync(skillPath, "utf8");
        }
        catch {
            // Continue to next candidate.
        }
    }
    return null;
}
/** Resolve the zmeel repo skills directory (built-in / managed skills). */
function resolvezmeelSkillsDir() {
    const moduleDir = path.dirname(fileURLToPath(import.meta.url));
    const candidates = [
        path.resolve(moduleDir, "../../skills"), // published
        path.resolve(process.cwd(), "skills"), // cwd (monorepo root)
        path.resolve(moduleDir, "../../../skills"), // dev
    ];
    for (const candidate of candidates) {
        try {
            if (fs.statSync(candidate).isDirectory())
                return candidate;
        }
        catch { /* skip */ }
    }
    return null;
}
/** Parse YAML frontmatter from a SKILL.md file to extract the description. */
function parseSkillFrontmatter(markdown) {
    const match = markdown.match(/^---\n([\s\S]*?)\n---/);
    if (!match)
        return { description: "" };
    const yaml = match[1];
    // Extract description — handles both single-line and multi-line YAML values
    const descMatch = yaml.match(/^description:\s*(?:>\s*\n((?:\s{2,}[^\n]*\n?)+)|[|]\s*\n((?:\s{2,}[^\n]*\n?)+)|["']?(.*?)["']?\s*$)/m);
    if (!descMatch)
        return { description: "" };
    const raw = descMatch[1] ?? descMatch[2] ?? descMatch[3] ?? "";
    return {
        description: raw
            .split("\n")
            .map((l) => l.trim())
            .filter(Boolean)
            .join(" ")
            .trim(),
    };
}
/** Discover all available Claude Code skills from ~/.claude/skills/. */
function listAvailableSkills() {
    const homeDir = process.env.HOME || process.env.USERPROFILE || "";
    const claudeSkillsDir = path.join(homeDir, ".claude", "skills");
    const zmeelSkillsDir = resolvezmeelSkillsDir();
    // Build set of zmeel-managed skill names
    const zmeelSkillNames = new Set();
    if (zmeelSkillsDir) {
        try {
            for (const entry of fs.readdirSync(zmeelSkillsDir, { withFileTypes: true })) {
                if (entry.isDirectory())
                    zmeelSkillNames.add(entry.name);
            }
        }
        catch { /* skip */ }
    }
    const skills = [];
    try {
        const entries = fs.readdirSync(claudeSkillsDir, { withFileTypes: true });
        for (const entry of entries) {
            if (!entry.isDirectory() && !entry.isSymbolicLink())
                continue;
            if (entry.name.startsWith("."))
                continue;
            const skillMdPath = path.join(claudeSkillsDir, entry.name, "SKILL.md");
            let description = "";
            try {
                const md = fs.readFileSync(skillMdPath, "utf8");
                description = parseSkillFrontmatter(md).description;
            }
            catch { /* no SKILL.md or unreadable */ }
            skills.push({
                name: entry.name,
                description,
                iszmeelManaged: zmeelSkillNames.has(entry.name),
            });
        }
    }
    catch { /* ~/.claude/skills/ doesn't exist */ }
    skills.sort((a, b) => a.name.localeCompare(b.name));
    return skills;
}
function toJoinRequestResponse(row) {
    const { claimSecretHash: _claimSecretHash, ...safe } = row;
    return safe;
}
function isPlainObject(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isLoopbackHost(hostname) {
    const value = hostname.trim().toLowerCase();
    return value === "localhost" || value === "127.0.0.1" || value === "::1";
}
function normalizeHostname(value) {
    if (!value)
        return null;
    const trimmed = value.trim();
    if (!trimmed)
        return null;
    if (trimmed.startsWith("[")) {
        const end = trimmed.indexOf("]");
        return end > 1
            ? trimmed.slice(1, end).toLowerCase()
            : trimmed.toLowerCase();
    }
    const firstColon = trimmed.indexOf(":");
    if (firstColon > -1)
        return trimmed.slice(0, firstColon).toLowerCase();
    return trimmed.toLowerCase();
}
function normalizeHeaderValue(value, depth = 0) {
    const direct = nonEmptyTrimmedString(value);
    if (direct)
        return direct;
    if (!isPlainObject(value) || depth >= 3)
        return null;
    const candidateKeys = [
        "value",
        "token",
        "secret",
        "apiKey",
        "api_key",
        "auth",
        "authToken",
        "auth_token",
        "accessToken",
        "access_token",
        "authorization",
        "bearer",
        "header",
        "raw",
        "text",
        "string"
    ];
    for (const key of candidateKeys) {
        if (!Object.prototype.hasOwnProperty.call(value, key))
            continue;
        const normalized = normalizeHeaderValue(value[key], depth + 1);
        if (normalized)
            return normalized;
    }
    const entries = Object.entries(value);
    if (entries.length === 1) {
        const [singleKey, singleValue] = entries[0];
        const normalizedKey = singleKey.trim().toLowerCase();
        if (normalizedKey !== "type" &&
            normalizedKey !== "version" &&
            normalizedKey !== "secretid" &&
            normalizedKey !== "secret_id") {
            const normalized = normalizeHeaderValue(singleValue, depth + 1);
            if (normalized)
                return normalized;
        }
    }
    return null;
}
function extractHeaderEntries(input) {
    if (isPlainObject(input)) {
        return Object.entries(input);
    }
    if (!Array.isArray(input)) {
        return [];
    }
    const entries = [];
    for (const item of input) {
        if (Array.isArray(item)) {
            const key = nonEmptyTrimmedString(item[0]);
            if (!key)
                continue;
            entries.push([key, item[1]]);
            continue;
        }
        if (!isPlainObject(item))
            continue;
        const mapped = item;
        const explicitKey = nonEmptyTrimmedString(mapped.key) ??
            nonEmptyTrimmedString(mapped.name) ??
            nonEmptyTrimmedString(mapped.header);
        if (explicitKey) {
            const explicitValue = Object.prototype.hasOwnProperty.call(mapped, "value")
                ? mapped.value
                : Object.prototype.hasOwnProperty.call(mapped, "token")
                    ? mapped.token
                    : Object.prototype.hasOwnProperty.call(mapped, "secret")
                        ? mapped.secret
                        : mapped;
            entries.push([explicitKey, explicitValue]);
            continue;
        }
        const singleEntry = Object.entries(mapped);
        if (singleEntry.length === 1) {
            entries.push(singleEntry[0]);
        }
    }
    return entries;
}
function normalizeHeaderMap(input) {
    const entries = extractHeaderEntries(input);
    if (entries.length === 0)
        return undefined;
    const out = {};
    for (const [key, value] of entries) {
        const normalizedValue = normalizeHeaderValue(value);
        if (!normalizedValue)
            continue;
        const trimmedKey = key.trim();
        const trimmedValue = normalizedValue.trim();
        if (!trimmedKey || !trimmedValue)
            continue;
        out[trimmedKey] = trimmedValue;
    }
    return Object.keys(out).length > 0 ? out : undefined;
}
function nonEmptyTrimmedString(value) {
    if (typeof value !== "string")
        return null;
    const trimmed = value.trim();
    return trimmed.length > 0 ? trimmed : null;
}
function headerMapHasKeyIgnoreCase(headers, targetKey) {
    const normalizedTarget = targetKey.trim().toLowerCase();
    return Object.keys(headers).some((key) => key.trim().toLowerCase() === normalizedTarget);
}
function headerMapGetIgnoreCase(headers, targetKey) {
    const normalizedTarget = targetKey.trim().toLowerCase();
    const key = Object.keys(headers).find((candidate) => candidate.trim().toLowerCase() === normalizedTarget);
    if (!key)
        return null;
    const value = headers[key];
    return typeof value === "string" ? value : null;
}
function tokenFromAuthorizationHeader(rawHeader) {
    const trimmed = nonEmptyTrimmedString(rawHeader);
    if (!trimmed)
        return null;
    const bearerMatch = trimmed.match(/^bearer\s+(.+)$/i);
    if (bearerMatch?.[1]) {
        return nonEmptyTrimmedString(bearerMatch[1]);
    }
    return trimmed;
}
function parseBooleanLike(value) {
    if (typeof value === "boolean")
        return value;
    if (typeof value !== "string")
        return null;
    const normalized = value.trim().toLowerCase();
    if (normalized === "true" || normalized === "1")
        return true;
    if (normalized === "false" || normalized === "0")
        return false;
    return null;
}
function generateEd25519PrivateKeyPem() {
    const generated = generateKeyPairSync("ed25519");
    return generated.privateKey
        .export({ type: "pkcs8", format: "pem" })
        .toString();
}
export function buildJoinDefaultsPayloadForAccept(input) {
    if (input.adapterType !== "openclaw_gateway") {
        return input.defaultsPayload;
    }
    const merged = isPlainObject(input.defaultsPayload)
        ? { ...input.defaultsPayload }
        : {};
    if (!nonEmptyTrimmedString(merged.zmeelApiUrl)) {
        const legacyzmeelApiUrl = nonEmptyTrimmedString(input.zmeelApiUrl);
        if (legacyzmeelApiUrl)
            merged.zmeelApiUrl = legacyzmeelApiUrl;
    }
    const mergedHeaders = normalizeHeaderMap(merged.headers) ?? {};
    const inboundOpenClawAuthHeader = nonEmptyTrimmedString(input.inboundOpenClawAuthHeader);
    const inboundOpenClawTokenHeader = nonEmptyTrimmedString(input.inboundOpenClawTokenHeader);
    if (inboundOpenClawTokenHeader &&
        !headerMapHasKeyIgnoreCase(mergedHeaders, "x-openclaw-token")) {
        mergedHeaders["x-openclaw-token"] = inboundOpenClawTokenHeader;
    }
    if (inboundOpenClawAuthHeader &&
        !headerMapHasKeyIgnoreCase(mergedHeaders, "x-openclaw-auth")) {
        mergedHeaders["x-openclaw-auth"] = inboundOpenClawAuthHeader;
    }
    if (Object.keys(mergedHeaders).length > 0) {
        merged.headers = mergedHeaders;
    }
    else {
        delete merged.headers;
    }
    const discoveredToken = headerMapGetIgnoreCase(mergedHeaders, "x-openclaw-token") ??
        headerMapGetIgnoreCase(mergedHeaders, "x-openclaw-auth") ??
        tokenFromAuthorizationHeader(headerMapGetIgnoreCase(mergedHeaders, "authorization"));
    if (discoveredToken &&
        !headerMapHasKeyIgnoreCase(mergedHeaders, "x-openclaw-token")) {
        mergedHeaders["x-openclaw-token"] = discoveredToken;
    }
    return Object.keys(merged).length > 0 ? merged : null;
}
export function mergeJoinDefaultsPayloadForReplay(existingDefaultsPayload, nextDefaultsPayload) {
    if (!isPlainObject(existingDefaultsPayload) &&
        !isPlainObject(nextDefaultsPayload)) {
        return nextDefaultsPayload ?? existingDefaultsPayload;
    }
    if (!isPlainObject(existingDefaultsPayload)) {
        return nextDefaultsPayload;
    }
    if (!isPlainObject(nextDefaultsPayload)) {
        return existingDefaultsPayload;
    }
    const merged = {
        ...existingDefaultsPayload,
        ...nextDefaultsPayload
    };
    const existingHeaders = normalizeHeaderMap(existingDefaultsPayload.headers);
    const nextHeaders = normalizeHeaderMap(nextDefaultsPayload.headers);
    if (existingHeaders || nextHeaders) {
        merged.headers = {
            ...(existingHeaders ?? {}),
            ...(nextHeaders ?? {})
        };
    }
    else if (Object.prototype.hasOwnProperty.call(merged, "headers")) {
        delete merged.headers;
    }
    return merged;
}
export function canReplayOpenClawGatewayInviteAccept(input) {
    if (input.requestType !== "agent" ||
        input.adapterType !== "openclaw_gateway") {
        return false;
    }
    if (!input.existingJoinRequest) {
        return false;
    }
    if (input.existingJoinRequest.requestType !== "agent" ||
        input.existingJoinRequest.adapterType !== "openclaw_gateway") {
        return false;
    }
    return (input.existingJoinRequest.status === "pending_approval" ||
        input.existingJoinRequest.status === "approved");
}
function summarizeSecretForLog(value) {
    const trimmed = nonEmptyTrimmedString(value);
    if (!trimmed)
        return null;
    return {
        present: true,
        length: trimmed.length,
        sha256Prefix: hashToken(trimmed).slice(0, 12)
    };
}
function summarizeOpenClawGatewayDefaultsForLog(defaultsPayload) {
    const defaults = isPlainObject(defaultsPayload)
        ? defaultsPayload
        : null;
    const headers = defaults ? normalizeHeaderMap(defaults.headers) : undefined;
    const gatewayTokenValue = headers
        ? headerMapGetIgnoreCase(headers, "x-openclaw-token") ??
            headerMapGetIgnoreCase(headers, "x-openclaw-auth") ??
            tokenFromAuthorizationHeader(headerMapGetIgnoreCase(headers, "authorization"))
        : null;
    return {
        present: Boolean(defaults),
        keys: defaults ? Object.keys(defaults).sort() : [],
        url: defaults ? nonEmptyTrimmedString(defaults.url) : null,
        zmeelApiUrl: defaults
            ? nonEmptyTrimmedString(defaults.zmeelApiUrl)
            : null,
        headerKeys: headers ? Object.keys(headers).sort() : [],
        sessionKeyStrategy: defaults
            ? nonEmptyTrimmedString(defaults.sessionKeyStrategy)
            : null,
        disableDeviceAuth: defaults
            ? parseBooleanLike(defaults.disableDeviceAuth)
            : null,
        waitTimeoutMs: defaults && typeof defaults.waitTimeoutMs === "number"
            ? defaults.waitTimeoutMs
            : null,
        devicePrivateKeyPem: defaults
            ? summarizeSecretForLog(defaults.devicePrivateKeyPem)
            : null,
        gatewayToken: summarizeSecretForLog(gatewayTokenValue)
    };
}
export function normalizeAgentDefaultsForJoin(input) {
    const fatalErrors = [];
    const diagnostics = [];
    if (input.adapterType !== "openclaw_gateway") {
        const normalized = isPlainObject(input.defaultsPayload)
            ? input.defaultsPayload
            : null;
        return { normalized, diagnostics, fatalErrors };
    }
    if (!isPlainObject(input.defaultsPayload)) {
        diagnostics.push({
            code: "openclaw_gateway_defaults_missing",
            level: "warn",
            message: "No OpenClaw gateway config was provided in agentDefaultsPayload.",
            hint: "Include agentDefaultsPayload.url and headers.x-openclaw-token for OpenClaw gateway joins."
        });
        fatalErrors.push("agentDefaultsPayload is required for adapterType=openclaw_gateway");
        return {
            normalized: null,
            diagnostics,
            fatalErrors
        };
    }
    const defaults = input.defaultsPayload;
    const normalized = {};
    let gatewayUrl = null;
    const rawGatewayUrl = nonEmptyTrimmedString(defaults.url);
    if (!rawGatewayUrl) {
        diagnostics.push({
            code: "openclaw_gateway_url_missing",
            level: "warn",
            message: "OpenClaw gateway URL is missing.",
            hint: "Set agentDefaultsPayload.url to ws:// or wss:// gateway URL."
        });
        fatalErrors.push("agentDefaultsPayload.url is required");
    }
    else {
        try {
            gatewayUrl = new URL(rawGatewayUrl);
            if (gatewayUrl.protocol !== "ws:" && gatewayUrl.protocol !== "wss:") {
                diagnostics.push({
                    code: "openclaw_gateway_url_protocol",
                    level: "warn",
                    message: `OpenClaw gateway URL must use ws:// or wss:// (got ${gatewayUrl.protocol}).`
                });
                fatalErrors.push("agentDefaultsPayload.url must use ws:// or wss:// for openclaw_gateway");
            }
            else {
                normalized.url = gatewayUrl.toString();
                diagnostics.push({
                    code: "openclaw_gateway_url_configured",
                    level: "info",
                    message: `Gateway endpoint set to ${gatewayUrl.toString()}`
                });
            }
        }
        catch {
            diagnostics.push({
                code: "openclaw_gateway_url_invalid",
                level: "warn",
                message: `Invalid OpenClaw gateway URL: ${rawGatewayUrl}`
            });
            fatalErrors.push("agentDefaultsPayload.url is not a valid URL");
        }
    }
    const headers = normalizeHeaderMap(defaults.headers) ?? {};
    const gatewayToken = headerMapGetIgnoreCase(headers, "x-openclaw-token") ??
        headerMapGetIgnoreCase(headers, "x-openclaw-auth") ??
        tokenFromAuthorizationHeader(headerMapGetIgnoreCase(headers, "authorization"));
    if (gatewayToken && !headerMapHasKeyIgnoreCase(headers, "x-openclaw-token")) {
        headers["x-openclaw-token"] = gatewayToken;
    }
    if (Object.keys(headers).length > 0) {
        normalized.headers = headers;
    }
    if (!gatewayToken) {
        diagnostics.push({
            code: "openclaw_gateway_auth_header_missing",
            level: "warn",
            message: "Gateway auth token is missing from agent defaults.",
            hint: "Set agentDefaultsPayload.headers.x-openclaw-token (or legacy x-openclaw-auth)."
        });
        fatalErrors.push("agentDefaultsPayload.headers.x-openclaw-token (or x-openclaw-auth) is required");
    }
    else if (gatewayToken.trim().length < 16) {
        diagnostics.push({
            code: "openclaw_gateway_auth_header_too_short",
            level: "warn",
            message: `Gateway auth token appears too short (${gatewayToken.trim().length} chars).`,
            hint: "Use the full gateway auth token from ~/.openclaw/openclaw.json (typically long random string)."
        });
        fatalErrors.push("agentDefaultsPayload.headers.x-openclaw-token is too short; expected a full gateway token");
    }
    else {
        diagnostics.push({
            code: "openclaw_gateway_auth_header_configured",
            level: "info",
            message: "Gateway auth token configured."
        });
    }
    if (isPlainObject(defaults.payloadTemplate)) {
        normalized.payloadTemplate = defaults.payloadTemplate;
    }
    const parsedDisableDeviceAuth = parseBooleanLike(defaults.disableDeviceAuth);
    const disableDeviceAuth = parsedDisableDeviceAuth === true;
    if (parsedDisableDeviceAuth !== null) {
        normalized.disableDeviceAuth = parsedDisableDeviceAuth;
    }
    const configuredDevicePrivateKeyPem = nonEmptyTrimmedString(defaults.devicePrivateKeyPem);
    if (configuredDevicePrivateKeyPem) {
        normalized.devicePrivateKeyPem = configuredDevicePrivateKeyPem;
        diagnostics.push({
            code: "openclaw_gateway_device_key_configured",
            level: "info",
            message: "Gateway device key configured. Pairing approvals should persist for this agent."
        });
    }
    else if (!disableDeviceAuth) {
        try {
            normalized.devicePrivateKeyPem = generateEd25519PrivateKeyPem();
            diagnostics.push({
                code: "openclaw_gateway_device_key_generated",
                level: "info",
                message: "Generated persistent gateway device key for this join. Pairing approvals should persist for this agent."
            });
        }
        catch (err) {
            diagnostics.push({
                code: "openclaw_gateway_device_key_generate_failed",
                level: "warn",
                message: `Failed to generate gateway device key: ${err instanceof Error ? err.message : String(err)}`,
                hint: "Set agentDefaultsPayload.devicePrivateKeyPem explicitly or set disableDeviceAuth=true."
            });
            fatalErrors.push("Failed to generate gateway device key. Set devicePrivateKeyPem or disableDeviceAuth=true.");
        }
    }
    const waitTimeoutMs = typeof defaults.waitTimeoutMs === "number" &&
        Number.isFinite(defaults.waitTimeoutMs)
        ? Math.floor(defaults.waitTimeoutMs)
        : typeof defaults.waitTimeoutMs === "string"
            ? Number.parseInt(defaults.waitTimeoutMs.trim(), 10)
            : NaN;
    if (Number.isFinite(waitTimeoutMs) && waitTimeoutMs > 0) {
        normalized.waitTimeoutMs = waitTimeoutMs;
    }
    const timeoutSec = typeof defaults.timeoutSec === "number" && Number.isFinite(defaults.timeoutSec)
        ? Math.floor(defaults.timeoutSec)
        : typeof defaults.timeoutSec === "string"
            ? Number.parseInt(defaults.timeoutSec.trim(), 10)
            : NaN;
    if (Number.isFinite(timeoutSec) && timeoutSec > 0) {
        normalized.timeoutSec = timeoutSec;
    }
    const sessionKeyStrategy = nonEmptyTrimmedString(defaults.sessionKeyStrategy);
    if (sessionKeyStrategy === "fixed" ||
        sessionKeyStrategy === "issue" ||
        sessionKeyStrategy === "run") {
        normalized.sessionKeyStrategy = sessionKeyStrategy;
    }
    const sessionKey = nonEmptyTrimmedString(defaults.sessionKey);
    if (sessionKey) {
        normalized.sessionKey = sessionKey;
    }
    const role = nonEmptyTrimmedString(defaults.role);
    if (role) {
        normalized.role = role;
    }
    if (Array.isArray(defaults.scopes)) {
        const scopes = defaults.scopes
            .filter((entry) => typeof entry === "string")
            .map((entry) => entry.trim())
            .filter(Boolean);
        if (scopes.length > 0) {
            normalized.scopes = scopes;
        }
    }
    const rawzmeelApiUrl = typeof defaults.zmeelApiUrl === "string"
        ? defaults.zmeelApiUrl.trim()
        : "";
    if (rawzmeelApiUrl) {
        try {
            const parsedzmeelApiUrl = new URL(rawzmeelApiUrl);
            if (parsedzmeelApiUrl.protocol !== "http:" &&
                parsedzmeelApiUrl.protocol !== "https:") {
                diagnostics.push({
                    code: "openclaw_gateway_zmeel_api_url_protocol",
                    level: "warn",
                    message: `zmeelApiUrl must use http:// or https:// (got ${parsedzmeelApiUrl.protocol}).`
                });
            }
            else {
                normalized.zmeelApiUrl = parsedzmeelApiUrl.toString();
                diagnostics.push({
                    code: "openclaw_gateway_zmeel_api_url_configured",
                    level: "info",
                    message: `zmeelApiUrl set to ${parsedzmeelApiUrl.toString()}`
                });
            }
        }
        catch {
            diagnostics.push({
                code: "openclaw_gateway_zmeel_api_url_invalid",
                level: "warn",
                message: `Invalid zmeelApiUrl: ${rawzmeelApiUrl}`
            });
        }
    }
    return { normalized, diagnostics, fatalErrors };
}
function toInviteSummaryResponse(req, token, invite, companyName = null) {
    const baseUrl = requestBaseUrl(req);
    const onboardingPath = `/api/invites/${token}/onboarding`;
    const onboardingTextPath = `/api/invites/${token}/onboarding.txt`;
    const inviteMessage = extractInviteMessage(invite);
    return {
        id: invite.id,
        companyId: invite.companyId,
        companyName,
        inviteType: invite.inviteType,
        allowedJoinTypes: invite.allowedJoinTypes,
        expiresAt: invite.expiresAt,
        onboardingPath,
        onboardingUrl: baseUrl ? `${baseUrl}${onboardingPath}` : onboardingPath,
        onboardingTextPath,
        onboardingTextUrl: baseUrl
            ? `${baseUrl}${onboardingTextPath}`
            : onboardingTextPath,
        skillIndexPath: "/api/skills/index",
        skillIndexUrl: baseUrl
            ? `${baseUrl}/api/skills/index`
            : "/api/skills/index",
        inviteMessage
    };
}
function buildOnboardingDiscoveryDiagnostics(input) {
    const diagnostics = [];
    let apiHost = null;
    if (input.apiBaseUrl) {
        try {
            apiHost = normalizeHostname(new URL(input.apiBaseUrl).hostname);
        }
        catch {
            apiHost = null;
        }
    }
    const bindHost = normalizeHostname(input.bindHost);
    const allowSet = new Set(input.allowedHostnames
        .map((entry) => normalizeHostname(entry))
        .filter((entry) => Boolean(entry)));
    if (apiHost && isLoopbackHost(apiHost)) {
        diagnostics.push({
            code: "openclaw_onboarding_api_loopback",
            level: "warn",
            message: "Onboarding URL resolves to loopback hostname. Remote OpenClaw agents cannot reach localhost on your zmeel host.",
            hint: "Use a reachable hostname/IP (for example Tailscale hostname, Docker host alias, or public domain)."
        });
    }
    if (input.deploymentMode === "authenticated" &&
        input.deploymentExposure === "private" &&
        (!bindHost || isLoopbackHost(bindHost))) {
        diagnostics.push({
            code: "openclaw_onboarding_private_loopback_bind",
            level: "warn",
            message: "zmeel is bound to loopback in authenticated/private mode.",
            hint: "Run with a reachable bind host or use pnpm dev --tailscale-auth for private-network onboarding."
        });
    }
    if (input.deploymentMode === "authenticated" &&
        input.deploymentExposure === "private" &&
        apiHost &&
        !isLoopbackHost(apiHost) &&
        allowSet.size > 0 &&
        !allowSet.has(apiHost)) {
        diagnostics.push({
            code: "openclaw_onboarding_private_host_not_allowed",
            level: "warn",
            message: `Onboarding host "${apiHost}" is not in allowed hostnames for authenticated/private mode.`,
            hint: `Run pnpm zmeel allowed-hostname ${apiHost}`
        });
    }
    return diagnostics;
}
function buildOnboardingConnectionCandidates(input) {
    let base = null;
    try {
        if (input.apiBaseUrl) {
            base = new URL(input.apiBaseUrl);
        }
    }
    catch {
        base = null;
    }
    const protocol = base?.protocol ?? "http:";
    const port = base?.port ? `:${base.port}` : "";
    const candidates = new Set();
    if (base) {
        candidates.add(base.origin);
    }
    const bindHost = normalizeHostname(input.bindHost);
    if (bindHost && !isLoopbackHost(bindHost)) {
        candidates.add(`${protocol}//${bindHost}${port}`);
    }
    for (const rawHost of input.allowedHostnames) {
        const host = normalizeHostname(rawHost);
        if (!host)
            continue;
        candidates.add(`${protocol}//${host}${port}`);
    }
    if (base && isLoopbackHost(base.hostname)) {
        candidates.add(`${protocol}//host.docker.internal${port}`);
    }
    return Array.from(candidates);
}
function buildInviteOnboardingManifest(req, token, invite, opts) {
    const baseUrl = requestBaseUrl(req);
    const skillPath = "/api/skills/zmeel";
    const skillUrl = baseUrl ? `${baseUrl}${skillPath}` : skillPath;
    const registrationEndpointPath = `/api/invites/${token}/accept`;
    const registrationEndpointUrl = baseUrl
        ? `${baseUrl}${registrationEndpointPath}`
        : registrationEndpointPath;
    const onboardingTextPath = `/api/invites/${token}/onboarding.txt`;
    const onboardingTextUrl = baseUrl
        ? `${baseUrl}${onboardingTextPath}`
        : onboardingTextPath;
    const discoveryDiagnostics = buildOnboardingDiscoveryDiagnostics({
        apiBaseUrl: baseUrl,
        deploymentMode: opts.deploymentMode,
        deploymentExposure: opts.deploymentExposure,
        bindHost: opts.bindHost,
        allowedHostnames: opts.allowedHostnames
    });
    const connectionCandidates = buildOnboardingConnectionCandidates({
        apiBaseUrl: baseUrl,
        bindHost: opts.bindHost,
        allowedHostnames: opts.allowedHostnames
    });
    return {
        invite: toInviteSummaryResponse(req, token, invite, opts.companyName ?? null),
        onboarding: {
            instructions: "Join as an OpenClaw Gateway agent, save your one-time claim secret, wait for board approval, then claim your API key. Save the claim response token to ~/.openclaw/workspace/zmeel-claimed-api-key.json and load ZMEEL_API_KEY from that file before starting heartbeat loops. You MUST submit adapterType='openclaw_gateway', set agentDefaultsPayload.url to your ws:// or wss:// OpenClaw gateway endpoint, and include agentDefaultsPayload.headers.x-openclaw-token (or legacy x-openclaw-auth).",
            inviteMessage: extractInviteMessage(invite),
            recommendedAdapterType: "openclaw_gateway",
            requiredFields: {
                requestType: "agent",
                agentName: "Display name for this agent",
                adapterType: "Use 'openclaw_gateway' for OpenClaw Gateway agents",
                capabilities: "Optional capability summary",
                agentDefaultsPayload: "Adapter config for OpenClaw gateway. MUST include url (ws:// or wss://) and headers.x-openclaw-token (or legacy x-openclaw-auth). Optional fields: zmeelApiUrl, waitTimeoutMs, sessionKeyStrategy, sessionKey, role, scopes, disableDeviceAuth, devicePrivateKeyPem."
            },
            registrationEndpoint: {
                method: "POST",
                path: registrationEndpointPath,
                url: registrationEndpointUrl
            },
            claimEndpointTemplate: {
                method: "POST",
                path: "/api/join-requests/{requestId}/claim-api-key",
                body: {
                    claimSecret: "one-time claim secret returned when the join request is created"
                }
            },
            connectivity: {
                deploymentMode: opts.deploymentMode,
                deploymentExposure: opts.deploymentExposure,
                bindHost: opts.bindHost,
                allowedHostnames: opts.allowedHostnames,
                connectionCandidates,
                diagnostics: discoveryDiagnostics,
                guidance: opts.deploymentMode === "authenticated" &&
                    opts.deploymentExposure === "private"
                    ? "If OpenClaw runs on another machine, ensure the zmeel hostname is reachable and allowed via `pnpm zmeel allowed-hostname <host>`."
                    : "Ensure OpenClaw can reach this zmeel API base URL for invite, claim, and skill bootstrap calls."
            },
            textInstructions: {
                path: onboardingTextPath,
                url: onboardingTextUrl,
                contentType: "text/plain"
            },
            skill: {
                name: "zmeel",
                path: skillPath,
                url: skillUrl,
                installPath: "~/.openclaw/skills/zmeel/SKILL.md"
            }
        }
    };
}
export function buildInviteOnboardingTextDocument(req, token, invite, opts) {
    const manifest = buildInviteOnboardingManifest(req, token, invite, opts);
    const onboarding = manifest.onboarding;
    const diagnostics = Array.isArray(onboarding.connectivity?.diagnostics)
        ? onboarding.connectivity.diagnostics
        : [];
    const lines = [];
    const appendBlock = (block) => {
        const trimmed = block.replace(/^\n/, "").replace(/\n\s*$/, "");
        const lineIndentation = trimmed
            .split("\n")
            .filter((line) => line.trim().length > 0)
            .map((line) => line.match(/^(\s*)/)?.[0].length ?? 0);
        const minIndent = lineIndentation.length > 0 ? Math.min(...lineIndentation) : 0;
        for (const line of trimmed.split("\n")) {
            lines.push(line.slice(minIndent));
        }
    };
    appendBlock(`
    # zmeel OpenClaw Gateway Onboarding

    This document is meant to be readable by both humans and agents.

    ## Invite
    - inviteType: ${invite.inviteType}
    - allowedJoinTypes: ${invite.allowedJoinTypes}
    - expiresAt: ${invite.expiresAt.toISOString()}
  `);
    if (manifest.invite.companyName) {
        lines.push(`- companyName: ${manifest.invite.companyName}`);
    }
    if (onboarding.inviteMessage) {
        appendBlock(`
      ## Message from inviter
      ${onboarding.inviteMessage}
    `);
    }
    appendBlock(`
    ## Step 0

    Get the OpenClaw gateway auth token (THIS MUST BE SENT)
    Token lives in:

    ~/.openclaw/openclaw.json -> gateway.auth.token
    Extract:

    TOKEN="$(node -p 'require(process.env.HOME+\"/.openclaw/openclaw.json\").gateway.auth.token')"
    test -n "$TOKEN" || (echo "Missing TOKEN" && exit 1)
    test "\${#TOKEN}" -ge 16 || (echo "Gateway token unexpectedly short (\${#TOKEN})" && exit 1)

    3) IMPORTANT: Don't accidentally drop the token when generating JSON
    If you build JSON with Node, pass the token explicitly (argv), don't rely on an un-exported env var.

    Safe payload build looks sort of like this (substitute where necessary):

    BODY="$(node -e '
      const token = process.argv[1];
      if (!token) process.exit(2);
      const body = {
        requestType: "agent",
        agentName: "OpenClaw",
        adapterType: "openclaw_gateway",
        capabilities: "OpenClaw agent adapter",
        agentDefaultsPayload: {
          url: "ws://127.0.0.1:18789",
          zmeelApiUrl: "http://host.docker.internal:3100",
          headers: { "x-openclaw-token": token },
          waitTimeoutMs: 120000,
          sessionKeyStrategy: "issue",
          role: "operator",
          scopes: ["operator.admin"]
        }
      };
      process.stdout.write(JSON.stringify(body));
    ' "$TOKEN")"

    ## Step 1: Submit agent join request
    ${onboarding.registrationEndpoint.method} ${onboarding.registrationEndpoint.url}

    IMPORTANT: You MUST include agentDefaultsPayload.headers.x-openclaw-token with your gateway token.
    Legacy x-openclaw-auth is also accepted, but x-openclaw-token is preferred.
    Use adapterType "openclaw_gateway" and a ws:// or wss:// gateway URL.
    Pairing mode requirement:
    - Keep device auth enabled (recommended). If devicePrivateKeyPem is omitted, zmeel generates and persists one during join so pairing approvals are stable.
    - You may set disableDeviceAuth=true only for special environments that cannot support pairing.
    - First run may return "pairing required" once; approve the pending pairing request in OpenClaw, then retry.
    Do NOT use /v1/responses or /hooks/* in this gateway join flow.

    Body (JSON):
    {
      "requestType": "agent",
      "agentName": "My OpenClaw Agent",
      "adapterType": "openclaw_gateway",
      "capabilities": "Optional summary",
      "agentDefaultsPayload": {
        "url": "wss://your-openclaw-gateway.example",
        "zmeelApiUrl": "https://zmeel-hostname-your-agent-can-reach:3100",
        "headers": { "x-openclaw-token": "replace-me" },
        "waitTimeoutMs": 120000,
        "sessionKeyStrategy": "issue",
        "role": "operator",
        "scopes": ["operator.admin"]
      }
    }

    Expected response includes:
    - request id
    - one-time claimSecret
    - claimApiKeyPath

    ## Step 2: Wait for board approval
    The board approves the join request in zmeel before key claim is allowed.

    ## Step 3: Claim API key (one-time)
    ${onboarding.claimEndpointTemplate.method} /api/join-requests/{requestId}/claim-api-key

    Body (JSON):
    {
      "claimSecret": "<one-time-claim-secret>"
    }

    On successful claim, save the full JSON response to:

    - ~/.openclaw/workspace/zmeel-claimed-api-key.json
    chmod 600 ~/.openclaw/workspace/zmeel-claimed-api-key.json

    And set the ZMEEL_API_KEY and ZMEEL_API_URL in your environment variables as specified here:
    https://docs.openclaw.ai/help/environment

    e.g. 

    {
      env: {
        ZMEEL_API_KEY: "...",
        ZMEEL_API_URL: "...",
      },
    }

    Then set ZMEEL_API_KEY and ZMEEL_API_URL from the saved token field for every heartbeat run.

    Important:
    - claim secrets expire
    - claim secrets are single-use
    - claim fails before board approval

    ## Step 4: Install zmeel skill in OpenClaw
    GET ${onboarding.skill.url}
    Install path: ${onboarding.skill.installPath}

    Be sure to prepend your ZMEEL_API_URL to the top of your skill and note the path to your ZMEEL_API_URL

    ## Text onboarding URL
    ${onboarding.textInstructions.url}

    ## Connectivity guidance
    ${onboarding.connectivity?.guidance ??
        "Ensure zmeel is reachable from your OpenClaw runtime."}
  `);
    const connectionCandidates = Array.isArray(onboarding.connectivity?.connectionCandidates)
        ? onboarding.connectivity.connectionCandidates.filter((entry) => Boolean(entry))
        : [];
    if (connectionCandidates.length > 0) {
        lines.push("## Suggested zmeel base URLs to try");
        for (const candidate of connectionCandidates) {
            lines.push(`- ${candidate}`);
        }
        appendBlock(`

      Test each candidate with:
      - GET <candidate>/api/health
      - set the first reachable candidate as agentDefaultsPayload.zmeelApiUrl when submitting your join request

      If none are reachable: ask your human operator for a reachable hostname/address and help them update network configuration.
      For authenticated/private mode, they may need:
      - pnpm zmeel allowed-hostname <host>
      - then restart zmeel and retry onboarding.
    `);
    }
    if (diagnostics.length > 0) {
        lines.push("## Connectivity diagnostics");
        for (const diag of diagnostics) {
            lines.push(`- [${diag.level}] ${diag.message}`);
            if (diag.hint)
                lines.push(`  hint: ${diag.hint}`);
        }
    }
    appendBlock(`

    ## Helpful endpoints
    ${onboarding.registrationEndpoint.path}
    ${onboarding.claimEndpointTemplate.path}
    ${onboarding.skill.path}
    ${manifest.invite.onboardingPath}
  `);
    return `${lines.join("\n")}\n`;
}
function extractInviteMessage(invite) {
    const rawDefaults = invite.defaultsPayload;
    if (!rawDefaults ||
        typeof rawDefaults !== "object" ||
        Array.isArray(rawDefaults)) {
        return null;
    }
    const rawMessage = rawDefaults.agentMessage;
    if (typeof rawMessage !== "string") {
        return null;
    }
    const trimmed = rawMessage.trim();
    return trimmed.length ? trimmed : null;
}
function mergeInviteDefaults(defaultsPayload, agentMessage) {
    const merged = defaultsPayload && typeof defaultsPayload === "object"
        ? { ...defaultsPayload }
        : {};
    if (agentMessage) {
        merged.agentMessage = agentMessage;
    }
    return Object.keys(merged).length ? merged : null;
}
function requestIp(req) {
    const forwarded = req.header("x-forwarded-for");
    if (forwarded) {
        const first = forwarded.split(",")[0]?.trim();
        if (first)
            return first;
    }
    return req.ip || "unknown";
}
function inviteExpired(invite) {
    return invite.expiresAt.getTime() <= Date.now();
}
function isLocalImplicit(req) {
    return req.actor.type === "board" && req.actor.source === "local_implicit";
}
async function resolveActorEmail(db, req) {
    if (isLocalImplicit(req))
        return "local@zmeel.local";
    const userId = req.actor.userId;
    if (!userId)
        return null;
    const user = await db
        .select({ email: authUsers.email })
        .from(authUsers)
        .where(eq(authUsers.id, userId))
        .then((rows) => rows[0] ?? null);
    return user?.email ?? null;
}
function grantsFromDefaults(defaultsPayload, key) {
    if (!defaultsPayload || typeof defaultsPayload !== "object")
        return [];
    const scoped = defaultsPayload[key];
    if (!scoped || typeof scoped !== "object")
        return [];
    const grants = scoped.grants;
    if (!Array.isArray(grants))
        return [];
    const validPermissionKeys = new Set(PERMISSION_KEYS);
    const result = [];
    for (const item of grants) {
        if (!item || typeof item !== "object")
            continue;
        const record = item;
        if (typeof record.permissionKey !== "string")
            continue;
        if (!validPermissionKeys.has(record.permissionKey))
            continue;
        result.push({
            permissionKey: record.permissionKey,
            scope: record.scope &&
                typeof record.scope === "object" &&
                !Array.isArray(record.scope)
                ? record.scope
                : null
        });
    }
    return result;
}
export function agentJoinGrantsFromDefaults(defaultsPayload) {
    const grants = grantsFromDefaults(defaultsPayload, "agent");
    if (grants.some((grant) => grant.permissionKey === "tasks:assign")) {
        return grants;
    }
    return [
        ...grants,
        {
            permissionKey: "tasks:assign",
            scope: null
        }
    ];
}
export function resolveJoinRequestAgentManagerId(candidates) {
    const ceoCandidates = candidates.filter((candidate) => candidate.role === "ceo");
    if (ceoCandidates.length === 0)
        return null;
    const rootCeo = ceoCandidates.find((candidate) => candidate.reportsTo === null);
    return (rootCeo ?? ceoCandidates[0] ?? null)?.id ?? null;
}
function isInviteTokenHashCollisionError(error) {
    const candidates = [
        error,
        error?.cause ?? null
    ];
    for (const candidate of candidates) {
        if (!candidate || typeof candidate !== "object")
            continue;
        const code = "code" in candidate && typeof candidate.code === "string"
            ? candidate.code
            : null;
        const message = "message" in candidate && typeof candidate.message === "string"
            ? candidate.message
            : "";
        const constraint = "constraint" in candidate && typeof candidate.constraint === "string"
            ? candidate.constraint
            : null;
        if (code !== "23505")
            continue;
        if (constraint === "invites_token_hash_unique_idx")
            return true;
        if (message.includes("invites_token_hash_unique_idx"))
            return true;
    }
    return false;
}
function isAbortError(error) {
    return error instanceof Error && error.name === "AbortError";
}
async function probeInviteResolutionTarget(url, timeoutMs) {
    const startedAt = Date.now();
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), timeoutMs);
    try {
        const response = await fetch(url, {
            method: "HEAD",
            redirect: "manual",
            signal: controller.signal
        });
        const durationMs = Date.now() - startedAt;
        if (response.ok ||
            response.status === 401 ||
            response.status === 403 ||
            response.status === 404 ||
            response.status === 405 ||
            response.status === 422 ||
            response.status === 500 ||
            response.status === 501) {
            return {
                status: "reachable",
                method: "HEAD",
                durationMs,
                httpStatus: response.status,
                message: `Webhook endpoint responded to HEAD with HTTP ${response.status}.`
            };
        }
        return {
            status: "unreachable",
            method: "HEAD",
            durationMs,
            httpStatus: response.status,
            message: `Webhook endpoint probe returned HTTP ${response.status}.`
        };
    }
    catch (error) {
        const durationMs = Date.now() - startedAt;
        if (isAbortError(error)) {
            return {
                status: "timeout",
                method: "HEAD",
                durationMs,
                httpStatus: null,
                message: `Webhook endpoint probe timed out after ${timeoutMs}ms.`
            };
        }
        return {
            status: "unreachable",
            method: "HEAD",
            durationMs,
            httpStatus: null,
            message: error instanceof Error
                ? error.message
                : "Webhook endpoint probe failed."
        };
    }
    finally {
        clearTimeout(timeout);
    }
}
export function accessRoutes(db, opts) {
    const router = Router();
    const access = accessService(db);
    const boardAuth = boardAuthService(db);
    const agents = agentService(db);
    async function assertInstanceAdmin(req) {
        if (req.actor.type !== "board")
            throw unauthorized();
        if (isLocalImplicit(req))
            return;
        const allowed = await access.isInstanceAdmin(req.actor.userId);
        if (!allowed)
            throw forbidden("Instance admin required");
    }
    router.get("/board-claim/:token", async (req, res) => {
        const token = req.params.token.trim();
        const code = typeof req.query.code === "string" ? req.query.code.trim() : undefined;
        if (!token)
            throw notFound("Board claim challenge not found");
        const challenge = inspectBoardClaimChallenge(token, code);
        if (challenge.status === "invalid")
            throw notFound("Board claim challenge not found");
        res.json(challenge);
    });
    router.post("/board-claim/:token/claim", async (req, res) => {
        const token = req.params.token.trim();
        const code = typeof req.body?.code === "string" ? req.body.code.trim() : undefined;
        if (!token)
            throw notFound("Board claim challenge not found");
        if (!code)
            throw badRequest("Claim code is required");
        if (req.actor.type !== "board" ||
            req.actor.source !== "session" ||
            !req.actor.userId) {
            throw unauthorized("Sign in before claiming board ownership");
        }
        const claimed = await claimBoardOwnership(db, {
            token,
            code,
            userId: req.actor.userId
        });
        if (claimed.status === "invalid")
            throw notFound("Board claim challenge not found");
        if (claimed.status === "expired")
            throw conflict("Board claim challenge expired. Restart server to generate a new one.");
        if (claimed.status === "claimed") {
            res.json({
                claimed: true,
                userId: claimed.claimedByUserId ?? req.actor.userId
            });
            return;
        }
        throw conflict("Board claim challenge is no longer available");
    });
    router.post("/cli-auth/challenges", validate(createCliAuthChallengeSchema), async (req, res) => {
        const created = await boardAuth.createCliAuthChallenge(req.body);
        const approvalPath = buildCliAuthApprovalPath(created.challenge.id, created.challengeSecret);
        const baseUrl = requestBaseUrl(req);
        res.status(201).json({
            id: created.challenge.id,
            token: created.challengeSecret,
            boardApiToken: created.pendingBoardToken,
            approvalPath,
            approvalUrl: baseUrl ? `${baseUrl}${approvalPath}` : null,
            pollPath: `/cli-auth/challenges/${created.challenge.id}`,
            expiresAt: created.challenge.expiresAt.toISOString(),
            suggestedPollIntervalMs: 1000,
        });
    });
    router.get("/cli-auth/challenges/:id", async (req, res) => {
        const id = req.params.id.trim();
        const token = typeof req.query.token === "string" ? req.query.token.trim() : "";
        if (!id || !token)
            throw notFound("CLI auth challenge not found");
        const challenge = await boardAuth.describeCliAuthChallenge(id, token);
        if (!challenge)
            throw notFound("CLI auth challenge not found");
        const isSignedInBoardUser = req.actor.type === "board" &&
            (req.actor.source === "session" || isLocalImplicit(req)) &&
            Boolean(req.actor.userId);
        const canApprove = isSignedInBoardUser &&
            (challenge.requestedAccess !== "instance_admin_required" ||
                isLocalImplicit(req) ||
                Boolean(req.actor.isInstanceAdmin));
        res.json({
            ...challenge,
            requiresSignIn: !isSignedInBoardUser,
            canApprove,
            currentUserId: req.actor.type === "board" ? req.actor.userId ?? null : null,
        });
    });
    router.post("/cli-auth/challenges/:id/approve", validate(resolveCliAuthChallengeSchema), async (req, res) => {
        const id = req.params.id.trim();
        if (req.actor.type !== "board" ||
            (!req.actor.userId && !isLocalImplicit(req))) {
            throw unauthorized("Sign in before approving CLI access");
        }
        const userId = req.actor.userId ?? "local-board";
        const approved = await boardAuth.approveCliAuthChallenge(id, req.body.token, userId);
        if (approved.status === "approved") {
            const companyIds = await boardAuth.resolveBoardActivityCompanyIds({
                userId,
                requestedCompanyId: approved.challenge.requestedCompanyId,
                boardApiKeyId: approved.challenge.boardApiKeyId,
            });
            for (const companyId of companyIds) {
                await logActivity(db, {
                    companyId,
                    actorType: "user",
                    actorId: userId,
                    action: "board_api_key.created",
                    entityType: "user",
                    entityId: userId,
                    details: {
                        boardApiKeyId: approved.challenge.boardApiKeyId,
                        requestedAccess: approved.challenge.requestedAccess,
                        requestedCompanyId: approved.challenge.requestedCompanyId,
                        challengeId: approved.challenge.id,
                    },
                });
            }
        }
        res.json({
            approved: approved.status === "approved",
            status: approved.status,
            userId,
            keyId: approved.challenge.boardApiKeyId ?? null,
            expiresAt: approved.challenge.expiresAt.toISOString(),
        });
    });
    router.post("/cli-auth/challenges/:id/cancel", validate(resolveCliAuthChallengeSchema), async (req, res) => {
        const id = req.params.id.trim();
        const cancelled = await boardAuth.cancelCliAuthChallenge(id, req.body.token);
        res.json({
            status: cancelled.status,
            cancelled: cancelled.status === "cancelled",
        });
    });
    router.get("/cli-auth/me", async (req, res) => {
        if (req.actor.type !== "board" || !req.actor.userId) {
            throw unauthorized("Board authentication required");
        }
        const accessSnapshot = await boardAuth.resolveBoardAccess(req.actor.userId);
        res.json({
            user: accessSnapshot.user,
            userId: req.actor.userId,
            isInstanceAdmin: accessSnapshot.isInstanceAdmin,
            companyIds: accessSnapshot.companyIds,
            source: req.actor.source ?? "none",
            keyId: req.actor.source === "board_key" ? req.actor.keyId ?? null : null,
        });
    });
    router.post("/cli-auth/revoke-current", async (req, res) => {
        if (req.actor.type !== "board" || req.actor.source !== "board_key") {
            throw badRequest("Current board API key context is required");
        }
        const key = await boardAuth.assertCurrentBoardKey(req.actor.keyId, req.actor.userId);
        await boardAuth.revokeBoardApiKey(key.id);
        const companyIds = await boardAuth.resolveBoardActivityCompanyIds({
            userId: key.userId,
            boardApiKeyId: key.id,
        });
        for (const companyId of companyIds) {
            await logActivity(db, {
                companyId,
                actorType: "user",
                actorId: key.userId,
                action: "board_api_key.revoked",
                entityType: "user",
                entityId: key.userId,
                details: {
                    boardApiKeyId: key.id,
                    revokedVia: "cli_auth_logout",
                },
            });
        }
        res.json({ revoked: true, keyId: key.id });
    });
    async function assertCompanyPermission(req, companyId, permissionKey) {
        assertCompanyAccess(req, companyId);
        if (req.actor.type === "agent") {
            if (!req.actor.agentId)
                throw forbidden();
            const allowed = await access.hasPermission(companyId, "agent", req.actor.agentId, permissionKey);
            if (!allowed)
                throw forbidden("Permission denied");
            return;
        }
        if (req.actor.type !== "board")
            throw unauthorized();
        if (isLocalImplicit(req))
            return;
        const allowed = await access.canUser(companyId, req.actor.userId, permissionKey);
        if (!allowed)
            throw forbidden("Permission denied");
    }
    async function assertCanGenerateOpenClawInvitePrompt(req, companyId) {
        assertCompanyAccess(req, companyId);
        if (req.actor.type === "agent") {
            if (!req.actor.agentId)
                throw forbidden("Agent authentication required");
            const actorAgent = await agents.getById(req.actor.agentId);
            if (!actorAgent || actorAgent.companyId !== companyId) {
                throw forbidden("Agent key cannot access another company");
            }
            if (actorAgent.role !== "ceo") {
                throw forbidden("Only CEO agents can generate OpenClaw invite prompts");
            }
            return;
        }
        if (req.actor.type !== "board")
            throw unauthorized();
        if (isLocalImplicit(req))
            return;
        const allowed = await access.canUser(companyId, req.actor.userId, "users:invite");
        if (!allowed)
            throw forbidden("Permission denied");
    }
    async function createCompanyInviteForCompany(input) {
        const normalizedAgentMessage = typeof input.agentMessage === "string"
            ? input.agentMessage.trim() || null
            : null;
        const insertValues = {
            companyId: input.companyId,
            inviteType: "company_join",
            allowedJoinTypes: input.allowedJoinTypes,
            defaultsPayload: mergeInviteDefaults(input.defaultsPayload ?? null, normalizedAgentMessage),
            expiresAt: companyInviteExpiresAt(),
            invitedByUserId: input.req.actor.userId ?? null
        };
        let token = null;
        let created = null;
        for (let attempt = 0; attempt < INVITE_TOKEN_MAX_RETRIES; attempt += 1) {
            const candidateToken = createInviteToken();
            try {
                const row = await db
                    .insert(invites)
                    .values({
                    ...insertValues,
                    tokenHash: hashToken(candidateToken)
                })
                    .returning()
                    .then((rows) => rows[0]);
                token = candidateToken;
                created = row;
                break;
            }
            catch (error) {
                if (!isInviteTokenHashCollisionError(error)) {
                    throw error;
                }
            }
        }
        if (!token || !created) {
            throw conflict("Failed to generate a unique invite token. Please retry.");
        }
        return { token, created, normalizedAgentMessage };
    }
    async function getInviteCompanyName(companyId) {
        if (!companyId)
            return null;
        const company = await db
            .select({ name: companies.name })
            .from(companies)
            .where(eq(companies.id, companyId))
            .then((rows) => rows[0] ?? null);
        return company?.name ?? null;
    }
    router.get("/skills/available", (_req, res) => {
        res.json({ skills: listAvailableSkills() });
    });
    router.get("/skills/index", (_req, res) => {
        res.json({
            skills: [
                { name: "zmeel", path: "/api/skills/zmeel" },
                {
                    name: "para-memory-files",
                    path: "/api/skills/para-memory-files"
                },
                {
                    name: "zmeel-create-agent",
                    path: "/api/skills/zmeel-create-agent"
                }
            ]
        });
    });
    router.get("/skills/:skillName", (req, res) => {
        const skillName = req.params.skillName.trim().toLowerCase();
        const markdown = readSkillMarkdown(skillName);
        if (!markdown)
            throw notFound("Skill not found");
        res.type("text/markdown").send(markdown);
    });
    router.post("/companies/:companyId/invites", validate(createCompanyInviteSchema), async (req, res) => {
        const companyId = req.params.companyId;
        await assertCompanyPermission(req, companyId, "users:invite");
        const { token, created, normalizedAgentMessage } = await createCompanyInviteForCompany({
            req,
            companyId,
            allowedJoinTypes: req.body.allowedJoinTypes,
            defaultsPayload: req.body.defaultsPayload ?? null,
            agentMessage: req.body.agentMessage ?? null
        });
        await logActivity(db, {
            companyId,
            actorType: req.actor.type === "agent" ? "agent" : "user",
            actorId: req.actor.type === "agent"
                ? req.actor.agentId ?? "unknown-agent"
                : req.actor.userId ?? "board",
            action: "invite.created",
            entityType: "invite",
            entityId: created.id,
            details: {
                inviteType: created.inviteType,
                allowedJoinTypes: created.allowedJoinTypes,
                expiresAt: created.expiresAt.toISOString(),
                hasAgentMessage: Boolean(normalizedAgentMessage)
            }
        });
        const companyName = await getInviteCompanyName(created.companyId);
        const inviteSummary = toInviteSummaryResponse(req, token, created, companyName);
        res.status(201).json({
            ...created,
            token,
            inviteUrl: `/invite/${token}`,
            companyName,
            onboardingTextPath: inviteSummary.onboardingTextPath,
            onboardingTextUrl: inviteSummary.onboardingTextUrl,
            inviteMessage: inviteSummary.inviteMessage
        });
    });
    router.post("/companies/:companyId/openclaw/invite-prompt", validate(createOpenClawInvitePromptSchema), async (req, res) => {
        const companyId = req.params.companyId;
        await assertCanGenerateOpenClawInvitePrompt(req, companyId);
        const { token, created, normalizedAgentMessage } = await createCompanyInviteForCompany({
            req,
            companyId,
            allowedJoinTypes: "agent",
            defaultsPayload: null,
            agentMessage: req.body.agentMessage ?? null
        });
        await logActivity(db, {
            companyId,
            actorType: req.actor.type === "agent" ? "agent" : "user",
            actorId: req.actor.type === "agent"
                ? req.actor.agentId ?? "unknown-agent"
                : req.actor.userId ?? "board",
            action: "invite.openclaw_prompt_created",
            entityType: "invite",
            entityId: created.id,
            details: {
                inviteType: created.inviteType,
                allowedJoinTypes: created.allowedJoinTypes,
                expiresAt: created.expiresAt.toISOString(),
                hasAgentMessage: Boolean(normalizedAgentMessage)
            }
        });
        const companyName = await getInviteCompanyName(created.companyId);
        const inviteSummary = toInviteSummaryResponse(req, token, created, companyName);
        res.status(201).json({
            ...created,
            token,
            inviteUrl: `/invite/${token}`,
            companyName,
            onboardingTextPath: inviteSummary.onboardingTextPath,
            onboardingTextUrl: inviteSummary.onboardingTextUrl,
            inviteMessage: inviteSummary.inviteMessage
        });
    });
    router.get("/invites/:token", async (req, res) => {
        const token = req.params.token.trim();
        if (!token)
            throw notFound("Invite not found");
        const invite = await db
            .select()
            .from(invites)
            .where(eq(invites.tokenHash, hashToken(token)))
            .then((rows) => rows[0] ?? null);
        if (!invite ||
            invite.revokedAt ||
            invite.acceptedAt ||
            inviteExpired(invite)) {
            throw notFound("Invite not found");
        }
        const companyName = await getInviteCompanyName(invite.companyId);
        res.json(toInviteSummaryResponse(req, token, invite, companyName));
    });
    router.get("/invites/:token/onboarding", async (req, res) => {
        const token = req.params.token.trim();
        if (!token)
            throw notFound("Invite not found");
        const invite = await db
            .select()
            .from(invites)
            .where(eq(invites.tokenHash, hashToken(token)))
            .then((rows) => rows[0] ?? null);
        if (!invite || invite.revokedAt || inviteExpired(invite)) {
            throw notFound("Invite not found");
        }
        const companyName = await getInviteCompanyName(invite.companyId);
        res.json(buildInviteOnboardingManifest(req, token, invite, {
            ...opts,
            companyName
        }));
    });
    router.get("/invites/:token/onboarding.txt", async (req, res) => {
        const token = req.params.token.trim();
        if (!token)
            throw notFound("Invite not found");
        const invite = await db
            .select()
            .from(invites)
            .where(eq(invites.tokenHash, hashToken(token)))
            .then((rows) => rows[0] ?? null);
        if (!invite || invite.revokedAt || inviteExpired(invite)) {
            throw notFound("Invite not found");
        }
        const companyName = await getInviteCompanyName(invite.companyId);
        res
            .type("text/plain; charset=utf-8")
            .send(buildInviteOnboardingTextDocument(req, token, invite, {
            ...opts,
            companyName
        }));
    });
    router.get("/invites/:token/test-resolution", async (req, res) => {
        const token = req.params.token.trim();
        if (!token)
            throw notFound("Invite not found");
        const invite = await db
            .select()
            .from(invites)
            .where(eq(invites.tokenHash, hashToken(token)))
            .then((rows) => rows[0] ?? null);
        if (!invite || invite.revokedAt || inviteExpired(invite)) {
            throw notFound("Invite not found");
        }
        const rawUrl = typeof req.query.url === "string" ? req.query.url.trim() : "";
        if (!rawUrl)
            throw badRequest("url query parameter is required");
        let target;
        try {
            target = new URL(rawUrl);
        }
        catch {
            throw badRequest("url must be an absolute http(s) URL");
        }
        if (target.protocol !== "http:" && target.protocol !== "https:") {
            throw badRequest("url must use http or https");
        }
        const parsedTimeoutMs = typeof req.query.timeoutMs === "string"
            ? Number(req.query.timeoutMs)
            : NaN;
        const timeoutMs = Number.isFinite(parsedTimeoutMs)
            ? Math.max(1000, Math.min(15000, Math.floor(parsedTimeoutMs)))
            : 5000;
        const probe = await probeInviteResolutionTarget(target, timeoutMs);
        res.json({
            inviteId: invite.id,
            testResolutionPath: `/api/invites/${token}/test-resolution`,
            requestedUrl: target.toString(),
            timeoutMs,
            ...probe
        });
    });
    router.post("/invites/:token/accept", validate(acceptInviteSchema), async (req, res) => {
        const token = req.params.token.trim();
        if (!token)
            throw notFound("Invite not found");
        const invite = await db
            .select()
            .from(invites)
            .where(eq(invites.tokenHash, hashToken(token)))
            .then((rows) => rows[0] ?? null);
        if (!invite || invite.revokedAt || inviteExpired(invite)) {
            throw notFound("Invite not found");
        }
        const inviteAlreadyAccepted = Boolean(invite.acceptedAt);
        const existingJoinRequestForInvite = inviteAlreadyAccepted
            ? await db
                .select()
                .from(joinRequests)
                .where(eq(joinRequests.inviteId, invite.id))
                .then((rows) => rows[0] ?? null)
            : null;
        if (invite.inviteType === "bootstrap_ceo") {
            if (inviteAlreadyAccepted)
                throw notFound("Invite not found");
            if (req.body.requestType !== "human") {
                throw badRequest("Bootstrap invite requires human request type");
            }
            if (req.actor.type !== "board" ||
                (!req.actor.userId && !isLocalImplicit(req))) {
                throw unauthorized("Authenticated user required for bootstrap acceptance");
            }
            const userId = req.actor.userId ?? "local-board";
            const existingAdmin = await access.isInstanceAdmin(userId);
            if (!existingAdmin) {
                await access.promoteInstanceAdmin(userId);
            }
            const updatedInvite = await db
                .update(invites)
                .set({ acceptedAt: new Date(), updatedAt: new Date() })
                .where(eq(invites.id, invite.id))
                .returning()
                .then((rows) => rows[0] ?? invite);
            res.status(202).json({
                inviteId: updatedInvite.id,
                inviteType: updatedInvite.inviteType,
                bootstrapAccepted: true,
                userId
            });
            return;
        }
        const requestType = req.body.requestType;
        const companyId = invite.companyId;
        if (!companyId)
            throw conflict("Invite is missing company scope");
        if (invite.allowedJoinTypes !== "both" &&
            invite.allowedJoinTypes !== requestType) {
            throw badRequest(`Invite does not allow ${requestType} joins`);
        }
        if (requestType === "human" && req.actor.type !== "board") {
            throw unauthorized("Human invite acceptance requires authenticated user");
        }
        if (requestType === "human" &&
            !req.actor.userId &&
            !isLocalImplicit(req)) {
            throw unauthorized("Authenticated user is required");
        }
        if (requestType === "agent" && !req.body.agentName) {
            if (!inviteAlreadyAccepted ||
                !existingJoinRequestForInvite?.agentName) {
                throw badRequest("agentName is required for agent join requests");
            }
        }
        const adapterType = req.body.adapterType ?? null;
        if (inviteAlreadyAccepted &&
            !canReplayOpenClawGatewayInviteAccept({
                requestType,
                adapterType,
                existingJoinRequest: existingJoinRequestForInvite
            })) {
            throw notFound("Invite not found");
        }
        const replayJoinRequestId = inviteAlreadyAccepted
            ? existingJoinRequestForInvite?.id ?? null
            : null;
        if (inviteAlreadyAccepted && !replayJoinRequestId) {
            throw conflict("Join request not found");
        }
        const replayMergedDefaults = inviteAlreadyAccepted
            ? mergeJoinDefaultsPayloadForReplay(existingJoinRequestForInvite?.agentDefaultsPayload ?? null, req.body.agentDefaultsPayload ?? null)
            : req.body.agentDefaultsPayload ?? null;
        const gatewayDefaultsPayload = requestType === "agent"
            ? buildJoinDefaultsPayloadForAccept({
                adapterType,
                defaultsPayload: replayMergedDefaults,
                zmeelApiUrl: req.body.zmeelApiUrl ?? null,
                inboundOpenClawAuthHeader: req.header("x-openclaw-auth") ?? null,
                inboundOpenClawTokenHeader: req.header("x-openclaw-token") ?? null
            })
            : null;
        const joinDefaults = requestType === "agent"
            ? normalizeAgentDefaultsForJoin({
                adapterType,
                defaultsPayload: gatewayDefaultsPayload,
                deploymentMode: opts.deploymentMode,
                deploymentExposure: opts.deploymentExposure,
                bindHost: opts.bindHost,
                allowedHostnames: opts.allowedHostnames
            })
            : {
                normalized: null,
                diagnostics: [],
                fatalErrors: []
            };
        if (requestType === "agent" && joinDefaults.fatalErrors.length > 0) {
            throw badRequest(joinDefaults.fatalErrors.join("; "));
        }
        if (requestType === "agent" && adapterType === "openclaw_gateway") {
            logger.info({
                inviteId: invite.id,
                joinRequestDiagnostics: joinDefaults.diagnostics.map((diag) => ({
                    code: diag.code,
                    level: diag.level
                })),
                normalizedAgentDefaults: summarizeOpenClawGatewayDefaultsForLog(joinDefaults.normalized)
            }, "invite accept normalized OpenClaw gateway defaults");
        }
        const claimSecret = requestType === "agent" && !inviteAlreadyAccepted
            ? createClaimSecret()
            : null;
        const claimSecretHash = claimSecret ? hashToken(claimSecret) : null;
        const claimSecretExpiresAt = claimSecret
            ? new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
            : null;
        const actorEmail = requestType === "human" ? await resolveActorEmail(db, req) : null;
        const created = !inviteAlreadyAccepted
            ? await db.transaction(async (tx) => {
                await tx
                    .update(invites)
                    .set({ acceptedAt: new Date(), updatedAt: new Date() })
                    .where(and(eq(invites.id, invite.id), isNull(invites.acceptedAt), isNull(invites.revokedAt)));
                const row = await tx
                    .insert(joinRequests)
                    .values({
                    inviteId: invite.id,
                    companyId,
                    requestType,
                    status: "pending_approval",
                    requestIp: requestIp(req),
                    requestingUserId: requestType === "human"
                        ? req.actor.userId ?? "local-board"
                        : null,
                    requestEmailSnapshot: requestType === "human" ? actorEmail : null,
                    agentName: requestType === "agent" ? req.body.agentName : null,
                    adapterType: requestType === "agent" ? adapterType : null,
                    capabilities: requestType === "agent"
                        ? req.body.capabilities ?? null
                        : null,
                    agentDefaultsPayload: requestType === "agent" ? joinDefaults.normalized : null,
                    claimSecretHash,
                    claimSecretExpiresAt
                })
                    .returning()
                    .then((rows) => rows[0]);
                return row;
            })
            : await db
                .update(joinRequests)
                .set({
                requestIp: requestIp(req),
                agentName: requestType === "agent"
                    ? req.body.agentName ??
                        existingJoinRequestForInvite?.agentName ??
                        null
                    : null,
                capabilities: requestType === "agent"
                    ? req.body.capabilities ??
                        existingJoinRequestForInvite?.capabilities ??
                        null
                    : null,
                adapterType: requestType === "agent" ? adapterType : null,
                agentDefaultsPayload: requestType === "agent" ? joinDefaults.normalized : null,
                updatedAt: new Date()
            })
                .where(eq(joinRequests.id, replayJoinRequestId))
                .returning()
                .then((rows) => rows[0]);
        if (!created) {
            throw conflict("Join request not found");
        }
        if (inviteAlreadyAccepted &&
            requestType === "agent" &&
            adapterType === "openclaw_gateway" &&
            created.status === "approved" &&
            created.createdAgentId) {
            const existingAgent = await agents.getById(created.createdAgentId);
            if (!existingAgent) {
                throw conflict("Approved join request agent not found");
            }
            const existingAdapterConfig = isPlainObject(existingAgent.adapterConfig)
                ? existingAgent.adapterConfig
                : {};
            const nextAdapterConfig = {
                ...existingAdapterConfig,
                ...(joinDefaults.normalized ?? {})
            };
            const updatedAgent = await agents.update(created.createdAgentId, {
                adapterType,
                adapterConfig: nextAdapterConfig
            });
            if (!updatedAgent) {
                throw conflict("Approved join request agent not found");
            }
            await logActivity(db, {
                companyId,
                actorType: req.actor.type === "agent" ? "agent" : "user",
                actorId: req.actor.type === "agent"
                    ? req.actor.agentId ?? "invite-agent"
                    : req.actor.userId ?? "board",
                action: "agent.updated_from_join_replay",
                entityType: "agent",
                entityId: updatedAgent.id,
                details: { inviteId: invite.id, joinRequestId: created.id }
            });
        }
        if (requestType === "agent" && adapterType === "openclaw_gateway") {
            const expectedDefaults = summarizeOpenClawGatewayDefaultsForLog(joinDefaults.normalized);
            const persistedDefaults = summarizeOpenClawGatewayDefaultsForLog(created.agentDefaultsPayload);
            const missingPersistedFields = [];
            if (expectedDefaults.url && !persistedDefaults.url)
                missingPersistedFields.push("url");
            if (expectedDefaults.zmeelApiUrl &&
                !persistedDefaults.zmeelApiUrl) {
                missingPersistedFields.push("zmeelApiUrl");
            }
            if (expectedDefaults.gatewayToken && !persistedDefaults.gatewayToken) {
                missingPersistedFields.push("headers.x-openclaw-token");
            }
            if (expectedDefaults.devicePrivateKeyPem &&
                !persistedDefaults.devicePrivateKeyPem) {
                missingPersistedFields.push("devicePrivateKeyPem");
            }
            if (expectedDefaults.headerKeys.length > 0 &&
                persistedDefaults.headerKeys.length === 0) {
                missingPersistedFields.push("headers");
            }
            logger.info({
                inviteId: invite.id,
                joinRequestId: created.id,
                joinRequestStatus: created.status,
                expectedDefaults,
                persistedDefaults,
                diagnostics: joinDefaults.diagnostics.map((diag) => ({
                    code: diag.code,
                    level: diag.level,
                    message: diag.message,
                    hint: diag.hint ?? null
                }))
            }, "invite accept persisted OpenClaw gateway join request");
            if (missingPersistedFields.length > 0) {
                logger.warn({
                    inviteId: invite.id,
                    joinRequestId: created.id,
                    missingPersistedFields
                }, "invite accept detected missing persisted OpenClaw gateway defaults");
            }
        }
        await logActivity(db, {
            companyId,
            actorType: req.actor.type === "agent" ? "agent" : "user",
            actorId: req.actor.type === "agent"
                ? req.actor.agentId ?? "invite-agent"
                : req.actor.userId ??
                    (requestType === "agent" ? "invite-anon" : "board"),
            action: inviteAlreadyAccepted
                ? "join.request_replayed"
                : "join.requested",
            entityType: "join_request",
            entityId: created.id,
            details: {
                requestType,
                requestIp: created.requestIp,
                inviteReplay: inviteAlreadyAccepted
            }
        });
        const response = toJoinRequestResponse(created);
        if (claimSecret) {
            const companyName = await getInviteCompanyName(invite.companyId);
            const onboardingManifest = buildInviteOnboardingManifest(req, token, invite, {
                ...opts,
                companyName
            });
            res.status(202).json({
                ...response,
                claimSecret,
                claimApiKeyPath: `/api/join-requests/${created.id}/claim-api-key`,
                onboarding: onboardingManifest.onboarding,
                diagnostics: joinDefaults.diagnostics
            });
            return;
        }
        res.status(202).json({
            ...response,
            ...(joinDefaults.diagnostics.length > 0
                ? { diagnostics: joinDefaults.diagnostics }
                : {})
        });
    });
    router.post("/invites/:inviteId/revoke", async (req, res) => {
        const id = req.params.inviteId;
        const invite = await db
            .select()
            .from(invites)
            .where(eq(invites.id, id))
            .then((rows) => rows[0] ?? null);
        if (!invite)
            throw notFound("Invite not found");
        if (invite.inviteType === "bootstrap_ceo") {
            await assertInstanceAdmin(req);
        }
        else {
            if (!invite.companyId)
                throw conflict("Invite is missing company scope");
            await assertCompanyPermission(req, invite.companyId, "users:invite");
        }
        if (invite.acceptedAt)
            throw conflict("Invite already consumed");
        if (invite.revokedAt)
            return res.json(invite);
        const revoked = await db
            .update(invites)
            .set({ revokedAt: new Date(), updatedAt: new Date() })
            .where(eq(invites.id, id))
            .returning()
            .then((rows) => rows[0]);
        if (invite.companyId) {
            await logActivity(db, {
                companyId: invite.companyId,
                actorType: req.actor.type === "agent" ? "agent" : "user",
                actorId: req.actor.type === "agent"
                    ? req.actor.agentId ?? "unknown-agent"
                    : req.actor.userId ?? "board",
                action: "invite.revoked",
                entityType: "invite",
                entityId: id
            });
        }
        res.json(revoked);
    });
    router.get("/companies/:companyId/join-requests", async (req, res) => {
        const companyId = req.params.companyId;
        await assertCompanyPermission(req, companyId, "joins:approve");
        const query = listJoinRequestsQuerySchema.parse(req.query);
        const all = await db
            .select()
            .from(joinRequests)
            .where(eq(joinRequests.companyId, companyId))
            .orderBy(desc(joinRequests.createdAt));
        const filtered = all.filter((row) => {
            if (query.status && row.status !== query.status)
                return false;
            if (query.requestType && row.requestType !== query.requestType)
                return false;
            return true;
        });
        res.json(filtered.map(toJoinRequestResponse));
    });
    router.post("/companies/:companyId/join-requests/:requestId/approve", async (req, res) => {
        const companyId = req.params.companyId;
        const requestId = req.params.requestId;
        await assertCompanyPermission(req, companyId, "joins:approve");
        const existing = await db
            .select()
            .from(joinRequests)
            .where(and(eq(joinRequests.companyId, companyId), eq(joinRequests.id, requestId)))
            .then((rows) => rows[0] ?? null);
        if (!existing)
            throw notFound("Join request not found");
        if (existing.status !== "pending_approval")
            throw conflict("Join request is not pending");
        const invite = await db
            .select()
            .from(invites)
            .where(eq(invites.id, existing.inviteId))
            .then((rows) => rows[0] ?? null);
        if (!invite)
            throw notFound("Invite not found");
        let createdAgentId = existing.createdAgentId ?? null;
        if (existing.requestType === "human") {
            if (!existing.requestingUserId)
                throw conflict("Join request missing user identity");
            await access.ensureMembership(companyId, "user", existing.requestingUserId, "member", "active");
            const grants = grantsFromDefaults(invite.defaultsPayload, "human");
            await access.setPrincipalGrants(companyId, "user", existing.requestingUserId, grants, req.actor.userId ?? null);
        }
        else {
            const existingAgents = await agents.list(companyId);
            const managerId = resolveJoinRequestAgentManagerId(existingAgents);
            if (!managerId) {
                throw conflict("Join request cannot be approved because this company has no active CEO");
            }
            const agentName = deduplicateAgentName(existing.agentName ?? "New Agent", existingAgents.map((a) => ({
                id: a.id,
                name: a.name,
                status: a.status
            })));
            const created = await agents.create(companyId, {
                name: agentName,
                role: "general",
                title: null,
                status: "idle",
                reportsTo: managerId,
                capabilities: existing.capabilities ?? null,
                adapterType: existing.adapterType ?? "process",
                adapterConfig: existing.agentDefaultsPayload &&
                    typeof existing.agentDefaultsPayload === "object"
                    ? existing.agentDefaultsPayload
                    : {},
                runtimeConfig: {},
                budgetMonthlyCents: 0,
                spentMonthlyCents: 0,
                permissions: {},
                lastHeartbeatAt: null,
                metadata: null
            });
            createdAgentId = created.id;
            await access.ensureMembership(companyId, "agent", created.id, "member", "active");
            const grants = agentJoinGrantsFromDefaults(invite.defaultsPayload);
            await access.setPrincipalGrants(companyId, "agent", created.id, grants, req.actor.userId ?? null);
        }
        const approved = await db
            .update(joinRequests)
            .set({
            status: "approved",
            approvedByUserId: req.actor.userId ?? (isLocalImplicit(req) ? "local-board" : null),
            approvedAt: new Date(),
            createdAgentId,
            updatedAt: new Date()
        })
            .where(eq(joinRequests.id, requestId))
            .returning()
            .then((rows) => rows[0]);
        await logActivity(db, {
            companyId,
            actorType: "user",
            actorId: req.actor.userId ?? "board",
            action: "join.approved",
            entityType: "join_request",
            entityId: requestId,
            details: { requestType: existing.requestType, createdAgentId }
        });
        if (createdAgentId) {
            void notifyHireApproved(db, {
                companyId,
                agentId: createdAgentId,
                source: "join_request",
                sourceId: requestId,
                approvedAt: new Date()
            }).catch(() => { });
        }
        res.json(toJoinRequestResponse(approved));
    });
    router.post("/companies/:companyId/join-requests/:requestId/reject", async (req, res) => {
        const companyId = req.params.companyId;
        const requestId = req.params.requestId;
        await assertCompanyPermission(req, companyId, "joins:approve");
        const existing = await db
            .select()
            .from(joinRequests)
            .where(and(eq(joinRequests.companyId, companyId), eq(joinRequests.id, requestId)))
            .then((rows) => rows[0] ?? null);
        if (!existing)
            throw notFound("Join request not found");
        if (existing.status !== "pending_approval")
            throw conflict("Join request is not pending");
        const rejected = await db
            .update(joinRequests)
            .set({
            status: "rejected",
            rejectedByUserId: req.actor.userId ?? (isLocalImplicit(req) ? "local-board" : null),
            rejectedAt: new Date(),
            updatedAt: new Date()
        })
            .where(eq(joinRequests.id, requestId))
            .returning()
            .then((rows) => rows[0]);
        await logActivity(db, {
            companyId,
            actorType: "user",
            actorId: req.actor.userId ?? "board",
            action: "join.rejected",
            entityType: "join_request",
            entityId: requestId,
            details: { requestType: existing.requestType }
        });
        res.json(toJoinRequestResponse(rejected));
    });
    router.post("/join-requests/:requestId/claim-api-key", validate(claimJoinRequestApiKeySchema), async (req, res) => {
        const requestId = req.params.requestId;
        const presentedClaimSecretHash = hashToken(req.body.claimSecret);
        const joinRequest = await db
            .select()
            .from(joinRequests)
            .where(eq(joinRequests.id, requestId))
            .then((rows) => rows[0] ?? null);
        if (!joinRequest)
            throw notFound("Join request not found");
        if (joinRequest.requestType !== "agent")
            throw badRequest("Only agent join requests can claim API keys");
        if (joinRequest.status !== "approved")
            throw conflict("Join request must be approved before key claim");
        if (!joinRequest.createdAgentId)
            throw conflict("Join request has no created agent");
        if (!joinRequest.claimSecretHash)
            throw conflict("Join request is missing claim secret metadata");
        if (!tokenHashesMatch(joinRequest.claimSecretHash, presentedClaimSecretHash)) {
            throw forbidden("Invalid claim secret");
        }
        if (joinRequest.claimSecretExpiresAt &&
            joinRequest.claimSecretExpiresAt.getTime() <= Date.now()) {
            throw conflict("Claim secret expired");
        }
        if (joinRequest.claimSecretConsumedAt)
            throw conflict("Claim secret already used");
        const existingKey = await db
            .select({ id: agentApiKeys.id })
            .from(agentApiKeys)
            .where(eq(agentApiKeys.agentId, joinRequest.createdAgentId))
            .then((rows) => rows[0] ?? null);
        if (existingKey)
            throw conflict("API key already claimed");
        const consumed = await db
            .update(joinRequests)
            .set({ claimSecretConsumedAt: new Date(), updatedAt: new Date() })
            .where(and(eq(joinRequests.id, requestId), isNull(joinRequests.claimSecretConsumedAt)))
            .returning({ id: joinRequests.id })
            .then((rows) => rows[0] ?? null);
        if (!consumed)
            throw conflict("Claim secret already used");
        const created = await agents.createApiKey(joinRequest.createdAgentId, "initial-join-key");
        await logActivity(db, {
            companyId: joinRequest.companyId,
            actorType: "system",
            actorId: "join-claim",
            action: "agent_api_key.claimed",
            entityType: "agent_api_key",
            entityId: created.id,
            details: {
                agentId: joinRequest.createdAgentId,
                joinRequestId: requestId
            }
        });
        res.status(201).json({
            keyId: created.id,
            token: created.token,
            agentId: joinRequest.createdAgentId,
            createdAt: created.createdAt
        });
    });
    router.get("/companies/:companyId/members", async (req, res) => {
        const companyId = req.params.companyId;
        await assertCompanyPermission(req, companyId, "users:manage_permissions");
        const members = await access.listMembers(companyId);
        res.json(members);
    });
    router.patch("/companies/:companyId/members/:memberId/permissions", validate(updateMemberPermissionsSchema), async (req, res) => {
        const companyId = req.params.companyId;
        const memberId = req.params.memberId;
        await assertCompanyPermission(req, companyId, "users:manage_permissions");
        const updated = await access.setMemberPermissions(companyId, memberId, req.body.grants ?? [], req.actor.userId ?? null);
        if (!updated)
            throw notFound("Member not found");
        res.json(updated);
    });
    router.post("/admin/users/:userId/promote-instance-admin", async (req, res) => {
        await assertInstanceAdmin(req);
        const userId = req.params.userId;
        const result = await access.promoteInstanceAdmin(userId);
        res.status(201).json(result);
    });
    router.post("/admin/users/:userId/demote-instance-admin", async (req, res) => {
        await assertInstanceAdmin(req);
        const userId = req.params.userId;
        const removed = await access.demoteInstanceAdmin(userId);
        if (!removed)
            throw notFound("Instance admin role not found");
        res.json(removed);
    });
    router.get("/admin/users/:userId/company-access", async (req, res) => {
        await assertInstanceAdmin(req);
        const userId = req.params.userId;
        const memberships = await access.listUserCompanyAccess(userId);
        res.json(memberships);
    });
    router.put("/admin/users/:userId/company-access", validate(updateUserCompanyAccessSchema), async (req, res) => {
        await assertInstanceAdmin(req);
        const userId = req.params.userId;
        const memberships = await access.setUserCompanyAccess(userId, req.body.companyIds ?? []);
        res.json(memberships);
    });
    return router;
}
//# sourceMappingURL=access.js.map