Private CLI workflow for reviewed depowit operations
This commit is contained in:
Executable
+30
@@ -0,0 +1,30 @@
|
||||
export const CUSTODY_DB_ACTION_ARGUMENT_COUNTS = {
|
||||
migrate: 0,
|
||||
'grant-runtime': 1,
|
||||
'grant-monero-credit-runtime': 1,
|
||||
'import-sqlite': 1,
|
||||
audit: 0,
|
||||
'resolve-integrity': 1,
|
||||
'deposit-prepare-credit': 2,
|
||||
'deposit-stage-credit': 1,
|
||||
'deposit-prepare-release': 1,
|
||||
'deposit-release': 1,
|
||||
'deposit-reconcile-holds': 1,
|
||||
'deposit-prepare-resolve-hold': 2,
|
||||
'deposit-resolve-hold': 1,
|
||||
} as const;
|
||||
|
||||
export type CustodyDbAction = keyof typeof CUSTODY_DB_ACTION_ARGUMENT_COUNTS;
|
||||
export type CustodyDbArguments = { action: CustodyDbAction; argument?: string; extraArgument?: string };
|
||||
|
||||
export function parseCustodyDbArguments(args: readonly string[]): CustodyDbArguments {
|
||||
const [action, ...values] = args;
|
||||
if (!action || !Object.hasOwn(CUSTODY_DB_ACTION_ARGUMENT_COUNTS, action)) {
|
||||
throw new Error(`Expected one of: ${Object.keys(CUSTODY_DB_ACTION_ARGUMENT_COUNTS).join(', ')}`);
|
||||
}
|
||||
const count = CUSTODY_DB_ACTION_ARGUMENT_COUNTS[action as CustodyDbAction];
|
||||
if (values.length !== count || values.some(value => value.length === 0)) {
|
||||
throw new Error(`Invalid argument count for custody operation: ${action}`);
|
||||
}
|
||||
return { action: action as CustodyDbAction, argument: values[0], extraArgument: values[1] };
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
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<unknown> {
|
||||
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<DepositCreditPolicy> {
|
||||
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<unknown> {
|
||||
try { return await readPrivateJson(path, MAX_APPROVAL_BYTES); }
|
||||
catch { throw new Error('Invalid or inaccessible protected Monero deposit review approval'); }
|
||||
}
|
||||
Reference in New Issue
Block a user