Skip to content

feat(auth): add account-domain OIDC, actions, and access UI#112

Closed
tnunamak wants to merge 43 commits intomainfrom
tim/account-domain-identity-issuer
Closed

feat(auth): add account-domain OIDC, actions, and access UI#112
tnunamak wants to merge 43 commits intomainfrom
tim/account-domain-identity-issuer

Conversation

@tnunamak
Copy link
Copy Markdown
Member

@tnunamak tnunamak commented Apr 24, 2026

Summary

Adds the current account-domain implementation for the Login with Vana demo slice: OIDC foundations, transitional Privy-backed account sessions, account-hosted action requests, account access visibility, mock revocation controls, IAM-protected Hydra admin calls, and the feature-flagged Vana-root Privy JWT auth path prepared for later activation.

This PR contains two planning layers plus production-code seams:

  • account-domain-identity-issuer: earlier minimal account-domain issuer plan.
  • account-oidc-privy-actions: current implementation source of truth; this supersedes the earlier plan where they differ.
  • App/API/db code needed for the current local and preview Memory App demo in vana-connect-mobile#1.

Current Direction

  • OIDC sub is a Vana-owned opaque vana_user_id, not wallet address, email, Google subject, Privy id, WorkOS id, or Stytch id.
  • Privy is the first wallet/auth provider behind a Vana-owned account boundary. Current login still supports transitional Privy-native login evidence; this PR also adds a gated Privy JWT-based custom-auth path where Vana signs the root identity JWT.
  • Login with Vana is standard OIDC-shaped: account-domain issuer, Authorization Code + PKCE, state/nonce, Hydra proof, client policy, and RP fixture coverage.
  • OIDC login does not grant data access. Account-hosted actions are separate request/review/result-code flows.
  • ChatGPT access in this slice uses real account action request/exchange plumbing but mock execution/result behavior. Real data/RPC/encrypted references are explicit follow-up work.

Implementation State

  • Adds vana_account_session support so /login can establish a Vana-owned HTTP-only session after a successful Privy-authenticated login.
  • Wires /api/auth/session for the demo/client flow and preserves safe OIDC return_to behavior across login.
  • Adds Vana custom-auth JWT signing with RS256, sub=vana_user_id, issuer/audience/ttl validation, config inspection, and public JWKS at /.well-known/jwks.json.
  • Adds GET /api/auth/privy-custom-auth-jwt, which resolves verified login evidence to a Vana account and returns a Vana-signed JWT for Privy JWT-based auth.
  • Adds non-secret custom-auth diagnostics at /api/auth/privy-custom-auth-jwt/diagnostics for non-production / explicitly enabled environments.
  • Adds a client useSyncJwtBasedAuthState bridge under PrivyProvider, gated by NEXT_PUBLIC_PRIVY_JWT_AUTH_SYNC_ENABLED until the Privy dashboard is configured and verified.
  • Adds pnpm auth:smoke-custom-jwt -- <base-url> to smoke JWKS, diagnostics, and unauthenticated JWT behavior without requiring Privy Scale-plan access.
  • Adds .env.local.example placeholders and docs/privy-custom-auth-runbook.md covering the Privy Scale-plan gate, dashboard setup, switch-on steps, and rollback.
  • Extends account actions persistence and route handlers with approval/exchange events, display-safe action details, and mock-first result handling.
  • Adds /account/access for users to review connected apps and grants.
  • Adds account access APIs for summary, grant revoke, and app disconnect.
  • Revocation is intentionally mock-first: local grant state is marked revoked and action.revoked events are recorded; RPC revocation is not live yet.
  • Improves account shell/navigation so /server is no longer surfaced in the app chrome.
  • Adds display/copy helpers so action and access pages show ChatGPT scope/purpose/access semantics clearly instead of raw internal payloads.
  • Adds local account/Hydra support scripts and config for the Memory App client, including demo and dev callback URIs.
  • Adds safe account DB migration tooling for local and remote DBs. Remote migrations require both ACCOUNT_DB_MIGRATE_ALLOW_REMOTE=true and --allow-remote.
  • Adds Google service-account JWT exchange for Hydra admin calls, so account can call IAM-protected Cloud Run admin with an audience-bound Google ID token when GCP_SERVICE_ACCOUNT_KEY is configured.

