deploy-account-session-identity.mjs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. import fs from 'node:fs';
  2. import path from 'node:path';
  3. import crypto from 'node:crypto';
  4. import {fileURLToPath} from 'node:url';
  5. const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
  6. const hash = value => crypto.createHash('sha256').update(value).digest('hex');
  7. const block = text => {
  8. const start = text.indexOf(' async function resolve(ownerId, key, account');
  9. const end = text.indexOf(' const originalSave =', start);
  10. if (start < 0 || end < 0) throw Error('SESSION_IDENTITY_BLOCK_MISSING');
  11. return text.slice(start, end).replaceAll('\r\n', '\n');
  12. };
  13. const endpoint = {id: 'yKpUay6RDG', name: 'ksfStartWorkshopSession',
  14. beforeSha256: 'c3791b03e1183bdb1373569be0c0ebf99fe4c3b22e4261618bacb379278900c0',
  15. firstFixSha256: 'd843f6380d9b871f89bbaff8d687c5ec91493d4fc859bec263b12f34628372f3',
  16. afterSha256: '1db6ea3a59edb6ea842b3927ed67c3874f1b4fae58b6dfc816eb8816e782a0a4'};
  17. export function sessionIdentityFunction(code) {
  18. const previous = fs.readFileSync(path.join(root, 'deploy/account-binding-short-links-20261112/previous-account-binding.mjs'));
  19. if (hash(previous) !== 'd02811b864e8cc75e5e061c9120ac769c09af8cb3b421e9c7757c723841dc2b4') throw Error('SESSION_IDENTITY_BASELINE_CHANGED');
  20. const firstFix = fs.readFileSync(path.join(root, 'deploy/account-binding-short-links-20261112/session-identity-first-fix.txt'), 'utf8');
  21. if (hash(firstFix) !== 'c5a99c34408b269828ff3c496b6fc22111df4d05ccefd2776dbdd237114957e9') throw Error('SESSION_IDENTITY_BASELINE_CHANGED');
  22. const candidates = [block(previous.toString('utf8')), firstFix];
  23. const matches = candidates.filter(candidate => code.split(candidate).length === 2);
  24. if (matches.length !== 1) throw Error('SESSION_IDENTITY_BLOCK_MISMATCH');
  25. const before = matches[0];
  26. const next = block(fs.readFileSync(path.join(root, 'server/src/account-binding.mjs'), 'utf8'));
  27. const patched = code.replace(before, next);
  28. new Function(patched);
  29. return patched;
  30. }
  31. export async function deployAccountSessionIdentity({request, apply = false, outDir = path.join(root, 'work/chat-single-reset-20261112')}) {
  32. const row = await request('GET', '/classes/Function/' + endpoint.id);
  33. if (row.name !== endpoint.name || row.path !== '/' + endpoint.name) throw Error('SESSION_IDENTITY_ENDPOINT_MISMATCH');
  34. if (hash(row.code) === endpoint.afterSha256) return {ok: true, applied: false, alreadyApplied: true, endpoint: endpoint.name, afterSha256: endpoint.afterSha256};
  35. if (![endpoint.beforeSha256,endpoint.firstFixSha256].includes(hash(row.code))) throw Error('UNKNOWN_SESSION_IDENTITY_FUNCTION_VERSION');
  36. const code = sessionIdentityFunction(row.code);
  37. if (hash(code) !== endpoint.afterSha256) throw Error('SESSION_IDENTITY_PAYLOAD_CHANGED');
  38. const receipt = {ok: true, applied: false, endpoint: endpoint.name, objectId: endpoint.id, beforeSha256: hash(row.code), afterSha256: hash(code), schemaChanges: 0, permissionsChanged: false};
  39. fs.mkdirSync(outDir, {recursive: true});
  40. fs.writeFileSync(path.join(outDir, 'session-identity-deploy-plan.json'), JSON.stringify(receipt, null, 2));
  41. if (!apply) return receipt;
  42. const backup = path.join(outDir, 'session-identity-function-before-' + crypto.randomUUID() + '.private.json');
  43. fs.writeFileSync(backup, JSON.stringify(row, null, 2), {flag: 'wx', mode: 0o600});
  44. const current = await request('GET', '/classes/Function/' + endpoint.id);
  45. if (current.code !== row.code || JSON.stringify(current.ACL) !== JSON.stringify(row.ACL)) throw Error('CONCURRENT_SESSION_IDENTITY_CHANGE');
  46. let written = false;
  47. try {
  48. await request('PUT', '/classes/Function/' + endpoint.id, {code}); written = true;
  49. const read = await request('GET', '/classes/Function/' + endpoint.id);
  50. if (read.code !== code || read.name !== row.name || read.path !== row.path || JSON.stringify(read.ACL) !== JSON.stringify(row.ACL)) throw Error('SESSION_IDENTITY_READBACK_MISMATCH');
  51. receipt.applied = true; receipt.readbackVerified = true; receipt.backup = backup;
  52. fs.writeFileSync(path.join(outDir, 'session-identity-deploy-receipt.json'), JSON.stringify(receipt, null, 2));
  53. return receipt;
  54. } catch (error) {
  55. receipt.ok = false; receipt.error = /^[A-Z_]+$/.test(error.message) ? error.message : 'SESSION_IDENTITY_UPDATE_FAILED';
  56. if (written) {
  57. const read = await request('GET', '/classes/Function/' + endpoint.id);
  58. if (read.code === code) {
  59. await request('PUT', '/classes/Function/' + endpoint.id, {code: row.code});
  60. receipt.rolledBack = (await request('GET', '/classes/Function/' + endpoint.id)).code === row.code;
  61. }
  62. }
  63. fs.writeFileSync(path.join(outDir, 'session-identity-deploy-failure.json'), JSON.stringify(receipt, null, 2));
  64. throw Object.assign(Error(receipt.error), {receipt});
  65. }
  66. }
  67. if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  68. if (!['plan', 'apply'].includes(process.argv[2]) || process.argv.length !== 3) throw Error('USAGE_PLAN_OR_APPLY');
  69. const {request} = await import('../server/scripts/platform-client.mjs');
  70. try { console.log(JSON.stringify(await deployAccountSessionIdentity({request, apply: process.argv[2] === 'apply'}))); }
  71. catch (error) { console.error(JSON.stringify(error.receipt || {ok: false, error: /^[A-Z_]+$/.test(error.message) ? error.message : 'SESSION_IDENTITY_DEPLOY_FAILED'})); process.exitCode = 1; }
  72. }