55 file(s), +4265 / -3 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +20 | -0 |
| messages/hi.json | Config | +20 | -0 |
| openapi/app-api.json | Config | +589 | -0 |
| openapi/permission-map.json | Config | +39 | -0 |
| src/app/[locale]/(workspace)/settings/account/security/actions.test.ts | App route | +250 | -0 |
| src/app/[locale]/(workspace)/settings/account/security/actions.ts | App route | +154 | -0 |
| src/app/[locale]/(workspace)/settings/account/security/page.tsx | App route | +40 | -0 |
| src/components/settings/security/account-security-page.stories.tsx | Component | +36 | -0 |
| src/components/settings/security/account-security-page.test.tsx | Component | +171 | -0 |
| src/components/settings/security/account-security-page.tsx | Component | +125 | -0 |
| src/components/settings/security/remote-logout-dialog.stories.tsx | Component | +63 | -0 |
| src/components/settings/security/remote-logout-dialog.tsx | Component | +73 | -0 |
| src/components/settings/settings-sidebar.tsx | Component | +15 | -1 |
| src/gen/client/appIssueEnrollmentCode.ts | Generated contract | +67 | -0 |
| src/gen/client/appListFirmBindings.ts | Generated contract | +54 | -0 |
| src/gen/client/appListMyBindings.ts | Generated contract | +54 | -0 |
| src/gen/client/appRevokeFirmBinding.ts | Generated contract | +63 | -0 |
| src/gen/client/appUnbindBinding.ts | Generated contract | +63 | -0 |
| src/gen/client/index.ts | Generated contract | +5 | -0 |
| src/gen/hooks/index.ts | Generated contract | +28 | -0 |
| src/gen/hooks/useAppIssueEnrollmentCode.ts | Generated contract | +144 | -0 |
| src/gen/hooks/useAppListFirmBindings.ts | Generated contract | +114 | -0 |
| src/gen/hooks/useAppListFirmBindingsSuspense.ts | Generated contract | +113 | -0 |
| src/gen/hooks/useAppListMyBindings.ts | Generated contract | +114 | -0 |
| src/gen/hooks/useAppListMyBindingsSuspense.ts | Generated contract | +113 | -0 |
| src/gen/hooks/useAppRevokeFirmBinding.ts | Generated contract | +146 | -0 |
| src/gen/hooks/useAppUnbindBinding.ts | Generated contract | +146 | -0 |
| src/gen/index.ts | Generated contract | +146 | -0 |
| src/gen/schemas/channelBindingListResponse.json | Generated contract | +76 | -0 |
| src/gen/schemas/channelBindingView.json | Generated contract | +62 | -0 |
| src/gen/schemas/enrollmentCodeView.json | Generated contract | +25 | -0 |
| src/gen/schemas/issueEnrollmentCodeRequest.json | Generated contract | +16 | -0 |
| src/gen/types/AppIssueEnrollmentCode.ts | Generated contract | +61 | -0 |
| src/gen/types/AppListFirmBindings.ts | Generated contract | +54 | -0 |
| src/gen/types/AppListMyBindings.ts | Generated contract | +54 | -0 |
| src/gen/types/AppRevokeFirmBinding.ts | Generated contract | +69 | -0 |
| src/gen/types/AppUnbindBinding.ts | Generated contract | +69 | -0 |
| src/gen/types/ChannelBindingListResponse.ts | Generated contract | +19 | -0 |
| src/gen/types/ChannelBindingView.ts | Generated contract | +50 | -0 |
| src/gen/types/EnrollmentCodeView.ts | Generated contract | +27 | -0 |
| src/gen/types/IssueEnrollmentCodeRequest.ts | Generated contract | +19 | -0 |
| src/gen/types/index.ts | Generated contract | +59 | -0 |
| src/gen/zod/appIssueEnrollmentCodeSchema.ts | Generated contract | +81 | -0 |
| src/gen/zod/appListFirmBindingsSchema.ts | Generated contract | +73 | -0 |
| src/gen/zod/appListMyBindingsSchema.ts | Generated contract | +73 | -0 |
| src/gen/zod/appRevokeFirmBindingSchema.ts | Generated contract | +88 | -0 |
| src/gen/zod/appUnbindBindingSchema.ts | Generated contract | +88 | -0 |
| src/gen/zod/channelBindingListResponseSchema.ts | Generated contract | +28 | -0 |
| src/gen/zod/channelBindingViewSchema.ts | Generated contract | +42 | -0 |
| src/gen/zod/enrollmentCodeViewSchema.ts | Generated contract | +27 | -0 |
| src/gen/zod/index.ts | Generated contract | +54 | -0 |
| src/gen/zod/issueEnrollmentCodeRequestSchema.ts | Generated contract | +27 | -0 |
| src/lib/channels/gateway.ts | Library | +44 | -0 |
| src/lib/surface-registry/page-card/catalog.ts | Library | +3 | -0 |
| src/lib/surface-registry/route-registry.ts | Library | +12 | -2 |
A lost or stolen phone must be killable from any web session immediately. RFD-AGT-013 (D18/D19, CC-25/CC-45) makes remote logout the stolen-phone kill switch: one action ends every PRAMAAN session on every device and disconnects the counsel's conversational channels, so a thief holding an enrolled WhatsApp number gets nothing — the channel refuses the number and every outstanding document link dies with the sessions. This story ships the app-side entry point under Settings → Account → Sign-in & devices.
Settings → Account → Sign-in & devices → "Log out everywhere" → destructive confirm → bindings revoked, every Clerk session revoked → channel refuses the counsel until re-enrollment with a one-time code.
| Step | Call | Why this order / scope |
|---|---|---|
| 1. Revoke channel bindings | GET /app/channels/bindings then DELETE /app/channels/bindings/{id} per active binding (functions T2a-2 contract, merged) |
FIRST, because the gateway call needs the caller's still-live Clerk bearer. Each revoke kills the binding's derived sticky context and its pending fetch links (functions _revoke_binding, CC-25). The channel then refuses the number via the functions-side zero-amp shield (Story-2850). |
| 2. Revoke every Clerk session | clerkClient.sessions.getSessionList({ userId, status: "active" }) (paged) → revokeSession per session, including this one |
Every wrapper/document open requires an active app session (D18), so this is the door slam (CC-45). Paging is defensive — a partial list would silently leave devices alive. |
| 3. Client sign-out | Clerk signOut({ redirectUrl: /{locale}/sign-in }) |
The current session is already dead server-side; this clears local Clerk state and lands on the localized sign-in page. |
remote_logout.binding_* /
remote_logout.session_* events with trace_id).
ok:false, localized formError, no sign-out —
the counsel stays signed in and can retry).
STATIC_REFUSAL_TEXT, Story-2850: the refusal tells the
sender to re-connect under Settings → Channels with a one-time code) —
the refusal itself is enforced functions-side, not invented here.
successToastChannels), before the
client signs out.
src/app/[locale]/(workspace)/settings/account/security/page.tsx
— server shell (auth via requireOkAppMe; deliberately NO
settings:write gate — remote logout is the counsel's own
kill switch), binds the action, exports agentSurface
(settings.account-security).
src/app/[locale]/(workspace)/settings/account/security/actions.ts
— remoteLogoutAction server action (typed
RemoteLogoutResult, localized errors via
getTranslations).
src/lib/channels/gateway.ts — server-only channels
gateway; both calls assertContract-validated against the
generated Zod schemas (HR-21/HR-22).
src/components/settings/security/account-security-page.tsx
— client orchestrator; AccountSecurityPage is pure
(action + sign-out injected), AccountSecurityPageWithClerk
is the live wrapper injecting Clerk signOut.
src/components/settings/security/remote-logout-dialog.tsx
— destructive AlertDialog confirm (repo-standard
bg-destructive action), pending-proofed.
src/components/settings/settings-sidebar.tsx — new
Account group with the Sign-in & devices item.
openapi/app-api.json + openapi/permission-map.json
— mirrors synced to pramaan-functions main (additive: the five
/app/channels/* endpoints only) per the openapi mirror
guard; src/gen regenerated
(PRAMAAN_FUNCTIONS_OPENAPI pinned to the mirror, per LI-6).
messages/en.json + messages/hi.json — new
AccountSecurity namespace, SettingsNav.groupAccount
/ signInAndDevices, and
AgentSurfaces.settingsAccountSecurity, in BOTH locales.
pnpm vitest run "src/app/[locale]/(workspace)/settings/account/security/actions.test.ts" \ src/components/settings/security/account-security-page.test.tsx
The LIVE half of the ticket's test ("logout → channel refused") needs a real enrolled number against staging: enroll a WhatsApp number (T2c-a flow), run remote logout from Settings → Account → Sign-in & devices, then message Mira from that number — the zero-amp shield's static refusal replies (or silence within the window), enforced functions-side. The app-side half (this story) is covered by the tests above.