feat(server): AuditLog, DB indexes, security hardening, observability#237
Merged
Conversation
…bility Comprehensive server-side hardening pass: closes auth/security gaps, adds an append-only AuditLog for security-relevant events, indexes frequently queried columns, centralizes ctx typing, and lands shared auth helpers. ## Database (already deployed to prod via prisma migrate deploy) - New `AuditLog` table for append-only security audit trail (auth flows, wallet/transaction mutations, privilege grants, signer changes). Five indexes for the common access patterns. - Btree indexes on Wallet/NewWallet `ownerAddress`, Signable+Transaction `walletId`/`state`/`(walletId, state)`, Proxy `walletId`/`userId`/ `(walletId, isActive)`/`(userId, isActive)`, Ballot `walletId`, BalanceSnapshot `walletId`/`(walletId, snapshotDate)`. - GIN indexes on Wallet/NewWallet `signersAddresses` (array_ops) — the signer-membership query was a full table scan. - Restored `Crowdfund` model declaration (production drift: table exists in prod but was never declared in main's schema; see PR description for full archaeology). Marked as retained-but-unused. - WalletBotAccess: `@@unique` -> `@@id` to match prod (drift from PR #207 / commit 1facdc3 where schema and migration disagreed at landing). - Ballot.updatedAt: restored `@default(now()) @updatedAt` to match prod's column default (drift accumulated across multiple commits). - Ballot.anchorUrls / anchorHashes: added `DEFAULT ARRAY[]::TEXT[]` to match the schema's `@default([])` annotation. ## Observability primitives - `src/lib/observability/audit.ts` — `audit(db, event)` emitter; never throws (audit miss must not break user flow); redacts secrets in metadata before write. - `src/lib/observability/logger.ts` — structured logger; JSON in prod, human-readable in dev; never logs raw tokens/signatures/cookies. ## Security fixes (Wave 1-3) - Closed `ownerAddress === "all"` bypass in `assertWalletAccess`. The string "all" was being treated as a wildcard owner — any session could claim ownership of any wallet whose `ownerAddress` happened to contain that literal. - `lookupMultisigWallet`: validate stake-credential-hash format before query (prevents prefix-match abuse and full-table scans on malformed input). - Centralized rate-limit and request-guard surface (`src/lib/security/ rateLimit.ts`, `requestGuards`). Bot routes now use bot-scoped rate limit; user routes use IP-scoped. - `verifyJwt`: stricter token-type narrowing; explicit `isBotJwt` predicate. - `walletSession`: tighter expiry handling, no implicit refresh. ## Auth helpers (Wave 8) - New `src/server/api/auth.ts` consolidates `requireSessionAddress`, `getSessionAddresses`, and wallet-access checks that were duplicated in nearly every router. One source of truth, one place to extend. - All routers and v1 API handlers migrated. ## ctx typing (Wave 2) - New `AuthCtx` and `TRPCContext` exported from `src/server/api/trpc.ts`. - All router helpers use `AuthCtx` instead of `any`. - `protectedProcedure` middleware: type-narrows `sessionWallets`, `primaryWallet`, `sessionAddress` correctly. ## Audit emitters (Wave 5) Wired into: - auth flow (login success/failure, JWT mint, bot auth) - wallet mutations (create, update, archive, transfer, signer changes) - signable + transaction mutations (sign, reject, broadcast) - bot privilege grants All emitters fire after the underlying action and never block it. ## SSRF defense for `/api/v1/og` The OG metadata endpoint now: - requires https, denies non-allowlisted hosts - DNS-resolves and rejects private/loopback/link-local addresses - denies upstream redirects (no auto-follow) `OG_ALLOWED_HOSTS` env var configures the allow list; "*" allows any public host (still SSRF-guarded). ## Test infrastructure - jest.config.mjs — moduleNameMapper for CSS, transformIgnorePatterns for ESM-only deps (superjson, @trpc, @meshsdk, jose, etc.) - setupEnv.cjs — pre-test env bootstrap (SKIP_ENV_VALIDATION=1, dummy DB/JWT/Blockfrost values) so `src/env.js` doesn't throw on import. - Frozen wall clock (`Date.now`/`new Date`) for byte-identical test runs; real timer APIs preserved. - `__mocks__/styleMock.cjs` — CSS imports mock for jest. ## Tests - New: `og.test.ts` (SSRF tripwire suite — 9 cases for the og handler). - New: `signing.test.ts` (source tripwires preventing the `return true ? signature : undefined` regression and similar). - Updated existing tests to match Jest 30 strict mock typing (jest.fn<...>() generics) and new ctx fields. ## Verification - Typecheck clean - All 165 staged-suite tests pass deterministically across two runs - Migration `20260510160404_audit_log_and_indexes` already applied to the multisig Supabase production DB — `prisma migrate deploy` on this branch is a no-op (idempotent). Depends on: #236 (build fix; without it `next build --webpack` will crash on `/wallets/[wallet]/transactions/new`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Comment on lines
+95
to
+100
| const response = await fetch(target, { | ||
| method: "GET", | ||
| redirect: "manual", | ||
| signal: controller.signal, | ||
| headers: { "user-agent": "MeshMultisigOGFetcher/1.0" }, | ||
| }); |
3 tasks
3 tasks
This was referenced May 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Comprehensive server-side hardening pass that:
AuditLogtable + 17 supporting indexes (already deployed to prod viaprisma migrate deploy)ownerAddress === \"all\"wildcard bypass)audit,logger) and wires emitters into auth + wallet/signable/transaction mutationsAuthCtx) and shared auth helpers (src/server/api/auth.ts)/api/v1/ogprisma migrate diffis non-destructive on prodDepends on #236 — the build-crash fix. Without it,
next build --webpackfails on/wallets/[wallet]/transactions/new.Production migration status
The migration
20260510160404_audit_log_and_indexeshas already been applied to the multisig Supabase production DB.prisma migrate deployon this branch is idempotent — Prisma sees the migration in_prisma_migrationsand skips it.The migration is purely additive: 1 AlterTable (adds defaults to two array columns), 1 CreateTable (
AuditLog), 22 CreateIndex. Zero destructive operations.Schema drift archaeology
Three drift items were resolved by reflecting prod in
schema.prisma(not by altering prod):Crowdfundtableaiken-crowdfund(never merged); applied to prod viadb push. Placeholder migrations onmainshow "applied but file not found locally."// Retained — feature unused in app code. Do not drop without migration plan.WalletBotAccessPK vs UNIQUE1facdc3) — schema authored with@@unique; migration SQL usedPRIMARY KEY. They disagreed at landing.@@unique→@@id.Ballot.updatedAtdefault109bade/97592cc/de1e762— annotation simplified to bare@updatedAtwithout regenerating migration; default still in prod DDL.@default(now()) @updatedAt.Recommend retiring the unmerged
aiken-crowdfundbranch on GitHub after this lands so nobody picks it up later — its3b10fd0commit contains a destructiveDROP TABLE Crowdfund.What's in this PR
Database
AuditLogmodel (5 fields + 5 indexes)Observability
src/lib/observability/audit.ts—audit(db, event)emitter; never throws (audit miss must not break user flow); redacts secretssrc/lib/observability/logger.ts— structured logger; JSON in prod, human-readable in devSecurity fixes
ownerAddress === \"all\"bypass inassertWalletAccesslookupMultisigWalletvalidates stake-credential-hash format before queryisBotJwt); tighterwalletSessionexpiry/api/v1/og: https-only, allow-listed hosts, private-IP DNS rejection, no redirect-followAudit emitters
Wired into auth flow, wallet mutations, signable/transaction mutations, bot privilege grants. All non-blocking.
Auth helpers
src/server/api/auth.tsconsolidatesrequireSessionAddress,getSessionAddresses, wallet-access checksctx typing
AuthCtxandTRPCContextexported fromsrc/server/api/trpc.ts(ctx as any)casts in router helpersprotectedProceduremiddleware: correctly narrowssessionWallets/primaryWallet/sessionAddressTest infrastructure
jest.config.mjs— CSS moduleNameMapper, ESM transformIgnorePatternssetupEnv.cjs— pre-test env bootstrap sosrc/env.jsdoesn't throwDate.now/new Date) for byte-identical runsog.test.ts(9 SSRF cases),signing.test.ts(regression tripwires)Removed dependencies
react-hook-form,@hookform/resolvers— last consumer wassrc/components/ui/form.tsx, deleted (no remaining imports anywhere insrc/)yaml— no remaining importsTest plan
AuditLogrow/api/v1/ogrejects http://, private IPs, redirectsStats
45 files changed, +1650 / -554
🤖 Generated with Claude Code