Files
Genarrative/scripts/deploy/production-runtime-writer-identity-rotate.mjs
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

183 lines
5.6 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);
}