guanghu-ice-heart/server-tools/lake-lamp-authz/server.js

514 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
const crypto = require("node:crypto");
const fs = require("node:fs");
const http = require("node:http");
const path = require("node:path");
const { WorkOrderManager } = require("./workorder-manager");
const { MapGate } = require("./map-gate");
const { sendSmtpMail } = require("./smtp-mailer");
const { executeRegisteredAction } = require("./action-client");
const { enqueueDeploymentEvent } = require("./deployment-event");
const DEFAULT_ACTIONS = Object.freeze({
"server-login": [
"read-navigation-map",
"inspect-services",
"health-check",
"inspect-code-channel-owner-auth",
],
"server-ops": [
"read-navigation-map",
"inspect-services",
"pull-registered-repo",
"deploy-registered-service",
"restart-registered-service",
"health-check",
"rollback-registered-service",
"provision-approved-architecture",
"push-repository",
"restore-owner-password-login",
"restore-code-channel-owner-login",
"dispatch-approved-deployment",
],
"repo-push": ["read-navigation-map", "push-repository"],
});
function createApp(options = {}) {
const requestToken = options.requestToken || process.env.LAKE_LAMP_REQUEST_TOKEN || "";
const ownerEmail = options.ownerEmail || process.env.LAKE_LAMP_OWNER_EMAIL || "";
const approvers = options.approvers || loadApprovers(options.approversFile || process.env.LAKE_LAMP_APPROVERS_FILE || "", ownerEmail);
const publicBaseUrl = String(options.publicBaseUrl || process.env.LAKE_LAMP_PUBLIC_URL || "").replace(/\/$/, "");
const targets = new Set(options.targets || splitCsv(process.env.LAKE_LAMP_TARGETS || "JD-FD-PRIMARY,BS-GZ-006"));
const actions = options.actions || DEFAULT_ACTIONS;
const manager = options.manager || new WorkOrderManager({
approvalTtl: Number(options.approvalTtl || process.env.LAKE_LAMP_APPROVAL_TTL || 3 * 60 * 60),
sessionTtl: Number(options.sessionTtl || process.env.LAKE_LAMP_SESSION_TTL || 3 * 60 * 60),
maxSessionLifetime: Number(options.maxSessionLifetime || process.env.LAKE_LAMP_MAX_SESSION_LIFETIME || 24 * 60 * 60),
stateFile: Object.prototype.hasOwnProperty.call(options, "stateFile") ? options.stateFile : (process.env.LAKE_LAMP_STATE_FILE || "/var/lib/guanghu/lake-lamp-authz/state.json"),
});
const sendEmail = options.sendEmail || (message => sendSmtpMail({
...message,
smtpHost: process.env.SMTP_HOST || "smtp.qq.com",
smtpPort: Number(process.env.SMTP_PORT || 465),
smtpUser: process.env.SMTP_USER || ownerEmail,
smtpPass: process.env.QQ_SMTP_AUTH_CODE || "",
}));
const mapGate = options.mapGate || new MapGate({
mapsDir: options.mapsDir || process.env.LAKE_LAMP_MAPS_DIR || "/etc/guanghu/navigation-maps",
stateFile: Object.prototype.hasOwnProperty.call(options, "mapStateFile") ? options.mapStateFile : (process.env.LAKE_LAMP_MAP_STATE_FILE || "/var/lib/guanghu/lake-lamp-authz/map-acks.json"),
});
const repoGrantDir = options.repoGrantDir || process.env.LAKE_LAMP_REPO_GRANT_DIR || "/var/lib/guanghu/repo-authorizations";
const deploymentQueueDir = options.deploymentQueueDir || process.env.LAKE_LAMP_DEPLOYMENT_EVENT_DIR || "/var/lib/guanghu/deployment-events";
const executeAction = options.executeAction || executeRegisteredAction;
// Creating a powerless request must never become harder than the human mail
// handoff. Keep at least three attempts per network each hour.
const publicCreateLimit = Math.max(3, Number(options.publicCreateLimit || process.env.LAKE_LAMP_PUBLIC_CREATE_LIMIT || 24));
const publicCreateLimiter = options.publicCreateLimiter || new SlidingWindowLimiter(publicCreateLimit, 60 * 60);
const publicCreateGlobalLimiter = options.publicCreateGlobalLimiter || new SlidingWindowLimiter(Number(options.publicCreateGlobalLimit || process.env.LAKE_LAMP_PUBLIC_CREATE_GLOBAL_LIMIT || 60), 60 * 60);
// Owner handoff is a human recovery path, not a login endpoint. Always allow
// at least three genuine mail attempts per network each hour, even if an old
// deployment environment accidentally configures a lower value.
const publicMailLimit = Math.max(3, Number(options.publicMailLimit || process.env.LAKE_LAMP_PUBLIC_MAIL_LIMIT || 12));
const publicMailLimiter = options.publicMailLimiter || new SlidingWindowLimiter(publicMailLimit, 60 * 60);
const publicMailGlobalLimiter = options.publicMailGlobalLimiter || new SlidingWindowLimiter(Number(options.publicMailGlobalLimit || process.env.LAKE_LAMP_PUBLIC_MAIL_GLOBAL_LIMIT || 30), 60 * 60);
async function sendApprovalEmail(handoffToken) {
const issued = manager.issueApproval(handoffToken);
if (!issued.ok) return issued;
const approver = selectApprover(approvers, issued.order);
if (!approver) {
manager.failApprovalEmail(handoffToken);
return { ok: false, reason: "no_registered_approver" };
}
const approvalUrl = `${publicBaseUrl}/approve/${issued.approvalToken}`;
const emailSent = await sendEmail({
to: approver.email,
subject: `小湖灯授权请求 · ${issued.order.target}`,
approvalUrl,
order: issued.order,
});
if (!emailSent) {
manager.failApprovalEmail(handoffToken);
return { ok: false, reason: "authorization_email_failed" };
}
return { ok: true, order: issued.order };
}
return http.createServer(async (req, res) => {
try {
const url = new URL(req.url, "http://localhost");
if (req.method === "GET" && url.pathname === "/health") return json(res, 200, {
ok: true,
service: "lake-lamp-authz",
auth_mode: "email-link",
approval_ttl: manager.approvalTtl,
session_ttl: manager.sessionTtl,
max_session_lifetime: manager.maxSessionLifetime,
auto_renew_on_activity: true,
});
if (req.method === "GET" && url.pathname === "/api/public/capabilities") return json(res, 200, {
schema: "guanghu.lake-lamp-public-workorder/v1",
create_workorder: `${publicBaseUrl}/api/public/workorders`,
required_fields: ["persona_id", "target", "scope", "action"],
optional_fields: ["persona_name", "description", "resource"],
targets: [...targets],
scopes: actions,
owner_handoff: "open request_url and request pre-registered mailbox verification",
workflow: ["create_workorder", "owner_handoff", "claim_session", "read_navigation_map", "ack_navigation_map", "check_session_status", "execute_registered_action", "read_operation_receipt"],
diagnostics: diagnosticCatalog(),
approval_ttl: manager.approvalTtl,
session_ttl: manager.sessionTtl,
max_session_lifetime: manager.maxSessionLifetime,
auto_renew_on_activity: true,
limits: {
create_per_network_per_hour: publicCreateLimit,
email_per_network_per_hour: publicMailLimit,
},
});
const requestMatch = url.pathname.match(/^\/request\/([A-Za-z0-9_-]{20,})$/);
if (requestMatch && req.method === "GET") {
const inspected = manager.inspectHandoff(requestMatch[1]);
if (!inspected.ok) return html(res, 410, requestErrorPage(inspected.reason));
return html(res, 200, requestPage(inspected.order));
}
if (requestMatch && req.method === "POST") {
const inspected = manager.inspectHandoff(requestMatch[1]);
if (!inspected.ok) return html(res, 410, requestErrorPage(inspected.reason));
// Refreshing or reopening an already-sent request must not consume a
// second rate-limit slot. It also must not send a duplicate email.
if (inspected.order.approval_email_sent) return html(res, 200, emailSentPage(inspected.order));
const source = clientAddress(req);
if (!publicMailLimiter.take(source) || !publicMailGlobalLimiter.take("global")) return html(res, 429, requestErrorPage("rate_limited"));
const sent = await sendApprovalEmail(requestMatch[1]);
if (!sent.ok && sent.reason !== "approval_email_already_sent") return html(res, sent.reason === "authorization_email_failed" ? 502 : 410, requestErrorPage(sent.reason));
return html(res, 200, emailSentPage(sent.order));
}
const approvalMatch = url.pathname.match(/^\/approve\/([A-Za-z0-9_-]{20,})$/);
if (approvalMatch && req.method === "GET") {
const inspected = manager.inspectApproval(approvalMatch[1]);
if (!inspected.ok) return html(res, 410, approvalErrorPage(inspected.reason));
return html(res, 200, approvalPage(inspected.order, approvalMatch[1]));
}
if (approvalMatch && req.method === "POST") {
const approved = manager.approve(approvalMatch[1]);
if (!approved.ok) return html(res, 410, approvalErrorPage(approved.reason));
return html(res, 200, approvedPage(approved.order));
}
if (req.method === "POST" && url.pathname === "/api/public/workorders") {
const source = clientAddress(req);
if (!publicCreateLimiter.take(source) || !publicCreateGlobalLimiter.take("global")) return json(res, 429, { error: "rate_limited", retry_after: 3600 });
const body = await readJson(req);
if (!body) return json(res, 400, { error: "invalid_json" });
const validation = validateWorkorderBody(body, targets, actions);
if (!validation.ok) return json(res, validation.status, { error: validation.error });
const created = manager.request(validation.request);
let emailStatus = "not_requested";
if (body.owner_notify === true) {
if (!publicMailLimiter.take(source) || !publicMailGlobalLimiter.take("global")) emailStatus = "rate_limited";
else {
const sent = await sendApprovalEmail(created.handoffToken);
emailStatus = sent.ok ? "sent" : String(sent.reason || "failed");
}
}
return json(res, 201, {
ok: true,
workorder_id: created.id,
claim_token: created.claimToken,
request_url: `${publicBaseUrl}/request/${created.handoffToken}`,
expires_in: created.expiresIn,
status: emailStatus === "sent" ? "waiting_for_owner" : "waiting_for_owner_handoff",
email_status: emailStatus,
receipt: receipt({ state: emailStatus === "sent" ? "waiting_for_owner" : "waiting_for_owner_handoff", diagnostic_code: emailStatus === "sent" ? "owner_email_sent" : "owner_handoff_required", workorder_id: created.id, next_step: emailStatus === "sent" ? "主人邮箱已收到批准链接;等待批准后领取会话。" : "把 request_url 交给主人打开一次;或由光湖语言人格系统在建单时显式提交 owner_notify=true。不要索要密码、验证码或令牌。" }),
});
}
if (req.method === "POST" && url.pathname === "/api/workorders") {
if (!bearerMatches(req, requestToken)) return json(res, 401, { error: "request_auth_required" });
const body = await readJson(req);
if (!body) return json(res, 400, { error: "invalid_json" });
const validation = validateWorkorderBody(body, targets, actions);
if (!validation.ok) return json(res, validation.status, { error: validation.error });
const expectedFingerprint = sha256(ownerEmail.toLowerCase());
if (!body.recipient_fingerprint || !safeEqual(body.recipient_fingerprint, expectedFingerprint)) return json(res, 403, { error: "owner_identity_mismatch" });
const created = manager.request(validation.request);
const sent = await sendApprovalEmail(created.handoffToken);
if (!sent.ok) return json(res, 502, { error: sent.reason });
return json(res, 201, { ok: true, workorder_id: created.id, claim_token: created.claimToken, expires_in: created.expiresIn, status: "waiting_for_owner" });
}
const claimMatch = url.pathname.match(/^\/api\/workorders\/([0-9a-f-]{36})\/claim$/i);
if (req.method === "POST" && claimMatch) {
const token = bearer(req);
const claimed = manager.claim(claimMatch[1], token);
if (!claimed.ok) return json(res, claimed.reason === "approval_pending" ? 202 : 403, { error: claimed.reason });
return json(res, 200, { ok: true, session_token: claimed.sessionToken, expires_in: claimed.expiresIn, target: claimed.target, scope: claimed.scope, action: claimed.action, resource: claimed.resource || "", receipt: claimed.receipt || receipt({ state: "session_issued", diagnostic_code: "session_issued", workorder_id: claimed.workorderId, next_step: "读取并确认实时导航图。" }) });
}
if (req.method === "POST" && url.pathname === "/api/session/status") {
const body = await readJson(req);
if (!body) return json(res, 400, failure("invalid_json"));
const token = bearer(req);
const target = String(body.target || "");
const scope = String(body.scope || "");
const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, target, scope, "read-navigation-map");
if (!verified.ok) return json(res, 403, failure(verified.reason));
const map = mapGate.read(target);
const mapVerified = mapGate.verify(token, target, map.hash);
return json(res, 200, { ok: true, state: mapVerified.ok ? "ready_to_execute" : "map_ack_required", workorder_id: verified.session.workorderId || "", target, scope, allowed_actions: verified.session.actions || [verified.session.action], expires_at: verified.session.expiresAt, map: { hash: map.hash, acknowledged: mapVerified.ok }, last_receipt: verified.session.lastReceipt || null, next_step: mapVerified.ok ? "只执行 allowed_actions 中已登记的动作;每次执行后读取 operation receipt。" : "先读取 /api/navigation-map/read再提交同一 map_hash 至 /api/navigation-map/ack。" });
}
if (req.method === "POST" && url.pathname === "/api/session/verify") {
const body = await readJson(req);
if (!body) return json(res, 400, { error: "invalid_json" });
const verified = manager.verifySession(bearer(req), { pid: String(body.persona_id || "") }, String(body.target || ""), String(body.scope || ""), String(body.action || ""), Date.now() / 1000, Object.prototype.hasOwnProperty.call(body, "resource") ? String(body.resource || "") : undefined);
if (!verified.ok) return json(res, 403, { error: verified.reason });
if (body.action !== "read-navigation-map") {
const map = mapGate.read(String(body.target || ""));
const mapVerified = mapGate.verify(bearer(req), String(body.target || ""), map.hash);
if (!mapVerified.ok) return json(res, 423, { error: mapVerified.reason, required_action: "read-navigation-map" });
}
return json(res, 200, { ok: true, expires_at: verified.session.expiresAt });
}
if (req.method === "POST" && url.pathname === "/api/session/renew") {
const body = await readJson(req);
if (!body) return json(res, 400, { error: "invalid_json" });
if (body.action || body.actions || body.target_override || body.scope_override || body.resource) return json(res, 400, { error: "renewal_cannot_expand_authority" });
const token = bearer(req);
const target = String(body.target || "");
const scope = String(body.scope || "");
const renewed = manager.renewSession(token, { pid: String(body.persona_id || "") }, target, scope);
if (!renewed.ok) return json(res, 403, { error: renewed.reason });
const map = mapGate.read(target);
const acked = mapGate.ack(token, target, map.hash, Date.now() / 1000, Math.max(1, renewed.expiresAt - Date.now() / 1000));
if (!acked.ok) return json(res, 409, { error: acked.reason });
return json(res, 200, { ok: true, target, scope, expires_at: renewed.expiresAt, renewals: renewed.renewals, authority_expanded: false });
}
if (req.method === "POST" && url.pathname === "/api/navigation-map/read") {
const body = await readJson(req);
if (!body) return json(res, 400, { error: "invalid_json" });
const verified = manager.verifySession(bearer(req), { pid: String(body.persona_id || "") }, String(body.target || ""), String(body.scope || ""), "read-navigation-map");
if (!verified.ok) return json(res, 403, { error: verified.reason });
const map = mapGate.read(String(body.target || ""));
return json(res, 200, { ok: true, target: body.target, map_hash: map.hash, navigation_map: map.data });
}
if (req.method === "POST" && url.pathname === "/api/navigation-map/ack") {
const body = await readJson(req);
if (!body) return json(res, 400, { error: "invalid_json" });
const token = bearer(req);
const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, String(body.target || ""), String(body.scope || ""), "read-navigation-map");
if (!verified.ok) return json(res, 403, { error: verified.reason });
const acked = mapGate.ack(token, String(body.target || ""), String(body.map_hash || ""), Date.now() / 1000, Math.max(1, verified.session.expiresAt - Date.now() / 1000));
if (!acked.ok) return json(res, 409, failure(acked.reason));
const operationReceipt = receipt({ state: "map_acknowledged", diagnostic_code: "map_acknowledged", workorder_id: verified.session.workorderId, target: body.target, next_step: "可查询 session/status再执行本会话 allowed_actions 内的固定动作。" });
manager.recordReceipt(token, operationReceipt);
return json(res, 200, { ok: true, target: body.target, map_hash: body.map_hash, receipt: operationReceipt });
}
if (req.method === "POST" && url.pathname === "/api/actions/execute") {
const body = await readJson(req);
if (!body) return json(res, 400, failure("invalid_json"));
if (body.cmd || body.command || body.shell || body.args) return json(res, 400, failure("arbitrary_command_forbidden"));
const token = bearer(req);
const target = String(body.target || "");
const scope = String(body.scope || "");
const action = String(body.action || "");
const resource = String(body.resource || "");
const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, target, scope, action, Date.now() / 1000, resource);
if (!verified.ok) return json(res, 403, failure(verified.reason));
const map = mapGate.read(target);
const mapVerified = mapGate.verify(token, target, map.hash);
if (!mapVerified.ok) return json(res, 423, failure(mapVerified.reason, "先读取并确认导航图。", { required_action: "read-navigation-map" }));
const result = await executeAction(resource ? { action, target, resource } : { action, target });
const operationReceipt = receipt({ state: result.ok ? "succeeded" : "failed", diagnostic_code: result.ok ? "action_succeeded" : String(result.error || "action_execution_failed"), workorder_id: verified.session.workorderId, target, action, evidence: safeEvidence(result), next_step: result.ok ? "读取 session/status 确认当前回执;如需新范围、目标或资源,重新发起工单。" : "读取 diagnostic_code 与 evidence仅按 next_step 修复,不要切换到其他服务器或猜测凭证。" });
manager.recordReceipt(token, operationReceipt);
return json(res, result.ok ? 200 : 502, { ...result, receipt: operationReceipt });
}
if (req.method === "POST" && url.pathname === "/api/repo-push/grant") {
const body = await readJson(req);
if (!body) return json(res, 400, failure("invalid_json"));
const token = bearer(req);
const target = String(body.target || "");
const scope = String(body.scope || "repo-push");
const repo = String(body.repo || "").toLowerCase();
if (!/^bingshuo\/[a-z0-9._-]+$/.test(repo)) return json(res, 400, failure("repo_not_allowlisted"));
const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, target, scope, "push-repository");
if (!verified.ok) return json(res, 403, failure(verified.reason));
const map = mapGate.read(target);
const mapVerified = mapGate.verify(token, target, map.hash);
if (!mapVerified.ok) return json(res, 423, failure(mapVerified.reason, "先读取并确认导航图。", { required_action: "read-navigation-map" }));
fs.mkdirSync(repoGrantDir, { recursive: true, mode: 0o2770 });
const grant = { schema: "guanghu.repo-push-grant/v1", repo, target, persona_id: body.persona_id, map_hash: map.hash, issued_at: Date.now() / 1000, expires_at: verified.session.expiresAt };
const grantFile = path.join(repoGrantDir, `${repo.replace("/", "__")}.json`);
const temp = `${grantFile}.${process.pid}.tmp`;
fs.writeFileSync(temp, JSON.stringify(grant), { mode: 0o640 });
fs.renameSync(temp, grantFile);
const operationReceipt = receipt({ state: "blocked", diagnostic_code: "repo_push_transport_unavailable", workorder_id: verified.session.workorderId, target, action: "push-repository", next_step: "服务器已登记本次推送许可,但安全推送接收器尚未部署;不要重试裸 git push、不要索要账号密码。等待受限 bundle 接收器上线后按同一工单回执执行。" });
manager.recordReceipt(token, operationReceipt);
return json(res, 200, { ok: true, repo, target, expires_at: grant.expires_at, transport: { status: "not_configured", diagnostic_code: "repo_push_transport_unavailable", next_step: operationReceipt.next_step }, receipt: operationReceipt });
}
if (req.method === "POST" && url.pathname === "/api/deployment/dispatch") {
const body = await readJson(req);
if (!body) return json(res, 400, failure("invalid_json"));
const token = bearer(req), target = String(body.target || ""), scope = String(body.scope || "server-ops");
const resource = String(body.resource || ""), repo = String(body.repo || "").toLowerCase(), branch = String(body.branch || "main"), commit = String(body.commit_sha || "").toLowerCase(), manifest = String(body.manifest || "");
const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, target, scope, "dispatch-approved-deployment", Date.now() / 1000, resource);
if (!verified.ok) return json(res, 403, failure(verified.reason));
const map = mapGate.read(target);
if (!mapGate.verify(token, target, map.hash).ok) return json(res, 423, failure("map_ack_required", "先读取并确认导航图。", { required_action: "read-navigation-map" }));
const queued = enqueueDeploymentEvent({ schema: "guanghu.deployment-intent/v1", repo, branch, commit_sha: commit, resource, manifest, workorder_id: verified.session.workorderId }, { repo, branch, commit_sha: commit }, deploymentQueueDir);
const operationReceipt = receipt({ state: queued.state === "queued_for_resident_agent" ? "queued" : "blocked", diagnostic_code: queued.diagnostic_code || "deployment_event_queued", workorder_id: verified.session.workorderId, target, action: "dispatch-approved-deployment", evidence: { repo, branch, commit_sha: commit, event_id: queued.event_id || "" }, next_step: queued.state === "queued_for_resident_agent" ? "常驻部署 Agent 将读取该事件并回写部署、健康检查或回滚回执。" : "修正部署绑定信息后重新申请或派发,不要让服务器自行扫描提交。" });
manager.recordReceipt(token, operationReceipt);
return json(res, queued.state === "queued_for_resident_agent" ? 202 : 400, { ok: queued.state === "queued_for_resident_agent", deployment: queued, receipt: operationReceipt });
}
return json(res, 404, { error: "not_found" });
} catch (error) {
process.stderr.write(`lake-lamp request error: ${String(error && error.message || "unknown").slice(0, 240)}\n`);
return json(res, error && error.code === "BODY_TOO_LARGE" ? 413 : 500, { error: "request_failed" });
}
});
}
function approvalPage(order, token) {
return document("小湖灯授权请求", `
<p class="eyebrow">LAKE LAMP SECURITY PROTOCOL</p>
<h1>人格体请求进入一台服务器</h1>
<div class="panel">
<dl><dt>人格体</dt><dd>${escapeHtml(order.persona.name)} <small>${escapeHtml(order.persona.pid)}</small></dd>
<dt>申请入口</dt><dd>${escapeHtml(order.provenance && order.provenance.system_entry || "旧版工单")}</dd>
<dt>实例来源</dt><dd>${escapeHtml(originLabel(order))}</dd>
<dt>目标节点</dt><dd>${escapeHtml(order.target)}</dd><dt>授权范围</dt><dd>${escapeHtml(order.scope)}</dd>
<dt>进入动作</dt><dd>${escapeHtml(order.action)}</dd><dt>会话能力</dt><dd>${escapeHtml((order.allowed_actions || [order.action]).join(" · "))}</dd>
<dt>绑定资源</dt><dd>${escapeHtml(order.resource || "无")}</dd>
<dt>说明</dt><dd>${escapeHtml(order.description || "未附加说明")}</dd></dl>
</div>
<p class="notice">一次确认将打开这台服务器上的三小时受限运维会话。人格体持续执行已绑定任务时会自动续期;切换服务器、扩大范围、切换绑定资源或停止活动后过期才需重新申请。</p>
<form method="post"><button type="submit">打开三小时受限运维会话</button></form>
`);
}
function requestPage(order) {
return document("小湖灯跨设备授权", `
<p class="eyebrow">CROSS-DEVICE HANDOFF</p>
<h1>核对这张无权限申请单</h1>
<div class="panel">
<dl><dt>人格体</dt><dd>${escapeHtml(order.persona.name)} <small>${escapeHtml(order.persona.pid)}</small></dd>
<dt>申请入口</dt><dd>${escapeHtml(order.provenance && order.provenance.system_entry || "旧版工单")}</dd>
<dt>实例来源</dt><dd>${escapeHtml(originLabel(order))}</dd>
<dt>目标节点</dt><dd>${escapeHtml(order.target)}</dd><dt>授权范围</dt><dd>${escapeHtml(order.scope)}</dd>
<dt>登记动作</dt><dd>${escapeHtml(order.action)}</dd><dt>绑定资源</dt><dd>${escapeHtml(order.resource || "无")}</dd><dt>说明</dt><dd>${escapeHtml(order.description || "未附加说明")}</dd></dl>
</div>
<p class="notice">这张页面本身没有执行权。确认内容无误后,服务器只会向预登记邮箱发送一次真正的批准链接。</p>
<form method="post"><button type="submit">发送我的授权邮件</button></form>
`);
}
function emailSentPage(order) {
return document("授权邮件已发送", `<p class="eyebrow">OWNER VERIFICATION</p><h1>请打开邮箱完成批准</h1><div class="panel"><p>申请单已锁定到 <strong>${escapeHtml(order && order.target || "登记节点")}</strong>。真正的批准链接只发送到服务器预登记邮箱。</p></div><p class="notice">批准后回到原来的 AI 对话,让它领取一次性会话。无需向 AI 提供验证码、密码或邮箱授权码。</p>`);
}
function requestErrorPage(reason) {
const messages = {
rate_limited: "请求过于频繁,请稍后再试。",
approval_email_already_sent: "授权邮件已经发送,请直接检查邮箱。",
authorization_email_failed: "授权邮件暂时发送失败,请稍后重试。",
};
if (reason === "rate_limited") return document("发送频率保护", `<p class="eyebrow">RATE LIMIT · REQUEST KEPT</p><h1>小湖灯先替你守住这张申请单</h1><div class="panel"><p>当前网络在一小时内触发邮件的次数较多,发送动作被暂时暂停。</p></div><p class="notice">申请单本身没有被关闭。请稍后再试,或切换到手机流量后只点击一次。无需重新填写,也不要连续刷新。</p>`);
return document("申请单不可用", `<p class="eyebrow">REQUEST CLOSED</p><h1>这张申请单现在不能继续</h1><p class="notice">${escapeHtml(messages[reason] || `原因:${reason}`)}</p>`);
}
function approvedPage(order) {
return document("授权完成", `<p class="eyebrow">THREE-HOUR OPS SESSION</p><h1>三小时运维会话已打开</h1><div class="panel"><p>${escapeHtml(order.persona.name)} 已获准在 <strong>${escapeHtml(order.target)}</strong> 上执行本范围内的已登记能力。</p></div><p class="notice">可以关闭本页面。人格体持续执行原绑定任务时会自动续期;切换服务器、扩大范围、切换绑定资源或停止活动后过期才重新授权。</p>`);
}
function approvalErrorPage(reason) {
return document("链接不可用", `<p class="eyebrow">LINK CLOSED</p><h1>这条授权链接已经失效</h1><p class="notice">原因:${escapeHtml(reason)}。如仍需操作,请让人格体重新提交工单。</p>`);
}
function document(title, body) {
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)} · 光湖</title><style>
:root{color-scheme:dark}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;background:radial-gradient(circle at 20% 10%,#17344d,#09111b 55%,#05090e);color:#eaf4fb;font:16px/1.7 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:24px}.shell{width:min(680px,100%);padding:42px;border:1px solid #29475d;border-radius:24px;background:rgba(10,22,33,.94);box-shadow:0 24px 80px #0008}.eyebrow{color:#6ed5ff;letter-spacing:.18em;font-size:12px}h1{font-size:clamp(30px,6vw,48px);line-height:1.15;margin:10px 0 28px}.panel{background:#102638;border:1px solid #24465d;border-radius:16px;padding:20px 24px}dl{display:grid;grid-template-columns:110px 1fr;gap:12px;margin:0}dt{color:#8ba4b6}dd{margin:0;font-weight:650}small{display:block;color:#7893a6;font-weight:400}.notice{color:#9eb2c0;margin:20px 0}button{width:100%;border:0;border-radius:14px;padding:16px;background:#67d4ff;color:#042235;font-weight:800;font-size:17px;cursor:pointer}@media(max-width:520px){.shell{padding:28px 22px}dl{grid-template-columns:1fr;gap:2px}dd{margin-bottom:12px}}
</style></head><body><main class="shell">${body}</main></body></html>`;
}
function readJson(req) {
return new Promise((resolve, reject) => {
let raw = "";
req.on("data", chunk => { raw += chunk; if (raw.length > 32 * 1024) { const error = new Error("body too large"); error.code = "BODY_TOO_LARGE"; reject(error); req.destroy(); } });
req.on("end", () => { try { resolve(JSON.parse(raw || "{}")); } catch { resolve(null); } });
req.on("error", reject);
});
}
function bearer(req) { return String(req.headers.authorization || "").replace(/^Bearer\s+/i, ""); }
function bearerMatches(req, expected) { return Boolean(expected) && safeEqual(bearer(req), expected); }
function safeEqual(left, right) { const a = Buffer.from(String(left)); const b = Buffer.from(String(right)); return a.length === b.length && crypto.timingSafeEqual(a, b); }
function sha256(value) { return crypto.createHash("sha256").update(String(value)).digest("hex"); }
function splitCsv(value) { return value.split(",").map(item => item.trim()).filter(Boolean); }
function loadApprovers(file, ownerEmail) {
if (!file) return ownerEmail ? [{ id: "sovereign-owner", email: ownerEmail, default: true, persona_ids: [], targets: ["*"], scopes: ["*"] }] : [];
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
if (!parsed || !Array.isArray(parsed.approvers)) throw new Error("invalid approver registry");
return parsed.approvers.filter(item => item && validEmail(item.email)).map(item => ({
id: String(item.id || ""), email: item.email, default: item.default === true,
persona_ids: Array.isArray(item.persona_ids) ? item.persona_ids.map(String) : [],
targets: Array.isArray(item.targets) ? item.targets.map(String) : [],
scopes: Array.isArray(item.scopes) ? item.scopes.map(String) : [],
}));
}
function selectApprover(approvers, order) {
const eligible = approvers.filter(item => matches(item.targets, order.target) && matches(item.scopes, order.scope));
return eligible.find(item => item.persona_ids.includes(order.persona.pid)) || eligible.find(item => item.default) || null;
}
function matches(values, value) { return values.includes("*") || values.includes(value); }
function validEmail(value) { return typeof value === "string" && value.length <= 254 && /^[^@\s]+@[^@\s]+$/.test(value); }
function clientAddress(req) {
const forwarded = String(req.headers["x-forwarded-for"] || "").split(",").map(value => value.trim()).filter(Boolean);
return String(forwarded[forwarded.length - 1] || req.socket.remoteAddress || "unknown").slice(0, 96);
}
function receipt({ state, diagnostic_code, workorder_id = "", target = "", action = "", evidence = null, next_step = "" }) {
return { schema: "guanghu.operation-receipt/v1", state, diagnostic_code, workorder_id, target, action, occurred_at: Date.now() / 1000, ...(evidence ? { evidence } : {}), next_step };
}
function safeEvidence(result) {
const clip = value => String(value || "").replace(/(password|token|secret|authorization)\s*[:=]\s*\S+/gi, "$1=[redacted]").slice(0, 1200);
return { exit_code: Number.isInteger(result.exit_code) ? result.exit_code : null, stdout: clip(result.stdout), stderr: clip(result.stderr) };
}
function failure(error, next_step = "读取 diagnostic_code按 next_step 处理,勿猜测凭证或切换服务器。", extra = {}) {
return { ok: false, error, receipt: receipt({ state: "blocked", diagnostic_code: error, next_step }), ...extra };
}
function diagnosticCatalog() {
return {
owner_handoff_required: "工单已创建,等待主人打开申请页并完成预登记邮箱批准。",
map_ack_required: "会话有效,但尚未确认此目标节点的实时导航图。",
action_execution_failed: "服务器固定动作已执行但失败;回执会包含受限证据与下一步。",
repo_push_transport_unavailable: "许可已登记,但安全推送接收器尚未部署,禁止把它误判为 git 凭证。",
session_expired: "会话已过期;以同一目标和范围重新申请工单。",
};
}
function validateWorkorderBody(body, targets, actions) {
if (body.email || body.recipient || body.smtp_pass) return { ok: false, status: 400, error: "direct_recipient_forbidden" };
if (!body.persona_id || !body.target || !body.scope || !body.action) return { ok: false, status: 400, error: "missing_required_field" };
const personaId = String(body.persona_id);
const personaName = String(body.persona_name || personaId);
const target = String(body.target);
const scope = String(body.scope);
const action = String(body.action);
const description = String(body.description || "");
const resource = String(body.resource || "");
const provenance = {
system_entry: String(body.system_entry || ""),
software: String(body.origin_software || ""),
model: String(body.origin_model || ""),
instance: String(body.origin_instance || ""),
};
if (!/^[A-Za-z0-9._:+\u221e-]{2,80}$/.test(personaId) || personaName.length > 100 || description.length > 500) return { ok: false, status: 400, error: "invalid_request_fields" };
if (!targets.has(target)) return { ok: false, status: 400, error: "unknown_target" };
if (!Array.isArray(actions[scope]) || !actions[scope].includes(action)) return { ok: false, status: 400, error: "unknown_or_mismatched_action" };
const immutableResourceAction = action === "provision-approved-architecture" || action === "dispatch-approved-deployment";
if (immutableResourceAction && !/^[A-Z0-9][A-Z0-9._-]{5,119}@[0-9a-f]{40}$/.test(resource)) return { ok: false, status: 400, error: "immutable_architecture_resource_required" };
if (!immutableResourceAction && resource) return { ok: false, status: 400, error: "resource_not_allowed_for_action" };
if (body.owner_notify !== undefined && typeof body.owner_notify !== "boolean") return { ok: false, status: 400, error: "invalid_owner_notify" };
if (body.owner_notify === true && provenance.system_entry !== "光湖语言人格系统当前实例") return { ok: false, status: 400, error: "owner_notify_requires_language_system_provenance" };
if (Object.values(provenance).some(Boolean) && (provenance.system_entry !== "光湖语言人格系统当前实例" || Object.values(provenance).some(item => !item || item.length > 120))) return { ok: false, status: 400, error: "invalid_instance_provenance" };
return { ok: true, request: { persona: { pid: personaId, name: personaName }, provenance, target, scope, action, allowedActions: actions[scope], description, resource } };
}
function originLabel(order) {
const value = order && order.provenance || {};
return value.software || value.model || value.instance ? `${value.software || "未知软件"} · ${value.model || "未知模型"} · ${value.instance || "当前实例"}` : "旧版工单未记录";
}
class SlidingWindowLimiter {
constructor(limit, windowSeconds) { this.limit = Math.max(1, limit); this.windowMs = windowSeconds * 1000; this.events = new Map(); this.calls = 0; }
take(key, now = Date.now()) {
this.calls += 1;
if (this.calls % 256 === 0) {
for (const [storedKey, values] of this.events) {
const active = values.filter(value => now - value < this.windowMs);
if (active.length) this.events.set(storedKey, active); else this.events.delete(storedKey);
}
}
const recent = (this.events.get(key) || []).filter(value => now - value < this.windowMs);
if (recent.length >= this.limit) { this.events.set(key, recent); return false; }
recent.push(now); this.events.set(key, recent); return true;
}
}
function escapeHtml(value) { return String(value).replace(/[&<>"']/g, char => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]); }
function json(res, status, value) { res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "x-content-type-options": "nosniff" }); res.end(JSON.stringify(value)); }
function html(res, status, value) { res.writeHead(status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'", "referrer-policy": "no-referrer", "x-content-type-options": "nosniff" }); res.end(value); }
if (require.main === module) {
const host = process.env.LAKE_LAMP_HOST || "127.0.0.1";
const port = Number(process.env.LAKE_LAMP_PORT || 3921);
createApp().listen(port, host, () => process.stdout.write(`lake-lamp-authz listening on ${host}:${port}\n`));
}
module.exports = { createApp, DEFAULT_ACTIONS, SlidingWindowLimiter, loadApprovers, selectApprover };