feat: agent onboarding, governance overview, wallet transfer, ballot UX#254
Open
QSchlegel wants to merge 18 commits into
Open
feat: agent onboarding, governance overview, wallet transfer, ballot UX#254QSchlegel wants to merge 18 commits into
QSchlegel wants to merge 18 commits into
Conversation
…ecurity headers) (#233) * refactor: remove unused recharts and busboy deps; relocate swagger CSS - Delete src/components/ui/chart.tsx (recharts wrapper, zero consumers) - Remove recharts from dependencies - Remove busboy and @types/busboy (formidable is the actual uploader) - Move swagger-ui CSS imports out of _app.tsx into api-docs.tsx * refactor(react): use signer address as stable key in ReviewSignersCard Replaces array index with signer address (which is unique and stable across reorder/edit) on both desktop TableRow and mobile card view, preventing form-state misalignment when signers are removed or reordered. * ci: add PR checks workflow, basic security headers, env comment - Add .github/workflows/pr-checks.yml (lint/typecheck/test/build, continue-on-error initially) - Add basic security headers in next.config.js (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy). CSP and HSTS intentionally omitted. - Add comment to src/env.js explaining why NextAuth env vars are commented (PrismaAdapter only, no providers configured) * chore: add typecheck, format, format:check scripts Used by .github/workflows/pr-checks.yml and developer workflow. - typecheck: tsc --noEmit - format: prettier --write . - format:check: prettier --check . * chore: refresh package-lock.json after recharts/busboy removal Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: keep swagger-overrides.css in _app.tsx (Next requires global CSS at root) Reverts only the swagger-overrides.css move from commit 4abe300. The swagger-ui-react/swagger-ui.css import (from node_modules) remains local to api-docs.tsx, so the original goal of keeping that bundle out of every page is still achieved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR checks were silently passing because typecheck, test, and build steps were marked continue-on-error, so the gates only reported status, never blocked merges. Drop continue-on-error from those three; keep it on lint until the rule set is cleaned up. Add dependabot config for npm + github-actions, with grouping for @meshsdk/*, next/@next/*, prisma/@prisma/*, @trpc/*, and @types/*. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two interacting issues caused the production build to crash on routes that imported resolve-adahandle (notably /wallets/[wallet]/transactions/new): 1. resolve-adahandle.tsx called getProvider(1) at module top level. Under `next build` page-data collection, every server-rendered page imports that module, which constructs BlockfrostProvider(undefined) when NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET isn't readable at build time (e.g. SKIP_ENV_VALIDATION=true). The constructor throws and the webpack runtime reports it as a generic "factory error". 2. next.config.js had `optimization.sideEffects: false` set globally, which tells webpack that *every* file is side-effect-free. That silently strips global CSS imports and any module-level initialization, masking issues like (1) until you hit a route that exercises them. Fix: - Lazy-init the mainnet provider with a cached singleton, so import is free and instantiation only happens when a caller actually resolves a handle (always client-side). - Remove the global sideEffects:false override. Per-package sideEffects declarations in package.json are the correct mechanism; the global override was masking real bugs. - Move swagger-ui-react CSS import from api-docs.tsx into _app.tsx so Next.js Pages Router's "global CSS only from _app" rule is satisfied. Verified locally: `next build --webpack` completes; both /wallets/[wallet]/transactions/new and /api-docs render. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…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 new-wallet-flow and wallet-migration flow used the raw signer address as a React key in ReviewSignersCard. Two issues: 1. The address can be empty/undefined while the user is editing, which makes the key non-unique → React reuses inputs across rows, swapping names and addresses while typing. 2. Two signers can momentarily share the same partial input, again producing duplicate keys. Fix: every row gets a stable opaque `signerId` generated when the row is created; the component uses `signerId` as the React key. Address becomes regular state, free to be empty/duplicate transiently without breaking React identity. The same fix applied to both `useWalletFlowState` (new wallet) and `useMigrationWalletFlowState` (migration). The shared `signerRows.ts` module emits the id and keeps the parallel arrays in sync. `reviewSignersCardKey.test.ts` is a tripwire suite that: - greps the source to assert the raw address is never used as a key - verifies both flow-state hooks expose signerIds parallel to signersAddresses This catches future regressions structurally — the type system can't enforce \"don't use address as key\", but a regex over source can. Test plan - 171/171 tests pass deterministically on top of #237 - Typecheck clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Small UI/hook cleanup that fell out of the audit:
- overall-layout: add skip-link + main-content anchor; aria-label on the
main form. Trims unused legacy nav code.
- transaction-card: useMemo the JSON.parse(transaction.txJson) so the
parse only runs when txJson changes, not on every render. Removes a
dead `import { get } from \"http\"` that was sneaking into the client
bundle.
- signable-card: defensive parse for legacy payload shapes.
- card-show-signers, signing/index: small render fixes.
- ImgDragAndDrop, MeshProviderClient, BotManagementCard, background.tsx:
drop dead state vars / unused imports surfaced by the audit.
- useAppWallet, useMultisigWallet: stable returns; missing-wallet path
no longer spins indefinitely.
- Delete `src/components/multisig/proxy/ProxyControlExample.tsx`. It was
example-only code, not exported from the proxy index, never rendered
anywhere. The barrel import is updated.
Test plan
- 165/165 staged-suite tests pass on top of #237
- Typecheck clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The IPFS branch of the asset card was passing a single hardcoded ipfs:// literal to <IPFSImage>, so every IPFS-based NFT in the wallet rendered the same placeholder image instead of its own metadata.image. The non-IPFS sibling already uses imageSrc / name / 60x60 correctly; this brings the IPFS branch in line. Also fixes: - alt text now uses the asset name (was "IPFS Image" — bad a11y) - width/height set to 60 to match the parent 60x60 div (was 300, was being clipped by overflow-hidden + wasting bandwidth) Single occurrence in the codebase. Introduced 2025-03-27.
ProxyControl had three state variables whose values were never read, only their setters. Calling them produced empty re-renders. - setLocalLoading: paralleled setSetupLoading and setSpendLoading; the read-side `setupLoading` and `spendLoading` are the live signals - setSelectedUtxos / setManualSelected: stored values from the UTxO selector callback that nothing downstream consumed; the contract already uses all UTxOs from the multisig wallet (per existing comment), so the selection is purely visual The UTxOSelector callback is preserved as a no-op (the prop is required) with a comment explaining the visual-only intent. Net -11 lines, zero behavior change.
fix(build): lazy-init mainnet provider; drop global sideEffects:false
feat(server): AuditLog, DB indexes, security hardening, observability
chore(ci): gate typecheck/test/build; add dependabot
refactor(wallet-flow): stable signerIds for React keys; tripwire test
chore(ui): a11y skip-link, useMemo, hook fixes, dead-code cleanup
fix(wallet-assets): show actual IPFS NFT image instead of hardcoded CID
chore(proxy): remove three write-only useState declarations
Removes the Nostr-based wallet chat that was throwing `wss://relay.damus.io/` WebSocket errors. Drops the @jinglescode/nostr-chat-plugin dependency along with the chat page, component, sidebar link, and homepage feature card. Server-side: removes nostrKey from createUser input and deletes the unused getNostrKeysByAddresses procedure. Adds a migration making User.nostrKey nullable so new signups insert without a key (existing rows preserved). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore: remove Nostr chat system
- Bot onboarding for AI agents: public /bot-setup page, agent-readable
/api/v1/botSetupGuide markdown endpoint, /llms.txt discovery, and a
"Copy agent prompt" button on the user Bot accounts card so any AI
agent pointed at the instance URL can register itself end-to-end.
- Cross-instance wallet transfer: new /api/v1/wallet/transfer/export
(owner JWT) and /api/v1/wallet/transfer/import endpoints with a
shared WalletTransferPayloadV1 type. UI exposes "Transfer wallet"
on the wallet info page (download JSON or push directly to a remote
instance URL, optional contacts/ballots payloads) and "Import
Transfer" on the wallets list page. Imports land as NewWallet so the
existing invite/claim flow takes over.
- Governance overview improvements:
- Wallet governance dashboard summary card (proposal status counts,
ballot progress, voting power, last ballot activity)
- Live network stats strip on the public /governance landing
- DRep list aggregate header, active/inactive filter, surfaced
active_epoch and hex per row
- Proposal detail "Your ballot entry" (rationale + anchor) and
"Technical details" sections surfacing fields that were already
fetched but never rendered
- Ballot UX + standalone rationale: shared rationale module
(build JSON-LD, hash, upload to IPFS, load-from-URL) and a reusable
RationaleEditor component. The vote card now offers an "Attach
voting rationale" toggle so a user can attach a CIP-100 anchor to a
single-proposal vote without creating a Ballot. VoteButton threads
the anchor through to txBuilder.vote(). Ballot summary now reports
rationale-uploaded / draft counts; moving a proposal between
ballots is gated by a Keep / Add to both / Move here dialog instead
of silently relocating it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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
Four loosely-related improvements bundled together:
/bot-setuppage, agent-readable/api/v1/botSetupGuidemarkdown endpoint,/llms.txtdiscovery at the instance root, and a "Copy agent prompt" button on the user Bot accounts card.GET /api/v1/wallet/transfer/export(owner JWT) andPOST /api/v1/wallet/transfer/import, plus UI: a "Transfer wallet" card on the wallet info page (download JSON or push to a remote instance, optional contacts/ballots payload) and an "Import Transfer" button on the wallets list page. Imports land asNewWalletso the existing invite/claim flow takes over./governancelanding; DRep list aggregate header + active/inactive filter + surfacedactive_epoch/hexper row; proposal detail surfaces fields that were already fetched but never rendered ("Your ballot entry" + "Technical details").RationaleEditorcomponent. The vote card now has an "Attach voting rationale" toggle, so a user can attach an anchor URL + hash to a single-proposal vote without creating a Ballot.VoteButtonthreads the anchor through totxBuilder.vote(). Ballot summary reports rationale-uploaded/draft counts; moving a proposal between ballots now goes through a Keep / Add to both / Move here dialog instead of silently relocating it.Test plan
npm run typecheck— passes locallynpm run build— succeeded locally (BUILD_EXIT=0)/bot-setupand/llms.txtunauthenticated — renders/governance— see the "Live Cardano governance" stats card above the educational cards/governance/drep— see aggregate header + Active/Inactive filter; per-row epoch/hex/user— click "Copy agent prompt", paste into a fresh Claude session, confirm the agent can completebotRegister → claim → pickup → auth → botMe/wallets/[id]/info— "Transfer wallet" card: download JSON, then on the wallets list page import it with "Import Transfer", confirm the resulting invite URL works/wallets/[id]/governance, click "Attach voting rationale" in the Vote card, write a comment, upload to IPFS, vote — confirm the on-chain tx carries the anchor (description includes "+ rationale")🤖 Generated with Claude Code