387a1c26e3
清理已跟踪原始日志与个人本地配置 修复前后端有效门禁、测试和构建问题 补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束 收紧图片编辑器状态、附件与媒体引用测试 排除已下线旧创作入口测试并清理 warning
165 lines
5.5 KiB
JavaScript
165 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawn } from 'node:child_process';
|
|
|
|
import {
|
|
callSpacetimeProcedureViaCli,
|
|
ensureProcedureOk,
|
|
validateSpacetimeDatabaseName,
|
|
} from '../spacetime-migration-common.mjs';
|
|
|
|
const PROCEDURE = 'rotate_editor_generation_runtime_service_identity_and_return';
|
|
|
|
function usage() {
|
|
console.error(
|
|
'用法: production-runtime-writer-identity-rotate.mjs --database <database> --server-url <http(s)://host:port> --operator-identity <64-hex> --operator-user-id <user-id> --next-writer-identity <64-hex> --confirm-next-writer-identity <64-hex> --note <audit-note>',
|
|
);
|
|
}
|
|
|
|
function readValue(argv, index, name) {
|
|
const value = argv[index + 1];
|
|
if (!value || value.startsWith('--')) {
|
|
throw new Error(`${name} 缺少参数值。`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function normalizeIdentity(value, name) {
|
|
const identity = value.trim().replace(/^0x/u, '').toLowerCase();
|
|
if (!/^[0-9a-f]{64}$/u.test(identity)) {
|
|
throw new Error(`${name} 必须是 64 位十六进制 SpacetimeDB identity。`);
|
|
}
|
|
return identity;
|
|
}
|
|
|
|
async function readCurrentCliIdentity() {
|
|
const output = await new Promise((resolve, reject) => {
|
|
const child = spawn('spacetime', ['login', 'show'], {
|
|
shell: false,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
let text = '';
|
|
child.stdout.on('data', (chunk) => {
|
|
text += chunk.toString();
|
|
});
|
|
child.stderr.on('data', (chunk) => {
|
|
text += chunk.toString();
|
|
});
|
|
child.on('error', reject);
|
|
child.on('exit', (code, signal) => {
|
|
if (signal) {
|
|
reject(new Error(`spacetime login show 被信号中断: ${signal}`));
|
|
} else if (code !== 0) {
|
|
reject(new Error(`无法读取当前 SpacetimeDB CLI identity,退出码 ${code}`));
|
|
} else {
|
|
resolve(text);
|
|
}
|
|
});
|
|
});
|
|
const identities = [...output.matchAll(/\b[0-9a-f]{64}\b/giu)].map((match) =>
|
|
match[0].toLowerCase(),
|
|
);
|
|
if (identities.length !== 1) {
|
|
throw new Error('spacetime login show 未返回唯一的当前 CLI identity。');
|
|
}
|
|
return identities[0];
|
|
}
|
|
|
|
function parseOptions(argv) {
|
|
const options = {
|
|
database: '',
|
|
serverUrl: '',
|
|
operatorIdentity: '',
|
|
operatorUserId: '',
|
|
nextWriterIdentity: '',
|
|
confirmNextWriterIdentity: '',
|
|
note: '',
|
|
passthrough: [],
|
|
};
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === '--help' || arg === '-h') {
|
|
usage();
|
|
process.exit(0);
|
|
}
|
|
if (arg === '--database') {
|
|
options.database = readValue(argv, index, arg);
|
|
} else if (arg === '--server-url') {
|
|
options.serverUrl = readValue(argv, index, arg);
|
|
} else if (arg === '--operator-identity') {
|
|
options.operatorIdentity = readValue(argv, index, arg);
|
|
} else if (arg === '--operator-user-id') {
|
|
options.operatorUserId = readValue(argv, index, arg);
|
|
} else if (arg === '--next-writer-identity') {
|
|
options.nextWriterIdentity = readValue(argv, index, arg);
|
|
} else if (arg === '--confirm-next-writer-identity') {
|
|
options.confirmNextWriterIdentity = readValue(argv, index, arg);
|
|
} else if (arg === '--note') {
|
|
options.note = readValue(argv, index, arg);
|
|
} else {
|
|
throw new Error(`未知参数: ${arg}`);
|
|
}
|
|
index += 1;
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
try {
|
|
const options = parseOptions(process.argv.slice(2));
|
|
validateSpacetimeDatabaseName(options.database);
|
|
if (!/^https?:\/\/[A-Za-z0-9.-]+(?::[0-9]{1,5})?$/u.test(options.serverUrl)) {
|
|
throw new Error('--server-url 必须是无路径、无查询参数的显式 http(s) URL。');
|
|
}
|
|
|
|
const operatorIdentity = normalizeIdentity(options.operatorIdentity, '--operator-identity');
|
|
const nextWriterIdentity = normalizeIdentity(
|
|
options.nextWriterIdentity,
|
|
'--next-writer-identity',
|
|
);
|
|
const confirmedIdentity = normalizeIdentity(
|
|
options.confirmNextWriterIdentity,
|
|
'--confirm-next-writer-identity',
|
|
);
|
|
if (nextWriterIdentity !== confirmedIdentity) {
|
|
throw new Error('--confirm-next-writer-identity 必须与 --next-writer-identity 完全一致。');
|
|
}
|
|
if (nextWriterIdentity === operatorIdentity) {
|
|
throw new Error('runtime writer identity 不能设为 migration operator identity。');
|
|
}
|
|
|
|
const currentCliIdentity = await readCurrentCliIdentity();
|
|
if (operatorIdentity !== currentCliIdentity) {
|
|
throw new Error('--operator-identity 必须与当前 SpacetimeDB CLI 登录 identity 一致。');
|
|
}
|
|
|
|
const operatorUserId = options.operatorUserId.trim();
|
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$/u.test(operatorUserId)) {
|
|
throw new Error('--operator-user-id 格式不合法。');
|
|
}
|
|
const note = options.note.trim();
|
|
if (!note || note.length > 500) {
|
|
throw new Error('--note 必填且不能超过 500 个字符。');
|
|
}
|
|
|
|
const result = await callSpacetimeProcedureViaCli(
|
|
{ database: options.database, serverUrl: options.serverUrl, passthrough: [] },
|
|
PROCEDURE,
|
|
{
|
|
next_writer_identity_hex: nextWriterIdentity,
|
|
operator_user_id: operatorUserId,
|
|
note,
|
|
},
|
|
);
|
|
ensureProcedureOk(result);
|
|
console.log(
|
|
`[runtime-writer-identity-rotation] 已完成 identity rotation: database=${options.database}, operator_identity=${operatorIdentity}, next_writer_identity=${nextWriterIdentity}; 审计记录已写入 editor_generation_runtime_identity_rotation。`,
|
|
);
|
|
} catch (error) {
|
|
console.error(
|
|
`[runtime-writer-identity-rotation] 操作失败;必须使用已授权的 migration operator CLI 登录态。${error instanceof Error ? ` ${error.message}` : String(error)}`,
|
|
);
|
|
process.exit(1);
|
|
}
|