| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384 |
- import fs from 'node:fs';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import { fileURLToPath } from 'node:url';
- const sha = text => crypto.createHash('sha256').update(text).digest('hex');
- const fail = code => { throw Object.assign(Error(code), { code }); };
- const canonical = value => JSON.stringify(value && typeof value === 'object'
- ? Array.isArray(value) ? value.map(v => JSON.parse(canonical(v))) : Object.fromEntries(Object.keys(value).sort().map(k => [k, JSON.parse(canonical(value[k]))]))
- : value);
- /** Preserve the actual deployed function, including identity/account wrappers. Only its artifact whitelist changes. */
- export function patchPublicBoardCode(code) {
- if (typeof code !== 'string' || !code.includes('ksfGetPublicBoard')) fail('UNRECOGNIZED_PUBLIC_BOARD');
- const pattern = /(\bartifact\s*:\s*['"])(objectId\s+[^'"\r\n]+)(['"])/g;
- const matches = [...code.matchAll(pattern)];
- if (matches.length !== 1) fail('PUBLIC_ARTIFACT_WHITELIST_AMBIGUOUS');
- const fields = matches[0][2].split(/\s+/);
- if (!['rawText', 'report', 'ownerName', 'version', 'dataMode', 'dashboardState', 'workshopKey'].every(k => fields.includes(k))) fail('PUBLIC_ARTIFACT_CONTRACT_CHANGED');
- if (fields.includes('reportSeriesKey')) return code;
- const index = fields.indexOf('version');fields.splice(index + 1, 0, 'reportSeriesKey');
- const next = code.replace(pattern, (_all, prefix, _fields, suffix) => prefix + fields.join(' ') + suffix);
- new Function(next);
- return next;
- }
- export async function deployBoardSeries({ request, manifest = null, apply = false, backupDir }) {
- const rows = (await request('GET', '/classes/Function?' + new URLSearchParams({ where: JSON.stringify({ name: 'ksfGetPublicBoard' }), limit: '2' }))).results;
- if (!Array.isArray(rows) || rows.length !== 1 || rows[0].name !== 'ksfGetPublicBoard' || rows[0].path !== '/ksfGetPublicBoard') fail('PUBLIC_BOARD_IDENTITY_MISMATCH');
- const before = rows[0], next = patchPublicBoardCode(before.code);
- const plan = { schema: 'ksf.workshop-board-series.v1', functionName: before.name, objectId: before.objectId,
- beforeSha256: sha(before.code), afterSha256: sha(next), changed: next !== before.code,
- fieldAdded: 'reportSeriesKey', fullFunctionRebuild: false, businessRowsChanged: 0 };
- if (!apply) return { ok: true, readOnly: true, ...plan };
- if (!manifest || manifest.objectId !== before.objectId || manifest.afterSha256 !== plan.afterSha256
- || ![manifest.beforeSha256, manifest.afterSha256].includes(plan.beforeSha256)) fail('DEPLOYMENT_BASELINE_CHANGED');
- if (!plan.changed) return { ok: true, readOnly: true, ...plan, applied: false, alreadyDeployed: true };
- if (!path.isAbsolute(backupDir || '')) fail('PRIVATE_BACKUP_REQUIRED');
- const schemas = await request('GET', '/schemas');
- fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
- const backup = path.join(backupDir, 'public-board-before-' + crypto.randomUUID() + '.json');
- fs.writeFileSync(backup, JSON.stringify(before), { flag: 'wx', mode: 0o600 });
- const route = '/classes/Function/' + before.objectId;
- if (sha((await request('GET', route)).code) !== plan.beforeSha256) fail('CONCURRENT_FUNCTION_CHANGE');
- let attempted = false;
- try {
- attempted = true;await request('PUT', route, { code: next });
- const after = await request('GET', route);
- if (after.code !== next) fail('FUNCTION_READBACK_MISMATCH');
- for (const key of ['name', 'path', 'ACL']) if (canonical(after[key] ?? null) !== canonical(before[key] ?? null)) fail('FUNCTION_IDENTITY_OR_ACL_CHANGED');
- if (canonical(await request('GET', '/schemas')) !== canonical(schemas)) fail('SCHEMA_OR_PERMISSIONS_CHANGED');
- return { ok: true, readOnly: false, ...plan, applied: true, backup,
- readbackVerified: true, schemaAndPermissionsPreserved: true, otherFunctionBodiesModified: false };
- } catch (error) {
- let rollbackComplete = !attempted;
- if (attempted) try {
- const actual = await request('GET', route);
- if (actual.code === before.code) rollbackComplete = true;
- else if (actual.code === next) {
- await request('PUT', route, { code: before.code });
- rollbackComplete = (await request('GET', route)).code === before.code;
- }
- } catch { rollbackComplete = false; }
- error.receipt = { ok: false, ...plan, backup, rollbackComplete, errorCode: error.code || 'DEPLOYMENT_UNCERTAIN' };
- throw error;
- }
- }
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
- try {
- const args = process.argv.slice(2), apply = args.includes('--apply');
- const root = path.resolve('work/ksf-workshop-board-20261110-r1');
- const planPath = path.join(root, 'series-plan.json');
- const { request } = await import('../server/scripts/platform-client.mjs');
- const result = await deployBoardSeries({ request, apply, manifest: apply ? JSON.parse(fs.readFileSync(planPath, 'utf8')) : null,
- backupDir: path.join(root, 'private') });
- fs.mkdirSync(root, { recursive: true });
- fs.writeFileSync(apply ? path.join(root, 'series-deployment.json') : planPath, JSON.stringify(result, null, 2) + '\n');
- console.log(JSON.stringify(result));
- } catch (e) {
- console.error(JSON.stringify(e.receipt || { ok: false, errorCode: e.code || 'PUBLIC_BOARD_DEPLOYMENT_STOPPED' }));
- process.exitCode = 1;
- }
- }
|