# Next.js — the files and the code

Use exactly these files unless a concrete fact in this repository requires otherwise. `inspect` prints
the resolved paths as `files` (with `src/` when the app uses `src/app`). Install the package first with
`adapter.install` from inspect (for example `pnpm add @onetwoagent/integration`).

| Piece | App Router | Pages Router |
| --- | --- | --- |
| Widget | root `app/layout.tsx` | `pages/_document.tsx` |
| Identity | `app/api/onetwoagent/identity/route.ts` (GET) | `pages/api/onetwoagent/identity.ts` |
| Account lookup | `app/api/onetwoagent/account/route.ts` (POST) | `pages/api/onetwoagent/account.ts` |
| Your mapping | `lib/onetwoagent/account-data.ts`: session, workspace, live check, projections | same |
| Browser lifecycle | `lib/onetwoagent/identity-client.tsx`, rendered in the signed-in layout | same, rendered from `_app.tsx` for signed-in pages |
| Sign-out | wrap the app's existing sign-out function once | same |

Middleware or a proxy must not redirect or authenticate `/api/onetwoagent/*`: OneTwoAgent calls the
account route server-to-server, without cookies.

`.onetwoagent.json` sits in the app folder: import it as `@/.onetwoagent.json` when the `@/*` alias maps to
the app folder, `@/../.onetwoagent.json` when it maps to `src/` (check `tsconfig.json`), or with a relative path.

## Widget

A plain tag in the root layout's `<body>`, so it is in the served HTML (doctor checks the HTML):

```tsx
import onetwoagent from '../.onetwoagent.json' // relative path from the layout to the app folder

// inside <body>, after the app's content:
<script src="https://widget.onetwoagent.com/widget/v1.js" data-id={onetwoagent.publicId} defer />
```

With a nonce-based Content-Security-Policy, also pass `nonce={nonce}` (`references/csp.md`).

## Your mapping (`lib/onetwoagent/account-data.ts`)

The only app-specific code. Pure reads, explicit fields (`references/account-data.md`):

```ts
import { supportView } from '@onetwoagent/integration/server'

// The signed-in user, or null. sessionId is the session ROW id (never a cookie or token value).
export async function currentSession() { /* the app's existing helper */ }
// The workspace the user is acting in, checked against membership the way the app's pages do.
export async function supportAccount(userId: string) { /* → { workspaceId, facts: { name, plan, role, usage } } or null */ }
// EVERY account read: is that session still signed in, is the user still a member? Two plain SELECTs.
export async function isSessionLive(grant: { userId: string; workspaceId: string; sessionId: string }) {
  return { session: /* session row exists, belongs to grant.userId, not expired or revoked */ false,
           member: /* membership of grant.userId in grant.workspaceId still exists */ false }
}
export async function readSection(subject: { userId: string; workspaceId: string }, section: string) { /* e.g. { usage: [...] } */ }
export async function readView(subject: { userId: string; workspaceId: string }, view: string) {
  // e.g. projects: newest first, ≤ 6 records, explicit fields
  return supportView({ summary: '2 projects need attention', total: 14, records: [/* { id, type, title, status, updatedAt, summary, fields } */] })
}
```

## Identity route (App Router)

```ts
import { createIdentityWebHandler } from '@onetwoagent/integration/server'
import onetwoagent from '@/.onetwoagent.json' // '@/../.onetwoagent.json' when @/* maps to src/
import { currentSession, supportAccount } from '@/lib/onetwoagent/account-data'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

export const GET = createIdentityWebHandler({
  apiBaseUrl: onetwoagent.apiBaseUrl,
  identitySecret: process.env.ONETWOAGENT_WIDGET_IDENTITY_SECRET,
  readCredential: process.env.ONETWOAGENT_ACCOUNT_READ_CREDENTIAL,
  getSession: async () => {
    const session = await currentSession()
    return session ? { userId: session.user.id, email: session.user.email, name: session.user.name, sessionId: session.id } : null
  },
  getAccount: session => supportAccount(session.userId),
})
```

It answers 401 when nobody is signed in, identifies the person without account data if `getAccount`
fails, adds the signed grant itself, and never sends the secret anywhere but OneTwoAgent. Its log lines
are codes and field paths only (`[onetwoagent] IDENTITY_EXCHANGE_REJECTED at email (schema)`).

## Account lookup route (App Router)

```ts
import { createAccountLookupWebHandler } from '@onetwoagent/integration/server'
import onetwoagent from '@/.onetwoagent.json' // '@/../.onetwoagent.json' when @/* maps to src/
import { isSessionLive, readSection, readView } from '@/lib/onetwoagent/account-data'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

export const POST = createAccountLookupWebHandler({
  readCredential: process.env.ONETWOAGENT_ACCOUNT_READ_CREDENTIAL,
  businessId: onetwoagent.businessId,
  allowedSections: ['usage'],   // exactly what readSection implements and the descriptor lists
  allowedViews: ['projects'],   // exactly what readView implements; none if the app has no such concept
  isSessionLive,
  readSection,
  readView,
})
```

`subject` comes from the verified grant, never from the browser or the model. The handler rejects
everything else (credential, schema, scope, forged or expired grants, ended sessions) with fixed errors.

**Pages Router:** `export default createIdentityHandler({ ...same options, getSession: async req => … })` and
`export default createAccountLookupHandler({ ...same options })`. The default body parser may stay on (requests
OneTwoAgent sends are unaffected); `export const config = { api: { bodyParser: false } }` on the account route
makes even malformed bodies get the fixed error body.

## Browser lifecycle (`lib/onetwoagent/identity-client.tsx`)

```tsx
'use client'
import { useEffect } from 'react'
import { getWidgetIdentity } from '@onetwoagent/integration/browser'

export function OneTwoAgentIdentity() { // render it in the signed-in layout
  useEffect(() => {
    const identity = getWidgetIdentity()
    const stop = identity.start()  // answers otw:identity:required; one listener however often it mounts
    void identity.refresh()        // 'identified' | 'signed_out' | 'unavailable' | 'superseded'
    return stop
  }, [])
  return null
}
```

## Sign-out

Find the app's sign-out function (usually one export used by every sign-out button) and wrap it once:

```ts
import { getWidgetIdentity } from '@onetwoagent/integration/browser'

export const signOut: typeof authClient.signOut = (...args) => {
  getWidgetIdentity().invalidate() // synchronous: drops in-flight tokens and resets the Widget
  return authClient.signOut(...args)
}
```

If sign-out is spread over several call sites, call `getWidgetIdentity().invalidate()` at each one, before
the app's own sign-out. A plain HTML sign-out form keeps posting as it does: render it from a small client
component whose `onSubmit` calls `invalidate()`. Do the same before switching workspace, then `refresh()`.
