import { constants } from 'node:fs'; import { open } from 'node:fs/promises'; import { isAbsolute } from 'node:path'; import { z } from 'zod'; import type { DepositCreditPolicy } from './monero-deposit-credit.ts'; const MAX_REVIEW_CONFIG_BYTES = 4096; const MAX_APPROVAL_BYTES = 64 * 1024; const reviewerPolicySchema = z.object({ approvedReviewerOwners: z.array(z.string().regex(/^[a-f0-9]{64}$/)).min(1).max(8), minimumConfirmations: z.number().int().min(10).max(100_000), maximumObservationAgeMs: z.number().int().min(10_000).max(15 * 60_000), }).strict().superRefine((value, context) => { if (new Set(value.approvedReviewerOwners).size !== value.approvedReviewerOwners.length) { context.addIssue({ code: 'custom', message: 'Reviewer owner allowlist must not contain duplicates' }); } }); async function readPrivateJson(path: string, maximumBytes: number): Promise { if (process.platform === 'win32') throw new Error('Owner-only ACL verification is unavailable on Windows'); if (!isAbsolute(path)) throw new Error('Review files require an absolute path'); const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW); try { const info = await file.stat(); if (!info.isFile() || info.size < 1 || info.size > maximumBytes) { throw new Error('Review file must be a bounded regular file'); } if ((info.mode & 0o077) !== 0) { throw new Error('Review file permissions must be owner-only'); } const chunks: Buffer[] = []; let total = 0, position = 0; while (total <= maximumBytes) { const chunk = Buffer.allocUnsafe(Math.min(4096, maximumBytes + 1 - total)); const { bytesRead } = await file.read(chunk, 0, chunk.length, position); if (bytesRead === 0) break; total += bytesRead; position += bytesRead; if (total > maximumBytes) throw new Error('Review file exceeds byte limit'); chunks.push(chunk.subarray(0, bytesRead)); } const text = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)); return JSON.parse(text) as unknown; } finally { await file.close(); } } /** Public keys/policy only; no reviewer private key is loaded by the application. */ export async function loadMoneroDepositReviewPolicy(env: NodeJS.ProcessEnv = process.env): Promise { const path = env.WIKIDECK_MONERO_DEPOSIT_REVIEW_POLICY_FILE; if (!path) throw new Error('Monero deposit review policy file is required'); try { return reviewerPolicySchema.parse(await readPrivateJson(path, MAX_REVIEW_CONFIG_BYTES)); } catch { throw new Error('Invalid or inaccessible protected Monero deposit review policy'); } } /** Signed approvals are bearer authorizations, so read only owner-protected regular files. */ export async function loadMoneroDepositReviewApproval(path: string): Promise { try { return await readPrivateJson(path, MAX_APPROVAL_BYTES); } catch { throw new Error('Invalid or inaccessible protected Monero deposit review approval'); } }