Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
337
api/src/routes/admin-crypto.ts
Normal file
337
api/src/routes/admin-crypto.ts
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
/**
|
||||
* Admin crypto treasury endpoints.
|
||||
*
|
||||
* All endpoints require the admin token (X-Admin-Token header) — same
|
||||
* gate used by admin-filings and reseller-certs. Read-only endpoints
|
||||
* return 200; mutating endpoints audit to order_audit_log.
|
||||
*/
|
||||
|
||||
import crypto from "node:crypto";
|
||||
import { Router } from "express";
|
||||
import type { Request, Response } from "express";
|
||||
import { pool } from "../db.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// ── Auth middleware ─────────────────────────────────────────────────────
|
||||
|
||||
function requireAdminToken(req: Request, res: Response): boolean {
|
||||
const expected = process.env.ADMIN_API_TOKEN || "";
|
||||
const supplied = (req.headers["x-admin-token"] || "").toString().trim();
|
||||
if (!expected) {
|
||||
// If token not configured, reject — fail-closed.
|
||||
res.status(503).json({ error: "ADMIN_API_TOKEN not set" });
|
||||
return false;
|
||||
}
|
||||
// Timing-safe compare. timingSafeEqual throws on length mismatch, so
|
||||
// guard with a length check first (a cheap length-disclosure trade-off
|
||||
// that's negligible compared to the attack surface of naïve ==).
|
||||
const sb = Buffer.from(supplied);
|
||||
const eb = Buffer.from(expected);
|
||||
const ok = sb.length === eb.length && crypto.timingSafeEqual(sb, eb);
|
||||
if (!ok) {
|
||||
res.status(403).json({ error: "forbidden" });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function auditLog(
|
||||
actor: string, action: string, target: string, details?: unknown,
|
||||
) {
|
||||
// order_audit_log schema requires: order_type IN ('formation','service','quote'),
|
||||
// order_id (integer, NOT NULL), action, actor_type IN
|
||||
// ('system','admin','worker','customer') + optional order_number,
|
||||
// actor_name, metadata. We use order_type='service' (closest match —
|
||||
// crypto treasury is an internal service action) and store the real
|
||||
// crypto-treasury target (order_number or 'sweep:N') in order_number.
|
||||
try {
|
||||
await pool.query(
|
||||
`INSERT INTO order_audit_log
|
||||
(order_type, order_id, order_number, action, actor_type, actor_name, metadata)
|
||||
VALUES ('service', 0, $1, $2, 'admin', $3, $4::jsonb)`,
|
||||
[target, action, actor, JSON.stringify(details ?? {})],
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[admin-crypto] audit log failed:", err);
|
||||
// non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
// ── GET /api/v1/admin/crypto-payments ───────────────────────────────────
|
||||
|
||||
router.get(
|
||||
"/api/v1/admin/crypto-payments",
|
||||
async (req: Request, res: Response) => {
|
||||
if (!requireAdminToken(req, res)) return;
|
||||
|
||||
const stateFilter = typeof req.query.state === "string" ? req.query.state : "";
|
||||
const params: (string | number)[] = [];
|
||||
const where: string[] = [];
|
||||
if (stateFilter) {
|
||||
where.push(`j.state = $${params.length + 1}`);
|
||||
params.push(stateFilter);
|
||||
}
|
||||
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
||||
|
||||
const r = await pool.query(
|
||||
`
|
||||
SELECT j.order_id, j.order_type, j.state, j.coin,
|
||||
j.amount_coin, j.amount_usd_cents, j.needed_usd_cents,
|
||||
j.offramp_provider, j.offramp_ref, j.relay_deposit_id,
|
||||
j.target_card_id, j.last_error, j.attempt_count,
|
||||
j.next_retry_at, j.received_at, j.funds_at_relay_at, j.settled_at,
|
||||
j.created_at, j.updated_at,
|
||||
(
|
||||
SELECT COALESCE(SUM(amount_usd_cents), 0)
|
||||
FROM vendor_obligations
|
||||
WHERE order_id = j.order_id
|
||||
) AS total_obligations_cents,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM vendor_obligations
|
||||
WHERE order_id = j.order_id AND status = 'paid'
|
||||
) AS obligations_paid
|
||||
FROM crypto_payment_jobs j
|
||||
${whereSql}
|
||||
ORDER BY j.created_at DESC
|
||||
LIMIT 200
|
||||
`,
|
||||
params,
|
||||
);
|
||||
res.json({ jobs: r.rows, count: r.rows.length });
|
||||
},
|
||||
);
|
||||
|
||||
// NOTE: specific paths (sweeps, tax-export) are registered BELOW before
|
||||
// the /:order_id param route to avoid Express pattern-match conflicts
|
||||
// (order_id='sweeps' would otherwise match this handler).
|
||||
|
||||
// ── GET /api/v1/admin/crypto-payments/:order_id ────────────────────────
|
||||
// Registered AFTER the specific sub-paths below. Express matches in
|
||||
// registration order; putting this last ensures /sweeps and
|
||||
// /tax-export aren't interpreted as order_ids.
|
||||
|
||||
// ── Detail view (must come AFTER specific sub-paths below) ────────────
|
||||
//
|
||||
// We re-register this at the end of the file so Express pattern-matches
|
||||
// /sweeps and /tax-export first.
|
||||
|
||||
// ── POST /api/v1/admin/crypto-payments/:order_id/retry-offramp ─────────
|
||||
|
||||
router.post(
|
||||
"/api/v1/admin/crypto-payments/:order_id/retry-offramp",
|
||||
async (req: Request, res: Response) => {
|
||||
if (!requireAdminToken(req, res)) return;
|
||||
const orderId = req.params.order_id;
|
||||
|
||||
const r = await pool.query(
|
||||
`UPDATE crypto_payment_jobs
|
||||
SET state = 'sizing',
|
||||
last_error = NULL,
|
||||
next_retry_at = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE order_id = $1
|
||||
AND state IN ('manual','failed','offramping')
|
||||
RETURNING state, attempt_count`,
|
||||
[orderId],
|
||||
);
|
||||
if (r.rows.length === 0) {
|
||||
res.status(409).json({
|
||||
error: "job not in a retry-able state (must be manual / failed / offramping)",
|
||||
});
|
||||
return;
|
||||
}
|
||||
await auditLog(
|
||||
req.headers["x-admin-user"]?.toString() || "admin",
|
||||
"crypto_retry_offramp", orderId, { new_state: "sizing" },
|
||||
);
|
||||
res.json({ ok: true, state: r.rows[0].state });
|
||||
},
|
||||
);
|
||||
|
||||
// ── POST /api/v1/admin/crypto-payments/:order_id/mark-settled ──────────
|
||||
|
||||
router.post(
|
||||
"/api/v1/admin/crypto-payments/:order_id/mark-settled",
|
||||
async (req: Request, res: Response) => {
|
||||
if (!requireAdminToken(req, res)) return;
|
||||
const orderId = req.params.order_id;
|
||||
const { note } = req.body ?? {};
|
||||
|
||||
const r = await pool.query(
|
||||
`UPDATE crypto_payment_jobs
|
||||
SET state = 'settled',
|
||||
settled_at = NOW(),
|
||||
last_error = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE order_id = $1
|
||||
RETURNING state`,
|
||||
[orderId],
|
||||
);
|
||||
if (r.rows.length === 0) {
|
||||
res.status(404).json({ error: "job not found" }); return;
|
||||
}
|
||||
await auditLog(
|
||||
req.headers["x-admin-user"]?.toString() || "admin",
|
||||
"crypto_manual_settle", orderId, { note },
|
||||
);
|
||||
res.json({ ok: true });
|
||||
},
|
||||
);
|
||||
|
||||
// ── GET /api/v1/admin/crypto-payments/sweeps ───────────────────────────
|
||||
|
||||
router.get(
|
||||
"/api/v1/admin/crypto-payments/sweeps",
|
||||
async (req: Request, res: Response) => {
|
||||
if (!requireAdminToken(req, res)) return;
|
||||
const r = await pool.query(
|
||||
`SELECT * FROM cold_wallet_sweeps
|
||||
WHERE status IN ('pending','approved','broadcast')
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100`,
|
||||
);
|
||||
res.json({ sweeps: r.rows });
|
||||
},
|
||||
);
|
||||
|
||||
// ── POST /api/v1/admin/crypto-payments/sweeps/:id/approve ──────────────
|
||||
|
||||
router.post(
|
||||
"/api/v1/admin/crypto-payments/sweeps/:id/approve",
|
||||
async (req: Request, res: Response) => {
|
||||
if (!requireAdminToken(req, res)) return;
|
||||
const sweepId = Number(req.params.id);
|
||||
if (!Number.isFinite(sweepId)) {
|
||||
res.status(400).json({ error: "bad sweep id" }); return;
|
||||
}
|
||||
const actor = req.headers["x-admin-user"]?.toString() || "admin";
|
||||
const r = await pool.query(
|
||||
`UPDATE cold_wallet_sweeps
|
||||
SET status = 'approved',
|
||||
approved_by = $2,
|
||||
approved_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
AND status = 'pending'
|
||||
RETURNING coin, amount_coin`,
|
||||
[sweepId, actor],
|
||||
);
|
||||
if (r.rows.length === 0) {
|
||||
res.status(409).json({ error: "sweep not in pending state" }); return;
|
||||
}
|
||||
await auditLog(actor, "crypto_sweep_approve", `sweep:${sweepId}`,
|
||||
{ coin: r.rows[0].coin, amount_coin: r.rows[0].amount_coin });
|
||||
res.json({ ok: true });
|
||||
},
|
||||
);
|
||||
|
||||
// ── GET /api/v1/admin/crypto-payments/tax-export?year=YYYY ─────────────
|
||||
//
|
||||
// IRS Form 8949 columns: description, date_acquired, date_sold,
|
||||
// proceeds, cost_basis, gain_loss. Covers all offramp/disposal rows
|
||||
// whose disposed_at is in the given tax year.
|
||||
router.get(
|
||||
"/api/v1/admin/crypto-payments/tax-export",
|
||||
async (req: Request, res: Response) => {
|
||||
if (!requireAdminToken(req, res)) return;
|
||||
const year = Number(req.query.year) || new Date().getUTCFullYear() - 1;
|
||||
|
||||
const r = await pool.query(
|
||||
`
|
||||
SELECT order_id, coin,
|
||||
amount_coin, fx_rate_usd,
|
||||
acquired_at, disposed_at,
|
||||
basis_usd_cents, proceeds_usd_cents,
|
||||
provider, provider_ref
|
||||
FROM crypto_payment_ledger
|
||||
WHERE movement_type = 'offramp'
|
||||
AND state IN ('pending','confirmed')
|
||||
AND disposed_at >= make_timestamptz($1, 1, 1, 0, 0, 0, 'UTC')
|
||||
AND disposed_at < make_timestamptz($1 + 1, 1, 1, 0, 0, 0, 'UTC')
|
||||
ORDER BY disposed_at ASC, id ASC
|
||||
`,
|
||||
[year],
|
||||
);
|
||||
|
||||
// Build the CSV
|
||||
const header = [
|
||||
"Description", // e.g., "0.00873 BTC — Order CO-SMOKE02"
|
||||
"Date Acquired", // MM/DD/YYYY
|
||||
"Date Sold",
|
||||
"Proceeds (USD)",
|
||||
"Cost Basis (USD)",
|
||||
"Gain/(Loss) (USD)",
|
||||
"Provider Reference",
|
||||
];
|
||||
const lines: string[] = [header.join(",")];
|
||||
const fmt = (d: Date) =>
|
||||
`${String(d.getUTCMonth() + 1).padStart(2, "0")}/${String(d.getUTCDate()).padStart(2, "0")}/${d.getUTCFullYear()}`;
|
||||
for (const row of r.rows) {
|
||||
const amount = Number(row.amount_coin);
|
||||
const proceeds = Number(row.proceeds_usd_cents || 0) / 100;
|
||||
const basis = Number(row.basis_usd_cents || 0) / 100;
|
||||
const gain = proceeds - basis;
|
||||
const acquired = row.acquired_at ? fmt(new Date(row.acquired_at)) : "";
|
||||
const sold = row.disposed_at ? fmt(new Date(row.disposed_at)) : "";
|
||||
const desc = `${Math.abs(amount).toFixed(8)} ${row.coin} — Order ${row.order_id}`;
|
||||
lines.push([
|
||||
`"${desc}"`,
|
||||
`"${acquired}"`,
|
||||
`"${sold}"`,
|
||||
proceeds.toFixed(2),
|
||||
basis.toFixed(2),
|
||||
gain.toFixed(2),
|
||||
`"${row.provider_ref || ""}"`,
|
||||
].join(","));
|
||||
}
|
||||
res.setHeader("Content-Type", "text/csv");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="crypto-disposals-${year}.csv"`,
|
||||
);
|
||||
res.send(lines.join("\n"));
|
||||
},
|
||||
);
|
||||
|
||||
// ── GET /api/v1/admin/crypto-payments/:order_id (must be LAST) ─────────
|
||||
// Registered last so specific paths above match first.
|
||||
router.get(
|
||||
"/api/v1/admin/crypto-payments/:order_id",
|
||||
async (req: Request, res: Response) => {
|
||||
if (!requireAdminToken(req, res)) return;
|
||||
const orderId = req.params.order_id;
|
||||
|
||||
const job = await pool.query(
|
||||
"SELECT * FROM crypto_payment_jobs WHERE order_id = $1",
|
||||
[orderId],
|
||||
);
|
||||
if (job.rows.length === 0) {
|
||||
res.status(404).json({ error: "job not found" }); return;
|
||||
}
|
||||
|
||||
const [ledger, obligations, deposit] = await Promise.all([
|
||||
pool.query(
|
||||
`SELECT * FROM crypto_payment_ledger WHERE order_id = $1 ORDER BY created_at ASC`,
|
||||
[orderId],
|
||||
),
|
||||
pool.query(
|
||||
`SELECT * FROM vendor_obligations WHERE order_id = $1 ORDER BY obligation_kind, id`,
|
||||
[orderId],
|
||||
),
|
||||
job.rows[0].relay_deposit_id
|
||||
? pool.query("SELECT * FROM relay_deposits WHERE id = $1", [job.rows[0].relay_deposit_id])
|
||||
: Promise.resolve({ rows: [] }),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
job: job.rows[0],
|
||||
ledger: ledger.rows,
|
||||
obligations: obligations.rows,
|
||||
relay_deposit: deposit.rows[0] || null,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
Loading…
Add table
Add a link
Reference in a new issue