Related PRs

Linear Follow-Ups

Validation

Latest validation:

  • cd connect && pnpm exec tsc --noEmit
  • cd connect && pnpm lint
  • cd connect && pnpm test => 322 passed, 17 DB-backed skipped.
  • cd connect && pnpm auth:smoke-custom-jwt -- https://account-dev.vana.org => ok, JWKS kid account-dev-rs256-20260430, jwtSyncEnabled=false, Privy plan gate Scale.
  • cd connect && node --check scripts/migrate-account-db.mjs
  • cd connect && node --check scripts/migrate-local.mjs
  • cd connect && env -u DATABASE_URL node scripts/migrate-account-db.mjs exits safely.
  • Remote fake-URL migration smoke exits safely without printing the fake URL/secret.
  • git diff --cached --check

Data Gateway / Real Grants Path

The current ChatGPT access flow is intentionally mock-first for execution/result payloads, but the grant/protocol slice has a clear path to become real without inventing a new RPC integration. The existing recent repos show the intended chain:

  • DataConnect talks to its bundled Personal Server over localhost for grant create/list/revoke.
  • The bundled Personal Server uses published @opendatalabs/personal-server-ts-* packages.
  • personal-server-ts owns the GatewayClient abstraction and calls Data Gateway HTTP endpoints like /v1/grants, /v1/servers, /v1/builders, and /v1/files.
  • vana-com/data-gateway is the Data Portability RPC Gateway: it stores/indexes protocol state, validates EIP-712 payloads, uses Data Portability contract configuration, and is backed by RPC_URL.

Practical next slice: replace mock grant create/revoke with the existing Personal Server -> Data Gateway path, store the returned grantId on the account action, and keep ChatGPT connector execution/result references mocked until connector runtime + encrypted reference delivery are wired. Direct account -> Data Gateway calls are possible but lower-level and more brittle because they require server/builder identity, file IDs, EIP-712 signatures, and protocol payload details that Personal Server already abstracts.

Known Gaps

  • Privy Custom authentication / JWT-based auth is gated behind the Privy Scale plan. Until enabled in the dashboard, NEXT_PUBLIC_PRIVY_JWT_AUTH_SYNC_ENABLED remains false.
  • We still need empirical dev verification of Privy's exact custom-auth user shape and migration behavior for existing Privy-native users/wallets after the paid feature is available.
  • Real ChatGPT data execution, encrypted bundle/reference return, Personal Server enforcement, live DP RPC/L1 writes, and continuous sync are out of scope for this PR.
  • Revocation records local/mock grant revocation only; real dependency/RPC revocation still needs implementation.

Deployment Note

Hydra admin calls support HYDRA_ADMIN_AUDIENCE separately from HYDRA_ADMIN_URL. For Cloud Run, keep HYDRA_ADMIN_URL on the readable admin domain (for example https://oauth-admin-dev.vana.org) and set HYDRA_ADMIN_AUDIENCE to the underlying Cloud Run service URL so Google ID tokens pass IAM audience validation.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
connect Ready Ready Preview May 4, 2026 6:59pm
vana-connect Error Error May 4, 2026 6:59pm

Request Review

Add the minimal account identity model from openspec/changes/account-oidc-privy-actions:

- Migration 004 introduces vana_users, vana_linked_wallets, vana_provider_links
  with idempotent CREATE statements matching the existing direct-Neon-SQL style.
- connect/src/lib/auth/vana-account.ts holds pure helpers (id generation,
  EVM address normalization, row mappers, OIDC claim builder). The vana_user_id
  is generated independently of any wallet/provider/email evidence.
- connect/src/lib/db/account.ts holds Neon-backed persistence and a transitional
  resolveVanaUserByPrivyEvidence merge step keyed on provider subject and
  embedded wallet address. Email is stored as audit metadata only and is never
  a merge key.
- Unit tests prove sub = vana_user_id, that wallet addresses surface as linked
  claims, that provider ids/email are stored as evidence, and that EVM address
  storage is normalized to lowercase. Tests do not require DATABASE_URL.

OpenSpec tasks 4.1-4.6 marked done; runtime OIDC integration remains out of
scope for this seam.
@tnunamak
Copy link
Copy Markdown
Member Author

Latest auth/custom-JWT update pushed in 3b5be115c and deployed to https://account-dev.vana.org via Vercel preview connect-1jflj710r-opendatalabs.vercel.app. Smoke: /.well-known/jwks.json returns 200 with public RS256 JWKS/kid account-dev-rs256-20260430; /api/auth/privy-custom-auth-jwt returns 401/no-store when unauthenticated; /login returns 200. Client sync remains gated by NEXT_PUBLIC_PRIVY_JWT_AUTH_SYNC_ENABLED until Privy dashboard trusts the JWKS and uses sub as the JWT ID claim.

@tnunamak
Copy link
Copy Markdown
Member Author

Second prep batch pushed in db4ebd580 and deployed to https://account-dev.vana.org via preview connect-nf8ezfklw-opendatalabs.vercel.app. Added custom-auth diagnostics, env example, smoke script, runbook, browser sync guardrail tests, and split jwt_not_configured vs jwt_signing_failed. Validation: pnpm exec tsc --noEmit, pnpm lint, pnpm test => 322 passed / 17 DB-backed skipped. Live smoke: pnpm auth:smoke-custom-jwt -- https://account-dev.vana.org => ok, JWKS kid account-dev-rs256-20260430, jwtSyncEnabled=false, Privy plan gate Scale. Remaining blocker is only Privy Custom authentication / JWT-based auth access on the paid plan before flipping the sync flag.

tnunamak added a commit that referenced this pull request May 1, 2026
## Summary

DELETE `/api/servers/{id}` was returning a generic 500 (`Failed to
deprovision server. Resources may still be running.`) regardless of
which step inside `provider.deprovision()` actually failed. Vercel's
events API only exposes build logs, so the underlying `console.error`
lines were not reachable from the dashboard. Three prior fixes (#113,
#114, #115) addressed plausible causes blind, and the deprovision still
500s in the field.

This change makes the next reproduction self-diagnosing.

## Changes

- `lib/server-provider/gcp.ts`
- Each error inside `deprovision()` is now tagged with its step
(`tunnel`, `vm-delete`, `vm-wait`, `disk:<name>`, `disks-init`) and the
gRPC/HTTP `code`.
- The thrown `Error` carries a `deprovisionErrors` array (step + code +
message per failure) in addition to the human-readable message.
- Split the initial VM `delete` from the LRO `op.promise()` wait so a
stale-op timeout is distinguishable from an immediate API error.
- Skip VM/disk steps when `serverId` is empty (defensive — the route
already gates on that, but the contract is now obvious).
- `app/api/servers/[id]/route.ts`
- 500 response body now includes the actual error message + step
summary, e.g. `Failed to deprovision server: Deprovision partially
failed: vm-wait(code=4): Deadline exceeded [steps: vm-wait(code=4)]`.
- Structured single-line `console.error` with `serverId`, `providerId`,
`tunnelId`, `dnsRecordId`, `errorName`, `errorMessage` so runtime logs
are greppable when they do become accessible.

No secrets are returned: only step labels and the provider's own error
text (GCP/Cloudflare error messages).

## Validation

- `pnpm exec tsc --noEmit` clean (excluding pre-existing `.next/`
validator stubs that reference unmerged PR #112 routes).
- `pnpm exec biome check` clean on changed files.
- `pnpm test`: same 7 pre-existing `use-login-page` failures as on main
(`localStorage.clear is not a function` — jsdom env, OIDC scope,
unrelated). My target `src/app/server/use-server.test.ts` passes.

## Test plan

- [ ] After merge + Vercel deploy on `account.vana.org/server`:
provision a fresh PS, click Remove, capture the 500 response body in
DevTools, share with team for the next targeted fix.
- [ ] Confirm the structured log line shows up in Vercel runtime logs
when accessible.
…ns (#124)

Stacked on PR #112. Targets that branch so the integration lands as part
of the OIDC slice.

## Summary

When a user approves an account-action with `execution_mode ===
"embedded_wallet_account_hosted"`, account.vana.org now mints a real
on-chain grant on the user's Personal Server. Replaces the mock-only
path. Also addresses the hardcoded/missing-data audit findings flagged
earlier.

## What's wired

1. **`oauth_clients` registry table** (migration 007) — replaces the
localStorage admin store. Builder identity (`grantee_address`,
`builder_id`, `public_key`) is optional (Sign-in-with-Vana works without
it) but all-or-nothing when set.
2. **Admin API** (`/api/admin/oauth-clients`) — POST upsert, GET list,
DELETE; owner-auth via masterKeySignature.
3. **`executeGrantViaPersonalServer` helper** — pure function: resolves
user PS + OAuth client, POSTs `<ps-url>/v1/grants` with `Bearer
<control_plane_token>`.
4. **`handleActionDecision` real-grant branch** — when execution_mode is
`embedded_wallet_account_hosted`, calls executor before persisting;
failure aborts approval with a typed error.
5. **Consent event audit** — populates `subject_wallet_address` (primary
linked wallet), `application_id` (oauth_clients.protocolPrincipal),
`authorization_reference` ({grantId, granteeAddress, personalServer}).
Subject wallet on denial too.
6. **`DEFAULT_ACCOUNT_ACTION_ISSUER`** reads `VANA_ACCOUNT_ISSUER` env
with literal fallback.
7. **DB-backed registry** with `DEV_MEMORY_APP_CLIENT` fallback so demo
flows keep working when the table is empty.

## Migration

`007_add_oauth_clients.sql` already applied to dev (`ep-red-river`) and
prod (`ep-hidden-glade`) Neon branches.

## Tests

322 passed / 17 skipped (matches baseline).

## Out of scope

- Migrate device-code state from sessionStorage → DB (#20)
- Migrate passport agreement from localStorage → consent event (#21)
- Wire admin UI to use the new API (currently still writes localStorage;
the API is in place)
- Action-result revocation wiring

## Test plan

- [ ] CI passes
- [ ] On account-dev: register an OAuth client with builder identity via
POST /api/admin/oauth-clients
- [ ] Trigger an action request from the demo Memory App with
execution_mode=embedded_wallet_account_hosted
- [ ] Approve the action — observe POST to `<user-ps>/v1/grants`
succeed, `grantId` populated, `authorization_reference` set on consent
event
tnunamak added a commit that referenced this pull request May 4, 2026
Brings the OIDC slice (PR #112's branch tip) into develop so
account-dev.vana.org can serve the full Login-with-Vana flow + real-PS
grants (PR #124).

One conflict resolved: server/page.tsx import keeps the Discoverable UI
(RegistrationStatus type) added in PR #122/#123.

Production untouched; this PR targets develop only.
@tnunamak
Copy link
Copy Markdown
Member Author

tnunamak commented May 5, 2026

Closing — content of this PR is on develop via #125 (May 4), and has since been superseded by the auth redesign in #134 (Hydra headless OIDC + Vana session) which replaces the Privy-JWT-custom-auth path this branch was preparing. A clean develop→main cutover will be a fresh PR.

@tnunamak tnunamak closed this May 5, 2026
@tnunamak tnunamak deleted the tim/account-domain-identity-issuer branch May 5, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant