31 file(s), +1705 / -11 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +43 | -0 |
| messages/hi.json | Config | +43 | -0 |
| src/app/[locale]/(workspace)/settings/firm/channels/page.tsx | App route | +56 | -0 |
| src/app/api/internal/channels/bindings/[bindingId]/route.ts | App route | +32 | -0 |
| src/app/api/internal/channels/bindings/route.ts | App route | +11 | -0 |
| src/app/api/internal/channels/enrollment-codes/route.ts | App route | +29 | -0 |
| src/components/settings/channels/channel-binding-card.stories.tsx | Component | +47 | -0 |
| src/components/settings/channels/channel-binding-card.tsx | Component | +80 | -0 |
| src/components/settings/channels/channel-history.stories.tsx | Component | +32 | -0 |
| src/components/settings/channels/channel-history.tsx | Component | +31 | -0 |
| src/components/settings/channels/channels-fixtures.ts | Component | +21 | -0 |
| src/components/settings/channels/channels-page.stories.tsx | Component | +62 | -0 |
| src/components/settings/channels/channels-page.tsx | Component | +143 | -0 |
| src/components/settings/channels/connect-channel-card.stories.tsx | Component | +32 | -0 |
| src/components/settings/channels/connect-channel-card.tsx | Component | +36 | -0 |
| src/components/settings/channels/copy-code-button.stories.tsx | Component | +26 | -0 |
| src/components/settings/channels/copy-code-button.tsx | Component | +43 | -0 |
| src/components/settings/channels/disconnect-binding-dialog.stories.tsx | Component | +35 | -0 |
| src/components/settings/channels/disconnect-binding-dialog.tsx | Component | +63 | -0 |
| src/components/settings/channels/enrollment-code-card.stories.tsx | Component | +49 | -0 |
| src/components/settings/channels/enrollment-code-card.tsx | Component | +90 | -0 |
| src/components/settings/channels/presentation.ts | Component | +28 | -0 |
| src/components/settings/channels/use-countdown.ts | Component | +22 | -0 |
| src/components/settings/channels/use-unbind-binding.ts | Component | +69 | -0 |
| src/components/settings/settings-sidebar.tsx | Component | +8 | -0 |
| src/lib/channels/gateway.ts | Library | +84 | -9 |
| src/lib/kubb-client.ts | Library | +13 | -0 |
| src/lib/surface-registry/page-card/catalog.ts | Library | +3 | -0 |
| src/lib/surface-registry/route-registry.ts | Library | +12 | -2 |
| tests/unit/channels-bindings-route.test.ts | Test | +67 | -0 |
| tests/unit/channels-page.test.tsx | Test | +395 | -0 |
RFD-AGT-013 (T2c-a, acceptance UAT M-01) gives every
counsel a self-service surface to manage their own channel bindings:
Settings → Channels (/settings/firm/channels). A counsel mints a
single-use 10-minute enrollment code, sends it to Mira from their own WhatsApp, watches
the page flip to the bound state on its own, sees the binding lifecycle status
(active / needs_reattestation / revoked) rendered
from API state, and disconnects (unbinds) behind a confirm dialog. The API is
pramaan-functions Story-2848 (merged #788) — enrollment ceremony, own-bindings list,
and self-service unbind, all scoped to the caller (no settings:write needed).
Connect → code on screen → send it on WhatsApp → page confirms automatically → bound card with masked number → Disconnect → confirm → instantly refused on the channel.
| Layer | What landed |
|---|---|
| Contract mirror | openapi/app-api.json + permission-map.json re-synced from
pramaan-functions main (per the openapi mirror guard + LI-6, codegen was
forced onto the vendored mirror), then pnpm codegen — new generated hooks:
useAppListMyBindings, useAppIssueEnrollmentCode,
useAppUnbindBinding (+ firm-register hooks for the later T2m-b admin UI). |
| BFF proxy | src/app/api/internal/channels/bindings/route.ts (GET),
bindings/[bindingId]/route.ts (DELETE),
enrollment-codes/route.ts (POST, body validated with the generated
issueEnrollmentCodeRequestSchema, 201). toBrowserRoute() in
src/lib/kubb-client.ts maps the three /app/channels/* paths —
the browser only ever talks to /api/internal/*. |
| Gateway (HR-21) | src/lib/channels/gateway.ts — every call fetches as
unknown and passes through assertContract
(channelBindingListResponseSchema / channelBindingViewSchema /
enrollmentCodeViewSchema) before a typed value crosses into the app. |
| Route | settings/firm/channels/page.tsx — auth gate, RSC-prefetch of the
bindings list into a dehydrated cache (pramaan-docs ADR-095; prefetch 5xx degrades to
CatalogLoadError, Bug-1781), agentSurface export
(settings.channels), route registry regenerated (pnpm gen:routes,
check:routes green). |
| Client UI | src/components/settings/channels/ — channels-page.tsx
(orchestrator), channel-binding-card.tsx (provider, masked subject, status
badge, disconnect), enrollment-code-card.tsx (one-time code, 3-step
instructions, live expiry countdown, regenerate), presentation.ts
(status→tone/label mappings), plus co-located stories for each component. |
| Nav + i18n | Sidebar gains Channels under Firm configuration (icon
MessageSquare); Channels namespace +
SettingsNav.channels + AgentSurfaces.settingsChannels added to
BOTH en.json and hi.json. |
POST /app/channels/enrollment-codes → the plaintext code is shown exactly
once (the backend stores only its SHA-256 hash) with a live 10-minute countdown and a
copy button. Expired → Get a new code.GET /app/channels/bindings every 4s
while a code is outstanding; polling stops by itself once a binding lands.•••1234) is ever rendered — the
full number never crosses the wire.
Disconnect opens an AlertDialog naming the masked subject and the
consequences (channel refusal + dead document links). Confirming fires
DELETE /app/channels/bindings/{id} through optimisticCollectionPatch:
the cached binding flips to revoked instantly, rolls back + error-toasts on
failure, and reconciles on settle — per the optimistic-writes doctrine.
API status | Badge | Note |
|---|---|---|
active | Active (green) | Disconnect action available. |
revoked | Disconnected (red) | Moves to Previous connections with date + reason (unbind / admin_revoke / offboard / remote_logout). |
These are the two statuses the current contract (Story-2848) ships; dates render
locale-aware via next-intl useFormatter (Hindi UI gets Hindi month names).
SettingsPageHeader,
StatusBadge, Button/PrimaryButton,
AlertDialog, and catalog card chrome only.tests/unit/channels-page.test.tsx — 9 tests: bound-state render
(masked subject + status), disconnect confirm dialog → unbind → cache flips revoked →
success toast, unbind failure → cache rolls back + error toast, enrollment happy path
(empty state → code issued → code card with steps), auto-confirm when the binding
appears (toast + code hidden), enrollment polling option (4s while a code is out,
stops once bound), issue failure toast, load-failure inline error, revoked
history rows with reason and no actions.
tests/unit/channels-bindings-route.test.ts — 2 tests: non-UUID
bindingId rejected 422 against the generated path contract (HR-21),
valid UUID forwarded with upstream headers.pnpm lint clean · pnpm typecheck clean ·
pnpm test green · pnpm check:routes fresh.en + hi.