1462 lines
75 KiB
JavaScript
1462 lines
75 KiB
JavaScript
"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 { GuanghuRouter, loadDevices } = require("./guanghu-router");
|
||
const { HoloLakeSessionManager } = require("./hololake-session");
|
||
const { GhdrAuthorizer } = require("./ghdr-authorizer");
|
||
const { GhdrControllerBroker, loadControllers } = require("./ghdr-controller-broker");
|
||
const {
|
||
HoloLakeAiGateway,
|
||
HoloLakeKnowledgeProvider,
|
||
loadAiProviders,
|
||
} = require("./hololake-capabilities");
|
||
const {
|
||
loadRegistry: loadRepoPushRegistry,
|
||
receiveBundle,
|
||
resolveRepository,
|
||
} = require("./repo-push-broker");
|
||
|
||
const DEFAULT_ACTIONS = Object.freeze({
|
||
"server-login": [
|
||
"read-navigation-map",
|
||
"inspect-services",
|
||
"health-check",
|
||
"inspect-owner-ssh-login",
|
||
"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",
|
||
"inspect-owner-ssh-login",
|
||
"disable-owner-password-login",
|
||
"restore-owner-password-login",
|
||
"restore-code-channel-owner-login",
|
||
"dispatch-approved-deployment",
|
||
],
|
||
"repo-push": ["read-navigation-map", "push-repository"],
|
||
"linked-node-ops": ["authorize-linked-node-session"],
|
||
"native-recovery": ["read-navigation-map", "sign-native-layout-plan"],
|
||
});
|
||
|
||
function createApp(options = {}) {
|
||
const requestToken = options.requestToken || process.env.LAKE_LAMP_REQUEST_TOKEN || "";
|
||
const broadcastToken = options.broadcastToken || process.env.LAKE_LAMP_BROADCAST_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"));
|
||
targets.add("GH-CVM-MAIN-PROD-01");
|
||
const actions = options.actions || DEFAULT_ACTIONS;
|
||
const devices = options.devices || loadDevices(
|
||
options.devicesFile
|
||
|| process.env.GUANGHU_ROUTER_DEVICES_FILE
|
||
|| "/etc/guanghu/lake-lamp/hololake-devices.json",
|
||
);
|
||
const router = options.router || new GuanghuRouter({ devices });
|
||
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 hololakePepper = String(
|
||
options.hololakeSessionPepper
|
||
|| process.env.HOLOLAKE_SESSION_PEPPER
|
||
|| "",
|
||
);
|
||
const hololakeSessionManager = options.hololakeSessionManager || (
|
||
hololakePepper
|
||
? new HoloLakeSessionManager({
|
||
registeredEmails: approvers.map(item => item.email),
|
||
pepper: hololakePepper,
|
||
stateFile: Object.prototype.hasOwnProperty.call(options, "hololakeSessionStateFile")
|
||
? options.hololakeSessionStateFile
|
||
: (
|
||
process.env.HOLOLAKE_SESSION_STATE_FILE
|
||
|| "/var/lib/guanghu/lake-lamp-authz/hololake-sessions.json"
|
||
),
|
||
otpTtlSeconds: Number(
|
||
options.hololakeOtpTtlSeconds
|
||
|| process.env.HOLOLAKE_OTP_TTL
|
||
|| 10 * 60,
|
||
),
|
||
sessionTtlSeconds: Number(
|
||
options.hololakeSessionTtlSeconds
|
||
|| process.env.HOLOLAKE_ACCOUNT_SESSION_TTL
|
||
|| 24 * 60 * 60,
|
||
),
|
||
requestLimit: Number(
|
||
options.hololakeOtpRequestLimit
|
||
|| process.env.HOLOLAKE_OTP_REQUEST_LIMIT
|
||
|| 6,
|
||
),
|
||
sendEmail,
|
||
})
|
||
: null
|
||
);
|
||
const hololakeKnowledgePath = String(
|
||
options.hololakeKnowledgeRepositoryPath
|
||
|| process.env.HOLOLAKE_KNOWLEDGE_REPOSITORY_PATH
|
||
|| "",
|
||
);
|
||
const hololakeKnowledgeProvider = options.hololakeKnowledgeProvider || (
|
||
hololakeKnowledgePath
|
||
? new HoloLakeKnowledgeProvider({
|
||
repositoryId: "bingshuo/hololake-knowledge-base",
|
||
repositoryPath: hololakeKnowledgePath,
|
||
maxArchiveBytes: Number(
|
||
options.hololakeKnowledgeMaxArchiveBytes
|
||
|| process.env.HOLOLAKE_KNOWLEDGE_MAX_ARCHIVE_BYTES
|
||
|| 128 * 1024 * 1024,
|
||
),
|
||
})
|
||
: null
|
||
);
|
||
const hololakeAiProvidersFile = String(
|
||
options.hololakeAiProvidersFile
|
||
|| process.env.HOLOLAKE_AI_PROVIDERS_FILE
|
||
|| "",
|
||
);
|
||
const hololakeAiProviders = options.hololakeAiProviders || (
|
||
hololakeAiProvidersFile ? loadAiProviders(hololakeAiProvidersFile) : {}
|
||
);
|
||
const hololakeAiGateway = options.hololakeAiGateway || (
|
||
Object.keys(hololakeAiProviders).length > 0
|
||
? new HoloLakeAiGateway({ providers: hololakeAiProviders })
|
||
: null
|
||
);
|
||
const mapGate = options.mapGate || new MapGate({
|
||
mapsDir: options.mapsDir || process.env.LAKE_LAMP_MAPS_DIR || "/etc/guanghu/navigation-maps",
|
||
fallbackMapsDir: options.fallbackMapsDir || path.join(__dirname, "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 repoUploadDir = options.repoUploadDir || process.env.LAKE_LAMP_REPO_UPLOAD_DIR || "/var/lib/guanghu/repo-push-uploads";
|
||
const repoPushRegistryFile = options.repoPushRegistryFile || process.env.LAKE_LAMP_REPO_PUSH_REGISTRY || "/etc/guanghu/lake-lamp/repo-push-registry.json";
|
||
const repoPushRegistry = options.repoPushRegistry || (
|
||
fs.existsSync(repoPushRegistryFile) ? loadRepoPushRegistry(repoPushRegistryFile) : {}
|
||
);
|
||
const receiveRepoBundle = options.receiveRepoBundle || (
|
||
request => receiveBundle(request, {
|
||
registry: repoPushRegistry,
|
||
uploadDir: repoUploadDir,
|
||
})
|
||
);
|
||
const maxRepoBundleBytes = Math.max(
|
||
1024 * 1024,
|
||
Number(options.maxRepoBundleBytes || process.env.LAKE_LAMP_MAX_REPO_BUNDLE_BYTES || 256 * 1024 * 1024),
|
||
);
|
||
const maxRepoChunkBytes = Math.max(
|
||
8 * 1024,
|
||
Math.min(
|
||
48 * 1024,
|
||
Number(options.maxRepoChunkBytes || process.env.LAKE_LAMP_MAX_REPO_CHUNK_BYTES || 32 * 1024),
|
||
),
|
||
);
|
||
const deploymentQueueDir = options.deploymentQueueDir || process.env.LAKE_LAMP_DEPLOYMENT_EVENT_DIR || "/var/lib/guanghu/deployment-events";
|
||
const deploymentRegistryFile = options.deploymentRegistryFile || process.env.LAKE_LAMP_DEPLOYMENT_REPOSITORIES || "/etc/guanghu/lake-lamp/deployment-repositories.json";
|
||
const executeAction = options.executeAction || executeRegisteredAction;
|
||
let ghdrAuthorizer = null;
|
||
const getGhdrAuthorizer = options.getGhdrAuthorizer || (() => {
|
||
if (!ghdrAuthorizer) {
|
||
ghdrAuthorizer = new GhdrAuthorizer({
|
||
privateKeyFile: options.ghdrAuthorizerPrivateKeyFile
|
||
|| process.env.GHDR_AUTHORIZER_PRIVATE_KEY_FILE
|
||
|| "/var/lib/guanghu/lake-lamp-authz/ghdr-authorizer-private.pem",
|
||
});
|
||
}
|
||
return ghdrAuthorizer;
|
||
});
|
||
let ghdrControllerBroker = null;
|
||
const getGhdrControllerBroker = options.getGhdrControllerBroker || (() => {
|
||
if (!ghdrControllerBroker) {
|
||
const registryFile = options.ghdrControllerRegistryFile
|
||
|| process.env.GHDR_CONTROLLER_REGISTRY_FILE
|
||
|| path.join(__dirname, "ghdr-controllers.json");
|
||
ghdrControllerBroker = new GhdrControllerBroker({
|
||
controllers: loadControllers(registryFile),
|
||
stateDir: options.ghdrJobStateDir
|
||
|| process.env.GHDR_JOB_STATE_DIR
|
||
|| "/var/lib/guanghu/lake-lamp-authz/ghdr-jobs",
|
||
authorizer: getGhdrAuthorizer(),
|
||
});
|
||
}
|
||
return ghdrControllerBroker;
|
||
});
|
||
const signGhdrPlan = options.signGhdrPlan || (
|
||
request => getGhdrControllerBroker().queueAndWait(request)
|
||
);
|
||
// 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" };
|
||
}
|
||
if (!manager.bindApprover(handoffToken, approver.id)) {
|
||
manager.failApprovalEmail(handoffToken);
|
||
return { ok: false, reason: "approver_binding_failed" };
|
||
}
|
||
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 };
|
||
}
|
||
|
||
function authenticateHoloLake(req) {
|
||
if (!hololakeSessionManager) {
|
||
return { ok: false, status: 503, error: "hololake_session_unavailable" };
|
||
}
|
||
const token = bearer(req);
|
||
if (!token) return { ok: false, status: 401, error: "session_required" };
|
||
const deviceId = String(req.headers["x-hololake-device-id"] || "");
|
||
const authenticated = hololakeSessionManager.authenticate(token, deviceId);
|
||
if (!authenticated.ok) {
|
||
return {
|
||
...authenticated,
|
||
status: authenticated.error === "session_device_mismatch" ? 403 : 401,
|
||
};
|
||
}
|
||
return { ...authenticated, token, deviceId };
|
||
}
|
||
|
||
function bindAndDeliver(created) {
|
||
const inspected = manager.inspectHandoff(created.handoffToken);
|
||
if (!inspected.ok) return { delivered: 0, approver: null, order: null };
|
||
const approver = selectApprover(approvers, inspected.order);
|
||
if (!approver || !manager.bindApprover(created.handoffToken, approver.id)) {
|
||
return { delivered: 0, approver: null, order: inspected.order };
|
||
}
|
||
const bound = manager.inspectHandoff(created.handoffToken);
|
||
const order = bound.ok ? bound.order : inspected.order;
|
||
return {
|
||
approver,
|
||
order,
|
||
delivered: router.deliver(approver.id, 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: "guanghu-router-with-email-fallback",
|
||
primary_authorization_channel: "guanghu_router",
|
||
approval_ttl: manager.approvalTtl,
|
||
session_ttl: manager.sessionTtl,
|
||
max_session_lifetime: manager.maxSessionLifetime,
|
||
auto_renew_on_activity: true,
|
||
hololake_mobile: {
|
||
email_session: Boolean(hololakeSessionManager),
|
||
knowledge_snapshot: Boolean(hololakeKnowledgeProvider),
|
||
ai_gateway: Boolean(hololakeAiGateway),
|
||
},
|
||
});
|
||
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: "an online HoloLake receives the authorization card through the Guanghu Router; request_url is an email recovery fallback only",
|
||
email_visibility: "the requesting AI never receives the submitted mailbox address",
|
||
public_auto_email: false,
|
||
workflow: ["create_workorder", "guanghu_router_authorization_or_email_fallback", "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,
|
||
},
|
||
});
|
||
|
||
if (
|
||
req.method === "POST"
|
||
&& url.pathname === "/api/hololake/session/email/request"
|
||
) {
|
||
if (!hololakeSessionManager) {
|
||
return json(res, 503, failure("hololake_session_unavailable"));
|
||
}
|
||
const body = await readJson(req);
|
||
if (!body) return json(res, 400, failure("invalid_json"));
|
||
const headerDeviceId = String(
|
||
req.headers["x-hololake-device-id"] || "",
|
||
);
|
||
if (
|
||
headerDeviceId
|
||
&& body.device_id
|
||
&& !safeEqual(headerDeviceId, String(body.device_id))
|
||
) {
|
||
return json(res, 400, failure("invalid_device"));
|
||
}
|
||
const requested = await hololakeSessionManager.requestOtp({
|
||
email: body.email,
|
||
deviceId: String(body.device_id || headerDeviceId),
|
||
networkKey: clientAddress(req),
|
||
});
|
||
if (!requested.accepted) {
|
||
return json(
|
||
res,
|
||
requested.error === "rate_limited" ? 429 : 400,
|
||
failure(requested.error),
|
||
);
|
||
}
|
||
return json(res, 202, {
|
||
accepted: true,
|
||
request_id: requested.request_id,
|
||
expires_in: Number(
|
||
options.hololakeOtpTtlSeconds
|
||
|| process.env.HOLOLAKE_OTP_TTL
|
||
|| 10 * 60,
|
||
),
|
||
next_step: "如果邮箱已登记,输入邮件中的六位验证码。",
|
||
});
|
||
}
|
||
|
||
if (
|
||
req.method === "POST"
|
||
&& url.pathname === "/api/hololake/session/email/verify"
|
||
) {
|
||
if (!hololakeSessionManager) {
|
||
return json(res, 503, failure("hololake_session_unavailable"));
|
||
}
|
||
const body = await readJson(req);
|
||
if (!body) return json(res, 400, failure("invalid_json"));
|
||
const headerDeviceId = String(
|
||
req.headers["x-hololake-device-id"] || "",
|
||
);
|
||
if (
|
||
headerDeviceId
|
||
&& body.device_id
|
||
&& !safeEqual(headerDeviceId, String(body.device_id))
|
||
) {
|
||
return json(res, 400, failure("invalid_device"));
|
||
}
|
||
const verified = hololakeSessionManager.verifyOtp({
|
||
requestId: body.request_id,
|
||
code: body.code,
|
||
deviceId: String(body.device_id || headerDeviceId),
|
||
});
|
||
if (!verified.ok) return json(res, 403, failure(verified.error));
|
||
return json(res, 200, verified);
|
||
}
|
||
|
||
if (
|
||
(req.method === "GET" || req.method === "DELETE")
|
||
&& url.pathname === "/api/hololake/session"
|
||
) {
|
||
const authenticated = authenticateHoloLake(req);
|
||
if (!authenticated.ok) {
|
||
return json(
|
||
res,
|
||
authenticated.status,
|
||
failure(authenticated.error),
|
||
);
|
||
}
|
||
if (req.method === "DELETE") {
|
||
const revoked = hololakeSessionManager.revoke(
|
||
authenticated.token,
|
||
authenticated.deviceId,
|
||
);
|
||
return json(res, revoked.ok ? 200 : 401, revoked);
|
||
}
|
||
return json(res, 200, {
|
||
ok: true,
|
||
session: authenticated.session,
|
||
});
|
||
}
|
||
|
||
if (
|
||
req.method === "GET"
|
||
&& url.pathname === "/api/hololake/knowledge/manifest"
|
||
) {
|
||
const authenticated = authenticateHoloLake(req);
|
||
if (!authenticated.ok) {
|
||
return json(
|
||
res,
|
||
authenticated.status,
|
||
failure(authenticated.error),
|
||
);
|
||
}
|
||
if (!hololakeKnowledgeProvider) {
|
||
return json(res, 503, failure("knowledge_repository_unavailable"));
|
||
}
|
||
try {
|
||
return json(res, 200, hololakeKnowledgeProvider.manifest());
|
||
} catch {
|
||
return json(res, 503, failure("knowledge_repository_unavailable"));
|
||
}
|
||
}
|
||
|
||
if (
|
||
req.method === "GET"
|
||
&& url.pathname === "/api/hololake/knowledge/archive"
|
||
) {
|
||
const authenticated = authenticateHoloLake(req);
|
||
if (!authenticated.ok) {
|
||
return json(
|
||
res,
|
||
authenticated.status,
|
||
failure(authenticated.error),
|
||
);
|
||
}
|
||
if (!hololakeKnowledgeProvider) {
|
||
return json(res, 503, failure("knowledge_repository_unavailable"));
|
||
}
|
||
try {
|
||
const archive = hololakeKnowledgeProvider.archive(
|
||
url.searchParams.get("commit"),
|
||
);
|
||
res.writeHead(200, {
|
||
"content-type": archive.content_type,
|
||
"content-length": archive.body.length,
|
||
"content-disposition": `attachment; filename="hololake-knowledge-${archive.commit}.zip"`,
|
||
"cache-control": "private, no-store",
|
||
"x-content-type-options": "nosniff",
|
||
"x-hololake-commit": archive.commit,
|
||
"x-content-sha256": archive.sha256,
|
||
});
|
||
res.end(archive.body);
|
||
return;
|
||
} catch (error) {
|
||
const code = error && error.message === "knowledge_commit_not_current"
|
||
? "knowledge_commit_not_current"
|
||
: "knowledge_archive_unavailable";
|
||
return json(res, code === "knowledge_commit_not_current" ? 409 : 503, failure(code));
|
||
}
|
||
}
|
||
|
||
if (
|
||
req.method === "GET"
|
||
&& url.pathname === "/api/hololake/ai/catalog"
|
||
) {
|
||
const authenticated = authenticateHoloLake(req);
|
||
if (!authenticated.ok) {
|
||
return json(
|
||
res,
|
||
authenticated.status,
|
||
failure(authenticated.error),
|
||
);
|
||
}
|
||
if (!hololakeAiGateway) {
|
||
return json(res, 503, failure("ai_gateway_unavailable"));
|
||
}
|
||
return json(res, 200, hololakeAiGateway.catalog());
|
||
}
|
||
|
||
if (
|
||
req.method === "POST"
|
||
&& url.pathname === "/api/hololake/ai/execute"
|
||
) {
|
||
const authenticated = authenticateHoloLake(req);
|
||
if (!authenticated.ok) {
|
||
return json(
|
||
res,
|
||
authenticated.status,
|
||
failure(authenticated.error),
|
||
);
|
||
}
|
||
if (!hololakeAiGateway) {
|
||
return json(res, 503, failure("ai_gateway_unavailable"));
|
||
}
|
||
const body = await readJson(req);
|
||
if (!body) return json(res, 400, failure("invalid_json"));
|
||
try {
|
||
return json(res, 200, await hololakeAiGateway.execute(body));
|
||
} catch (error) {
|
||
const code = String(error && error.message || "ai_gateway_failed");
|
||
const status = code.startsWith("ai_upstream_") ? 502 : 400;
|
||
return json(res, status, failure(code));
|
||
}
|
||
}
|
||
|
||
if (req.method === "POST" && url.pathname === "/api/repositories/resolve") {
|
||
const body = await readJson(req);
|
||
if (!body) return json(res, 400, failure("invalid_json"));
|
||
const resolved = resolveRepository(body.remote_url, repoPushRegistry);
|
||
return json(res, resolved.ok ? 200 : 404, resolved);
|
||
}
|
||
|
||
if (req.method === "POST" && url.pathname === "/api/guanghu-router/challenge") {
|
||
const body = await readJson(req);
|
||
if (!body) return json(res, 400, { error: "invalid_json" });
|
||
const challenged = router.challenge(
|
||
String(body.device_id || ""),
|
||
Math.floor(Date.now() / 1000),
|
||
);
|
||
if (!challenged.ok) return json(res, 403, { error: challenged.reason });
|
||
return json(res, 200, {
|
||
ok: true,
|
||
schema: challenged.schema,
|
||
challenge_id: challenged.challengeId,
|
||
nonce: challenged.nonce,
|
||
expires_at: challenged.expiresAt,
|
||
server_time: challenged.serverTime,
|
||
});
|
||
}
|
||
|
||
if (req.method === "POST" && url.pathname === "/api/guanghu-router/connect") {
|
||
const body = await readJson(req);
|
||
if (!body) return json(res, 400, { error: "invalid_json" });
|
||
const connected = router.authorizeConnection({
|
||
deviceId: String(body.device_id || ""),
|
||
challengeId: String(body.challenge_id || ""),
|
||
clientTimestamp: Number(body.client_timestamp),
|
||
signature: String(body.signature || ""),
|
||
}, Math.floor(Date.now() / 1000));
|
||
if (!connected.ok) return json(res, 403, { error: connected.reason });
|
||
return json(res, 200, {
|
||
ok: true,
|
||
device_id: connected.deviceId,
|
||
device_label: connected.deviceLabel,
|
||
owner_id: connected.ownerId,
|
||
route_token: connected.routeToken,
|
||
expires_at: connected.expiresAt,
|
||
next_step: "使用一次性 route_token 打开光湖路由持续连接;只有收到 router.connected 回执后才显示上线。",
|
||
});
|
||
}
|
||
|
||
if (req.method === "GET" && url.pathname === "/api/guanghu-router/stream") {
|
||
const queued = [];
|
||
let streaming = false;
|
||
const send = event => {
|
||
if (!streaming) {
|
||
queued.push(event);
|
||
} else if (!res.destroyed && !res.writableEnded) {
|
||
res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
|
||
}
|
||
};
|
||
const opened = router.open(bearer(req), send, Math.floor(Date.now() / 1000));
|
||
if (!opened.ok) return json(res, 403, { error: opened.reason });
|
||
res.writeHead(200, {
|
||
"content-type": "text/event-stream; charset=utf-8",
|
||
"cache-control": "no-store, no-transform",
|
||
"connection": "keep-alive",
|
||
"x-accel-buffering": "no",
|
||
"x-content-type-options": "nosniff",
|
||
});
|
||
streaming = true;
|
||
for (const event of queued) send(event);
|
||
for (const order of manager.pendingForApprover(opened.ownerId)) {
|
||
router.deliver(opened.ownerId, order);
|
||
}
|
||
// This is transport framing only: it carries no application event,
|
||
// mutates no online state, and writes no heartbeat record. Its sole
|
||
// purpose is to stop the public nginx front door from treating an
|
||
// otherwise healthy, idle SSE route as a dead upstream after 60s.
|
||
const transportKeepalive = setInterval(() => {
|
||
if (!res.destroyed && !res.writableEnded) {
|
||
res.write(": guanghu-router-transport\n\n");
|
||
}
|
||
}, 15_000);
|
||
transportKeepalive.unref?.();
|
||
res.on("close", () => {
|
||
clearInterval(transportKeepalive);
|
||
opened.close(Date.now() / 1000, "transport_closed");
|
||
});
|
||
return;
|
||
}
|
||
|
||
const routerApprovalMatch = url.pathname.match(/^\/api\/guanghu-router\/authorizations\/([0-9a-f-]{36})\/approve$/i);
|
||
if (req.method === "POST" && routerApprovalMatch) {
|
||
const body = await readJson(req);
|
||
if (!body) return json(res, 400, { error: "invalid_json" });
|
||
const inspected = manager.inspectPending(routerApprovalMatch[1]);
|
||
if (!inspected.ok) return json(res, 410, { error: inspected.reason });
|
||
const verified = router.verifyApproval(
|
||
String(body.device_id || ""),
|
||
inspected.order,
|
||
String(body.signature || ""),
|
||
);
|
||
if (!verified.ok) return json(res, 403, { error: verified.reason });
|
||
const approved = manager.approveById(
|
||
routerApprovalMatch[1],
|
||
verified.authorizerId,
|
||
);
|
||
if (!approved.ok) return json(res, 409, { error: approved.reason });
|
||
return json(res, 200, {
|
||
ok: true,
|
||
receipt: receipt({
|
||
state: "approved",
|
||
diagnostic_code: "broadcast_console_approved",
|
||
workorder_id: approved.order.id,
|
||
target: approved.order.target,
|
||
action: approved.order.action,
|
||
evidence: {
|
||
device_id: verified.deviceId,
|
||
authorization_digest: verified.digest,
|
||
},
|
||
next_step: "申请方现在可以使用原 claim_token 领取受限三小时会话。",
|
||
}),
|
||
});
|
||
}
|
||
|
||
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));
|
||
const form = await readForm(req);
|
||
// The response deliberately stays generic whether the submitted
|
||
// mailbox is registered, invalid, or already used. The browser posts
|
||
// directly to this service over HTTPS; the requesting AI never sees
|
||
// or stores the mailbox value.
|
||
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 approver = selectApprover(approvers, inspected.order);
|
||
const submittedEmail = normalizeEmail(form && form.email);
|
||
const registeredEmail = normalizeEmail(approver && approver.email);
|
||
if (validEmail(submittedEmail) && registeredEmail && safeEqual(sha256(submittedEmail), sha256(registeredEmail))) {
|
||
const sent = await sendApprovalEmail(requestMatch[1]);
|
||
if (!sent.ok && sent.reason !== "approval_email_already_sent") {
|
||
process.stderr.write(`lake-lamp owner handoff failed: ${String(sent.reason || "unknown").slice(0, 80)}\n`);
|
||
}
|
||
}
|
||
return html(res, 200, emailSentPage(inspected.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);
|
||
const delivery = bindAndDeliver(created);
|
||
const throughRouter = delivery.delivered > 0;
|
||
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: throughRouter ? "waiting_for_broadcast_console" : "waiting_for_owner_handoff",
|
||
delivery: {
|
||
channel: throughRouter ? "guanghu_router" : "email_recovery_fallback",
|
||
delivered_devices: delivery.delivered,
|
||
},
|
||
email_status: throughRouter ? "fallback_not_needed" : "owner_input_required",
|
||
public_auto_email: false,
|
||
receipt: receipt({
|
||
state: throughRouter ? "waiting_for_broadcast_console" : "waiting_for_owner_handoff",
|
||
diagnostic_code: throughRouter ? "broadcast_console_delivery_confirmed" : "owner_handoff_required",
|
||
workorder_id: created.id,
|
||
evidence: throughRouter ? { delivered_devices: delivery.delivered } : {},
|
||
next_step: throughRouter
|
||
? "等待冰朔在 HoloLake 广播主控台核对并点击授权;不要发送邮件。"
|
||
: "HoloLake 当前没有在线路由连接。把 request_url 交给主人,通过服务器托管页面恢复设备绑定或完成邮件灾备授权。",
|
||
}),
|
||
});
|
||
}
|
||
|
||
if (req.method === "POST" && url.pathname === "/api/broadcast/workorders") {
|
||
if (!bearerMatches(req, broadcastToken)) {
|
||
return json(res, 401, { error: "broadcast_tower_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 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",
|
||
delivery: {
|
||
channel: "fifth_domain_broadcast_email",
|
||
delivered_devices: 0,
|
||
},
|
||
email_status: "sent",
|
||
receipt: receipt({
|
||
state: "waiting_for_owner",
|
||
diagnostic_code: "owner_email_sent_by_registered_broadcast_tower",
|
||
workorder_id: created.id,
|
||
target: validation.request.target,
|
||
action: validation.request.action,
|
||
next_step: "等待目标节点主人点击邮件批准链接;广播塔随后使用原 claim_token 领取会话。",
|
||
}),
|
||
});
|
||
}
|
||
|
||
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 delivery = bindAndDeliver(created);
|
||
if (delivery.delivered > 0) {
|
||
return json(res, 201, {
|
||
ok: true,
|
||
workorder_id: created.id,
|
||
claim_token: created.claimToken,
|
||
expires_in: created.expiresIn,
|
||
status: "waiting_for_broadcast_console",
|
||
delivery: { channel: "guanghu_router", delivered_devices: delivery.delivered },
|
||
});
|
||
}
|
||
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",
|
||
delivery: { channel: "email_recovery_fallback", delivered_devices: 0 },
|
||
});
|
||
}
|
||
|
||
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 branch = String(body.branch || "main");
|
||
const resource = `${repo}@${branch}`;
|
||
const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, target, scope, "push-repository");
|
||
if (!verified.ok) return json(res, 403, failure(verified.reason));
|
||
if (verified.session.resource && verified.session.resource !== resource) {
|
||
return json(res, 403, failure("resource_mismatch"));
|
||
}
|
||
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 entry = repoPushRegistry[repo];
|
||
const configured = Boolean(entry);
|
||
const operationReceipt = receipt({
|
||
state: configured ? "ready" : "blocked",
|
||
diagnostic_code: configured ? "repo_push_transport_ready" : "repo_push_transport_unavailable",
|
||
workorder_id: verified.session.workorderId,
|
||
target,
|
||
action: "push-repository",
|
||
next_step: configured
|
||
? "使用同一会话向受限 bundle 接收器上传一次 Git bundle;服务器将核验仓库、分支、精确远端基线和快进关系。"
|
||
: "服务器已登记本次推送许可,但安全推送接收器尚未部署;不要重试裸 git push、不要索要账号密码。等待受限 bundle 接收器上线后按同一工单回执执行。",
|
||
});
|
||
manager.recordReceipt(token, operationReceipt);
|
||
return json(res, 200, {
|
||
ok: true,
|
||
repo,
|
||
branch,
|
||
target,
|
||
expires_at: grant.expires_at,
|
||
transport: {
|
||
status: configured ? "ready" : "not_configured",
|
||
diagnostic_code: operationReceipt.diagnostic_code,
|
||
upload_url: configured ? `${publicBaseUrl}/api/repo-push/bundle` : "",
|
||
max_bundle_bytes: configured ? maxRepoBundleBytes : 0,
|
||
max_request_bytes: configured ? maxRepoChunkBytes : 0,
|
||
next_step: operationReceipt.next_step,
|
||
},
|
||
receipt: operationReceipt,
|
||
});
|
||
}
|
||
|
||
if (req.method === "PUT" && url.pathname === "/api/repo-push/bundle") {
|
||
const token = bearer(req);
|
||
const repo = String(url.searchParams.get("repo") || "").toLowerCase();
|
||
const branch = String(url.searchParams.get("branch") || "");
|
||
const expectedHead = String(url.searchParams.get("expected_head") || "").toLowerCase();
|
||
const personaId = String(url.searchParams.get("persona_id") || "");
|
||
const target = String(url.searchParams.get("target") || "");
|
||
const scope = String(url.searchParams.get("scope") || "repo-push");
|
||
const resource = `${repo}@${branch}`;
|
||
if (!repoPushRegistry[repo]) return json(res, 404, failure("repository_not_registered"));
|
||
const verified = manager.verifySession(
|
||
token,
|
||
{ pid: personaId },
|
||
target,
|
||
scope,
|
||
"push-repository",
|
||
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 declaredLength = Number(req.headers["content-length"]);
|
||
const uploadId = String(req.headers["x-guanghu-upload-id"] || "");
|
||
const chunkIndex = Number(req.headers["x-guanghu-chunk-index"]);
|
||
const chunkCount = Number(req.headers["x-guanghu-chunk-count"]);
|
||
const chunkOffset = Number(req.headers["x-guanghu-chunk-offset"]);
|
||
const chunked = uploadId !== "";
|
||
if (!Number.isSafeInteger(declaredLength)
|
||
|| declaredLength < 1
|
||
|| declaredLength > (chunked ? maxRepoChunkBytes : maxRepoBundleBytes)) {
|
||
return json(res, 413, failure("repo_bundle_size_invalid"));
|
||
}
|
||
fs.mkdirSync(repoUploadDir, { recursive: true, mode: 0o2770 });
|
||
if (chunked && (
|
||
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uploadId)
|
||
|| !Number.isSafeInteger(chunkIndex)
|
||
|| !Number.isSafeInteger(chunkCount)
|
||
|| !Number.isSafeInteger(chunkOffset)
|
||
|| chunkIndex < 0
|
||
|| chunkCount < 1
|
||
|| chunkIndex >= chunkCount
|
||
|| chunkCount > Math.ceil(maxRepoBundleBytes / (8 * 1024))
|
||
|| chunkOffset < 0
|
||
|| chunkOffset + declaredLength > maxRepoBundleBytes
|
||
)) {
|
||
return json(res, 400, failure("repo_bundle_chunk_invalid"));
|
||
}
|
||
const bundlePath = path.join(
|
||
repoUploadDir,
|
||
chunked ? `${uploadId}.bundle.part` : `${crypto.randomUUID()}.bundle`,
|
||
);
|
||
if (chunked) {
|
||
const currentSize = fs.existsSync(bundlePath) ? fs.statSync(bundlePath).size : 0;
|
||
if ((chunkIndex === 0 && currentSize !== 0) || (chunkIndex > 0 && currentSize !== chunkOffset)) {
|
||
return json(res, 409, failure("repo_bundle_chunk_out_of_order", "", {
|
||
expected_offset: currentSize,
|
||
}));
|
||
}
|
||
}
|
||
try {
|
||
await readBinaryBody(req, bundlePath, chunked ? maxRepoChunkBytes : maxRepoBundleBytes, {
|
||
append: chunked && chunkIndex > 0,
|
||
});
|
||
if (chunked && chunkIndex + 1 < chunkCount) {
|
||
return json(res, 202, {
|
||
ok: true,
|
||
upload: {
|
||
state: "partial",
|
||
upload_id: uploadId,
|
||
next_chunk_index: chunkIndex + 1,
|
||
received_bytes: chunkOffset + declaredLength,
|
||
},
|
||
});
|
||
}
|
||
const result = await receiveRepoBundle({
|
||
repo,
|
||
branch,
|
||
expected_head: expectedHead,
|
||
bundle_path: bundlePath,
|
||
});
|
||
const operationReceipt = receipt({
|
||
state: result.ok ? "succeeded" : "blocked",
|
||
diagnostic_code: result.diagnostic_code || (result.ok ? "repo_push_succeeded" : "repo_push_receiver_failed"),
|
||
workorder_id: verified.session.workorderId,
|
||
target,
|
||
action: "push-repository",
|
||
evidence: {
|
||
repo,
|
||
branch,
|
||
expected_head: expectedHead,
|
||
commit_sha: result.commit_sha || "",
|
||
verification_url: result.verification_url || "",
|
||
receiver_evidence: String(result.evidence || "").slice(0, 600),
|
||
},
|
||
next_step: result.ok
|
||
? "从光湖代码频道回读分支与提交 SHA;本次上传不自动部署。"
|
||
: "读取 diagnostic_code 和精确基线回执;不要改用裸 git push 或猜测账号密码。",
|
||
});
|
||
manager.recordReceipt(token, operationReceipt);
|
||
return json(res, result.ok ? 200 : 409, {
|
||
ok: result.ok,
|
||
repository: result,
|
||
receipt: operationReceipt,
|
||
});
|
||
} finally {
|
||
if (!chunked || chunkIndex + 1 === chunkCount) {
|
||
fs.rmSync(bundlePath, { force: true });
|
||
}
|
||
}
|
||
}
|
||
|
||
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 deploymentRepositories = options.deploymentRepositories || loadDeploymentRepositories(deploymentRegistryFile);
|
||
const queued = enqueueDeploymentEvent({
|
||
schema: "guanghu.deployment-intent/v1",
|
||
repo,
|
||
branch,
|
||
commit_sha: commit,
|
||
resource,
|
||
manifest,
|
||
workorder_id: verified.session.workorderId,
|
||
deployment_source: body.deployment_source || null,
|
||
}, { repo, branch, commit_sha: commit }, deploymentQueueDir, {
|
||
authorizer_id: verified.session.authorizerId,
|
||
persona_id: verified.session.persona.pid,
|
||
execution_runtime_id: String(body.execution_runtime_id || ""),
|
||
target,
|
||
registry: deploymentRepositories,
|
||
});
|
||
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 });
|
||
}
|
||
|
||
if (req.method === "POST" && url.pathname === "/api/ghdr/controllers/poll") {
|
||
const body = await readJson(req);
|
||
if (!body) return json(res, 400, failure("invalid_json"));
|
||
const result = getGhdrControllerBroker().poll(body);
|
||
return json(res, result.ok ? 200 : 403, result);
|
||
}
|
||
|
||
if (req.method === "POST" && url.pathname === "/api/ghdr/controllers/result") {
|
||
const body = await readJson(req);
|
||
if (!body) return json(res, 400, failure("invalid_json"));
|
||
try {
|
||
const result = getGhdrControllerBroker().submit(body);
|
||
return json(res, result.ok ? 200 : 403, result);
|
||
} catch {
|
||
return json(res, 400, failure("ghdr_result_invalid"));
|
||
}
|
||
}
|
||
|
||
if (req.method === "POST" && url.pathname === "/api/ghdr/sign-layout") {
|
||
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 || "native-recovery");
|
||
const resource = String(body.resource || "");
|
||
const binding = validateGhdrLayoutRequest(body.plan, target, resource);
|
||
if (!binding.ok) return json(res, 400, failure(binding.error));
|
||
const verified = manager.verifySession(
|
||
token,
|
||
{ pid: String(body.persona_id || "") },
|
||
target,
|
||
scope,
|
||
"sign-native-layout-plan",
|
||
Date.now() / 1000,
|
||
resource,
|
||
);
|
||
if (!verified.ok) return json(res, 403, failure(verified.reason));
|
||
if (verified.session.approvalChannel !== "email") {
|
||
return json(res, 403, failure(
|
||
"ghdr_email_approval_required",
|
||
"企业原生布局必须由京东主控的预登记邮箱完成本次批准。",
|
||
));
|
||
}
|
||
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 result = await signGhdrPlan({
|
||
plan: body.plan,
|
||
binding,
|
||
workorderId: verified.session.workorderId,
|
||
authorizer: getGhdrAuthorizer(),
|
||
});
|
||
const signatures = Array.isArray(result.signatures) ? result.signatures : [];
|
||
const independent = signatures.length === 2
|
||
&& new Set(signatures.map(item => item && item.node_id)).size === 2
|
||
&& new Set(signatures.map(item => item && item.failure_domain)).size === 2;
|
||
const succeeded = Boolean(result.ok && independent);
|
||
const operationReceipt = receipt({
|
||
state: succeeded ? "succeeded" : "failed",
|
||
diagnostic_code: succeeded
|
||
? "ghdr_layout_double_signature_succeeded"
|
||
: String(result.error || "ghdr_layout_double_signature_failed"),
|
||
workorder_id: verified.session.workorderId,
|
||
target,
|
||
action: "sign-native-layout-plan",
|
||
evidence: {
|
||
layout_payload_sha256: binding.payload_sha256,
|
||
generation: binding.generation,
|
||
controller_count: signatures.length,
|
||
controller_ids: signatures.map(item => String(item && item.node_id || "")),
|
||
},
|
||
next_step: succeeded
|
||
? "把两份签名合并回同一份布局计划,并由企业目标机独立验证公钥、故障域、实时读回与有效期。"
|
||
: "读取 diagnostic_code;不得降级为单签、复制私钥或绕过邮件授权。",
|
||
});
|
||
manager.recordReceipt(token, operationReceipt);
|
||
return json(res, succeeded ? 200 : 502, {
|
||
ok: succeeded,
|
||
layout_payload_sha256: binding.payload_sha256,
|
||
signatures: succeeded ? signatures : [],
|
||
receipt: operationReceipt,
|
||
});
|
||
}
|
||
|
||
if (req.method === "GET" && url.pathname === "/api/ghdr/authorizer-public-key") {
|
||
return json(res, 200, {
|
||
ok: true,
|
||
binding: getGhdrAuthorizer().publicBinding(),
|
||
});
|
||
}
|
||
|
||
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 loadDeploymentRepositories(file) {
|
||
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
||
if (!parsed || !parsed.repos || typeof parsed.repos !== "object") throw new Error("invalid_deployment_repository_registry");
|
||
return parsed.repos;
|
||
}
|
||
|
||
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">这张页面本身没有执行权。邮箱只通过本页的加密连接直达京东节点,不会返回给发起申请的AI。无论是否匹配,页面都会显示相同结果。</p>
|
||
<form method="post">
|
||
<label for="owner-email">冰朔登记邮箱</label>
|
||
<input id="owner-email" name="email" type="email" inputmode="email" autocomplete="email" maxlength="254" required placeholder="请输入你的邮箱">
|
||
<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}label{display:block;margin:0 0 8px;color:#c9dae5;font-weight:700}input{width:100%;border:1px solid #365d76;border-radius:14px;padding:15px 16px;margin:0 0 14px;background:#07131d;color:#eaf4fb;font:inherit;outline:none}input:focus{border-color:#67d4ff;box-shadow:0 0 0 3px #67d4ff22}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 readBinaryBody(req, destination, maxBytes, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
let received = 0;
|
||
const output = fs.createWriteStream(destination, {
|
||
flags: options.append ? "a" : "wx",
|
||
mode: 0o640,
|
||
});
|
||
const fail = error => {
|
||
output.destroy();
|
||
fs.rmSync(destination, { force: true });
|
||
reject(error);
|
||
};
|
||
req.on("data", chunk => {
|
||
received += chunk.length;
|
||
if (received > maxBytes) {
|
||
const error = new Error("repo bundle too large");
|
||
error.code = "BODY_TOO_LARGE";
|
||
req.destroy(error);
|
||
return;
|
||
}
|
||
if (!output.write(chunk)) req.pause();
|
||
});
|
||
output.on("drain", () => req.resume());
|
||
req.on("end", () => output.end());
|
||
req.on("error", fail);
|
||
output.on("error", fail);
|
||
output.on("finish", () => {
|
||
if (received < 1) return fail(new Error("repo bundle empty"));
|
||
resolve(received);
|
||
});
|
||
});
|
||
}
|
||
|
||
function readForm(req) {
|
||
return new Promise((resolve, reject) => {
|
||
let raw = "";
|
||
req.on("data", chunk => {
|
||
raw += chunk;
|
||
if (raw.length > 4 * 1024) {
|
||
const error = new Error("body too large");
|
||
error.code = "BODY_TOO_LARGE";
|
||
reject(error);
|
||
req.destroy();
|
||
}
|
||
});
|
||
req.on("end", () => {
|
||
try { resolve(Object.fromEntries(new URLSearchParams(raw))); }
|
||
catch { resolve({}); }
|
||
});
|
||
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: "ICE-GL∞", 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.targets.includes("*") && !item.scopes.includes("*"))
|
||
|| 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 normalizeEmail(value) { return String(value || "").trim().normalize("NFKC").toLowerCase(); }
|
||
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";
|
||
const repoPushResourceAction = action === "push-repository";
|
||
const linkedNodeResourceAction = action === "authorize-linked-node-session";
|
||
const ghdrLayoutResourceAction = action === "sign-native-layout-plan";
|
||
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 (repoPushResourceAction && resource && !/^bingshuo\/[a-z0-9._-]+@[a-z0-9][a-z0-9._/-]{0,199}$/.test(resource)) return { ok: false, status: 400, error: "repo_push_resource_invalid" };
|
||
if (linkedNodeResourceAction && !/^[A-Z0-9][A-Z0-9._-]{5,119}:[A-Za-z0-9._-]{3,120}$/.test(resource)) return { ok: false, status: 400, error: "linked_node_resource_required" };
|
||
if (ghdrLayoutResourceAction && !/^GH-CVM-MAIN-PROD-01:[0-9a-f]{64}:[1-9][0-9]{0,19}$/.test(resource)) return { ok: false, status: 400, error: "ghdr_layout_resource_required" };
|
||
if (!immutableResourceAction && !repoPushResourceAction && !linkedNodeResourceAction && !ghdrLayoutResourceAction && 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 validateGhdrLayoutRequest(plan, target, resource) {
|
||
if (target !== "GH-CVM-MAIN-PROD-01"
|
||
|| !plan
|
||
|| plan.schema !== "guanghu.ghdr-signed-layout-plan/v1"
|
||
|| !plan.payload
|
||
|| !Array.isArray(plan.signatures)
|
||
|| plan.signatures.length !== 0) {
|
||
return { ok: false, error: "ghdr_layout_plan_invalid" };
|
||
}
|
||
const payload = plan.payload;
|
||
const generation = Number(payload.generation);
|
||
if (payload.node_id !== target
|
||
|| payload.provider !== "tencent_cloud"
|
||
|| payload.region !== "ap-guangzhou"
|
||
|| payload.system_disk !== "/dev/vda"
|
||
|| payload.operation !== "install_native_ab"
|
||
|| !Number.isSafeInteger(generation)
|
||
|| generation < 1) {
|
||
return { ok: false, error: "ghdr_layout_binding_mismatch" };
|
||
}
|
||
const canonical = JSON.stringify(payload);
|
||
const payloadSha256 = crypto.createHash("sha256").update(canonical).digest("hex");
|
||
if (resource !== `${target}:${payloadSha256}:${generation}`) {
|
||
return { ok: false, error: "ghdr_layout_resource_mismatch" };
|
||
}
|
||
return { ok: true, payload_sha256: payloadSha256, generation, 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 => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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,
|
||
validateGhdrLayoutRequest,
|
||
};
|