import { Pool } from 'pg'; import { open, unlink, mkdir } from 'node:fs/promises'; import { resolve, join } from 'node:path'; import { postgresConfig } from '../server/market/postgres-config.ts'; import { migrateCustody, grantCustodyRuntime, grantMoneroObserverRuntime, grantMoneroDepositCreditRuntime, grantMoneroDepositAllocatorRuntime, grantMoneroWithdrawalRuntime, grantPilotDepositReservationRuntime, grantPilotDepositAddressRuntime, grantPilotMoneroObserverRuntime, grantPilotMoneroDepositReviewRuntime } from '../server/market/postgres-schema.ts'; import { initializeMainnetCustodyIdentity, parseMainnetCustodianId } from '../server/market/mainnet-custody-identity.ts'; import { importSqliteCustody } from '../server/market/custody-import.ts'; import { PostgresCustody } from '../server/market/postgres-custody.ts'; import { IS_MAINNET, IS_PILOT } from '../shared/network.ts'; import { initializePilotCustodyIdentity, parsePilotCustodianId } from '../server/market/pilot-custody-identity.ts'; import { resolveCustodyIncident } from '../server/market/custody-incident.ts'; import { loadMoneroDepositReviewApproval, loadMoneroDepositReviewPolicy } from '../server/market/monero-deposit-review-config.ts'; import { assertCustodyDbActionAllowedOnNetwork, assertMainnetSchemaMigrationAcknowledged, assertMainnetCustodyOperationAcknowledged, assertPilotCustodyActionAllowed, assertPilotSchemaMigrationAcknowledged, assertPilotCustodyIdentityAcknowledged, assertPilotReservationGrantAcknowledged, assertPilotAddressGrantAcknowledged, assertPilotObserverGrantAcknowledged, assertPilotDepositReviewAcknowledged, parseCustodyDbArguments } from '../server/market/custody-db-args.ts'; import { prepareDepositCreditReview, postReviewedDepositPendingCredit, prepareDepositCreditReleaseReview, releaseReviewedDepositCredit, reconcileDepositCreditReorgHolds, prepareDepositReorgResolution, resolveReleasedDepositReorgHold, type DepositCreditApproval, type DepositCreditReleaseApproval, type DepositReorgResolutionApproval } from '../server/market/monero-deposit-credit.ts'; import { preparePilotMoneroDepositReview } from '../server/market/pilot-monero-deposit-review.ts'; // Explicit operator task, never invoked by node startup. No passwords or database URLs printed. const commandArgs = process.argv.slice(2); const depositActions = ['deposit-prepare-credit', 'deposit-stage-credit', 'deposit-prepare-release', 'deposit-release', 'deposit-reconcile-holds', 'deposit-prepare-resolve-hold', 'deposit-resolve-hold']; const pilotReviewActions = ['pilot-deposit-prepare-review']; const custodyActions = ['migrate', 'migrate-mainnet-schema', 'initialize-mainnet-custody-identity', 'migrate-pilot-schema', 'initialize-pilot-custody-identity', 'grant-runtime', 'grant-monero-observer-runtime', 'grant-pilot-deposit-reservation-runtime', 'grant-pilot-deposit-address-runtime', 'grant-pilot-monero-observer-runtime', 'grant-pilot-monero-review-runtime', 'grant-monero-withdrawal-runtime', 'grant-monero-deposit-allocator-runtime', 'grant-monero-credit-runtime', 'import-sqlite', 'audit', 'resolve-integrity']; const { action, argument, extraArgument } = parseCustodyDbArguments(commandArgs); assertCustodyDbActionAllowedOnNetwork(action, IS_MAINNET); assertPilotCustodyActionAllowed(action, IS_PILOT); assertMainnetSchemaMigrationAcknowledged(action, process.env.WIKIDECK_MAINNET_SCHEMA_MIGRATION); assertMainnetCustodyOperationAcknowledged(action, process.env.WIKIDECK_MAINNET_CUSTODY_OPERATIONS); assertPilotSchemaMigrationAcknowledged(action, process.env.WIKIDECK_PILOT_SCHEMA_MIGRATION); assertPilotCustodyIdentityAcknowledged(action, process.env.WIKIDECK_PILOT_CUSTODY_IDENTITY, process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS); assertPilotReservationGrantAcknowledged(action, process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS); assertPilotAddressGrantAcknowledged(action, process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS); assertPilotObserverGrantAcknowledged(action, process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS); assertPilotDepositReviewAcknowledged(action, process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS); if (action === 'initialize-mainnet-custody-identity' && process.env.WIKIDECK_MAINNET_CUSTODY_IDENTITY !== 'initialize-mainnet-custody-v1') { throw new Error('Mainnet custody identity initialization requires WIKIDECK_MAINNET_CUSTODY_IDENTITY=initialize-mainnet-custody-v1'); } if (!custodyActions.includes(action) && !depositActions.includes(action) && !pilotReviewActions.includes(action)) { throw new Error('Unsupported custody operation'); } function boundedPilotReviewSetting(name: string, fallback: number, minimum: number, maximum: number): number { const value = process.env[name]; if (value === undefined) return fallback; if (!/^(0|[1-9]\d{0,8})$/.test(value)) throw new Error(`Invalid ${name}`); const parsed = Number(value); if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) throw new Error(`Invalid ${name}`); return parsed; } try { const reviewRole = pilotReviewActions.includes(action) ? process.env.WIKIDECK_PILOT_REVIEW_DB_ROLE : undefined; let config; if (pilotReviewActions.includes(action)) { const reviewUrlFile = process.env.WIKIDECK_PILOT_REVIEW_PG_URL_FILE; const reviewCaFile = process.env.WIKIDECK_PILOT_REVIEW_PG_CA_FILE; if (!IS_PILOT || process.env.WIKIDECK_PILOT_MODE !== 'private-mock-only' || !reviewRole || !/^[a-z][a-z0-9_]{0,62}$/.test(reviewRole) || !reviewUrlFile || !reviewCaFile) { throw new Error('Pilot review requires the private mock-only profile, exact review-role name, and dedicated PostgreSQL TLS secret files'); } config = await postgresConfig({ ...process.env, WIKIDECK_CUSTODY_PG_URL_FILE: reviewUrlFile, WIKIDECK_CUSTODY_PG_CA_FILE: reviewCaFile, WIKIDECK_CUSTODY_PG_TRANSPORT: 'tls', WIKIDECK_CUSTODY_PG_LOCAL_TEST: undefined, WIKIDECK_CUSTODY_PG_INFRASTRUCTURE_ACK: undefined, }); if (config.user !== reviewRole) throw new Error('Pilot review PostgreSQL secret does not use the configured read-only review role'); } else config = await postgresConfig(); if (action === 'audit') { const market = await PostgresCustody.open(config, { feeBps: 250, card: () => undefined }); try { console.log(JSON.stringify(await market.audit())); } finally { await market.close(); } } else { const pool = new Pool(config); try { if (action === 'migrate' || action === 'migrate-mainnet-schema' || action === 'migrate-pilot-schema') { await migrateCustody(pool); console.log(action === 'migrate-mainnet-schema' ? 'Mainnet custody schema verified/migrated; no runtime grants, deposits, or payments enabled.' : action === 'migrate-pilot-schema' ? 'Pilot custody schema verified/migrated; no pilot identity, runtime grants, deposits, or payments enabled.' : 'Custody PostgreSQL schema verified/migrated. No funds activated.'); } if (action === 'initialize-mainnet-custody-identity') { if (!IS_MAINNET || extraArgument) throw new Error('Expected mainnet initialize-mainnet-custody-identity CUSTODIAN_UUID'); const identity = await initializeMainnetCustodyIdentity(pool, parseMainnetCustodianId(argument ?? '')); console.log(JSON.stringify({ initialized: true, network: identity.network, chainId: identity.chainId, custodianId: identity.id, crediting: 'reviewed-pending-only', withdrawals: 'disabled' })); } if (action === 'initialize-pilot-custody-identity') { if (!IS_PILOT || process.env.WIKIDECK_PILOT_MODE !== 'private-mock-only' || extraArgument) { throw new Error('Expected pilot profile, private-mock-only mode, and one custodian UUID'); } const identity = await initializePilotCustodyIdentity(pool, parsePilotCustodianId(argument ?? '')); console.log(JSON.stringify({ initialized: true, marketNetwork: identity.marketNetwork, marketChainId: identity.marketChainId, moneroNetwork: identity.moneroNetwork, custodianId: identity.custodianId, activation: identity.activationStatus, deposits: 'disabled', withdrawals: 'disabled' })); } if (action === 'grant-pilot-deposit-reservation-runtime') { if (!IS_PILOT || process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS !== 'reservation-only-no-addresses' || extraArgument) { throw new Error('Pilot reservation grant requires the pilot profile, reservation-only acknowledgement, and one role name'); } const identity = await pool.query('SELECT 1 FROM wk_custody.pilot_custody_identity WHERE singleton=1'); if (!identity.rowCount) throw new Error('Initialize the separate pilot custody identity before granting reservation access'); await grantPilotDepositReservationRuntime(pool, argument ?? ''); console.log('Pilot durable index-reservation role granted; no Wallet-RPC address creation, credit, or withdrawal access.'); } if (action === 'grant-pilot-deposit-address-runtime') { if (!IS_PILOT || process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS !== 'operator-triggered-addresses-only' || extraArgument) { throw new Error('Pilot address grant requires pilot profile, operator-triggered acknowledgement, and one role name'); } const identity = await pool.query('SELECT 1 FROM wk_custody.pilot_custody_identity WHERE singleton=1'); if (!identity.rowCount) throw new Error('Initialize pilot custody identity before granting address allocation access'); await grantPilotDepositAddressRuntime(pool, argument ?? ''); console.log('Pilot Wallet-RPC address allocator role granted for explicit operator action only; no HTTP enrollment or financial posting.'); } if (action === 'grant-pilot-monero-observer-runtime') { if (!IS_PILOT || process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS !== 'pilot-observation-only-no-credit' || extraArgument) { throw new Error('Pilot observer grant requires pilot profile, observation-only acknowledgement, and one role name'); } const identity = await pool.query('SELECT 1 FROM wk_custody.pilot_custody_identity WHERE singleton=1'); if (!identity.rowCount) throw new Error('Initialize pilot custody identity before granting observation access'); await grantPilotMoneroObserverRuntime(pool, argument ?? ''); console.log('Pilot Monero observation-only role granted; no address allocation, credit, withdrawal, or public enrollment privileges.'); } if (action === 'grant-pilot-monero-review-runtime') { if (!IS_PILOT || process.env.WIKIDECK_PILOT_MODE !== 'private-mock-only' || process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS !== 'read-only-pilot-review-role-only' || extraArgument) { throw new Error('Pilot review role grant requires the private pilot profile, read-only-role acknowledgement, and one role name'); } const identity = await pool.query('SELECT 1 FROM wk_custody.pilot_custody_identity WHERE singleton=1'); if (!identity.rowCount) throw new Error('Initialize pilot custody identity before granting review access'); await grantPilotMoneroDepositReviewRuntime(pool, argument ?? ''); console.log('Pilot deposit review role granted read-only pilot evidence access; financial tables and all writes remain denied.'); } if (action === 'grant-runtime') { await grantCustodyRuntime(pool, argument ?? ''); console.log('Runtime grants applied; no credentials printed.'); } if (action === 'grant-monero-observer-runtime') { await grantMoneroObserverRuntime(pool, argument ?? ''); console.log('Separate Monero observation-only grants applied; journal access and deposit posting remain denied.'); } if (action === 'grant-monero-deposit-allocator-runtime') { await grantMoneroDepositAllocatorRuntime(pool, argument ?? ''); console.log('Separate Monero deposit-allocation grants applied; balance, journal, observation, and credit tables remain denied.'); } if (action === 'grant-monero-withdrawal-runtime') { await grantMoneroWithdrawalRuntime(pool, argument ?? ''); console.log('Separate customer-signed withdrawal-reservation grants applied; transaction signing and relay are not granted.'); } if (action === 'grant-monero-credit-runtime') { await grantMoneroDepositCreditRuntime(pool, argument ?? ''); console.log('Separate reviewed pending-credit coordinator grants applied; no credentials printed and no live deposits enabled.'); } if (action === 'resolve-integrity') console.log(JSON.stringify(await resolveCustodyIncident(pool, argument ?? ''))); if (depositActions.includes(action)) { const policy = await loadMoneroDepositReviewPolicy(); if (action === 'deposit-prepare-credit') { if (!argument || !extraArgument) throw new Error('Expected deposit-prepare-credit WALLET_FINGERPRINT OBSERVATION_ID'); console.log(JSON.stringify(await prepareDepositCreditReview(pool, argument, extraArgument, policy))); } if (action === 'deposit-stage-credit') { if (!argument || extraArgument) throw new Error('Expected deposit-stage-credit APPROVAL_JSON_FILE'); const approval = await loadMoneroDepositReviewApproval(argument); // The coordinator validates the exact approval schema/signature before any write. console.log(JSON.stringify(await postReviewedDepositPendingCredit(pool, approval as DepositCreditApproval, policy))); } if (action === 'deposit-prepare-release') { if (!argument || extraArgument) throw new Error('Expected deposit-prepare-release CREDIT_UUID'); console.log(JSON.stringify(await prepareDepositCreditReleaseReview(pool, argument, policy))); } if (action === 'deposit-release') { if (!argument || extraArgument) throw new Error('Expected deposit-release APPROVAL_JSON_FILE'); const approval = await loadMoneroDepositReviewApproval(argument); // The coordinator validates the exact approval schema/signature before any write. console.log(JSON.stringify(await releaseReviewedDepositCredit(pool, approval as DepositCreditReleaseApproval, policy))); } if (action === 'deposit-reconcile-holds') { if (!argument || extraArgument) throw new Error('Expected deposit-reconcile-holds WALLET_FINGERPRINT'); console.log(JSON.stringify({ holdsCreated: await reconcileDepositCreditReorgHolds(pool, argument) })); } if (action === 'deposit-prepare-resolve-hold') { if (!argument || !extraArgument) throw new Error('Expected deposit-prepare-resolve-hold CREDIT_UUID HOLD_UUID'); console.log(JSON.stringify(await prepareDepositReorgResolution(pool, argument, extraArgument, policy))); } if (action === 'deposit-resolve-hold') { if (!argument || extraArgument) throw new Error('Expected deposit-resolve-hold APPROVAL_JSON_FILE'); const approval = await loadMoneroDepositReviewApproval(argument); // The coordinator validates the exact approval schema/signature before any write. console.log(JSON.stringify(await resolveReleasedDepositReorgHold(pool, approval as DepositReorgResolutionApproval, policy))); } } if (pilotReviewActions.includes(action)) { if (!IS_PILOT || process.env.WIKIDECK_PILOT_MODE !== 'private-mock-only' || !argument || !extraArgument || process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS !== 'pilot-deposit-review-preparation-only') { throw new Error('Pilot review requires the private pilot profile, preparation-only acknowledgement, wallet generation, and observation ID'); } const currentUser = await pool.query('SELECT current_user AS role'); if (currentUser.rows[0]?.role !== reviewRole) throw new Error('Pilot deposit review must use the dedicated read-only review role'); const subject = await preparePilotMoneroDepositReview(pool, { walletGenerationId: argument, observationId: extraArgument, }, { minimumConfirmations: boundedPilotReviewSetting('WIKIDECK_PILOT_DEPOSIT_REVIEW_MIN_CONFIRMATIONS', 10, 10, 100_000), maximumObservationAgeMs: boundedPilotReviewSetting('WIKIDECK_PILOT_DEPOSIT_REVIEW_MAX_OBSERVATION_AGE_MS', 300_000, 10_000, 15 * 60_000), }); console.log(JSON.stringify(subject)); } if (action === 'import-sqlite') { if (!argument) throw new Error('Stopped source node DATA_DIR required'); const directory = resolve(argument); await mkdir(directory, { recursive: true, mode: 0o700 }); const lockPath = join(directory, 'testnet-node.lock'), lock = await open(lockPath, 'wx', 0o600); try { await lock.writeFile(`custody-import:${process.pid}`); console.log(JSON.stringify(await importSqliteCustody(join(directory, 'testnet', 'custody-sandbox.sqlite'), pool))); } finally { await lock.close(); await unlink(lockPath); } } } finally { await pool.end(); } } } catch { console.error('Custody database operation failed. Check configuration, schema, stopped-node lock and migration state privately. No automatic fallback/reset was performed.'); process.exitCode = 1; }