把散落在代码里的真实部署值统一收进 config/,代码改为引用配置, 使仓库副本/开源导出不再带出生产域名、IP、内网路径与凭据。 新增 config/:platform.env.example(模板)· load.sh(shell 加载器)· index.cjs(node 加载器)· README.md(键一览与优先级)。 真实值放 config/platform.env —— 已 .gitignore 排除,不入库、不进导出。 TS 侧新增 src/platform-paths.ts 作部署路径的唯一解析处(零副作用): platformDir/stateDir/backupDir/artifactDir/installDir/scriptPath。 config.ts 接入这些字段;内置中继种子由生产 URL 改为空(改由 DSHS_OVERLAY_BOOTSTRAP_SEEDS 提供)。修掉 5 处硬编码绝对路径, src/** 注释中性化 116 行/53 文件。 scripts/** 36 个内部运维脚本:真令牌/PG 口令/隧道目标/主机号/路径 一律改从配置取;web/wake.html 的注册域白名单改为运行时从 location.hostname 推导;test/** 夹具 119 行/13 文件改 RFC 2606/5737 保留值,并把「内置种子必须为空」固化为回归断言。 取证:tsc 0 错;npm test 373/375(唯一失败 lease 属既有); 全仓扫描(大小写不敏感)代码面涉密标识 = 0;已部署 47 并零回归 (/opt/dsh/* 未搬家,/var/lib/dshs/platform 未被误建)。
128 lines
6.0 KiB
JavaScript
128 lines
6.0 KiB
JavaScript
#!/usr/bin/env node
|
||
const cfg = require('../config/index.cjs')
|
||
/**
|
||
* ensure-anysearch-pool.mjs —— 把 AnySearch 插件投放进「功能插件」候选池(档案 64 / 65)
|
||
*
|
||
* 与门户 `POST /api/plugins/business`(admin 上传)走**同一条校验路径**:复用平台编译产物
|
||
* 导出的 `stageTgzArchive()`(列成员防路径穿越 → 解压 → `package.json` 校验 → 危险内容扫描),
|
||
* 再按同一「替换策略」(同名先删旧 tgz、旧文件无残留)落盘进 `<dataRoot>/business-plugins/`,
|
||
* 并 upsert `business_plugins` 行。
|
||
*
|
||
* 投放完成后,用户在实例「设置 → 功能管理」里自助**启用/禁用**;启用/禁用会由
|
||
* `syncWebProviderPatch()`(档案 65)自动维护 profile 的 web provider 托管段。
|
||
*
|
||
* 为什么需要它:门户上传需要浏览器的 admin 会话;本脚本用于无会话场景(如本次迁移),
|
||
* 产物与走门户完全一致。
|
||
*
|
||
* 用法:
|
||
* node scripts/ensure-anysearch-pool.mjs # 干跑(只打印)
|
||
* node scripts/ensure-anysearch-pool.mjs --apply # 执行投放
|
||
*/
|
||
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs'
|
||
import { join } from 'node:path'
|
||
import Database from 'better-sqlite3'
|
||
|
||
const DATA_ROOT = cfg.dataRoot()
|
||
const ART_DIR = cfg.artifactDir()
|
||
const POOL_DIR = join(DATA_ROOT, 'business-plugins')
|
||
const DB_PATH = join(DATA_ROOT, 'dshs.db')
|
||
const PLUGIN_ROUTES = cfg.installPath('lib', 'web', 'routes', 'business-plugins.js')
|
||
const PREFIX = 'anysearch-dsh-'
|
||
|
||
const APPLY = process.argv.includes('--apply')
|
||
|
||
/** 取产物目录里版本号最大的 anysearch-dsh-*.tgz。 */
|
||
function pickArtifact() {
|
||
const cands = readdirSync(ART_DIR).filter((f) => f.startsWith(PREFIX) && f.endsWith('.tgz'))
|
||
if (cands.length === 0) throw new Error(`no ${PREFIX}*.tgz under ${ART_DIR}`)
|
||
const ver = (f) => f.slice(PREFIX.length, -4).split('.').map(Number)
|
||
cands.sort((a, b) => {
|
||
const va = ver(a); const vb = ver(b)
|
||
for (let i = 0; i < 3; i++) { const x = va[i] || 0; const y = vb[i] || 0; if (x !== y) return x - y }
|
||
return 0
|
||
})
|
||
return join(ART_DIR, cands[cands.length - 1])
|
||
}
|
||
|
||
const artifact = pickArtifact()
|
||
const size = statSync(artifact).size
|
||
console.log(`artifact = ${artifact}`)
|
||
console.log(`size = ${size} B`)
|
||
console.log(`pool dir = ${POOL_DIR}`)
|
||
console.log(`mode = ${APPLY ? 'APPLY' : 'DRY-RUN'}`)
|
||
|
||
if (!APPLY) {
|
||
console.log('\n(干跑结束;加 --apply 执行)')
|
||
process.exit(0)
|
||
}
|
||
|
||
// 1) 走平台自己的校验 + staging(与门户上传完全一致)
|
||
const { stageTgzArchive } = await import(PLUGIN_ROUTES)
|
||
const staged = stageTgzArchive(DATA_ROOT, readFileSync(artifact))
|
||
console.log(`staged = name=${staged.name} version=${staged.version} tgz=${staged.tgzName} files=${staged.fileCount}`)
|
||
|
||
// 1b) 安全检测裁决 —— 与上传接口同一套语义:**默认 fail-closed**,只有显式声明信任才放行。
|
||
const TRUST = process.argv.includes('--trust')
|
||
if (staged.blocked.length > 0) {
|
||
console.log(`\n⚠ 安全检测命中 P0 规则 ${staged.blocked.length} 处:`)
|
||
for (const b of staged.blocked) console.log(` - ${b.file} — ${b.why}`)
|
||
if (!TRUST) {
|
||
rmSync(staged.stage, { recursive: true, force: true })
|
||
console.log('\n默认拒绝投放(fail-closed)。逐条确认确属误报 / 风险可控后,加 --trust 重新执行;')
|
||
console.log('放行会写入 audit_log(trust_business_plugin),留痕「谁 / 何时 / 命中什么 / 理由」。')
|
||
process.exit(2)
|
||
}
|
||
console.log(' (--trust 已声明 → 继续投放并留痕)')
|
||
} else {
|
||
console.log('scan = clean(无 P0 命中)')
|
||
}
|
||
if (staged.warnings.length > 0) console.log(`scan = ${staged.warnings.length} 条 P1 告警(不阻断)`)
|
||
|
||
// 2) 替换策略落盘
|
||
mkdirSync(POOL_DIR, { recursive: true, mode: 0o755 })
|
||
const db = new Database(DB_PATH)
|
||
const existing = db.prepare('SELECT id, tgz_path FROM business_plugins WHERE id = ?').get(staged.name)
|
||
if (existing !== undefined && existsSync(existing.tgz_path)) {
|
||
rmSync(existing.tgz_path, { force: true })
|
||
console.log(`replaced = 已删除旧包 ${existing.tgz_path}`)
|
||
}
|
||
const dest = join(POOL_DIR, staged.tgzName)
|
||
renameSync(join(staged.stage, 'payload.tgz'), dest)
|
||
|
||
// 3) upsert 元数据行
|
||
const now = Date.now()
|
||
const admin = db.prepare("SELECT id FROM users WHERE username = 'admin'").get()
|
||
const uploadedBy = admin?.id ?? null
|
||
if (existing !== undefined) {
|
||
db.prepare(
|
||
'UPDATE business_plugins SET name=?, description=?, version=?, tgz_path=?, file_size=?, uploaded_by=?, updated_at=? WHERE id=?',
|
||
).run(staged.name, staged.name, staged.description, staged.version, dest, size, uploadedBy, now, staged.name)
|
||
} else {
|
||
db.prepare(
|
||
'INSERT INTO business_plugins (id, name, description, version, tgz_path, file_size, uploaded_by, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)',
|
||
).run(staged.name, staged.name, staged.description, staged.version, dest, size, uploadedBy, now, now)
|
||
}
|
||
// 留痕:与上传接口同一组 action(命中 P0 时另记一条 trust_business_plugin)
|
||
db.prepare('INSERT INTO audit_log (ts, actor, action, detail) VALUES (?, ?, ?, ?)').run(
|
||
now,
|
||
uploadedBy,
|
||
'upload_business_plugin',
|
||
JSON.stringify({ name: staged.name, version: staged.version, trustedOverride: staged.blocked.length > 0, via: 'ensure-anysearch-pool.mjs' }),
|
||
)
|
||
if (staged.blocked.length > 0) {
|
||
db.prepare('INSERT INTO audit_log (ts, actor, action, detail) VALUES (?, ?, ?, ?)').run(
|
||
now,
|
||
uploadedBy,
|
||
'trust_business_plugin',
|
||
JSON.stringify({ name: staged.name, version: staged.version, blocked: staged.blocked, reason: (process.env.TRUST_REASON ?? '').trim() }),
|
||
)
|
||
}
|
||
const rows = db.prepare('SELECT id, version, file_size, tgz_path FROM business_plugins ORDER BY id ASC').all()
|
||
db.close()
|
||
|
||
rmSync(staged.stage, { recursive: true, force: true })
|
||
|
||
console.log(`\n✓ 已投放:${dest}`)
|
||
console.log('候选池当前内容:')
|
||
for (const r of rows) console.log(` - ${r.id} @ ${r.version} (${r.file_size} B) → ${r.tgz_path}`)
|