Files
codex-bot e286139572
Verify and deploy Tabularium / verify-and-deploy (pull_request) Successful in 5m25s
Redirect legacy document URLs after authentication without prerendering aliases
2026-10-28 19:15:31 +08:00

318 lines
11 KiB
TypeScript

import {
mkdirSync,
existsSync,
writeFileSync,
renameSync,
chmodSync,
readFileSync,
readdirSync,
} from "node:fs";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import assert from "node:assert/strict";
import QRCode from "qrcode";
import {
authConfig,
newAuthConfig,
signSession,
totp,
isLoginRedirect,
} from "../src/lib/auth";
import { consumeCode, initializeStore } from "../src/lib/auth-store";
async function main() {
process.umask(0o077);
const command = process.argv[2];
const file = process.env.AUTH_CONFIG_FILE;
if (!file)
throw Error("Set AUTH_CONFIG_FILE before using the authentication tool");
if (command === "init" || command === "rotate") {
if (command === "rotate" && !process.argv.includes("--confirm"))
throw Error(
"Rotation requires --confirm; all previous authenticators and sessions will stop working",
);
if (command === "init" && existsSync(file)) {
authConfig();
initializeStore();
console.log("Existing authentication key preserved");
return;
}
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
chmodSync(dirname(file), 0o700);
const temporary = file + "." + process.pid + ".tmp";
writeFileSync(temporary, JSON.stringify(newAuthConfig()) + "\n", {
mode: 0o600,
flag: "wx",
});
renameSync(temporary, file);
initializeStore();
console.log(
"Private authentication configuration initialized; no key displayed",
);
return;
}
const config = authConfig();
if (!config) throw Error("Authentication is not configured");
if (command === "state-check") {
assert.equal(consumeCode(config, "invalid"), "limited");
console.log("Durable attempt limit survives restart");
return;
}
if (command === "qr" || command === "qr-png") {
if (process.env.CI)
throw Error("Never display or export authentication QR codes in CI");
const uri =
"otpauth://totp/Tabularium:shared?" +
new URLSearchParams({
secret: config.secret,
issuer: "Tabularium",
algorithm: "SHA1",
digits: "6",
period: "30",
});
if (command === "qr-png") {
process.stdout.write(
await QRCode.toBuffer(uri, {
width: 660,
margin: 4,
errorCorrectionLevel: "M",
}),
);
return;
}
if (!process.stdout.isTTY)
throw Error(
"Use an interactive SSH terminal for qr, or redirect qr-png to a private local PNG",
);
console.log(
await QRCode.toString(uri, {
type: "terminal",
small: true,
errorCorrectionLevel: "M",
}),
);
return;
}
if (command === "token") {
if (!process.stdout.isTTY)
throw Error(
"Tokens can only be displayed in an interactive SSH terminal",
);
console.log(signSession(config, undefined, 3600));
return;
}
const base = process.env.AUTH_CHECK_BASE_URL || "http://127.0.0.1:3000";
if (command === "run") {
const script = process.argv[3];
if (!script) throw Error("Provide a script path");
const original = globalThis.fetch;
const token = signSession(config, undefined, 600);
globalThis.fetch = (input, init) => {
const url = new URL(input instanceof Request ? input.url : String(input));
if (url.origin === new URL(base).origin) {
const headers = new Headers(
init?.headers ||
(input instanceof Request ? input.headers : undefined),
);
headers.set("Authorization", "Bearer " + token);
return original(input, { ...init, headers });
}
return original(input, init);
};
await import(pathToFileURL(resolve(script)).href);
return;
}
if (command !== "verify" && command !== "exercise")
throw Error(
"Usage: init | qr | qr-png | rotate --confirm | token | run <script> | verify | exercise",
);
const request = (path: string, init: RequestInit = {}) =>
fetch(base + path, { ...init, redirect: "manual" });
const buildId = readFileSync(
join(__dirname, ".next/BUILD_ID"),
"utf8",
).trim();
const routes = Object.keys(
JSON.parse(
readFileSync(join(__dirname, ".next/prerender-manifest.json"), "utf8"),
).routes,
) as string[];
for (const path of [
"/",
"/docs/00-endor-concepts-draft",
"/figures/endor-architecture.svg",
"/figures/endor-review-gate.webp",
"/figures/endor-review-desk.webp",
"/docs/pretext/01-chapter",
"/materials/pretext/verify.cjs",
"/materials/pretext/results.chrome156.json",
`/_next/data/${buildId}/index.json`,
`/_next/data/${buildId}/docs/00-endor-concepts-draft.json`,
]) {
const response = await request(path);
assert.equal(response.status, 307, path);
assert.ok(
isLoginRedirect(
response.headers.get("location") ||
response.headers.get("x-nextjs-redirect"),
base,
),
path,
);
}
for (const path of ["/api/private", "/api/unknown", "/api/search"])
assert.equal((await request(path)).status, 401, path);
assert.equal(
(
await request("/api/private", {
headers: { "x-middleware-subrequest": "proxy:proxy:proxy:proxy:proxy" },
})
).status,
401,
);
const login = await request("/login");
assert.equal(login.status, 200);
const loginHtml = await login.text();
assert.ok(loginHtml.includes("输入动态码"));
assert.ok(!loginHtml.includes(config.secret));
assert.deepEqual(await (await request("/api/health")).json(), {
status: "ok",
});
const headers = {
Authorization: "Bearer " + signSession(config, undefined, 600),
};
const searchResponse = await request("/api/search", { headers });
assert.equal(searchResponse.status, 200);
const searchIndex = await searchResponse.json();
assert.ok(searchIndex.some((doc: { slug: string; searchText: string }) => doc.slug === "source-studies/pretext/measurement-cache" && doc.searchText.includes("getSegmentMetrics")));
for (const doc of searchIndex as { slug: string; sourceSlug: string }[]) {
assert.ok(!doc.slug.split("/").some((part) => /^\d+-/.test(part)), doc.slug);
const legacy = await request(`/docs/${doc.sourceSlug}?from=legacy`, { headers });
assert.equal(legacy.status, 308, doc.sourceSlug);
const destination = new URL(legacy.headers.get("location")!, base);
assert.equal(destination.pathname, `/docs/${doc.slug}`);
assert.equal(destination.searchParams.get("from"), "legacy");
assert.ok(legacy.headers.get("cache-control")?.includes("no-store"));
}
let home = "";
let purpose = "";
for (const route of routes) {
const response = await request(route, { headers });
assert.equal(response.status, 200, route);
const html = await response.text();
if (route === "/") home = html;
if (route === "/docs/endor/endor-purpose-draft") purpose = html;
if (route.startsWith("/docs/")) {
assert.ok(html.includes('aria-label="文档选择"'), route);
assert.ok(html.includes('<optgroup label="语言与运行时">'), route);
assert.ok(!html.includes('class="nav-back"'), route);
const ids = [...html.matchAll(/<h[23] id="([^"]+)"/g)].map((match) => match[1]);
assert.equal(new Set(ids).size, ids.length, `Unique heading anchors: ${route}`);
for (const id of ids) assert.ok(html.includes(`href="#${id}"`), `TOC anchor: ${route}#${id}`);
}
}
assert.ok(home.includes("资料库"));
assert.ok(home.includes("/docs/source-studies/pretext/overview"));
// Inline preview and zoom dialog must both use the compressed browser asset.
assert.equal(
[...purpose.matchAll(/<img\b[^>]*\bsrc="\/figures\/endor-review-gate\.webp"/g)].length,
2,
"The comic preview and lightbox must use WebP",
);
assert.ok(purpose.includes('aria-label="上一张图片"'));
assert.ok(purpose.includes('aria-label="下一张图片"'));
assert.ok(purpose.includes('aria-label="显示第 1 张图片" aria-current="true"'));
assert.ok(purpose.includes('aria-label="显示第 2 张图片"'));
assert.equal(
[...purpose.matchAll(/<img\b[^>]*\bsrc="\/figures\/endor-review-desk\.webp"/g)].length,
0,
"The second slide should not download until selected",
);
for (const name of ["endor-review-gate", "endor-review-desk"]) {
assert.ok(!purpose.includes(`/figures/${name}.png`));
const comic = await request(`/figures/${name}.webp`, { headers });
assert.equal(comic.status, 200);
assert.match(comic.headers.get("content-type") || "", /image\/webp/);
assert.deepEqual(
Buffer.from(await comic.arrayBuffer()),
readFileSync(join(__dirname, `public/figures/${name}.webp`)),
);
assert.equal((await request(`/figures/${name}.png`, { headers })).status, 404);
}
const assets = new Set(
[...home.matchAll(/(?:src|href)="(\/_next\/static\/[^" ]+)"/g)].map((m) =>
m[1].replaceAll("&amp;", "&"),
),
);
assert.ok(assets.size);
for (const path of assets)
assert.equal((await request(path, { headers })).status, 200, path);
for (const name of ["verify.cjs", "results.chrome156.json"]) {
const response = await request("/materials/pretext/" + name, { headers });
assert.equal(response.status, 200, name);
assert.ok((await response.text()).length > 100);
}
for (const name of readdirSync(join(__dirname, "public/figures"))) {
if (!name.endsWith(".svg")) continue;
const response = await request("/figures/" + name, { headers });
assert.equal(response.status, 200, name);
assert.match(response.headers.get("content-type") || "", /image\/svg\+xml/);
}
assert.equal(
(await request("/docs/this-document-does-not-exist", { headers })).status,
404,
);
assert.equal(
(
await request("/", {
headers: { Authorization: headers.Authorization + "x" },
})
).status,
307,
);
if (command === "exercise") {
const origin = new URL(base).origin;
const postCode = (code: string, site = origin) =>
request("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json", Origin: site },
body: JSON.stringify({ code }),
});
assert.equal(
(await postCode("000000", "https://untrusted.example")).status,
403,
);
const code = totp(config.secret, Math.floor(Date.now() / 30000));
const results = await Promise.all([postCode(code), postCode(code)]);
assert.deepEqual(results.map((r) => r.status).sort(), [200, 401]);
const cookie = results
.find((r) => r.status === 200)!
.headers.get("set-cookie")!;
assert.match(cookie, /HttpOnly/);
assert.match(cookie, /SameSite=Lax/);
assert.match(cookie, /Max-Age=604800/);
const session = cookie.split(";")[0];
assert.equal(
(await request("/", { headers: { Cookie: session } })).status,
200,
);
const logout = await request("/api/auth/logout", {
method: "POST",
headers: { Cookie: session, Origin: origin },
});
assert.equal(logout.status, 200);
assert.match(logout.headers.get("set-cookie")!, /Max-Age=0/);
for (let i = 0; i < 21; i++) await postCode("invalid");
assert.equal((await postCode(code)).status, 429);
}
console.log(
`Authentication ${command} passed: protected routes/data/images, signed sessions, all documents, assets, SVG and 404; no secrets displayed`,
);
}
main().catch((error) => {
console.error(
error instanceof Error ? error.message : "Authentication command failed",
);
process.exitCode = 1;
});