From 41c29a35cb9127e94dccf456672749613179d911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Wed, 15 Jul 2026 21:34:27 +0800 Subject: [PATCH] fix: bind Gatekeeper sessions to approved actions --- .../gatekeeper/AUTHORIZATION-BOUNDARY-MAP.md | 50 ++++++++++++++++ .../core-channel/gatekeeper/DEPLOY-v3.2.md | 6 +- .../GLSV-SECURITY-VERIFICATION.hdlp | 4 +- .../gatekeeper/authorized-actions.js | 46 +++++++++++++++ .../gatekeeper/engine-v3.2-security.test.js | 13 +++++ .../core-channel/gatekeeper/engine-v3.js | 58 +++++++++---------- .../gatekeeper/session-manager.js | 9 ++- .../gatekeeper/session-manager.test.js | 22 +++---- 8 files changed, 163 insertions(+), 45 deletions(-) create mode 100644 zero-point/core-channel/gatekeeper/AUTHORIZATION-BOUNDARY-MAP.md create mode 100644 zero-point/core-channel/gatekeeper/authorized-actions.js diff --git a/zero-point/core-channel/gatekeeper/AUTHORIZATION-BOUNDARY-MAP.md b/zero-point/core-channel/gatekeeper/AUTHORIZATION-BOUNDARY-MAP.md new file mode 100644 index 0000000..317ac99 --- /dev/null +++ b/zero-point/core-channel/gatekeeper/AUTHORIZATION-BOUNDARY-MAP.md @@ -0,0 +1,50 @@ +# GLSV / Gatekeeper 3.2 授权边界与路径映射 + +更新时间:2026-07-15 + +## 修正结论 + +邮箱验证码只证明“本次已登记人格体、服务器和范围获得一次人工确认”。它不再解锁客户端提交的任意 shell 命令。Gatekeeper 只接受 `authorized-actions.js` 中登记的动作;未登记动作直接拒绝。 + +## 判断依据(可复核摘要) + +| 判断 | 依据文件 | 修正结果 | +|---|---|---| +| 人格体先发起会话,人类只提供验证码 | `zero-point/core-channel/GLSV-PERSONA-REMOTE-OPS.hdlp` | 保留 | +| 验证码不能等于任意命令执行权 | `zero-point/core-channel/gatekeeper/GLSV-SECURITY-VERIFICATION.hdlp` | 由代码强制 | +| 普通 push 不应直接部署 | `BROADCAST-TOWER.hdlp`、`server-tools/fifth-domain-sync-agent/MODULE.hdlp` | 同步 Agent 只做快进同步和回执 | +| 部署动作必须另有固定动作白名单 | `server-tools/deployment-receiver/config.example.json` | Gatekeeper 不越权代替部署接收器 | + +## 当前路径 + +```text +BROADCAST-TOWER + → GLSV / Gatekeeper v3.2 + → session/request(人格体 + 最小 scope + 已登记 action) + → 固定登记邮箱验证码 + → session/confirm + → session/exec(仍只能执行同一个 action) + → 只读结果与回执 + +第五域 main push + → Forgejo 签名 webhook /forgejo/sync + → fifth-domain-sync-agent + → 仓库、分支、SHA、导航守卫核验 + → 快进同步 + 服务器本地回执 + +需要部署、重启、迁移 + → deployment/requests/*.json + → deployment-receiver 固定动作白名单 + → 另行授权,不由普通 push 或 Gatekeeper 任意执行 +``` + +## 已登记动作 + +- `inspect-gatekeeper`:只读检查 Gatekeeper。 +- `sync-status`:只读检查第五域受控工作副本状态。 + +部署、重启、迁移目前没有登记为 Gatekeeper 动作,因此会被拒绝。这是有意的安全边界,不是功能缺失。 + +## 端口规则 + +Gatekeeper 兼容实现默认使用 3911;服务器实际端口仍以受控健康检查回执为准。3982 属于第五域同步 Agent,3981 属于部署接收器,三者不能互相替代。 diff --git a/zero-point/core-channel/gatekeeper/DEPLOY-v3.2.md b/zero-point/core-channel/gatekeeper/DEPLOY-v3.2.md index edf084f..2307720 100644 --- a/zero-point/core-channel/gatekeeper/DEPLOY-v3.2.md +++ b/zero-point/core-channel/gatekeeper/DEPLOY-v3.2.md @@ -26,7 +26,11 @@ curl -fsS http://127.0.0.1:3911/health ``` -5. 确认健康接口返回 `version: 3.2.0`。 +5. 确认健康接口返回 `version: 3.2.0`,并确认 Gatekeeper 与第五域同步 Agent(3982)、部署接收器(3981)分开核验。 + +## 动作边界 + +验证码流程只接受 `authorized-actions.js` 中的固定 action,不接受客户端传入 shell 命令。升级后旧的未绑定 action 会话必须重新授权;部署、重启和迁移仍需走 `deployment/requests/*.json` 与 deployment-receiver 的独立白名单。 ## 回滚 diff --git a/zero-point/core-channel/gatekeeper/GLSV-SECURITY-VERIFICATION.hdlp b/zero-point/core-channel/gatekeeper/GLSV-SECURITY-VERIFICATION.hdlp index 89e3943..e96e870 100644 --- a/zero-point/core-channel/gatekeeper/GLSV-SECURITY-VERIFICATION.hdlp +++ b/zero-point/core-channel/gatekeeper/GLSV-SECURITY-VERIFICATION.hdlp @@ -50,4 +50,6 @@ GLSV 不接受新文档中的任意 shell 命令。 服务凭据留在服务器保险库;仓库、聊天记录与回执不得存放 Token、验证码或密钥。 ``` -部署与兼容说明:`DEPLOY-v3.2.md`;固定动作接收器:`server-tools/deployment-receiver/`。 +部署与兼容说明:`DEPLOY-v3.2.md`;固定动作接收器:`server-tools/deployment-receiver/`;Gatekeeper 动作白名单:`authorized-actions.js`;完整判断与路径映射:`AUTHORIZATION-BOUNDARY-MAP.md`。 + +安全边界:`/auth/request`、`/auth/confirm`、`/auth/session/exec` 不接受客户端 shell 命令;必须提交已登记 action,且 action 的 scope 必须与会话范围一致。 diff --git a/zero-point/core-channel/gatekeeper/authorized-actions.js b/zero-point/core-channel/gatekeeper/authorized-actions.js new file mode 100644 index 0000000..855d330 --- /dev/null +++ b/zero-point/core-channel/gatekeeper/authorized-actions.js @@ -0,0 +1,46 @@ +"use strict"; + +const path = require("node:path"); +const { execFile } = require("node:child_process"); + +// This registry is intentionally small. Write or service actions require a +// separate reviewed entry and a matching deployment-receiver rule. +const REPO_ROOT = path.resolve(__dirname, "../../.."); +const INSPECT_SCRIPT = path.join(REPO_ROOT, "server-tools/deployment-receiver/scripts/inspect-gatekeeper.js"); + +const ACTIONS = Object.freeze({ + "inspect-gatekeeper": Object.freeze({ + label: "只读检查 Gatekeeper", + scope: "system-arch", + mode: "read-only", + run: (timeout) => runFile(process.execPath, [INSPECT_SCRIPT], timeout) + }), + "sync-status": Object.freeze({ + label: "只读检查第五域同步状态", + scope: "code-repo", + mode: "read-only", + run: (timeout) => runFile("/usr/bin/git", ["-C", REPO_ROOT, "status", "--short", "--branch"], timeout) + }) +}); + +function runFile(file, args, timeout) { + return new Promise((resolve) => { + execFile(file, args, { timeout: timeout || 30000, maxBuffer: 100000 }, (err, stdout, stderr) => { + resolve({ + code: err ? (typeof err.code === "number" ? err.code : 1) : 0, + stdout: (stdout || "").slice(0, 100000), + stderr: (stderr || "").slice(0, 100000) + }); + }); + }); +} + +function getAction(action) { + return typeof action === "string" ? ACTIONS[action] || null : null; +} + +function listActions() { + return Object.entries(ACTIONS).map(([id, action]) => ({ id, label: action.label, scope: action.scope, mode: action.mode })); +} + +module.exports = { getAction, listActions, INSPECT_SCRIPT }; diff --git a/zero-point/core-channel/gatekeeper/engine-v3.2-security.test.js b/zero-point/core-channel/gatekeeper/engine-v3.2-security.test.js index 5dc2d29..54ab888 100644 --- a/zero-point/core-channel/gatekeeper/engine-v3.2-security.test.js +++ b/zero-point/core-channel/gatekeeper/engine-v3.2-security.test.js @@ -3,6 +3,7 @@ const test = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const source = fs.readFileSync(__dirname + "/engine-v3.js", "utf8"); +const actions = require(__dirname + "/authorized-actions.js"); test("direct email bypass is removed", () => { assert.doesNotMatch(source, /requestEmail|email_mode:\s*requestEmail|直接使用,不查白名单/); @@ -17,3 +18,15 @@ test("Zhulan and Canger identities are registered", () => { assert.match(source, /PTS-VA-001-EED/); assert.match(source, /ICE-GL-CA001/); }); +test("arbitrary shell commands are not accepted by the authorization flow", () => { + assert.doesNotMatch(source, /execCmd\(body\.cmd/); + assert.doesNotMatch(source, /execCmd\(op\.cmd/); + assert.match(source, /必须指定已登记 action/); + assert.deepEqual(actions.listActions().map((item) => item.id), ["inspect-gatekeeper", "sync-status"]); + assert.equal(actions.getAction("not-registered"), null); +}); +test("Gatekeeper and sync-agent ports are separated", () => { + assert.match(source, /process\.argv\[2\] \|\| '3911'/); + const syncConfig = fs.readFileSync(__dirname + "/../../../server-tools/fifth-domain-sync-agent/config.example.json", "utf8"); + assert.match(syncConfig, /"listen_port": 3982/); +}); diff --git a/zero-point/core-channel/gatekeeper/engine-v3.js b/zero-point/core-channel/gatekeeper/engine-v3.js index 896840f..2f9aee2 100644 --- a/zero-point/core-channel/gatekeeper/engine-v3.js +++ b/zero-point/core-channel/gatekeeper/engine-v3.js @@ -22,11 +22,12 @@ ═══════════════════════════════════════════════════════════ */ const http = require('http'), fs = require('fs'), path = require('path'), - crypto = require('crypto'), { exec } = require('child_process'), + crypto = require('crypto'), os = require('os'), tls = require('tls'); const { SessionManager } = require('./session-manager'); +const { getAction, listActions } = require('./authorized-actions'); -const PORT = parseInt(process.env.ENGINE_PORT || process.env.GATEKEEPER_PORT || process.argv[2] || '3910', 10); +const PORT = parseInt(process.env.ENGINE_PORT || process.env.GATEKEEPER_PORT || process.argv[2] || '3911', 10); const DATA_DIR = path.join('/opt/zhuyuan', 'gatekeeper'); const CMD_TIMEOUT = 30000, MAX_OUTPUT = 100000; const CODE_TTL = 300; // 验证码 5 分钟过期 @@ -120,7 +121,7 @@ const SMTP = { // ═══════════════════════════════════════ // 运行时状态 // ═══════════════════════════════════════ -const pendingOps = {}; // { challenge_id: { code, expires, op_type, cmd, caller, target_email, target_name, server } } +const pendingOps = {}; // { challenge_id: { code, expires, op_type, action, caller, target_email, target_name, server } } const rateLimitMap = {}; // { ip: [timestamp, ...] } // ═══════════════════════════════════════ @@ -221,17 +222,10 @@ function fmtBytes(b) { return (b / 1073741824).toFixed(1) + ' GB'; } -function execCmd(cmd, timeout) { - return new Promise((resolve) => { - const t = timeout || CMD_TIMEOUT; - const child = exec(cmd, { timeout: t, maxBuffer: MAX_OUTPUT, shell: '/bin/bash' }, (err, stdout, stderr) => { - resolve({ - code: err ? (err.code || 1) : 0, - stdout: (stdout || '').slice(0, MAX_OUTPUT), - stderr: (stderr || '').slice(0, MAX_OUTPUT) - }); - }); - }); +function runAuthorizedAction(actionId, timeout) { + const action = getAction(actionId); + if (!action) return Promise.resolve({ code: 126, stdout: '', stderr: 'action_not_allowed' }); + return action.run(timeout); } // ═══════════════════════════════════════ @@ -541,12 +535,15 @@ const server = http.createServer(async (req, res) => { cleanExpiredOps(); const sourceServer = body.source_server || identity.server || SERVER_ID; const scopes = Array.isArray(body.scopes) ? [...new Set(body.scopes)] : ['code-repo']; + const action = getAction(body.action); + if (!action) { jr(res, 400, { error: '必须指定已登记 action', available: listActions() }); return; } + if (!scopes.includes(action.scope)) { jr(res, 400, { error: 'action 与 scopes 不匹配', required_scope: action.scope }); return; } if (!scopes.length || scopes.some(s => !OP_TYPES[s])) { jr(res, 400, { error: '无效 scopes', available: Object.keys(OP_TYPES) }); return; } const wlResult = checkWhitelist(sourceServer, identity.pid); if (!wlResult.ok) { jr(res, 403, { error: wlResult.reason }); return; } const triad = wlResult.triad; const code = generateCode(), challengeId = crypto.randomBytes(16).toString('hex'); - pendingOps[challengeId] = { code, expires: Date.now()/1000 + CODE_TTL, is_session: true, scopes, caller: identity, server: sourceServer, target_email: triad.email, target_name: triad.human }; + pendingOps[challengeId] = { code, expires: Date.now()/1000 + CODE_TTL, is_session: true, action: body.action, scopes, caller: identity, server: sourceServer, target_email: triad.email, target_name: triad.human }; const sent = await sendVerificationEmail(code, scopes[0], identity.name, identity.pid, '开启工作会话 · 范围: ' + scopes.join(', '), triad.email, triad.human, SERVER_ID); jr(res, 200, { ok:true, challenge_id:challengeId, email_sent:sent, email_mode:'whitelist', scopes, expires_in:CODE_TTL, message:'验证码已发送到固定登记邮箱' }); return; @@ -559,7 +556,7 @@ const server = http.createServer(async (req, res) => { if (op.caller.pid !== identity.pid) { jr(res, 403, { error:'会话身份不匹配' }); return; } if (op.code !== String(body.code || '')) { jr(res, 403, { error:'验证码错误' }); return; } delete pendingOps[body.challenge_id]; - const created = sessions.create(identity, op.server, op.scopes); + const created = sessions.create(identity, op.server, op.scopes, [op.action]); log('INFO','session_started','pid='+identity.pid+' scopes='+op.scopes.join(',')); jr(res,200,{ok:true,session_token:created.token,scopes:op.scopes,expires_in:created.expires_in,idle_timeout:created.idle_timeout,message:'工作会话已开启'}); return; @@ -569,10 +566,12 @@ const server = http.createServer(async (req, res) => { const sessionToken = req.headers['x-session-token'] || body.session_token || ''; const opType = body.op_type || 'code-repo'; const sourceServer = body.source_server || identity.server || SERVER_ID; - const verified = sessions.verify(sessionToken, identity, sourceServer, opType); + const action = getAction(body.action); + const verified = sessions.verify(sessionToken, identity, sourceServer, opType, body.action); if (!verified.ok) { jr(res,403,{error:verified.reason}); return; } - if (!body.cmd) { jr(res,400,{error:'缺少 cmd 参数'}); return; } - const result = await execCmd(body.cmd, body.timeout); + if (!action) { jr(res,400,{error:'必须指定已登记 action',available:listActions()}); return; } + if (action.scope !== opType) { jr(res,400,{error:'action 与 op_type 不匹配',required_scope:action.scope}); return; } + const result = await runAuthorizedAction(body.action, body.timeout); log('INFO','session_exec','pid='+identity.pid+' op='+opType+' code='+result.code); jr(res,200,{ok:true,executed:true,session:true,exit_code:result.code,stdout:result.stdout,stderr:result.stderr}); return; @@ -591,7 +590,6 @@ const server = http.createServer(async (req, res) => { cleanExpiredOps(); const opType = body.op_type || 'code-repo'; - const cmd = body.cmd || ''; const description = body.description || '未提供描述'; const sourceServer = body.source_server || identity.server || SERVER_ID; if (body.email || body.target_name) { jr(res, 400, { error: '禁止客户端指定邮箱 · 请先注册服务器白名单' }); return; } @@ -601,7 +599,9 @@ const server = http.createServer(async (req, res) => { return; } - if (!cmd) { jr(res, 400, { error: '缺少 cmd 参数' }); return; } + const action = getAction(body.action); + if (!action) { jr(res, 400, { error: '必须指定已登记 action', available: listActions() }); return; } + if (action.scope !== opType) { jr(res, 400, { error: 'action 与 op_type 不匹配', required_scope: action.scope }); return; } let targetEmail, targetName; @@ -627,7 +627,7 @@ const server = http.createServer(async (req, res) => { code: code, expires: now + CODE_TTL, op_type: opType, - cmd: cmd, + action: body.action, caller: { pid: identity.pid, name: identity.name }, target_email: targetEmail, target_name: targetName, @@ -635,7 +635,7 @@ const server = http.createServer(async (req, res) => { }; // 发送验证码邮件 - const cmdPreview = cmd.length > 200 ? cmd.slice(0, 200) + '...' : cmd; + const cmdPreview = 'action=' + body.action; const sent = await sendVerificationEmail(code, opType, identity.name, identity.pid, cmdPreview, targetEmail, targetName, SERVER_ID); log('INFO', 'auth_request', 'challenge=' + challengeId.slice(0, 16) + ' op=' + opType + ' email_sent=' + sent); @@ -645,7 +645,6 @@ const server = http.createServer(async (req, res) => { challenge_id: challengeId, email_sent: sent, email_mode: 'whitelist', - target_email: targetEmail, caller: { pid: identity.pid, name: identity.name }, op_type: opType, op_label: OP_TYPES[opType].label, @@ -692,12 +691,13 @@ const server = http.createServer(async (req, res) => { delete pendingOps[challengeId]; log('INFO', 'auth_confirm_ok', challengeId.slice(0, 16) + ' op=' + op.op_type + ' caller=' + op.caller.pid); - const result = await execCmd(op.cmd, body.timeout); + const result = await runAuthorizedAction(op.action, body.timeout); jr(res, 200, { ok: true, executed: true, op_type: op.op_type, + action: op.action, caller: op.caller, exit_code: result.code, stdout: result.stdout, @@ -775,7 +775,7 @@ const server = http.createServer(async (req, res) => { // ═══ /exec — 旧接口兼容(自动转发到验证码流程) ═══ if (route === '/exec') { - if (!body.cmd) { jr(res, 400, { error: '缺少 cmd 参数' }); return; } + if (!body.action) { jr(res, 400, { error: '缺少 action 参数' }); return; } // v3 安全策略: /exec 不再直接执行,返回提示要求走验证码流程 log('WARN', 'exec_blocked_v3', 'caller=' + identity.pid + ' 尝试绕过验证码调用 /exec'); @@ -783,9 +783,9 @@ const server = http.createServer(async (req, res) => { jr(res, 403, { error: 'Gatekeeper v3 安全策略: /exec 已停用 · 请使用分层授权流程', required_flow: 'POST /auth/request → 邮箱验证码 → POST /auth/confirm', - help: '将原有的 /exec 请求改为: ① POST /auth/request (相同 cmd + op_type) → ② 查收邮箱验证码 → ③ POST /auth/confirm (challenge_id + code)', + help: '将原有的 /exec 请求改为: ① POST /auth/request (action + op_type) → ② 查收邮箱验证码 → ③ POST /auth/confirm (challenge_id + code)', example: { - step1: 'curl -X POST http://' + SERVER_ID + ':' + PORT + '/auth/request -H "Authorization: Bearer " -H "Content-Type: application/json" -d \'{"cmd":"' + body.cmd.slice(0, 100) + '","op_type":"code-repo"}\'', + step1: 'curl -X POST http://' + SERVER_ID + ':' + PORT + '/auth/request -H "Authorization: Bearer " -H "Content-Type: application/json" -d \'{"action":"sync-status","op_type":"code-repo"}\'', step2: 'curl -X POST http://' + SERVER_ID + ':' + PORT + '/auth/confirm -H "Authorization: Bearer " -H "Content-Type: application/json" -d \'{"challenge_id":"<从step1返回>","code":"<邮箱验证码>"}\'' } }); @@ -831,7 +831,7 @@ const server = http.createServer(async (req, res) => { log('WARN', 'file_write_blocked_v3', '文件写入需要验证码授权'); jr(res, 403, { error: 'Gatekeeper v3: 文件写入需要验证码授权', - required_flow: 'POST /auth/request { cmd: "write file to ' + (body.path || '?') + '", op_type: "system-arch" } → 验证码 → /auth/confirm' + required_flow: 'POST /auth/request { action: "已登记动作", op_type: "system-arch" } → 验证码 → /auth/confirm' }); return; } diff --git a/zero-point/core-channel/gatekeeper/session-manager.js b/zero-point/core-channel/gatekeeper/session-manager.js index 854ab8b..5733a2b 100644 --- a/zero-point/core-channel/gatekeeper/session-manager.js +++ b/zero-point/core-channel/gatekeeper/session-manager.js @@ -11,20 +11,23 @@ class SessionManager { this.sessions = new Map(); this.load(); } - create(identity, server, scopes, now = Date.now() / 1000) { + create(identity, server, scopes, actions = [], now = Date.now() / 1000) { + if (typeof actions === "number") { now = actions; actions = []; } const token = crypto.randomBytes(32).toString("hex"); const hash = this.hash(token); - this.sessions.set(hash, { pid: identity.pid, name: identity.name, server, scopes: [...new Set(scopes)], created: now, last_used: now, expires: now + this.absoluteTtl }); + this.sessions.set(hash, { pid: identity.pid, name: identity.name, server, scopes: [...new Set(scopes)], actions: [...new Set(actions)], created: now, last_used: now, expires: now + this.absoluteTtl }); this.persist(); return { token, expires_in: this.absoluteTtl, idle_timeout: this.idleTtl }; } - verify(token, identity, server, scope, now = Date.now() / 1000) { + verify(token, identity, server, scope, action = "", now = Date.now() / 1000) { + if (typeof action === "number") { now = action; action = ""; } const hash = this.hash(token || ""); const session = this.sessions.get(hash); if (!session) return { ok: false, reason: "session_not_found" }; if (now > session.expires || now - session.last_used > this.idleTtl) { this.sessions.delete(hash); this.persist(); return { ok: false, reason: "session_expired" }; } if (session.pid !== identity.pid || session.server !== server) return { ok: false, reason: "session_binding_mismatch" }; if (!session.scopes.includes(scope)) return { ok: false, reason: "session_scope_denied" }; + if (!Array.isArray(session.actions) || !session.actions.includes(action)) return { ok: false, reason: "session_action_denied" }; session.last_used = now; this.persist(); return { ok: true, session }; diff --git a/zero-point/core-channel/gatekeeper/session-manager.test.js b/zero-point/core-channel/gatekeeper/session-manager.test.js index d941eaf..b19d75b 100644 --- a/zero-point/core-channel/gatekeeper/session-manager.test.js +++ b/zero-point/core-channel/gatekeeper/session-manager.test.js @@ -9,22 +9,22 @@ const zhulan = { pid: "ICE-GL-ZL-001", name: "铸澜" }; test("one approval creates a reusable bounded session", () => { const m = new SessionManager({ absoluteTtl: 100, idleTtl: 20 }); - const { token } = m.create(zhulan, "BS-SG-001", ["code-repo"], 10); - assert.equal(m.verify(token, zhulan, "BS-SG-001", "code-repo", 11).ok, true); - assert.equal(m.verify(token, zhulan, "BS-SG-001", "code-repo", 12).ok, true); + const { token } = m.create(zhulan, "BS-SG-001", ["code-repo"], ["sync-status"], 10); + assert.equal(m.verify(token, zhulan, "BS-SG-001", "code-repo", "sync-status", 11).ok, true); + assert.equal(m.verify(token, zhulan, "BS-SG-001", "code-repo", "sync-status", 12).ok, true); }); test("session is bound to identity server and scope", () => { const m = new SessionManager(); - const { token } = m.create(zhulan, "BS-SG-001", ["code-repo"], 10); - assert.equal(m.verify(token, { pid: "ICE-GL-ZY001" }, "BS-SG-001", "code-repo", 11).ok, false); - assert.equal(m.verify(token, zhulan, "OTHER", "code-repo", 11).ok, false); - assert.equal(m.verify(token, zhulan, "BS-SG-001", "system-arch", 11).ok, false); + const { token } = m.create(zhulan, "BS-SG-001", ["code-repo"], ["sync-status"], 10); + assert.equal(m.verify(token, { pid: "ICE-GL-ZY001" }, "BS-SG-001", "code-repo", "sync-status", 11).ok, false); + assert.equal(m.verify(token, zhulan, "OTHER", "code-repo", "sync-status", 11).ok, false); + assert.equal(m.verify(token, zhulan, "BS-SG-001", "system-arch", "sync-status", 11).ok, false); }); test("end signal revokes immediately", () => { const m = new SessionManager(); - const { token } = m.create(zhulan, "BS-SG-001", ["code-repo"], 10); + const { token } = m.create(zhulan, "BS-SG-001", ["code-repo"], ["sync-status"], 10); assert.equal(m.end(token, zhulan), true); - assert.equal(m.verify(token, zhulan, "BS-SG-001", "code-repo", 11).ok, false); + assert.equal(m.verify(token, zhulan, "BS-SG-001", "code-repo", "sync-status", 11).ok, false); }); test("session survives process reload without persisting plaintext token", () => { @@ -33,12 +33,12 @@ test("session survives process reload without persisting plaintext token", () => try { const now = Date.now() / 1000; const first = new SessionManager({ absoluteTtl: 100, idleTtl: 20, stateFile }); - const created = first.create(zhulan, "BS-SG-001", ["code-repo"], now); + const created = first.create(zhulan, "BS-SG-001", ["code-repo"], ["sync-status"], now); const onDisk = fs.readFileSync(stateFile, "utf8"); assert.equal(onDisk.includes(created.token), false); const reloaded = new SessionManager({ absoluteTtl: 100, idleTtl: 20, stateFile }); - assert.equal(reloaded.verify(created.token, zhulan, "BS-SG-001", "code-repo", now + 1).ok, true); + assert.equal(reloaded.verify(created.token, zhulan, "BS-SG-001", "code-repo", "sync-status", now + 1).ok, true); } finally { fs.rmSync(dir, { recursive: true, force: true }); }