canister-operator-full-export.mjs
142 lines 5.4 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 #!/usr/bin/env node
2 /**
3 * Operator full logical backup: all user ids (paginated index) + per-user notes and proposals.
4 * Requires hub canister with operator secret set: `admin_set_operator_export_secret` (controllers).
5 *
6 * Env:
7 * KNOWTATION_OPERATOR_EXPORT_URL — hub base URL, no trailing slash (or KNOWTATION_CANISTER_URL / KNOWTATION_CANISTER_BACKUP_URL)
8 * KNOWTATION_OPERATOR_EXPORT_KEY — must match canister stable secret (same value sent as X-Operator-Export-Key)
9 * KNOWTATION_CANISTER_BACKUP_ENCRYPT_KEY_HEX — optional; if set, writes .json.enc
10 * KNOWTATION_CANISTER_BACKUP_S3_BUCKET, AWS_*, KNOWTATION_CANISTER_BACKUP_S3_PREFIX — optional S3
11 * KNOWTATION_CANISTER_BACKUP_SKIP_S3=1 — skip S3
12 * KNOWTATION_OPERATOR_EXPORT_DIR — output directory (default backups)
13 *
14 * @see docs/OPERATOR-BACKUP.md
15 */
16 import fs from 'node:fs';
17 import path from 'node:path';
18 import { fileURLToPath } from 'node:url';
19 import dotenv from 'dotenv';
20 import { resolveBackupS3Prefix, resolveCanisterBackupBaseUrl } from '../lib/canister-export-env.mjs';
21 import {
22 buildFullOperatorExportJson,
23 encryptOperatorBackupUtf8,
24 putS3Object,
25 utcBackupStamp,
26 } from '../lib/operator-full-export.mjs';
27
28 const __dirname = path.dirname(fileURLToPath(import.meta.url));
29 const repoRoot = path.resolve(__dirname, '..');
30 const envPath = path.join(repoRoot, '.env');
31 if (fs.existsSync(envPath)) {
32 dotenv.config({ path: envPath });
33 }
34
35 const operatorKey = (process.env.KNOWTATION_OPERATOR_EXPORT_KEY ?? '').trim();
36 if (!operatorKey) {
37 console.error('ERROR: KNOWTATION_OPERATOR_EXPORT_KEY is required.');
38 process.exit(1);
39 }
40
41 const explicitUrl =
42 (process.env.KNOWTATION_OPERATOR_EXPORT_URL ?? '').trim() ||
43 (process.env.KNOWTATION_CANISTER_URL ?? '').trim() ||
44 (process.env.KNOWTATION_CANISTER_BACKUP_URL ?? '').trim();
45 const baseUrl = explicitUrl
46 ? explicitUrl.replace(/\/$/, '')
47 : resolveCanisterBackupBaseUrl({ ...process.env, KNOWTATION_CANISTER_BACKUP_USER_ID: 'x' }, repoRoot);
48 if (!baseUrl) {
49 console.error(
50 'ERROR: Set KNOWTATION_OPERATOR_EXPORT_URL (or KNOWTATION_CANISTER_URL / KNOWTATION_CANISTER_BACKUP_URL), or hub/icp/canister_ids.json for default hub URL.',
51 );
52 process.exit(1);
53 }
54
55 const rawDir = (process.env.KNOWTATION_OPERATOR_EXPORT_DIR ?? process.env.KNOWTATION_CANISTER_BACKUP_DIR ?? 'backups')
56 .trim() || 'backups';
57 const outDir = path.isAbsolute(rawDir) ? rawDir : path.resolve(repoRoot, rawDir);
58 fs.mkdirSync(outDir, { recursive: true });
59
60 const keyHex = (process.env.KNOWTATION_CANISTER_BACKUP_ENCRYPT_KEY_HEX ?? '').trim();
61 if (keyHex) {
62 const k = Buffer.from(keyHex, 'hex');
63 if (k.length !== 32) {
64 console.error(
65 'ERROR: KNOWTATION_CANISTER_BACKUP_ENCRYPT_KEY_HEX must be exactly 64 hex characters (32-byte AES key).',
66 `Decoded length is ${k.length} bytes (hex string length ${keyHex.length}).`,
67 'Generate with: openssl rand -hex 32',
68 );
69 process.exit(1);
70 }
71 }
72
73 const s3Bucket = (process.env.KNOWTATION_CANISTER_BACKUP_S3_BUCKET ?? '').trim();
74 const s3Prefix = resolveBackupS3Prefix(process.env);
75 const skipS3 = (process.env.KNOWTATION_CANISTER_BACKUP_SKIP_S3 ?? '').trim() === '1';
76
77 const stamp = utcBackupStamp();
78 const baseName = `operator-full-export-${stamp}`;
79
80 async function run() {
81 const urlSource = explicitUrl
82 ? 'KNOWTATION_OPERATOR_EXPORT_URL or CANISTER_* URL env'
83 : 'hub/icp/canister_ids.json (raw.icp0.io)';
84 console.log(`==> Full operator export — hub ${baseUrl} (from ${urlSource})`);
85 console.log(`==> Output directory: ${outDir}`);
86
87 const payload = await buildFullOperatorExportJson(baseUrl, operatorKey, console.log);
88 const json = JSON.stringify(payload);
89
90 let outBuf;
91 let outFile;
92 if (keyHex) {
93 outBuf = encryptOperatorBackupUtf8(json, keyHex);
94 outFile = `${baseName}.json.enc`;
95 console.log(` Encrypted ${json.length} bytes JSON → ${outBuf.length} bytes (${outFile})`);
96 } else {
97 outBuf = Buffer.from(json, 'utf8');
98 outFile = `${baseName}.json`;
99 console.log(` Wrote ${outBuf.length} bytes (${outFile})`);
100 }
101
102 const outPath = path.join(outDir, outFile);
103 fs.writeFileSync(outPath, outBuf);
104
105 if (s3Bucket && !skipS3) {
106 const key = `${s3Prefix}${outFile}`;
107 console.log(` S3: s3://${s3Bucket}/${key}`);
108 await putS3Object({
109 bucket: s3Bucket,
110 key,
111 body: outBuf,
112 region: process.env.AWS_REGION,
113 });
114 }
115
116 console.log(`canister-operator-full-export: OK (${outPath})`);
117 }
118
119 try {
120 await run();
121 } catch (err) {
122 const msg = err && typeof err.message === 'string' ? err.message : String(err);
123 console.error('canister-operator-full-export: FAILED');
124 console.error(msg);
125 if (err && err.stack) console.error(err.stack);
126 if (/^export \d+/.test(msg) || /^proposals list \d+/.test(msg) || /^proposal .+ \d+/.test(msg)) {
127 console.error(
128 'Hint: Per-user GET /api/v1/export and /api/v1/proposals failed. Confirm hub base URL uses https://<canister-id>.raw.icp0.io (not .icp0.io without raw).',
129 );
130 }
131 if (msg.includes('operator user index')) {
132 console.error(
133 'Hint: For 401, KNOWTATION_OPERATOR_EXPORT_KEY must match admin_set_operator_export_secret exactly.',
134 );
135 }
136 if (/S3|AWS|credentials|AccessDenied|PutObject/i.test(msg)) {
137 console.error(
138 'Hint: S3 upload failed. Check AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, bucket name, and IAM policy (s3:PutObject on prefix/*). Or set KNOWTATION_CANISTER_BACKUP_SKIP_S3=1 to skip S3.',
139 );
140 }
141 process.exit(1);
142 }
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago