Files
zeekay 1a59337bf2 feat(web): finance.hanzo.ai product surface + @hanzo/finance-ui shared package + CI
Deliverable 1 of the two customer-facing Hanzo Finance surfaces. The umbrella repo
now carries the frontend alongside the Go financial core (embedded in cloud).

packages/finance-ui (@hanzo/finance-ui, PUBLISHED 0.1.1 to npm) — the DRY core BOTH
surfaces render, so a spend/usage/credits card is identical in finance.hanzo.ai and the
console Finance module:
  - FinanceClient boundary: stubFinanceClient() (preview/dev) ⇄ httpFinanceClient(transport)
    (live /v1/finance/*); the host injects auth/transport, swap = one line, zero UI change.
  - Optional-safe normalizers (renamed/absent field -> em-dash; card masked to brand+last4
    by construction; USD cents end to end). FinanceDashboard + monochrome, theme-aware,
    self-injecting styles (no CSS import). tsc clean; 27 unit tests.

web (finance.hanzo.ai, Next 15) — landing + Hanzo IAM login + treasury/payments app shell:
  - Marketing landing: canonical Hanzo mark (viewBox 0 0 67 67 / 44.6369), tight monochrome
    copy, embeds the SHARED board as the product preview.
  - Auth: hanzo-finance PUBLIC PKCE OAuth (Hanzo IAM, canonical /v1/iam/oauth/*), secretless
    BFF code exchange (+optional KMS confidential upgrade), sealed httpOnly session, AuthGate.
  - App shell renders the shared FinanceDashboard over /v1/finance/* via a bearer BFF proxy
    (stub preview until NEXT_PUBLIC_FINANCE_MODE=live). Build green (11/11 routes); the Next
    standalone server boots + serves verified.
  - web/iam/: hanzo-finance app REGISTERED + VERIFIED LIVE in IAM (get-app-login resolves,
    no 'Invalid client_id'); JSON + runbook committed for re-provisioning.

CI: root hanzo.yml (images finance-web + test + deploy) + ~15-line cicd.yml importing
hanzoai/ci@main on arc (zero GHA minutes). web/Dockerfile builds the pnpm-workspace Next
standalone image — built by CI/CD, never locally.
2026-07-05 13:49:09 -07:00

33 lines
1.3 KiB
TypeScript

import { NextResponse } from 'next/server'
import { cookies } from 'next/headers'
import { resolveConfig } from '~/config'
import { refreshGrant, sessionFromTokens } from '~/lib/auth/iam-server'
import { readSession, writeSession, accountOf } from '~/lib/auth/session'
export const runtime = 'nodejs'
/**
* Silent token refresh (rotation-aware). On success it rotates the sealed session; on
* failure it NEVER clears the cookie (a multi-tab rotation race must not nuke the
* winner's fresh session) — only an explicit sign-out clears.
*/
export async function POST(req: Request): Promise<Response> {
const store = await cookies()
const current = readSession(store)
if (!current?.refresh) return NextResponse.json({ account: null }, { status: 401 })
const cfg = resolveConfig(req.headers.get('host'))
try {
const tokens = await refreshGrant({ refresh: current.refresh, clientId: cfg.iamClientId })
const session = sessionFromTokens(tokens, cfg.iamOrg)
if (!session) return NextResponse.json({ account: null }, { status: 401 })
// Keep the prior refresh token if the grant didn't rotate one.
if (!session.refresh) session.refresh = current.refresh
const res = NextResponse.json({ account: accountOf(session) })
writeSession(res, session)
return res
} catch {
return NextResponse.json({ account: null }, { status: 401 })
}
}