diff --git a/src/routes/treasury-proposals.ts b/src/routes/treasury-proposals.ts index 91408fa..7f7b0a8 100644 --- a/src/routes/treasury-proposals.ts +++ b/src/routes/treasury-proposals.ts @@ -24,7 +24,6 @@ import { Errors } from "../errors"; import { requireUser } from "../plugins/auth"; import { requireMembership, requireAdmin } from "../services/access"; import { stellar } from "../services/stellar"; -import { audit } from "../services/audit"; import { isPositive } from "../services/money"; import { serializeGroup, @@ -91,20 +90,6 @@ export default async function treasuryProposalRoutes(app: FastifyInstance) { threshold ); - await audit({ - userId: auth.id, - action: "treasury.proposal.created", - entityType: "treasury_proposal", - entityId: proposal.id, - metadata: { - groupId, - destination: body.destination, - amount: body.amount, - assetCode: body.assetCode, - threshold, - }, - }); - return { proposal: serializeTreasuryProposal(proposal), xdr, @@ -172,20 +157,8 @@ export default async function treasuryProposalRoutes(app: FastifyInstance) { userId: auth.id, }); - await audit({ - userId: auth.id, - action: - result.status === "confirmed" - ? "treasury.proposal.submitted" - : "treasury.proposal.signed", - entityType: "treasury_proposal", - entityId: proposalId, - metadata: { - signatureCount: result.signatureCount, - threshold: result.threshold, - stellarTxHash: result.stellarTxHash, - }, - }); + // The service writes signature/submission audit records inside its + // transaction; do not add a second best-effort record after commit. const proposal = await prisma.treasuryProposal.findUnique({ where: { id: proposalId }, diff --git a/src/routes/treasury-signatures.ts b/src/routes/treasury-signatures.ts index 9129691..b53c868 100644 --- a/src/routes/treasury-signatures.ts +++ b/src/routes/treasury-signatures.ts @@ -26,8 +26,6 @@ import { rateLimited } from "../lib/rate-limit"; import { requireAdmin, requireMembership } from "../services/access"; import { treasurySignaturesService } from "../services/treasury-signatures"; import { serializeTreasuryTxProposal } from "../serializers"; -import { audit } from "../services/audit"; -import { AuditAction } from "../services/audit-actions"; const createBodySchema = z.object({ groupId: z.string().min(1), @@ -95,51 +93,18 @@ export default async function treasurySignatureRoutes(app: FastifyInstance) { if (!existing) throw Errors.notFound("Treasury proposal not found"); await requireAdmin(existing.groupId, auth.id); - let result; - try { - result = await treasurySignaturesService.submitSignature({ - proposalId: id, - groupId: existing.groupId, - userId: auth.id, - signedXdr: body.signedXdr, - }); - } catch (e: any) { - await audit({ - userId: auth.id, - groupId: existing.groupId, - action: AuditAction.TREASURY_TX_PROPOSAL_FAILED, - entityType: "treasury_tx_proposal", - entityId: id, - outcome: "failure", - metadata: { - reason: e instanceof Error ? e.message : String(e), - }, - }); - throw e; - } + const result = await treasurySignaturesService.submitSignature({ + proposalId: id, + groupId: existing.groupId, + userId: auth.id, + signedXdr: body.signedXdr, + }); const proposal = await prisma.treasuryTxProposal.findUnique({ where: { id }, include: { signatures: true }, }); - await audit({ - userId: auth.id, - groupId: existing.groupId, - action: - result.status === "SUBMITTED" - ? AuditAction.TREASURY_TX_PROPOSAL_SUBMITTED - : AuditAction.TREASURY_TX_PROPOSAL_SIGNATURE_ADDED, - entityType: "treasury_tx_proposal", - entityId: id, - metadata: { - status: result.status, - totalWeight: result.totalWeight, - requiredWeight: result.requiredWeight, - stellarTxHash: result.stellarTxHash, - }, - }); - return { proposal: serializeTreasuryTxProposal(proposal), status: result.status, diff --git a/src/routes/webhooks.ts b/src/routes/webhooks.ts index 6d45a5e..0296d3c 100644 --- a/src/routes/webhooks.ts +++ b/src/routes/webhooks.ts @@ -270,6 +270,7 @@ async function webhookManagementRoutes(app: FastifyInstance) { groupId, }).catch(() => undefined); + return { queued: true }; }); diff --git a/src/services/audit-actions.ts b/src/services/audit-actions.ts index a31abec..d8ac5cb 100644 --- a/src/services/audit-actions.ts +++ b/src/services/audit-actions.ts @@ -20,6 +20,7 @@ export const AuditAction = { GROUP_JOIN: "group.join", GROUP_LEAVE: "group.leave", GROUP_MEMBER_REMOVE: "group.member_remove", + GROUP_MEMBER_ROLE_CHANGE: "group.member_role_change", // ── Treasury ──────────────────────────────────────────────────────── TREASURY_ENABLE: "treasury.enable", @@ -44,6 +45,9 @@ export const AuditAction = { // ── Settlements ───────────────────────────────────────────────────── SETTLEMENT_CREATED: "settlement.created", + EXPENSE_CREATE: "expense.create", + EXPENSE_UPDATE: "expense.update", + EXPENSE_DELETE: "expense.delete", SETTLEMENT_CONFIRM_RETRY: "settlement.confirm.retry", SETTLEMENT_CONFIRM_SUBMITTED: "settlement.confirm.submitted", SETTLEMENT_CONFIRM_VALIDATION_FAILED: "settlement.confirm.validation_failed", diff --git a/src/services/audit.ts b/src/services/audit.ts index df758a1..93bbfe4 100644 --- a/src/services/audit.ts +++ b/src/services/audit.ts @@ -1,6 +1,29 @@ import type { Prisma } from "@prisma/client"; import { prisma } from "../db"; +const SENSITIVE_KEYS = new Set([ + "privatekey", + "secretkey", + "signedxdr", + "transactionxdr", + "xdr", + "token", + "jwt", + "authorization", + "password", + "secret", +]); + +function sanitize(value: unknown): unknown { + if (Array.isArray(value)) return value.map(sanitize); + if (!value || typeof value !== "object") return value; + return Object.fromEntries( + Object.entries(value as Record) + .filter(([key]) => !SENSITIVE_KEYS.has(key.toLowerCase())) + .map(([key, item]) => [key, sanitize(item)]) + ); +} + /** Whether the audited action succeeded, for operator-facing filtering. */ export type AuditOutcome = "success" | "failure"; @@ -33,7 +56,7 @@ export function auditData(params: AuditParams) { entityType: params.entityType, entityId: params.entityId, metadata: { - ...(params.metadata ?? {}), + ...(sanitize(params.metadata ?? {}) as Record), ...(params.outcome ? { outcome: params.outcome } : {}), ...(params.actorType ? { actorType: params.actorType } : {}), } as any, diff --git a/src/services/treasury-proposals.ts b/src/services/treasury-proposals.ts index 160c7f6..c53c82b 100644 --- a/src/services/treasury-proposals.ts +++ b/src/services/treasury-proposals.ts @@ -49,7 +49,7 @@ import { config } from "../config"; import { Errors } from "../errors"; import { prisma } from "../db"; import { stellar } from "./stellar"; -import { audit, auditTx } from "./audit"; +import { auditTx } from "./audit"; import { AuditAction } from "./audit-actions"; export interface CreateProposalParams { @@ -426,10 +426,10 @@ export const treasuryProposalsService = { }; } - // Audit each new signature (best-effort: the proposal update above - // succeeded, and if an audit write fails the signature is still stored). + // Signature persistence, proposal status, and audit records must + // commit together. A failed audit write must roll back the mutation. for (const pk of verified.slice(stored.length).map((s) => s.publicKey)) { - await audit({ + await auditTx(tx, { groupId: proposal.groupId, action: AuditAction.TREASURY_PROPOSAL_SIGNED, entityType: "treasury_proposal",