canister-export-backup.mjs
sha256:8d46372e39d2d5a54fd93a8b1c27922fe0d9b22a72197345f1d2c71701cc4ce2
feat(auth): persistent login system + C7 session introspection
Human
minor
⚠ breaking
16 days ago
| 1 | #!/usr/bin/env node |
| 2 | /** |
| 3 | * Operator backup: canister notes + full proposals → JSON (optional AES-256-GCM, optional S3). |
| 4 | * Env matches operator notes in .env.example and canister HTTP export expectations. |
| 5 | * |
| 6 | * @see scripts/canister-export-backup.sh (invokes this file) |
| 7 | */ |
| 8 | import fs from 'node:fs'; |
| 9 | import path from 'node:path'; |
| 10 | import { fileURLToPath } from 'node:url'; |
| 11 | import dotenv from 'dotenv'; |
| 12 | import { |
| 13 | parseBackupVaultIds, |
| 14 | resolveBackupS3Prefix, |
| 15 | resolveCanisterBackupBaseUrl, |
| 16 | } from '../lib/canister-export-env.mjs'; |
| 17 | import { |
| 18 | buildOperatorVaultPayload, |
| 19 | encryptOperatorBackupUtf8, |
| 20 | fetchFullProposalsForOperatorExport, |
| 21 | fetchNotesFromExport, |
| 22 | putS3Object, |
| 23 | safeVaultFileToken, |
| 24 | utcBackupStamp, |
| 25 | } from '../lib/operator-canister-backup.mjs'; |
| 26 | |
| 27 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 28 | const repoRoot = path.resolve(__dirname, '..'); |
| 29 | const envPath = path.join(repoRoot, '.env'); |
| 30 | if (fs.existsSync(envPath)) { |
| 31 | dotenv.config({ path: envPath }); |
| 32 | } |
| 33 | |
| 34 | const userId = (process.env.KNOWTATION_CANISTER_BACKUP_USER_ID ?? '').trim(); |
| 35 | if (!userId) { |
| 36 | console.error('ERROR: KNOWTATION_CANISTER_BACKUP_USER_ID is required.'); |
| 37 | process.exit(1); |
| 38 | } |
| 39 | |
| 40 | const baseUrl = resolveCanisterBackupBaseUrl(process.env, repoRoot); |
| 41 | if (!baseUrl) { |
| 42 | console.error('ERROR: Set KNOWTATION_CANISTER_URL or KNOWTATION_CANISTER_BACKUP_URL, or rely on canister_ids.json with BACKUP_USER_ID set.'); |
| 43 | process.exit(1); |
| 44 | } |
| 45 | const hadExplicitUrl = |
| 46 | Boolean((process.env.KNOWTATION_CANISTER_URL ?? '').trim()) || |
| 47 | Boolean((process.env.KNOWTATION_CANISTER_BACKUP_URL ?? '').trim()); |
| 48 | if (!hadExplicitUrl) { |
| 49 | console.log('==> Defaulting KNOWTATION_CANISTER_URL from hub/icp/canister_ids.json'); |
| 50 | console.log(` ${baseUrl}`); |
| 51 | } |
| 52 | |
| 53 | const rawBackupDir = (process.env.KNOWTATION_CANISTER_BACKUP_DIR ?? 'backups').trim() || 'backups'; |
| 54 | const backupDir = path.isAbsolute(rawBackupDir) |
| 55 | ? rawBackupDir |
| 56 | : path.resolve(repoRoot, rawBackupDir); |
| 57 | fs.mkdirSync(backupDir, { recursive: true }); |
| 58 | |
| 59 | const stamp = utcBackupStamp(); |
| 60 | const vaultIds = parseBackupVaultIds(process.env); |
| 61 | if (vaultIds.length === 0) { |
| 62 | console.error('ERROR: No vault ids to export.'); |
| 63 | process.exit(1); |
| 64 | } |
| 65 | |
| 66 | const keyHex = (process.env.KNOWTATION_CANISTER_BACKUP_ENCRYPT_KEY_HEX ?? '').trim(); |
| 67 | const s3Bucket = (process.env.KNOWTATION_CANISTER_BACKUP_S3_BUCKET ?? '').trim(); |
| 68 | const s3Prefix = resolveBackupS3Prefix(process.env); |
| 69 | const skipS3 = (process.env.KNOWTATION_CANISTER_BACKUP_SKIP_S3 ?? '').trim() === '1'; |
| 70 | |
| 71 | for (const vaultId of vaultIds) { |
| 72 | if (!vaultId) continue; |
| 73 | const safe = safeVaultFileToken(vaultId); |
| 74 | const baseName = `canister-export-${safe}-${stamp}`; |
| 75 | console.log(`==> Export vault ${vaultId} (${baseUrl})`); |
| 76 | |
| 77 | const notes = await fetchNotesFromExport(baseUrl, userId, vaultId); |
| 78 | const proposals = await fetchFullProposalsForOperatorExport(baseUrl, userId, vaultId); |
| 79 | const payload = buildOperatorVaultPayload(vaultId, notes, proposals); |
| 80 | const json = JSON.stringify(payload); |
| 81 | |
| 82 | let outBuf; |
| 83 | let outFile; |
| 84 | if (keyHex) { |
| 85 | outBuf = encryptOperatorBackupUtf8(json, keyHex); |
| 86 | outFile = `${baseName}.json.enc`; |
| 87 | console.log(` Encrypted ${json.length} bytes JSON → ${outBuf.length} bytes (${outFile})`); |
| 88 | } else { |
| 89 | outBuf = Buffer.from(json, 'utf8'); |
| 90 | outFile = `${baseName}.json`; |
| 91 | console.log(` Wrote ${outBuf.length} bytes (${outFile})`); |
| 92 | } |
| 93 | |
| 94 | const outPath = path.join(backupDir, outFile); |
| 95 | fs.writeFileSync(outPath, outBuf); |
| 96 | |
| 97 | if (s3Bucket && !skipS3) { |
| 98 | const key = `${s3Prefix}${outFile}`; |
| 99 | console.log(` S3: s3://${s3Bucket}/${key}`); |
| 100 | await putS3Object({ |
| 101 | bucket: s3Bucket, |
| 102 | key, |
| 103 | body: outBuf, |
| 104 | region: process.env.AWS_REGION, |
| 105 | }); |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | console.log(`canister-export-backup: OK (${stamp})`); |
File History
2 commits
sha256:8d46372e39d2d5a54fd93a8b1c27922fe0d9b22a72197345f1d2c71701cc4ce2
feat(auth): persistent login system + C7 session introspection
Human
minor
⚠
16 days ago
sha256:6a102aafafdfe7e70a24f4e59740200f0ee713ce7915f1b53e9d4ba5ee8b4410
Initial Muse snapshot
Human
48 days ago