diff --git a/server-tools/lake-lamp-continuity-guard/README.md b/server-tools/lake-lamp-continuity-guard/README.md new file mode 100644 index 0000000..fc4ac95 --- /dev/null +++ b/server-tools/lake-lamp-continuity-guard/README.md @@ -0,0 +1,13 @@ +# 小湖灯连续性守卫 · 代码频道策略 + +本目录保存小湖灯本地协调 Agent 已采用的可公开恢复策略: + +- 代码频道 Web 根只有 `https://guanghulab.com/code/`。 +- 当前只允许 REPO-012 与 REPO-014。 +- HTTPS、`.git` 和 `repo://` 先归一化为同一个仓库身份。 +- 实际写租约必须是 `repo://.../#`。 +- 旧 `/fifth-domain/`、其他主机、未登记仓库和含凭证 URL 失败关闭。 + +本模块是策略事实源和回归样例,不代表 JZAO 上的本地协调运行时已经部署到服务器。运行时 +升级、仓库发布和服务器部署仍分别举证。 + diff --git a/server-tools/lake-lamp-continuity-guard/code-channel-policy.mjs b/server-tools/lake-lamp-continuity-guard/code-channel-policy.mjs new file mode 100644 index 0000000..0957a36 --- /dev/null +++ b/server-tools/lake-lamp-continuity-guard/code-channel-policy.mjs @@ -0,0 +1,54 @@ +const HOST = "guanghulab.com"; +const PREFIX = "/code/bingshuo/"; +const REPOSITORIES = new Set([ + "guanghu-ice-heart", + "hololake-system-architecture", +]); + +export function normalizeCodeChannelRepository(value) { + let url; + try { + url = new URL(String(value).trim().replace(/^repo:\/\//u, "https://")); + } catch { + throw new Error( + "INVALID_PUBLISH_REPOSITORY: use repo://guanghulab.com/code/bingshuo/", + ); + } + if (url.username || url.password) { + throw new Error("PUBLISH_REPOSITORY_CREDENTIALS_FORBIDDEN"); + } + const pathname = url.pathname.replace(/\.git$/u, "").replace(/\/+$/u, ""); + const slug = pathname.startsWith(PREFIX) + ? pathname.slice(PREFIX.length) + : ""; + if (url.hostname !== HOST || !REPOSITORIES.has(slug)) { + throw new Error( + "NON_CANONICAL_CODE_CHANNEL_REPOSITORY: only the two registered /code/ repositories are current", + ); + } + return `repo://${HOST}${PREFIX}${slug}`; +} + +export function normalizePublishTarget(value = "main") { + const target = String(value).trim() || "main"; + if ( + !/^(?!\/)(?!.*(?:\.\.|\/\/|@\{|[~^:?*\[\\]))[A-Za-z0-9._/-]+$/u.test( + target, + ) || + target.endsWith("/") || + target.endsWith(".") + ) { + throw new Error("INVALID_PUBLISH_TARGET: use an exact Git branch name"); + } + return target; +} + +export function publishLeaseResource(repository, target = "main") { + return `${normalizeCodeChannelRepository(repository)}#${normalizePublishTarget(target)}`; +} + +export const currentCodeChannelRepositories = Object.freeze([ + "repo://guanghulab.com/code/bingshuo/guanghu-ice-heart", + "repo://guanghulab.com/code/bingshuo/hololake-system-architecture", +]); + diff --git a/server-tools/lake-lamp-continuity-guard/code-channel-policy.test.mjs b/server-tools/lake-lamp-continuity-guard/code-channel-policy.test.mjs new file mode 100644 index 0000000..a055e5a --- /dev/null +++ b/server-tools/lake-lamp-continuity-guard/code-channel-policy.test.mjs @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + currentCodeChannelRepositories, + normalizeCodeChannelRepository, + publishLeaseResource, +} from "./code-channel-policy.mjs"; + +test("HTTPS, .git and repo forms resolve to one repository identity", () => { + const expected = + "repo://guanghulab.com/code/bingshuo/guanghu-ice-heart"; + assert.equal( + normalizeCodeChannelRepository( + "https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git", + ), + expected, + ); + assert.equal(normalizeCodeChannelRepository(expected), expected); + assert.equal(publishLeaseResource(expected), `${expected}#main`); +}); + +test("only the two current repositories are allowed", () => { + assert.equal(currentCodeChannelRepositories.length, 2); + for (const invalid of [ + "https://guanghulab.com/fifth-domain/bingshuo/fifth-domain.git", + "https://guanghubingshuo.com/code/bingshuo/guanghulab.git", + "repo://guanghulab.com/code/bingshuo/unknown", + ]) { + assert.throws(() => normalizeCodeChannelRepository(invalid)); + } +}); + +test("credential-bearing remotes are rejected", () => { + assert.throws( + () => + normalizeCodeChannelRepository( + "https://user:secret@guanghulab.com/code/bingshuo/guanghu-ice-heart.git", + ), + /CREDENTIALS_FORBIDDEN/u, + ); +}); +