chore: 文档库全量重新入库(2026-10-20)
build / build-and-scan (push) Canceled after 0s

This commit is contained in:
admin committed 2026-10-20 15:05:55 +08:00
commit 5ec99ce227
668 files changed
+164448

No files matched your search

+505
View File
@@ -0,0 +1,505 @@
/**
* 覆盖网络 · **邀请码发放端**(`POST/GET /api/admin/overlay-invites` + `…/revoke`)单测。
*
* 运行:`node --test test/overlay-invites.test.mjs`(测 `lib/` 产物 ⇒ 先 `npm run build`)。
* ⚠️ 本文件**不连库、不碰真文件系统、不写任何密钥文件**:
* · 会话解析 = 假 `db`(只够 `requireAdmin`);
* · 签发私钥 = **运行时生成的一次性测试密钥**(`generateAuthorityKey()`,⛔ 与生产密钥无关),
* 以**注入的 signer** 交给路由 ⇒ 不需要任何 env、更不需要落一个 PEM 文件;
* · 台账 = 内存实现(`ledger` 注入点)⇒ 不写 `<data-root>/overlay/invites`。
*
* ## 这个文件要回答的问题(= 本棒验收判据)
* | # | 判据 | 落点 |
* |---|---|---|
* | ① | **闭环**:管理员签发 ⇒ 消费端(`GET /api/overlay/zones`)能验通 ⇒ 名录真出区 | T1 |
* | ② | **非管理员 / 无凭据 ⇒ 拒**(403 / 401,三条端点全覆盖) | T2 |
* | ③ | **过期码 ⇒ 消费端不认**(具名 `expired`) | T3 |
* | ④ | **篡改载荷(网名 / 有效期 / nonce)⇒ 不认** | T4 |
* | ⑤ | **私钥没配 ⇒ 503 具名"未配置"**(⛔ 不瞎签、⛔ 无落账) | T5 |
* | ⑥ | **非法区名 / 非法有效期 ⇒ 400**(⛔ 不签废码、⛔ 无落账) | T6 |
* | ⑦ | **撤销**:占位 nonce + 列出台账状态 + 重复撤 ⇒ 409 | T7 |
* | ⑧ | **台账不含签名**(元数据不是一枚可用邀请码) | T8 |
* | ⑨ | **撤销不改变名录**(把已知边界钉住,⛔ 防止被误读成"彻底失效") | T7 |
*/
import assert from 'node:assert/strict'
import { createRequire } from 'node:module'
import { join } from 'node:path'
import { test } from 'node:test'
import Fastify from 'fastify'
import { generateAuthorityKey } from '../lib/net/relay/identity.js'
import {
INVITE_SIGNERS_ENV,
OVERLAY_INVITE_HEADER,
decodeInviteCode,
defaultInviteCheck,
overlayZoneRoutes,
} from '../lib/web/routes/overlay-zones.js'
import {
INVITE_SIGNER_KEY_ENV,
INVITE_SIGNER_KEY_FILE_ENV,
INVITE_TTL_MINUTES_DEFAULT,
INVITE_TTL_MINUTES_MAX,
buildInvite,
encodeInviteCode,
inviteLedgerDirs,
overlayInviteRoutes,
resolveInviteSigner,
} from '../lib/web/routes/overlay-invites.js'
// ── 夹具(⛔ 不连库、⛔ 不落盘、⛔ 不写密钥文件)──────────────────────────────
/** 可用用户行(`toPublicUser` 只读这五个字段)。 */
function userRow(id, role) {
return { id, username: `u-${id}`, role, created_at: 1, shared_model_granted: 0 }
}
/** 内存台账(与 `fileLedger` 同接口;`revoked` 集合就是 `consumeNonce` 在真实现里的占位目录)。 */
function memLedger() {
const records = new Map()
const consumed = new Set()
return {
dirs: { consumed: '(mem)/consumed', issued: '(mem)/invites' },
record(rec) {
records.set(rec.nonce, rec)
return { ok: true }
},
list() {
return [...records.values()].sort((a, b) => b.issuedAt.localeCompare(a.issuedAt))
},
isRevoked(nonce) {
return consumed.has(nonce)
},
revoke(nonce) {
if (consumed.has(nonce)) return { ok: false, reason: 'invite-already-used' }
consumed.add(nonce)
return { ok: true }
},
_size: () => records.size,
_consumed: consumed,
}
}
/** 一次性测试签名者(注入给路由 ⇒ 路由不需要任何 env 即可签发)。 */
function signerOf(key) {
return () => ({ ok: true, pem: key.privateKeyPem, publicKey: key.publicKey })
}
/**
* 造一个 Fastify 应用:**发放端 + 消费端同挂一个 app**(闭环用例要的正是这个)。
*
* @param opts.signer - 不传 ⇒ 走缺省 `resolveInviteSigner()`(读 env;T5 用它)。
* @param opts.now - 注入时钟(T3 的"过期"就靠"签发时刻在过去"构造,⛔ 不真等)。
*/
async function buildApp(opts = {}) {
const { role = 'admin', userId = 'admin1', signer, ledger, now, regions = ['r1', 'r2'], zonesFor } = opts
const app = Fastify()
app.decorate('db', {
findSessionWithUser: async () => ({ expiresAt: Date.now() + 60_000, user: userRow(userId, role) }),
findUserById: async () => undefined,
})
await app.register(overlayInviteRoutes, {
...(signer === undefined ? {} : { signer }),
ledger: ledger ?? memLedger(),
...(now === undefined ? {} : { now }),
})
await app.register(overlayZoneRoutes, {
source: { regionsOfMember: async (memberId) => (memberId === (zonesFor ?? userId) ? regions : []) },
})
return app
}
const COOKIE = { cookie: 'sid=test-session' }
async function issue(app, payload, headers = COOKIE) {
const res = await app.inject({ method: 'POST', url: '/api/admin/overlay-invites', headers, payload })
return { status: res.statusCode, body: res.json() }
}
async function listInvites(app, headers = COOKIE) {
const res = await app.inject({ method: 'GET', url: '/api/admin/overlay-invites', headers })
return { status: res.statusCode, body: res.json() }
}
async function revoke(app, nonce, headers = COOKIE) {
const res = await app.inject({
method: 'POST',
url: '/api/admin/overlay-invites/revoke',
headers,
payload: { nonce },
})
return { status: res.statusCode, body: res.json() }
}
/** 消费端取名录(邀请码走请求头)。 */
async function fetchZones(app, code, headers = COOKIE) {
const res = await app.inject({
method: 'GET',
url: '/api/overlay/zones',
headers: code === undefined ? headers : { ...headers, [OVERLAY_INVITE_HEADER]: code },
})
return { status: res.statusCode, body: res.json() }
}
/** 把一枚码解回 `{doc,sig}`(篡改用)。 */
function rawOf(code) {
return JSON.parse(Buffer.from(code, 'base64url').toString('utf8'))
}
function codeOf(raw) {
return Buffer.from(JSON.stringify(raw), 'utf8').toString('base64url')
}
/** 临时改 env 并在 finally 里原样还原(⛔ 不留污染)。 */
function withEnv(pairs, fn) {
const saved = new Map()
for (const [k] of pairs) saved.set(k, process.env[k])
for (const [k, v] of pairs) {
if (v === undefined) delete process.env[k]
else process.env[k] = v
}
return Promise.resolve()
.then(fn)
.finally(() => {
for (const [k, v] of saved) {
if (v === undefined) delete process.env[k]
else process.env[k] = v
}
})
}
const EMPTY = { zones: [], count: 0 }
// ── ① 闭环:签发 ⇒ 消费端验通 ────────────────────────────────────────────────
test('T1 闭环:管理员签发 ⇒ 消费端用同一对密钥验通 ⇒ 名录出区(⛔ 不是"签得出来")', async () => {
const key = generateAuthorityKey()
const app = await buildApp({ signer: signerOf(key) })
try {
const issued = await issue(app, { region: 'ops', ttlMinutes: 10 })
assert.equal(issued.status, 200, JSON.stringify(issued.body))
assert.equal(issued.body.ok, true)
assert.equal(issued.body.region, 'ops')
assert.equal(issued.body.header, OVERLAY_INVITE_HEADER)
assert.equal(issued.body.signer.publicKey, key.publicKey, '回执必须给出"要填进消费端的那把公钥"')
assert.equal('privateKeyPem' in issued.body.signer, false, '⛔ 回执里不许出现私钥')
assert.equal(JSON.stringify(issued.body).includes('PRIVATE KEY'), false, '⛔ 整份回执里不许出现私钥 PEM')
// 消费端受信集 = 签发公钥 ⇒ 名录真的出区
await withEnv([[INVITE_SIGNERS_ENV, key.publicKey]], async () => {
const got = await fetchZones(app, issued.body.code)
assert.equal(got.status, 200)
assert.deepEqual(got.body.zones, [{ region: 'r1' }, { region: 'r2' }])
// 反证:换一把不相关的受信签名者 ⇒ 同一枚码不认(证明上面的成功来自**真验签**)
await withEnv([[INVITE_SIGNERS_ENV, generateAuthorityKey().publicKey]], async () => {
const bad = await fetchZones(app, issued.body.code)
assert.equal(bad.status, 200)
assert.deepEqual(bad.body, EMPTY)
})
// 反证二:受信集**空**(未配)⇒ 一样不认(fail-closed)
await withEnv([[INVITE_SIGNERS_ENV, undefined]], async () => {
assert.deepEqual((await fetchZones(app, issued.body.code)).body, EMPTY)
})
})
} finally {
await app.close()
}
})
// ── ② 鉴权 ────────────────────────────────────────────────────────────────────
test('T2 非管理员 ⇒ 403;无凭据 ⇒ 401(三条端点全覆盖)', async () => {
const key = generateAuthorityKey()
const nonce = 'a'.repeat(32)
for (const role of ['user', 'admin']) {
const app = await buildApp({ signer: signerOf(key), role })
try {
if (role === 'user') {
assert.equal((await issue(app, { region: 'ops' })).status, 403)
assert.equal((await listInvites(app)).status, 403)
assert.equal((await revoke(app, nonce)).status, 403)
assert.deepEqual((await issue(app, { region: 'ops' })).body, { error: 'forbidden' })
} else {
assert.equal((await issue(app, { region: 'ops' })).status, 200, '同一条路对 admin 必须通')
}
// 无 cookie ⇒ 401(fail-closed,⛔ 不是"跳过校验放行")
const noAuth = {}
assert.equal((await issue(app, { region: 'ops' }, noAuth)).status, 401)
assert.equal((await listInvites(app, noAuth)).status, 401)
assert.equal((await revoke(app, nonce, noAuth)).status, 401)
} finally {
await app.close()
}
}
})
// ── ③ 过期 ────────────────────────────────────────────────────────────────────
test('T3 过期码 ⇒ 消费端不认(具名 expired;签发那一刻仍算有效,故签得出来)', async () => {
const key = generateAuthorityKey()
// 签发时钟拨到 1 小时前 + TTL 1 分钟 ⇒ 相对"现在"早已过期
const signAt = Date.now() - 3_600_000
const app = await buildApp({ signer: signerOf(key), now: () => signAt })
try {
const issued = await issue(app, { region: 'ops', ttlMinutes: 1 })
assert.equal(issued.status, 200, '签发时钟下它是有效的 ⇒ 应当签得出来')
assert.ok(Date.parse(issued.body.expiresAt) < Date.now(), '这枚码的有效期确实已过')
await withEnv([[INVITE_SIGNERS_ENV, key.publicKey]], async () => {
const got = await fetchZones(app, issued.body.code)
assert.equal(got.status, 200)
assert.deepEqual(got.body, EMPTY, '过期 ⇒ 空列表(⛔ 不降级放行)')
// 具名原因(排障面):时间到点 ⇒ expired
const verdict = defaultInviteCheck(issued.body.code, [key.publicKey])
assert.equal(verdict.ok, false)
assert.equal(verdict.reason, 'expired')
})
} finally {
await app.close()
}
})
// ── ④ 篡改 ────────────────────────────────────────────────────────────────────
test('T4 篡改载荷(网名 / 有效期 / nonce)⇒ 消费端不认', async () => {
const key = generateAuthorityKey()
const app = await buildApp({ signer: signerOf(key) })
try {
const issued = await issue(app, { region: 'ops', ttlMinutes: 10 })
assert.equal(issued.status, 200)
const good = rawOf(issued.body.code)
await withEnv([[INVITE_SIGNERS_ENV, key.publicKey]], async () => {
// ⓪ 未篡改 ⇒ 认(对照)
assert.deepEqual((await fetchZones(app, codeOf(good))).body.zones, [{ region: 'r1' }, { region: 'r2' }])
// ① 换网名(**合法**网名,仍会被签名挡下 ⇒ 证明判据覆盖载荷内容,不是"只看形状")
assert.deepEqual(
(await fetchZones(app, codeOf({ ...good, doc: { ...good.doc, network: 'anothernet' } }))).body,
EMPTY,
'改网名',
)
// ② 拉长有效期(10 年)
assert.deepEqual(
(
await fetchZones(
app,
codeOf({ ...good, doc: { ...good.doc, expiresAt: new Date(Date.now() + 315_360_000_000).toISOString() } }),
)
).body,
EMPTY,
'改有效期',
)
// ③ 换 nonce
assert.deepEqual(
(await fetchZones(app, codeOf({ ...good, doc: { ...good.doc, nonce: 'b'.repeat(32) } }))).body,
EMPTY,
'改 nonce',
)
// ④ 换签名(另一把钥匙对**同一份** doc 的签名 —— 载荷一字未改,只有签名换了)
const other = generateAuthorityKey()
const foreign = buildInvite({
network: 'ops',
ttlMinutes: 10,
pem: other.privateKeyPem,
nonce: good.doc.nonce,
now: Date.parse(good.doc.issuedAt),
})
assert.equal(foreign.ok, true)
assert.deepEqual(
(await fetchZones(app, codeOf({ doc: good.doc, sig: foreign.invite.sig }))).body,
EMPTY,
'换签名 ⇒ 不认',
)
// ⑤ 非签名者自己的钥匙签的**整枚**码 ⇒ 也不认(受信集里没有它)
assert.deepEqual((await fetchZones(app, foreign.invite.code)).body, EMPTY)
})
} finally {
await app.close()
}
})
// ── ⑤ 私钥没配 ⇒ 不瞎签 ──────────────────────────────────────────────────────
test('T5 未配私钥 ⇒ 503 invite-signer-unconfigured;同配两处 ⇒ ambiguous;路径读不到 ⇒ unreadable', async () => {
// 纯函数面(三种具名失败;⛔ 都不需要真密钥文件)
assert.deepEqual(resolveInviteSigner({}).reason, 'unconfigured')
assert.equal(
resolveInviteSigner({ [INVITE_SIGNER_KEY_ENV]: 'x', [INVITE_SIGNER_KEY_FILE_ENV]: '/no/such.pem' }).reason,
'ambiguous',
)
assert.equal(resolveInviteSigner({ [INVITE_SIGNER_KEY_FILE_ENV]: '/no/such/key.pem' }).reason, 'unreadable')
assert.equal(resolveInviteSigner({ [INVITE_SIGNER_KEY_ENV]: 'not-a-pem' }).reason, 'not-ed25519')
// 端点面:env 里什么都没配 ⇒ 503,且**台账一条都不落**(⛔ 没有"先记一笔再报错")
const ledger = memLedger()
const app = await buildApp({ ledger })
try {
await withEnv(
[
[INVITE_SIGNER_KEY_ENV, undefined],
[INVITE_SIGNER_KEY_FILE_ENV, undefined],
],
async () => {
const res = await issue(app, { region: 'ops' })
assert.equal(res.status, 503)
assert.equal(res.body.error, 'invite-signer-unconfigured')
assert.match(res.body.detail, /不瞎签|未配置/)
assert.equal('code' in res.body, false, '⛔ 失败回执里不许有一枚"签出来的码"')
assert.equal(ledger._size(), 0, '⛔ 未签成 ⇒ 不许落账')
},
)
} finally {
await app.close()
}
})
// ── ⑥ 非法入参 ⇒ 400(⛔ 不签废码)────────────────────────────────────────────
test('T6 非法区名 ⇒ 400 bad-region(⛔ 不签"永远验不通"的废码);非法 TTL ⇒ 400 bad-ttl', async () => {
const key = generateAuthorityKey()
const ledger = memLedger()
const app = await buildApp({ signer: signerOf(key), ledger })
try {
for (const region of ['华东', 'A B', 'u:', '-bad', '']) {
const res = await issue(app, { region })
assert.equal(res.status, 400, region)
assert.ok(['bad-region', 'bad-body'].includes(res.body.error), `${region} ⇒ ${res.body.error}`)
}
// region / network 同时给了不同的值 ⇒ 400(⛔ 不猜用哪个)
assert.equal((await issue(app, { region: 'ops', network: 'othernet' })).body.error, 'bad-body')
// 合法但非法 TTL
for (const ttlMinutes of [0, -1, 1.5, INVITE_TTL_MINUTES_MAX + 1, '10']) {
const res = await issue(app, { region: 'ops', ttlMinutes })
assert.equal(res.status, 400, String(ttlMinutes))
assert.equal(res.body.error, 'bad-ttl', String(ttlMinutes))
}
assert.equal(ledger._size(), 0, '⛔ 400 的请求一律不许落账')
// 合法边界:1 与上界都通;缺省 = INVITE_TTL_MINUTES_DEFAULT
for (const ttlMinutes of [1, INVITE_TTL_MINUTES_MAX]) {
const res = await issue(app, { region: 'ops', ttlMinutes })
assert.equal(res.status, 200, String(ttlMinutes))
assert.equal(res.body.ttlMinutes, ttlMinutes)
}
const dflt = await issue(app, { region: 'u:alice' })
assert.equal(dflt.body.ttlMinutes, INVITE_TTL_MINUTES_DEFAULT)
assert.equal(dflt.body.networkKind, 'tenant')
} finally {
await app.close()
}
})
// ── ⑦⑧⑨ 台账 / 撤销 ─────────────────────────────────────────────────────────
test('T7 撤销:列出台账状态 ⇒ 占位 nonce ⇒ 重复撤 409;且**不改变名录**(已知边界,⛔ 不夸大)', async () => {
const key = generateAuthorityKey()
const ledger = memLedger()
const app = await buildApp({ signer: signerOf(key), ledger, userId: 'admin9' })
try {
const issued = await issue(app, { region: 'ops', ttlMinutes: 10 })
const nonce = issued.body.invite.doc.nonce
assert.match(nonce, /^[0-9a-f]{32}$/)
assert.equal(ledger._size(), 1, '成功签发 ⇒ 落一条台账')
let l = await listInvites(app)
assert.equal(l.status, 200)
assert.equal(l.body.total, 1)
assert.equal(l.body.invites[0].nonce, nonce)
assert.equal(l.body.invites[0].network, 'ops')
assert.equal(l.body.invites[0].issuedBy, 'admin9', '台账要能回答"这枚码是谁发的"')
assert.equal(l.body.invites[0].revoked, false)
assert.equal(l.body.invites[0].expired, false)
assert.equal((await revoke(app, nonce)).status, 200)
l = await listInvites(app)
assert.equal(l.body.invites[0].revoked, true)
assert.equal(ledger._consumed.has(nonce), true, '撤销 = 占位 nonce(与 consumeNonce 同一判据)')
const again = await revoke(app, nonce)
assert.equal(again.status, 409)
assert.equal(again.body.error, 'invite-already-used-or-revoked')
// 形状不对的 nonce ⇒ 400(⛔ 不把"打错字"报成"已撤销")
assert.equal((await revoke(app, 'zz')).body.error, 'bad-nonce')
// ⑨ 已知边界:撤销**不改变**名录(名录端点只看签名与有效期)—— 钉住它,防止被误读成"彻底失效"
await withEnv([[INVITE_SIGNERS_ENV, key.publicKey]], async () => {
assert.deepEqual(
(await fetchZones(app, issued.body.code)).body.zones,
[{ region: 'r1' }, { region: 'r2' }],
'撤销只管收单/准入那条路;名录侧仍可见(说明书里必须这么写)',
)
})
} finally {
await app.close()
}
})
test('T8 台账里**没有签名**(元数据不是一枚可用邀请码)', async () => {
const key = generateAuthorityKey()
const ledger = memLedger()
const app = await buildApp({ signer: signerOf(key), ledger })
try {
assert.equal((await issue(app, { region: 'ops' })).status, 200)
const l = await listInvites(app)
const rec = l.body.invites[0]
assert.deepEqual(
Object.keys(rec).sort(),
['expired', 'expiresAt', 'issuedAt', 'issuedBy', 'network', 'nonce', 'revoked', 'version'].sort(),
)
assert.equal('sig' in rec, false)
assert.equal('code' in rec, false)
assert.equal(JSON.stringify(l.body).includes('sig'), false, '⛔ 列表里任何字段都不该携带签名')
} finally {
await app.close()
}
})
// ── 纯函数(签发本体)─────────────────────────────────────────────────────────
test('T9 buildInvite:合法区名 ⇒ 码能被消费端解码器读回;非法区名 ⇒ 拒签(bad-network)', () => {
const key = generateAuthorityKey()
const ok = buildInvite({ network: 'ops', ttlMinutes: 5, pem: key.privateKeyPem, now: Date.now(), nonce: 'c'.repeat(32) })
assert.equal(ok.ok, true)
assert.equal(ok.publicKey, key.publicKey, '签发自检要能推出与私钥成对的公钥')
assert.deepEqual(decodeInviteCode(ok.invite.code)?.doc, ok.invite.doc, '编码 ⇄ 消费端解码器必须闭环')
assert.equal(encodeInviteCode({ doc: ok.invite.doc, sig: ok.invite.sig }), ok.invite.code, '编码只有一处口径')
assert.equal(defaultInviteCheck(ok.invite.code, [key.publicKey]).ok, true)
for (const bad of ['not a net', 'A B', 'u:', '', '-x']) {
const res = buildInvite({ network: bad, ttlMinutes: 5, pem: key.privateKeyPem })
assert.equal(res.ok, false, bad)
assert.equal(res.reason, 'bad-network', bad)
}
// 签发私钥不可用(PEM 坏 / 非 ed25519)⇒ 走 `self-check-failed`,⛔ 不返回任何码
const broken = buildInvite({ network: 'ops', ttlMinutes: 5, pem: 'not-a-pem' })
assert.equal(broken.ok, false)
assert.equal(broken.reason, 'self-check-failed')
})
test('T10 台账落点与运维 CLI 同源(同一套生效配置下);⛔ 不另立第二本账', () => {
const cfg = createRequire(import.meta.url)('../config/index.cjs')
// ① 覆盖项优先(与 CLI 的 `--dir` / `DSHS_OVERLAY_REGISTRY_DIR` 同键)
assert.equal(inviteLedgerDirs({ DSHS_OVERLAY_REGISTRY_DIR: join('/tmp', 'x') }).consumed, join('/tmp', 'x', 'consumed'))
// ② 生效配置 = 系统 env(脚本侧 `get()` 的**最高优先级**,⛔ 不是"我重算一遍同样的式子"):
// 此时产品侧算出的目录与 CLI `cfg.overlayDir()` **逐字相同**
// ⇒ 本文件写下的 `consumed/<nonce>.used` 就是 CLI `apply` 读的那一个(撤销真的挡得住收单)。
const savedDir = process.env.DSHS_OVERLAY_REGISTRY_DIR
const savedRoot = process.env.DSHS_DATA_ROOT
try {
delete process.env.DSHS_DATA_ROOT
process.env.DSHS_OVERLAY_REGISTRY_DIR = join('/prod', 'ov')
assert.equal(inviteLedgerDirs().consumed, join(cfg.overlayDir(), 'consumed'))
// ③ 只有数据根(无覆盖项)时同样同源
delete process.env.DSHS_OVERLAY_REGISTRY_DIR
process.env.DSHS_DATA_ROOT = join('/prod', 'root')
assert.equal(inviteLedgerDirs().consumed, join(cfg.overlayDir(), 'consumed'))
assert.equal(inviteLedgerDirs().consumed, join('/prod', 'root', 'overlay', 'consumed'))
} finally {
if (savedDir === undefined) delete process.env.DSHS_OVERLAY_REGISTRY_DIR
else process.env.DSHS_OVERLAY_REGISTRY_DIR = savedDir
if (savedRoot === undefined) delete process.env.DSHS_DATA_ROOT
else process.env.DSHS_DATA_ROOT = savedRoot
}
})