Files
zeekay f27888bf91 feat(investor): G-22c wallet (10.4) + secondary market (10.5) + identity (10.7-10.9)
Wallet / payment-method management (Stage 10.4):
- /wallet route: linked methods table (ACH/card/wire/IRA), Add-method panel
  (Plaid/Stripe Elements/wire instructions/NCPS custody), stablecoin
  WalletConnect + Lux MPC linkage with per-row verify / set-default /
  unlink actions. Defaults scoped per use (settlement vs distributions).
- Backed by treasury/pkg/provider/* via gateway.wallet.

Secondary market (Stage 10.5):
- /market route: browse offerings, drill into per-offering order book +
  trade history + order entry; my-orders panel with status colouring.
- Order entry is limit-only (ATS time-price priority); tick / lot
  validated client-side; submit triggers the G-37 pre-trade gate
  (broker/pkg/pretrade) and renders the Decision (Allow / Deny with
  reasons + required-actions deep-linked to /kyc, /ofac; Escalate with
  reviewer-pending notice).
- Backed by gateway.market → broker/pkg/router (pretrade middleware
  wrapped) → broker/pkg/provider/northcapital for NCPS-backed trades.

Identity (Stage 10.7 + 10.8 + 10.9):
- /kyc: KYC refresh (W-9 US / W-8 non-US with TIN tokenised in-browser),
  accreditation renewal (income / net-worth / professional / entity /
  506(c) third-party), W-8 renewal (3-year Treas. Reg. §1.1441-1(e)(4)
  cycle).
- /profile: beneficial-owner address change with audit log + OFAC re-screen
  trigger; effective-date queued via beneficial-owner workflow.
- /beneficiary: jurisdiction-aware TOD designation (community-property
  US states + forced-heirship jurisdictions surface advisory warnings);
  share-sum = 1 enforced client-side and server-side; high-value
  accounts route through transfer/pkg/legalprocess dual-control.
- Backed by gateway.identity → captable/pkg/kyc + captable/pkg/tax +
  transfer/pkg/legalprocess.

Foundation contributions (cross-agent):
- Extended src/lib/gateway/index.ts with wallet / market / identity
  sub-clients (Agent A's `as const` widening pattern).
- src/lib/gateway/types.ts: PaymentMethod, MarketOffering, OrderBookSnapshot,
  TradePrint, MarketOrder, PlaceOrderRequest/Response, GateDecision,
  GateReason, IdentityProfile, PostalAddress, Beneficiary, KYCRefreshRequest,
  AccreditationRefreshRequest, W8RenewalRequest, AddressChangeRequest,
  BeneficiarySlate — all mirror canonical Go structs.
- src/lib/gateway/fixtures.ts: deterministic fixtures for every domain so
  routes are unit-renderable without a live gateway.

Tests (26 new):
- wallet.render.test.tsx (5) — MethodsTable rows, defaults rendering,
  Verify affordance for pending micro-deposits, empty state; StablecoinPanel
  table + link form.
- market.gate.test.tsx (8) — GateDecisionView allow / deny / escalate;
  OrderEntryPanel allow / deny / escalate gate responses; tick / lot
  client-side validation.
- identity.forms.test.tsx (8) — W-9 path with in-browser TIN tokenisation
  (verifies cleartext never leaves browser); W-8 path with treaty fields;
  AccreditationForm method + bands; W-8RenewalForm attestation gate;
  ProfileForm address change with reason + effective date.
- beneficiary.render.test.tsx (5) — slate render, share-sum gate (disables
  submit), valid submit, add row, pending dual-control row.

Build: pnpm build green (Next.js 15, 17 routes). pnpm test green (68/68).

Source-of-design: Lux-Prior-IP
Source-ref: broker 80e2f747 (pkg/pretrade + pkg/router), transfer 6f327c99
  (pkg/legalprocess), captable 3bcf2a83 (pkg/kyc + pkg/stakeholder +
  pkg/transfer + pkg/tax), treasury 3d27199d (pkg/provider/*)
Clean-Team-Member: dev-agent (G-22c wallet + market + identity)
Signed-off-by: dev <[email protected]>
2026-05-26 21:05:57 -07:00
..

@luxbank/investor — Lux Investor Portal

Investor-facing portal for investor.lux.financial. Next.js 15 + React 19 + Tailwind 4, TypeScript strict mode, deployed to Cloudflare Pages.

Counterpart to the operator console at app.lux.financial (@luxbank/console) and the marketing site at lux.financial (@luxbank/site). Reads from the four backend services (luxfi/captable, luxfi/transfer, luxfi/treasury, luxfi/broker) through the operator gateway — never directly.

What's in here

This portal implements the §10 substages of the Lux RWA / Digital Securities gap analysis (gap G-22). Substage ownership is split across three parallel build agents:

Stage Surface Owner
10.1 Account dashboard (positions, NAV, cost basis, unrealized P&L, pending settlement) A (foundation)
10.2 Document vault B
10.3 Communications inbox B
10.4 Wallet / payment methods C
10.5 Secondary market C
10.6 Tax documents (1099 / K-1 / 1042-S / cost basis) B
10.7 KYC refresh + W-8 renewal C
10.8 Beneficial-owner / address change C
10.9 Beneficiary / TOD designation C
10.10 Audit log (investor-initiated actions) A (this)

The foundation — auth middleware, gateway clients, shell, sign-in flow — is owned by agent A and used by all substages.

Architecture

investor.lux.financial  (this app)
        │
        │   1. browser holds an HTTP-only session cookie containing the
        │      JWT issued by Hanzo IAM (iam.luxfi.io)
        │
        │   2. src/middleware.ts verifies the JWT (jose + JWKS) and injects
        │      X-Lux-Sub / X-Lux-Investor-Id / X-Lux-Tenant-Id headers
        │
        │   3. server components call src/lib/gateway/* which proxies through
        │      the operator gateway at NEXT_PUBLIC_GATEWAY_URL
        │
        ▼
api.lux.financial  (operator gateway)
        │
        │   enforces tenant + investor scope; signs requests forward
        │
        ▼
    ┌───────────┬───────────┬────────────┬──────────┐
    │ captable  │ transfer  │  treasury  │  broker  │
    └───────────┴───────────┴────────────┴──────────┘

The browser never sees a backend URL. Every gateway client function in src/lib/gateway/* builds a request to the gateway and the gateway routes by URL prefix (/v1/captable/…, /v1/transfer/…, …) with the identity headers preserved.

Routes

Path What
/ Redirect to /dashboard.
/dashboard Positions, NAV, cost basis, unrealized P&L, pending settlements.
/activity Audit-log timeline of investor-initiated actions with CSV export.
/signin OIDC + magic-link sign-in.
/signout Clears the session cookie and bounces to IAM logout.
/api/auth/callback OIDC code-exchange handler.
/api/auth/magic-link Magic-link request handler.
/api/audit/export.csv Proxies the gateway CSV export endpoint.

Agent B / C own the routes for §10.2–§10.9; they live alongside this in src/app/(authed)/{documents,inbox,tax,wallet,market,kyc,profile,beneficiary}.

Run

pnpm -F @luxbank/investor dev
# → http://localhost:3002

Static export for Cloudflare Pages:

pnpm -F @luxbank/investor export
pnpm -F @luxbank/investor deploy:cf

Tests

pnpm -F @luxbank/investor test

Vitest + Testing Library + happy-dom. Covers:

  • auth/session — header-based session extraction and validation.
  • auth/sign-in-url — OIDC authorize / sign-out URL construction.
  • gateway/transport — URL routing, identity-header injection, JSON body handling, error mapping (GatewayError).
  • gateway/audit — CSV row serialization with escape rules.
  • format — currency / percent / number / date formatters.
  • dashboard — render of summary card, positions table, settlements card.
  • activity — render of audit timeline.
  • middleware — matcher configuration.

Environment variables

Key Default Used by
NEXT_PUBLIC_GATEWAY_URL https://api.lux.financial Browser + server fetch base.
NEXT_PUBLIC_IAM_URL https://iam.luxfi.io OIDC authorize / logout URLs.
NEXT_PUBLIC_IAM_CLIENT_ID lux-investor-portal OIDC client.
IAM_AUDIENCE lux-investor-portal JWT audience claim check.
IAM_JWKS_URL https://iam.luxfi.io/.well-known/jwks.json JWT signature verification.
IAM_ISSUER https://iam.luxfi.io JWT issuer claim check.
NEXT_PUBLIC_SESSION_COOKIE lux_investor_session Session cookie name.
NEXT_PUBLIC_PORTAL_URL https://investor.lux.financial Used by sign-out for absolute redirect.
LUX_BRAND_INLINE (unset) Optional JSON brand override for white-label deploys.

Secrets (anything not NEXT_PUBLIC_*) flow from KMS into the runtime via the standard deploy pipeline. Nothing is committed to git.

Auth contract — for other agents working in this app

The middleware sets these headers on every (authed) request; any new page, server component, or API route can read them via next/headers:

Header Meaning
X-Lux-Sub Stable IAM subject ID.
X-Lux-Investor-Id Investor record ID; the gateway scopes data to this.
X-Lux-Tenant-Id Issuer tenant ID.
X-Lux-Email Display email.
X-Lux-Name Display name.
X-Lux-Exp Token expiry (unix seconds).

Helpers:

import { headers } from 'next/headers';
import { readSessionFromHeaders } from '@/lib/auth';

const session = readSessionFromHeaders(await headers());

Gateway-client contract

import { gateway } from '@/lib/gateway';

const summary    = await gateway.captable.portfolioSummary('fifo');
const positions  = await gateway.captable.listPositions('fifo');
const pending    = await gateway.transfer.listPendingSettlements();
const balances   = await gateway.treasury.balances();
const orders     = await gateway.broker.listOpenOrders();
const audit      = await gateway.audit.list({ category: 'trade' });

Each method returns a typed Promise. Errors throw GatewayError with {service, status, body, message} for diagnostic logging. No silent failures, no soft-typed any.

Adding a new sub-client (e.g. wallet, market):

  1. Create src/lib/gateway/<name>.ts with export const <name> = { … }.
  2. Add the import + entry to src/lib/gateway/index.ts.
  3. Re-use callGateway() from transport.ts. Never call fetch directly.

Provenance

This module is built under the Clean-Room Engineering Protocol — see legal/INDEPENDENT-IMPLEMENTATION-CLEAN-ROOM-PROTOCOL.md. Each commit carries the provenance trailer required by §4 of that protocol.