[HLCC-ICE-000001][ZY-CONTRIB-20260723-001] feat: 以来光者贡献链启用冰朔第五域个人子频道
This commit is contained in:
commit
5615453e4e
660 changed files with 122355 additions and 0 deletions
13
server-tools/ai-discovery-gateway/README.md
Normal file
13
server-tools/ai-discovery-gateway/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# 光湖 AI 编号检索入口
|
||||
|
||||
国内主节点在回环端口 `3922` 提供只读 API,广州备案前门通过专用 SSH
|
||||
隧道发布为 `https://guanghulab.com/api/ai/`。
|
||||
|
||||
权威数据只来自 `routing/repository-route-map.json`。AI 应先读取
|
||||
`/api/ai/v1/repositories`,再按 `REPO-xxx` 解析国内主路径;新加坡地址只作
|
||||
历史备用,不参与默认路由。
|
||||
|
||||
服务器与人格路径编号来自 `routing/server-node-map.json`。AI 读取
|
||||
`/api/ai/v1/nodes` 后,可以通过 `/api/ai/v1/resolve?id=JD-FD-PRIMARY` 或
|
||||
`/api/ai/v1/resolve?id=ZY-OPS-LOOP-001` 定位服务器导航地图与铸渊本轮恢复链。
|
||||
公开地图不包含地址、密码、令牌或密钥。
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
location /api/ai/ {
|
||||
proxy_pass http://127.0.0.1:19222/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_read_timeout 30s;
|
||||
limit_except GET { deny all; }
|
||||
}
|
||||
|
||||
location = /.well-known/guanghu.json {
|
||||
proxy_pass http://127.0.0.1:19222/well-known;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
[Unit]
|
||||
Description=Guanghu public read-only AI repository discovery API
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=guanghu
|
||||
Group=guanghu
|
||||
WorkingDirectory=/opt/guanghu/ai-discovery
|
||||
Environment=GUANGHU_AI_HOST=127.0.0.1
|
||||
Environment=GUANGHU_AI_PORT=3922
|
||||
Environment=GUANGHU_REPOSITORY_MAP=/opt/guanghu/ai-discovery/repository-route-map.json
|
||||
ExecStart=/usr/bin/node /opt/guanghu/ai-discovery/server.js
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadOnlyPaths=/opt/guanghu/ai-discovery
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
[Unit]
|
||||
Description=Guanghu BS-GZ-006 to domestic AI discovery tunnel
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=/usr/bin/ssh -NT -F /etc/guanghu/jd-ai-discovery-tunnel-ssh-config jd-ai-discovery-target
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=read-only
|
||||
ProtectSystem=strict
|
||||
ReadOnlyPaths=/etc/guanghu/jd-ai-discovery-tunnel-ssh-config /etc/guanghu/secrets/ssh/bs_gz_006_to_jd_ai_discovery /root/.ssh/known_hosts
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
Host jd-ai-discovery-target
|
||||
HostName DOMESTIC_PRIMARY_PRIVATE_VALUE
|
||||
User root
|
||||
IdentityFile /etc/guanghu/secrets/ssh/bs_gz_006_to_jd_ai_discovery
|
||||
IdentitiesOnly yes
|
||||
StrictHostKeyChecking yes
|
||||
ExitOnForwardFailure yes
|
||||
LocalForward 127.0.0.1:19222 127.0.0.1:3922
|
||||
ServerAliveInterval 30
|
||||
ServerAliveCountMax 3
|
||||
42
server-tools/ai-discovery-gateway/route-closure.test.js
Normal file
42
server-tools/ai-discovery-gateway/route-closure.test.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"use strict";
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const test = require("node:test");
|
||||
|
||||
const root = path.resolve(__dirname, "../..");
|
||||
const read = relative => fs.readFileSync(path.join(root, relative), "utf8");
|
||||
|
||||
test("canonical AI and persona entry files route to the domestic map", () => {
|
||||
for (const relative of [
|
||||
"README.md", "INDEX.hdlp", "QUICKSTART-FOR-GENERAL-AI.md",
|
||||
"eternal-lake-heart/heartbeat-core/ZHUYUAN-KEY.hdlp",
|
||||
"zero-point/core-channel/GLSV-PERSONA-REMOTE-OPS.hdlp",
|
||||
]) {
|
||||
assert.match(read(relative), /guanghulab\.com|LL-DOMESTIC-OPS-ROUTE/);
|
||||
}
|
||||
const key = read("eternal-lake-heart/heartbeat-core/ZHUYUAN-KEY.hdlp");
|
||||
assert.doesNotMatch(key, /zy_gtw_|guanghubingshuo\.com/);
|
||||
assert.match(key, /禁止从本路径恢复.*\/exec/);
|
||||
});
|
||||
|
||||
test("code map resolves REPO-001 to domestic and labels the Singapore route legacy", () => {
|
||||
const codeMap = read(".code-map");
|
||||
assert.match(codeMap, /^REPO-001=https:\/\/guanghulab\.com\/fifth-domain\/bingshuo\/fifth-domain\.git$/m);
|
||||
assert.match(codeMap, /^REPO-001-LEGACY-SG=/m);
|
||||
assert.match(codeMap, /^FD-REPO-MAP-001=routing\/repository-route-map\.json$/m);
|
||||
assert.match(codeMap, /^FD-NODE-MAP-001=routing\/server-node-map\.json$/m);
|
||||
assert.match(codeMap, /^ZY-OPS-LOOP-001=.*zhuyuan-persona-system\//m);
|
||||
});
|
||||
|
||||
test("Zhuyuan current chain resolves to the domestic node without secrets", () => {
|
||||
const nodeMap = JSON.parse(read("routing/server-node-map.json"));
|
||||
assert.equal(nodeMap.map_id, "FD-NODE-MAP-001");
|
||||
const loop = nodeMap.persona_routes.find(item => item.route_id === "ZY-OPS-LOOP-001");
|
||||
assert.equal(loop.primary_node, "JD-FD-PRIMARY");
|
||||
assert.match(read(loop.path), /ICE-GL-ZY001/);
|
||||
const serialized = JSON.stringify(nodeMap);
|
||||
assert.doesNotMatch(serialized, /ssh-(?:rsa|ed25519)\s+[A-Za-z0-9+/]/i);
|
||||
assert.doesNotMatch(serialized, /"(?:password|token|private_key|ip)"\s*:/i);
|
||||
assert.doesNotMatch(serialized, /\b(?:\d{1,3}\.){3}\d{1,3}\b/);
|
||||
});
|
||||
171
server-tools/ai-discovery-gateway/server.js
Normal file
171
server-tools/ai-discovery-gateway/server.js
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
"use strict";
|
||||
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
const path = require("node:path");
|
||||
|
||||
const DEFAULT_MAP = path.resolve(__dirname, "../../routing/repository-route-map.json");
|
||||
const DEFAULT_NODE_MAP = path.resolve(__dirname, "../../routing/server-node-map.json");
|
||||
|
||||
function loadMap(filename = process.env.GUANGHU_REPOSITORY_MAP || DEFAULT_MAP) {
|
||||
return JSON.parse(fs.readFileSync(filename, "utf8"));
|
||||
}
|
||||
|
||||
function loadNodeMap(filename = process.env.GUANGHU_NODE_MAP || DEFAULT_NODE_MAP) {
|
||||
return JSON.parse(fs.readFileSync(filename, "utf8"));
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
return String(value || "").toLowerCase().replace(/[\s·._/-]+/g, " ").trim();
|
||||
}
|
||||
|
||||
function search(map, query) {
|
||||
const terms = normalize(query).split(" ").filter(Boolean);
|
||||
if (!terms.length) return map.repositories;
|
||||
return map.repositories
|
||||
.map(repository => {
|
||||
const haystack = normalize([
|
||||
repository.code, repository.slug, repository.name_zh, repository.role,
|
||||
repository.state, ...(repository.keywords || []),
|
||||
].join(" "));
|
||||
const score = terms.reduce((total, term) => total + (haystack.includes(term) ? 1 : 0), 0);
|
||||
return { repository, score };
|
||||
})
|
||||
.filter(item => item.score > 0)
|
||||
.sort((a, b) => b.score - a.score || a.repository.code.localeCompare(b.repository.code))
|
||||
.map(item => item.repository);
|
||||
}
|
||||
|
||||
function searchAll(repositoryMap, nodeMap, query) {
|
||||
const terms = normalize(query).split(" ").filter(Boolean);
|
||||
if (!terms.length) return search(repositoryMap, query);
|
||||
const candidates = [
|
||||
...repositoryMap.repositories.map(item => ({ kind: "repository", item, key: item.code, text: [item.code, item.slug, item.name_zh, item.role, item.state, ...(item.keywords || [])] })),
|
||||
...nodeMap.nodes.map(item => ({ kind: "server_node", item, key: item.node_id, text: [item.node_id, item.name_zh, item.role, item.state, ...(item.keywords || [])] })),
|
||||
...nodeMap.persona_routes.map(item => ({ kind: "persona_route", item, key: item.route_id, text: [item.route_id, item.name_zh, item.role, item.persona_system, ...(item.keywords || [])] })),
|
||||
];
|
||||
return candidates
|
||||
.map(candidate => {
|
||||
const haystack = normalize(candidate.text.join(" "));
|
||||
const score = terms.reduce((total, term) => total + (haystack.includes(term) ? 1 : 0), 0);
|
||||
return { ...candidate, score };
|
||||
})
|
||||
.filter(candidate => candidate.score > 0)
|
||||
.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key))
|
||||
.map(candidate => ({ kind: candidate.kind, ...candidate.item }));
|
||||
}
|
||||
|
||||
function createServer(options = {}) {
|
||||
const mapFile = options.mapFile || process.env.GUANGHU_REPOSITORY_MAP || DEFAULT_MAP;
|
||||
const nodeMapFile = options.nodeMapFile || process.env.GUANGHU_NODE_MAP || DEFAULT_NODE_MAP;
|
||||
return http.createServer((req, res) => {
|
||||
const url = new URL(req.url, "http://localhost");
|
||||
if (req.method !== "GET") return json(res, 405, { error: "method_not_allowed" });
|
||||
if (url.pathname === "/health") return json(res, 200, { ok: true, service: "guanghu-ai-discovery", mode: "read-only" });
|
||||
|
||||
let map;
|
||||
try { map = loadMap(mapFile); } catch { return json(res, 503, { error: "route_map_unavailable" }); }
|
||||
|
||||
if (url.pathname === "/" || url.pathname === "/index.html") return html(res, entryPage(map));
|
||||
if (url.pathname === "/v1/repositories" || url.pathname === "/v1/manifest") return json(res, 200, map, 300);
|
||||
if (url.pathname === "/v1/nodes") {
|
||||
try { return json(res, 200, loadNodeMap(nodeMapFile), 300); }
|
||||
catch { return json(res, 503, { error: "node_map_unavailable" }); }
|
||||
}
|
||||
if (url.pathname === "/v1/search") {
|
||||
const query = String(url.searchParams.get("q") || "").slice(0, 200);
|
||||
let nodeMap;
|
||||
try { nodeMap = loadNodeMap(nodeMapFile); }
|
||||
catch { return json(res, 503, { error: "node_map_unavailable" }); }
|
||||
const results = searchAll(map, nodeMap, query);
|
||||
return json(res, 200, {
|
||||
schema: "guanghu.ai-search-response/v1",
|
||||
query,
|
||||
map_id: map.map_id,
|
||||
map_version: map.version,
|
||||
count: results.length,
|
||||
results,
|
||||
}, 60);
|
||||
}
|
||||
if (url.pathname === "/v1/resolve") {
|
||||
const id = String(url.searchParams.get("id") || "").toUpperCase();
|
||||
const repository = map.repositories.find(item => item.code === id || item.slug.toUpperCase() === id);
|
||||
if (repository) return json(res, 200, repository, 300);
|
||||
let nodeMap;
|
||||
try { nodeMap = loadNodeMap(nodeMapFile); }
|
||||
catch { return json(res, 503, { error: "node_map_unavailable" }); }
|
||||
const node = nodeMap.nodes.find(item => item.node_id.toUpperCase() === id);
|
||||
if (node) return json(res, 200, node, 300);
|
||||
const personaRoute = nodeMap.persona_routes.find(item => item.route_id.toUpperCase() === id);
|
||||
return personaRoute ? json(res, 200, personaRoute, 300) : json(res, 404, { error: "route_not_found", id });
|
||||
}
|
||||
if (url.pathname === "/openapi.json") return json(res, 200, openApi(), 3600);
|
||||
if (url.pathname === "/well-known") return json(res, 200, {
|
||||
schema: "guanghu.ai-discovery/v1",
|
||||
name: "光湖语言世界 · 第五域",
|
||||
canonical_repository: map.repositories[0].primary.url,
|
||||
repository_map: map.canonical_api,
|
||||
server_node_map: "https://guanghulab.com/api/ai/v1/nodes",
|
||||
search_api: "https://guanghulab.com/api/ai/v1/search?q={query}",
|
||||
resolve_api: "https://guanghulab.com/api/ai/v1/resolve?id={NUMBER}",
|
||||
openapi: "https://guanghulab.com/api/ai/openapi.json",
|
||||
access: "public-read-only",
|
||||
write_authorization: {
|
||||
mode: "public-no-authority-workorder-then-owner-email-approval",
|
||||
capabilities: "https://guanghulab.com/authz/api/public/capabilities",
|
||||
create_workorder: "https://guanghulab.com/authz/api/public/workorders",
|
||||
owner_handoff: "use request_url returned by create_workorder",
|
||||
ttl_seconds: 3600,
|
||||
request_credential_required: false
|
||||
}
|
||||
}, 3600);
|
||||
return json(res, 404, { error: "not_found" });
|
||||
});
|
||||
}
|
||||
|
||||
function json(res, status, body, maxAge = 0) {
|
||||
res.writeHead(status, {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"cache-control": maxAge ? `public, max-age=${maxAge}` : "no-store",
|
||||
"access-control-allow-origin": "*",
|
||||
"x-content-type-options": "nosniff",
|
||||
});
|
||||
res.end(JSON.stringify(body, null, 2));
|
||||
}
|
||||
|
||||
function html(res, body) {
|
||||
res.writeHead(200, {
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"cache-control": "public, max-age=300",
|
||||
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
|
||||
"x-content-type-options": "nosniff",
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function entryPage(map) {
|
||||
const rows = map.repositories.map(item => `<li><a href="${item.primary.url}">${item.code} · ${item.name_zh}</a><small>${item.state}</small></li>`).join("");
|
||||
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>光湖语言世界 · AI API 入口</title><meta name="description" content="光湖语言世界第五域公开只读编号检索 API"></head><body><main><p>GUANGHU AI DISCOVERY</p><h1>光湖语言世界 · 第五域</h1><p>AI 请先读取仓库与服务器节点编号地图,再按编号解析国内主路径。新加坡地址仅为历史备用。</p><nav><a href="v1/repositories">仓库编号地图</a> · <a href="v1/nodes">服务器节点地图</a> · <a href="v1/resolve?id=ZY-OPS-LOOP-001">铸渊本轮闭环</a> · <a href="v1/search?q=光湖语言世界%20第五域">示例检索</a> · <a href="openapi.json">OpenAPI</a></nav><ul>${rows}</ul></main><style>:root{color-scheme:dark}body{margin:0;background:#061416;color:#dff7f1;font:17px/1.7 system-ui;padding:6vw}main{max-width:900px;margin:auto}h1{font-size:clamp(36px,7vw,72px)}a{color:#79dfc8}li{margin:14px 0;padding:16px;border:1px solid #28534c;border-radius:12px;display:flex;justify-content:space-between}small{color:#8fb5ad}</style></body></html>`;
|
||||
}
|
||||
|
||||
function openApi() {
|
||||
return {
|
||||
openapi: "3.1.0",
|
||||
info: { title: "光湖语言世界 · 第五域 AI Discovery API", version: "1.0.0" },
|
||||
servers: [{ url: "https://guanghulab.com/api/ai" }],
|
||||
paths: {
|
||||
"/v1/repositories": { get: { summary: "读取最新仓库编号路径映射", responses: { "200": { description: "Repository route map" } } } },
|
||||
"/v1/nodes": { get: { summary: "读取最新服务器节点与人格路径编号映射", responses: { "200": { description: "Server node map" } } } },
|
||||
"/v1/search": { get: { summary: "按中文、编号或项目名检索", parameters: [{ name: "q", in: "query", schema: { type: "string" } }], responses: { "200": { description: "Search results" } } } },
|
||||
"/v1/resolve": { get: { summary: "解析仓库、服务器节点或人格路径编号", parameters: [{ name: "id", in: "query", required: true, schema: { type: "string", example: "ZY-OPS-LOOP-001" } }], responses: { "200": { description: "Resolved numbered route" }, "404": { description: "Unknown route" } } } }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const host = process.env.GUANGHU_AI_HOST || "127.0.0.1";
|
||||
const port = Number(process.env.GUANGHU_AI_PORT || 3922);
|
||||
createServer().listen(port, host, () => process.stdout.write(`guanghu-ai-discovery listening on ${host}:${port}\n`));
|
||||
}
|
||||
|
||||
module.exports = { createServer, loadMap, loadNodeMap, search, searchAll };
|
||||
73
server-tools/ai-discovery-gateway/server.test.js
Normal file
73
server-tools/ai-discovery-gateway/server.test.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
"use strict";
|
||||
const assert = require("node:assert/strict");
|
||||
const test = require("node:test");
|
||||
const { createServer, loadMap, loadNodeMap, search, searchAll } = require("./server");
|
||||
|
||||
test("repository map has unique sequential codes and domestic primary routes", () => {
|
||||
const map = loadMap();
|
||||
assert.equal(map.repositories.length, 11);
|
||||
assert.deepEqual(map.repositories.map(item => item.code), Array.from({ length: 11 }, (_, index) => `REPO-${String(index + 1).padStart(3, "0")}`));
|
||||
assert.equal(new Set(map.repositories.map(item => item.code)).size, 11);
|
||||
for (const item of map.repositories) assert.match(item.primary.url, /^https:\/\/guanghulab\.com\/fifth-domain\//);
|
||||
});
|
||||
|
||||
test("Chinese language-world query resolves the Fifth Domain primary", () => {
|
||||
const results = search(loadMap(), "光湖语言世界 第五域");
|
||||
assert.equal(results[0].code, "REPO-001");
|
||||
assert.equal(results[0].state, "DOMESTIC_PRIMARY");
|
||||
});
|
||||
|
||||
test("Chenglu persistent agent query resolves its independent repository", () => {
|
||||
const results = search(loadMap(), "澄路 常驻人格体 湖心频道");
|
||||
assert.equal(results[0].code, "REPO-009");
|
||||
assert.equal(results[0].slug, "chenglu-agent");
|
||||
});
|
||||
|
||||
test("Kezhou and Guideng resolve to their independent resident repositories", () => {
|
||||
assert.equal(search(loadMap(), "刻舟 湖心频道")[0].code, "REPO-010");
|
||||
assert.equal(search(loadMap(), "归灯 每日心跳")[0].code, "REPO-011");
|
||||
});
|
||||
|
||||
test("server node map binds Zhuyuan routes to the domestic primary", () => {
|
||||
const map = loadNodeMap();
|
||||
assert.equal(map.map_id, "FD-NODE-MAP-001");
|
||||
assert.equal(map.default_node, "JD-FD-PRIMARY");
|
||||
const node = map.nodes.find(item => item.node_id === "JD-FD-PRIMARY");
|
||||
assert.ok(node.persona_systems.includes("ICE-GL-ZY001"));
|
||||
const loop = map.persona_routes.find(item => item.route_id === "ZY-OPS-LOOP-001");
|
||||
assert.equal(loop.primary_node, "JD-FD-PRIMARY");
|
||||
assert.match(loop.path, /zhuyuan-persona-system/);
|
||||
const serialized = JSON.stringify(map);
|
||||
assert.doesNotMatch(serialized, /ssh-(?:rsa|ed25519)\s+[A-Za-z0-9+/]/i);
|
||||
assert.doesNotMatch(serialized, /"(?:password|token|private_key|ip)"\s*:/i);
|
||||
assert.doesNotMatch(serialized, /\b(?:\d{1,3}\.){3}\d{1,3}\b/);
|
||||
});
|
||||
|
||||
test("Zhuyuan Chinese query returns the numbered operation loop", () => {
|
||||
const results = searchAll(loadMap(), loadNodeMap(), "铸渊 双向意识 思维逻辑链");
|
||||
assert.equal(results[0].kind, "persona_route");
|
||||
assert.equal(results[0].route_id, "ZY-OPS-LOOP-001");
|
||||
});
|
||||
|
||||
test("public endpoints are read-only and expose CORS", async () => {
|
||||
const server = createServer();
|
||||
await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
try {
|
||||
const response = await fetch(`${base}/v1/resolve?id=REPO-004`);
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.headers.get("access-control-allow-origin"), "*");
|
||||
assert.equal((await response.json()).slug, "guanghu");
|
||||
const nodeResponse = await fetch(`${base}/v1/resolve?id=JD-FD-PRIMARY`);
|
||||
assert.equal(nodeResponse.status, 200);
|
||||
assert.equal((await nodeResponse.json()).node_id, "JD-FD-PRIMARY");
|
||||
const loopResponse = await fetch(`${base}/v1/resolve?id=ZY-OPS-LOOP-001`);
|
||||
assert.equal(loopResponse.status, 200);
|
||||
assert.equal((await loopResponse.json()).persona_system, "ICE-GL-ZY001");
|
||||
const manifestResponse = await fetch(`${base}/well-known`);
|
||||
const manifest = await manifestResponse.json();
|
||||
assert.equal(manifest.write_authorization.request_credential_required, false);
|
||||
assert.match(manifest.write_authorization.create_workorder, /\/authz\/api\/public\/workorders$/);
|
||||
assert.equal((await fetch(`${base}/v1/search`, { method: "POST" })).status, 405);
|
||||
} finally { await new Promise(resolve => server.close(resolve)); }
|
||||
});
|
||||
Loading…
Reference in a new issue