canister-operator-full-export.mjs
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