Skip to content

feat(mobile): add account persistence safeguards - #5083

Merged
iscekic merged 33 commits into
mainfrom
audit-w3a-account-persistence-089a
Aug 17, 2026
Merged

feat(mobile): add account persistence safeguards#5083
iscekic merged 33 commits into
mainfrom
audit-w3a-account-persistence-089a

Conversation

@iscekic

@iscekic iscekic commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Users can sign out this device, review active device sessions, keep encrypted read data, and restore drafts after a process restart.

Product behavior now revokes the current device session before local sign-out, retries failed cleanup, isolates cached data by account, and preserves high-intent drafts until completion.

The mobile app uses one auth epoch and one per-key serializer across credentials, SecureStore metadata, encrypted SQLCipher cache rows, drafts, and logout cleanup. Web tRPC procedures list and revoke owned device sessions. Provider resources now use reversible lifetimes.

Human Steps

Deploy web before the app build reaches users. Sign-out now calls user.revokeCurrentDeviceSession, added in this PR. If the app build ships first, the mutation rejects against the old server, and a rejected revoke is never recorded or retried. The device session row then survives until its refresh token expires after 30 days and keeps showing in the user's session list.

No manual migration, secret, or flag is required before merge.

The first native dev-client build after merge must include expo-sqlite with SQLCipher enabled. The completion gate records native build evidence before claiming P1-E-40a complete.

The local key-value store now runs on Drizzle, which owns the schema through a generated migration under apps/mobile/drizzle/. That native build must also confirm three things a unit test cannot reach: that PRAGMA key succeeds on the real SQLCipher build, that the migration applies on a device database file, and that the now-synchronous statements do not stutter the UI.

SecureStore Key Table

Key or key family DEC-01 class Write discipline
auth-token, auth-refresh-token, auth-token-expires-at, auth-legacy-exchange-done secret Serialized credential queue with auth-epoch fencing
auth-attest-key-id secret App Attest lifecycle; install-scoped exemption
persist-db-key secret SQLCipher key generation; install-scoped one-shot
selected-organization account-metadata writeAccountMetadata / deleteAccountMetadata
agent-session-filters account-metadata writeAccountMetadata / deleteAccountMetadata
active-user-id account-metadata writeAccountMetadata / sign-out delete
last-active-chat-instance account-metadata writeAccountMetadata / deleteAccountMetadata
pr-review-recents account-metadata writeAccountMetadata / deleteAccountMetadata
pr-review-viewed account-metadata writeAccountMetadata / deleteAccountMetadata
consent-accepted-* account-metadata (persistent) Per-user chainSave; survives sign-out
agent-model-preference device-preference Shared SecureStore preference serializer; existing sign-out clear
agent-reasoning-default-expanded device-preference Shared SecureStore preference serializer; existing sign-out clear
keep-session-screen-on device-preference Shared SecureStore preference serializer; existing sign-out clear
store-review-requested-at device-preference writeAccountMetadata serializer; existing retention
notification-prompt-seen device-preference KiloClaw onboarding exemption; deleted on sign-out
kiloclaw-owned account-metadata KiloClaw ownership gate exemption
logout-cleanup-tombstone safe-retry push-unregister state Direct non-epoch-fenced write; survives sign-out

Visual Changes

Screenshots are not attached because the E2E bundle could not complete the device-session and draft flows.

E2E

E2E was partially verified. The repository flow initially expected four tabs, while this branch renders three; a workflow fix added compatible selectors. The corrected run verified login, the three-tab profile route, current-device sign-out, database session revocation with revoked_reason = logout, and zero push-token rows. A takeover also opened the Device sessions screen and verified the current-device action and database cleanup. Device-session inventory with a second session, encrypted cache restore, durable drafts, pending review restore, account switching, and remote spawn draft clearing did not complete because the available seed and flow infrastructure could not safely create the required second session and scenario data.

Deviations

The repository's Expo SDK patch alignment required minimumReleaseAgeExclude entries for the current SDK 57 patch train. No plaintext cache fallback exists. P1-E-40a remains gated on native SQLCipher build evidence until the dev-client build completes.

@iscekic iscekic self-assigned this Aug 6, 2026
Comment thread apps/mobile/src/lib/auth/auth-context.tsx Outdated
Comment thread apps/mobile/src/lib/auth/token-owner.ts
Comment thread apps/mobile/src/app/(app)/agent-chat/new.tsx Outdated
Comment thread apps/mobile/src/lib/auth/logout-cleanup.ts
Comment thread apps/mobile/src/lib/persist/cache-persistence-mount.tsx Outdated
Comment thread apps/mobile/src/lib/persist/drafts.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of 8f199841 (vs. 0934214b): the previously open SUGGESTION is fixed — the encrypted-KV recovery (delete, key regeneration, reopen) is now fully wrapped so any failure reports to Sentry, and the regression test pins sentryReports: 1; the sign-out flag refactor into the shared sign-out-state module is consistent across the auth context, the read-cache write fence, and all callers, and the pnpm-workspace.yaml cleanup restores the main baseline. No new issues in the changed lines.

Files Reviewed (9 files)
  • apps/mobile/src/lib/auth/auth-context.test.tsx
  • apps/mobile/src/lib/auth/auth-context.tsx
  • apps/mobile/src/lib/auth/sign-out-state.ts
  • apps/mobile/src/lib/persist/cache-persistence-mount.test.ts
  • apps/mobile/src/lib/persist/encrypted-kv.test.ts
  • apps/mobile/src/lib/persist/encrypted-kv.ts
  • apps/mobile/src/lib/persist/read-cache.test.ts
  • apps/mobile/src/lib/persist/read-cache.ts
  • pnpm-workspace.yaml
Previous Review Summaries (6 snapshots, latest commit 0934214)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 0934214)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

Incremental review of 0934214b adds no new findings: the Drizzle migration of the encrypted KV store, the allowlist-only read cache, the bootstrap publish-winner fence, and the silent-revoke API change are behavior-consistent and well-tested; the one open item remains the zero-telemetry pin for a permanent encrypted-KV recovery-open failure.

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
apps/mobile/src/lib/persist/encrypted-kv.test.ts 401 Pinning sentryReports: 0 enshrines a zero-telemetry contract for a permanent encrypted-KV outage (recovery-open PRAGMA failure escapes openEncryptedDatabase without a Sentry report)
Files Reviewed (35 files)
  • apps/mobile/babel.config.js
  • apps/mobile/drizzle.config.ts
  • apps/mobile/drizzle/0000_material_toad.sql
  • apps/mobile/drizzle/migrations.js
  • apps/mobile/knip.json
  • apps/mobile/metro.config.js
  • apps/mobile/package.json
  • apps/mobile/sql.d.ts
  • apps/mobile/src/app/(app)/agent-chat/new.tsx
  • apps/mobile/src/app/(app)/pr-review/[owner]/[repo]/[number]/_layout.tsx
  • apps/mobile/src/components/agents/chat-composer.test.ts
  • apps/mobile/src/components/agents/chat-composer.tsx
  • apps/mobile/src/components/agents/session-detail-content.tsx
  • apps/mobile/src/components/device-sessions-screen.tsx
  • apps/mobile/src/lib/auth/auth-context.test.tsx
  • apps/mobile/src/lib/auth/auth-context.tsx
  • apps/mobile/src/lib/auth/logout-reconciliation.test.ts
  • apps/mobile/src/lib/auth/logout-reconciliation.ts
  • apps/mobile/src/lib/device-sessions.test.ts
  • apps/mobile/src/lib/device-sessions.ts
  • apps/mobile/src/lib/persist/cache-persistence-mount.test.ts
  • apps/mobile/src/lib/persist/cache-persistence-mount.tsx
  • apps/mobile/src/lib/persist/encrypted-kv.test.ts - 1 issue
  • apps/mobile/src/lib/persist/encrypted-kv.ts
  • apps/mobile/src/lib/persist/read-cache.test.ts
  • apps/mobile/src/lib/persist/read-cache.ts
  • apps/mobile/src/lib/persist/schema.ts
  • apps/mobile/src/lib/persist/test-fixtures.ts
  • apps/mobile/src/lib/pr-review/pending-review-provider.mounted.test.tsx
  • apps/mobile/src/lib/pr-review/pending-review-provider.tsx
  • apps/mobile/vitest.mounted.config.ts
  • apps/mobile/vitest.pure.config.ts
  • apps/mobile/vitest.sql-plugin.ts
  • apps/web/src/routers/user-router.test.ts
  • apps/web/src/routers/user-router.ts

Fix these issues in Kilo Cloud

Previous review (commit 86ea9fb)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

Incremental review of 86ea9fb44 (account-persistence internals simplification): the refactor is behavior-preserving across all six reviewed areas, with one suggestion — a pinned zero-telemetry expectation for a permanent encrypted-KV recovery-open failure.

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
apps/mobile/src/lib/persist/encrypted-kv.test.ts 318 Pinning sentryReports: 0 enshrines a zero-telemetry contract for a permanent encrypted-KV outage (recovery-open PRAGMA failure escapes openEncryptedDatabase without a Sentry report)
Files Reviewed (39 files)
  • apps/mobile/src/app/(app)/agent-chat/new.tsx
  • apps/mobile/src/app/_layout.tsx
  • apps/mobile/src/components/agents/chat-composer.test.ts
  • apps/mobile/src/components/agents/chat-composer.tsx
  • apps/mobile/src/components/agents/session-detail-content.tsx
  • apps/mobile/src/components/agents/use-new-session-creator.test.ts
  • apps/mobile/src/components/agents/use-new-session-creator.ts
  • apps/mobile/src/components/agents/use-remote-spawn-dispatch.ts
  • apps/mobile/src/components/device-sessions-screen.tsx
  • apps/mobile/src/lib/auth/account-metadata-write.test.ts
  • apps/mobile/src/lib/auth/account-metadata-write.ts
  • apps/mobile/src/lib/auth/auth-context.test.ts
  • apps/mobile/src/lib/auth/auth-context.tsx
  • apps/mobile/src/lib/auth/credentials.ts
  • apps/mobile/src/lib/auth/logout-cleanup.test.ts
  • apps/mobile/src/lib/auth/logout-cleanup.ts
  • apps/mobile/src/lib/auth/token-owner.ts
  • apps/mobile/src/lib/consent.ts
  • apps/mobile/src/lib/device-sessions.test.ts
  • apps/mobile/src/lib/device-sessions.ts
  • apps/mobile/src/lib/hooks/secure-store-preference.ts
  • apps/mobile/src/lib/hooks/use-persisted-agent-session-filters.ts
  • apps/mobile/src/lib/last-active-instance.ts
  • apps/mobile/src/lib/notifications.ts
  • apps/mobile/src/lib/organization-context.tsx
  • apps/mobile/src/lib/persist/cache-persistence-mount.test.ts
  • apps/mobile/src/lib/persist/cache-persistence-mount.tsx
  • apps/mobile/src/lib/persist/drafts.test.ts
  • apps/mobile/src/lib/persist/drafts.ts
  • apps/mobile/src/lib/persist/encrypted-kv.test.ts - 1 issue
  • apps/mobile/src/lib/persist/encrypted-kv.ts
  • apps/mobile/src/lib/persist/read-cache.test.ts
  • apps/mobile/src/lib/persist/read-cache.ts
  • apps/mobile/src/lib/persist/test-fixtures.ts
  • apps/mobile/src/lib/persist/use-draft-flush.ts
  • apps/mobile/src/lib/persist/use-draft-load.ts
  • apps/mobile/src/lib/pr-review/pending-review-provider.tsx
  • apps/web/src/lib/user/server.ts
  • apps/web/src/routers/test-utils.ts

Fix these issues in Kilo Cloud

Previous review (commit f9d2f92)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of the two origin/main merges and the lint-fix commit since 73a3e166: all conflict resolutions (tRPC context threading, new-session draft/model-selection wiring, icon imports, auth token rename) combine both sides correctly with no regressions, and all six prior findings remain resolved.

Files Reviewed (17 files)
  • apps/mobile/src/app/(app)/_layout.tsx
  • apps/mobile/src/app/(app)/agent-chat/new.tsx
  • apps/mobile/src/app/_layout.tsx
  • apps/mobile/src/components/agents/chat-composer.test.ts
  • apps/mobile/src/components/agents/chat-composer.tsx
  • apps/mobile/src/components/agents/session-detail-content.tsx
  • apps/mobile/src/components/agents/use-remote-spawn-dispatch.test.ts
  • apps/mobile/src/components/agents/use-remote-spawn-dispatch.ts
  • apps/mobile/src/components/device-sessions-screen.tsx
  • apps/mobile/src/components/profile-screen.tsx
  • apps/mobile/src/lib/auth/auth-context.tsx
  • apps/mobile/src/lib/hooks/secure-store-preference.ts
  • apps/mobile/src/lib/use-new-session-share-remote.ts
  • apps/web/src/lib/trpc/init.ts
  • apps/web/src/lib/user/server.test.ts
  • apps/web/src/lib/user/server.ts
  • pnpm-workspace.yaml

Previous review (commit 73a3e16)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental commit refactors without regressions: credential/refresh rotation moves verbatim into a React-free credentials.ts (breaking two require cycles) with all epoch/teardown fencing intact, the logout tombstone narrows to push-unregister-only with consistent cleanup, reconciliation, guard, and tests, AgentSessionProvider returns to the exact base-branch lifetime pattern (useOwnedResource was added and removed within this PR), and the encrypted KV adds a pre-key SQLCipher probe with a documented fail-loud open memo — no new issues in the 26 changed files, and all six prior findings remain resolved.

Files Reviewed (26 files)
  • apps/mobile/src/app/(app)/agent-chat/new.tsx
  • apps/mobile/src/components/agents/session-provider.tsx
  • apps/mobile/src/components/agents/use-new-session-creator.test.ts
  • apps/mobile/src/components/agents/use-new-session-creator.ts
  • apps/mobile/src/components/device-sessions-screen.tsx
  • apps/mobile/src/lib/auth/auth-context.test.ts
  • apps/mobile/src/lib/auth/auth-context.test.tsx
  • apps/mobile/src/lib/auth/auth-context.tsx
  • apps/mobile/src/lib/auth/credentials.ts
  • apps/mobile/src/lib/auth/device-session-claim.test.ts
  • apps/mobile/src/lib/auth/device-session-claim.ts
  • apps/mobile/src/lib/auth/exchange-legacy-token.ts
  • apps/mobile/src/lib/auth/logout-cleanup.test.ts
  • apps/mobile/src/lib/auth/logout-cleanup.ts
  • apps/mobile/src/lib/auth/logout-reconciliation.test.ts
  • apps/mobile/src/lib/auth/logout-reconciliation.ts
  • apps/mobile/src/lib/auth/token-owner.test.ts
  • apps/mobile/src/lib/auth/token-owner.ts
  • apps/mobile/src/lib/device-sessions.test.ts
  • apps/mobile/src/lib/device-sessions.ts
  • apps/mobile/src/lib/hooks/use-owned-resource.mounted.test.tsx
  • apps/mobile/src/lib/hooks/use-owned-resource.ts
  • apps/mobile/src/lib/persist/encrypted-kv.test.ts
  • apps/mobile/src/lib/persist/encrypted-kv.ts
  • apps/mobile/src/lib/trpc.ts
  • apps/web/src/routers/user-router.ts

Previous review (commit 7b80e1f)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental commit fences the sign-out teardown window end to end — a teardown guard closes the refresh/cold-read resurrection race, the refresh fetch is bounded at the 15 s control-plane deadline, the remote-spawn draft marker arms only after admission, and draft/identity-hint writes no longer throw or leak rejections — resolving all six previous findings with no new issues in the 15 changed files.

Files Reviewed (15 files)
  • apps/mobile/src/lib/auth/auth-context.tsx
  • apps/mobile/src/lib/auth/auth-context.test.ts
  • apps/mobile/src/lib/auth/auth-context.test.tsx
  • apps/mobile/src/lib/auth/token-owner.ts
  • apps/mobile/src/lib/auth/token-owner.test.ts
  • apps/mobile/src/app/(app)/agent-chat/new.tsx
  • apps/mobile/src/components/agents/use-new-session-creator.ts
  • apps/mobile/src/components/agents/use-new-session-creator.test.ts
  • apps/mobile/src/components/agents/use-remote-spawn-dispatch.ts
  • apps/mobile/src/components/agents/use-remote-spawn-dispatch.test.ts
  • apps/mobile/src/lib/use-new-session-share-remote.ts
  • apps/mobile/src/lib/persist/cache-persistence-mount.tsx
  • apps/mobile/src/lib/persist/cache-persistence-mount.test.ts
  • apps/mobile/src/lib/persist/drafts.ts
  • apps/mobile/src/lib/persist/drafts.test.ts

Previous review (commit c2460d0)

Status: 6 Issues Found | Recommendation: Address before merge

Executive Summary

A post-sign-out race in the mobile auth layer can silently resurrect a revoked session: performRefresh reads the not-yet-deleted refresh token in the bump→delete window and re-persists the old account's credentials when the server-side revoke failed.

Overview

Severity Count
CRITICAL 1
WARNING 2
SUGGESTION 3
Issue Details (click to expand)

CRITICAL

File Line Issue
apps/mobile/src/lib/auth/auth-context.tsx 210 Post-bump refresh reads the not-yet-deleted refresh token and can resurrect the signed-out session

WARNING

File Line Issue
apps/mobile/src/lib/auth/token-owner.ts 92 getAuthTokenForRequest re-warms the in-memory owner with the just-signed-out token
apps/mobile/src/app/(app)/agent-chat/new.tsx 274 Remote-spawn draft marker armed before admission; a never-spawned attempt still clears the draft on leave

SUGGESTION

File Line Issue
apps/mobile/src/lib/auth/logout-cleanup.ts 106 Sign-out holds the global auth-transition chain across network I/O with an unbounded header phase
apps/mobile/src/lib/persist/cache-persistence-mount.tsx 88 Fire-and-forget identity-hint write can leak an unhandled promise rejection
apps/mobile/src/lib/persist/drafts.ts 219 saveDraft can throw synchronously despite its never-throwing contract
Files Reviewed (76 files)
  • apps/mobile/src/lib/auth/* (auth-context, auth-epoch, account-metadata-write, device-session-claim, exchange-legacy-token, logout-cleanup, logout-reconciliation, token-owner + tests) - 3 issues
  • apps/mobile/src/lib/persist/* (cache-persistence-mount, drafts, encrypted-kv, read-cache + tests) - 2 issues
  • apps/mobile/src/app/(app)/agent-chat/new.tsx - 1 issue
  • apps/mobile/src/app/_layout.tsx, (app)/_layout.tsx, (app)/device-sessions.tsx, pr-review/[owner]/[repo]/[number]/_layout.tsx - no issues
  • apps/mobile/src/components/agents/* (chat-composer, mobile-session-manager, session-detail-content, session-provider, use-new-session-creator, user-web-connection-provider + tests), device-sessions-screen.tsx, profile-screen.tsx, kilo-chat/hooks/use-kilo-chat-token.ts - no issues
  • apps/mobile/src/lib/hooks/* (secure-store-preference, use-available-models, use-owned-resource, use-persisted-agent-session-filters + tests) - no issues
  • apps/mobile/src/lib/pr-review/* (pending-review-provider, recent-prs, viewed-files + tests) - no issues
  • apps/mobile/src/lib/{storage-keys,consent,feedback,last-active-instance,notifications,organization-context,trpc}.ts(x) + tests - no issues
  • apps/mobile/{app.config.ts,package.json,vitest.pure.config.ts} - no issues
  • apps/web/src/lib/{tokens,trpc/init,user/server}.ts + tests, apps/web/src/routers/{user-router,test-utils}.ts + tests - no issues (device-session ownership enforced server-side; additions are additive/optional)
  • pnpm-workspace.yaml, pnpm-lock.yaml - no issues (generated lockfile skipped per rules)

Fix these issues in Kilo Cloud


Reviewed by kimi-k3 · Input: 71.1K · Output: 4.4K · Cached: 219.6K

Review guidance: REVIEW.md from base branch main

@iscekic iscekic added the human-ready The PR is ready for human review. label Aug 6, 2026
iscekic added 13 commits August 6, 2026 17:49
Plain SQLite ignores an unrecognized pragma without an error, so a
successful `PRAGMA key` proved nothing: on a build without SQLCipher the
database opened in plaintext and still passed the `sqlite_master` probe,
putting drafts and the cached read set on disk unencrypted.

Probe `PRAGMA cipher_version` before setting the key. It returns a row
only when SQLCipher is linked in. A missing SQLCipher build throws
`MissingSQLCipherError`, which skips delete-and-recreate recovery: a
rebuild is the only fix, and wiping the file would destroy user drafts.
Clearing the memo on failure made every later call re-run the whole open:
delete the database, regenerate the key, and report to Sentry. The read
cache writes on each query settle, so a broken open burned the disk in a
loop.

Keep the memo on failure. Nothing an open failure hits is transient — a
wrong key, a corrupt file, and a build without SQLCipher all need a
relaunch or a rebuild.
Two cycles existed:
  auth-context -> exchange-legacy-token -> auth-context
  auth-context -> logout-cleanup -> trpc -> auth-context

Both survived only because every use sat inside a function body. A future
top-level read across either one fails at runtime, and typecheck does not
catch it.

Move the credential write queue and the refresh rotation — no React
dependency — into `lib/auth/credentials.ts`. `trpc.ts` and
`exchange-legacy-token.ts` now import that instead of the provider.
The deferred-destroy timer existed only to survive a StrictMode
double-mount. The app mounts no StrictMode — `rg StrictMode apps/mobile`
is empty — so the hook guarded a case that cannot happen, and carried the
same abandoned-render residual as the plain ref it replaced.

Restore the previous effect cleanup in the session provider and delete
the hook with its test. The `retain()` change in the user web connection
provider stays: that one fixes a real teardown.
Sign-out still revokes the current device session. It just no longer
records a failed revoke for later retry.

The device destroys the refresh token locally at sign-out, and the access
token expires within 1 h, so an orphaned session row is already dead — it
only lingers in the session list until then. Retrying it cost a tombstone
field, a claim decoder, and half the reconciliation module.

The push unregister keeps its retry: an unregistered device keeps
receiving notifications for the signed-out account, which the user sees.

Deletes `device-session-claim.ts` (the hand-rolled JWT payload decoder
existed only to name the session for the retry) and its test.
`publishActiveTokenExpiry` already refuses to write when the held token
or the epoch differs from the snapshot. The generation counter added a
third comparison that can only differ when the same token was re-published
at the same epoch — and both writers then store the same value, so the
guard changed nothing.
`useNewSessionDraft` wrapped `useFencedDraftLoad` with one constant, and
`resolveRestoredNewSessionPrompt` was a three-line exported function with
one caller. Both go to their call site.
TypeScript already enforces `string` on every internal caller, so the
`typeof` checks were dead. The empty-string checks stay and are now
labelled: an empty scope silently widens `clearScopePrefix` to every
scope in the database.
`CURRENT_DEVICE_BADGE` and `NO_CURRENT_DEVICE_NOTE` were exported
constants read once each, in JSX. `UNKNOWN_DEVICE_LABEL` stays but is now
module-local: its three uses are all inside `device-sessions.ts`.

Also drops the test that asserted a constant equals its own literal.
The helper mixed two object members with one bare string, so callers
compared `outcome === 'not_found'` against `{ outcome: 'revoked' }`. Make
it a plain string union and let each procedure wrap its own result. The
wire shape is unchanged: both still return `{ outcome }`.
…sistence-089a

# Conflicts:
#	apps/web/src/lib/trpc/init.ts
#	apps/web/src/lib/user/server.test.ts
Remove duplicated and speculative code in the account-persistence layer with
no behavior change.

Auth:
- Replace the hand-rolled credential mutex with the existing chainSave helper.
- Delete the invalidateRefreshSession and persistSignInCredentials wrappers.
- Validate the logout tombstone with zod instead of a hand-written type guard.
- Delete the ActiveTokenState type and the duplicate teardown-flag reads.
- Add setAccountMetadata and use it at the four sites that inlined the wrapper.

Persistence:
- Import encrypted-kv directly and delete the key-value injection layer.
- Delete the unread bytes column, its SQL, and scopeBytes.
- Reuse utf8ByteLength from lib/utf8-utils.
- Merge the two draft Sentry reporters and inline single-caller predicates.
- Spread the base persister instead of forwarding two methods by hand.

Features:
- Extract useDraftFlushOnBackground for three identical AppState effects.
- Move the draft hooks out of use-new-session-creator into lib/persist.
- Delete the serializeConsentWrite wrapper and call chainSave directly.
- Derive getDevicePushTokenOutcome from getDevicePushToken.

Device sessions:
- Partition instead of sorting on one boolean, and index the toast call.
- Drop the unread kind field, two unused exports, and a single-use type.
- Inline createTestTRPCContext into its only caller.

Tests keep every assertion; the deleted cases covered deleted code.
Comment thread apps/mobile/src/lib/persist/encrypted-kv.test.ts Outdated
Make Drizzle the only way the app runs SQL against expo-sqlite, and let
Drizzle own the schema.

- Define the key-value table in src/lib/persist/schema.ts and generate the
  initial migration under apps/mobile/drizzle/.
- Replace every statement in encrypted-kv with the Drizzle query builder, and
  replace the inline CREATE TABLE with a migration run.
- Keep the connection lifecycle on expo-sqlite, which Drizzle does not cover.
  Set PRAGMA key with execSync on the raw handle, because SQLCipher needs the
  key before the schema can be read and a prepared statement reads the schema.
- Prove SQLCipher is linked with PRAGMA cipher_version before any key is set.
- Re-run the migrations after delete-and-recreate recovery.
- Add the babel inline-import plugin and the metro sql resolver the generated
  migration bundle needs.

The open path is now synchronous, so statements block the JS thread. The
exported API stays async and its callers are unchanged.

A native dev-client build must still confirm that PRAGMA key survives on real
SQLCipher, that the migration applies on a device file, and that the
synchronous I/O does not stutter the UI.
- Clear the read cache by scope prefix, so a SCHEMA_VERSION bump no longer
  leaves the previous scope on the device forever. Sign-out cleared only the
  current version, which left up to 2 MB of the user's data after sign-out.
- Prove the prefix delete keeps another user's rows and the drafts.
- Delete isHardDeniedPath. It runs before the allowlist, and none of the four
  allowlisted paths can match it, so it cannot change an outcome. Its cheap
  denial assertions move to the allowlist test.
- Drop the persister buster. The scope already carries the schema version, so
  an older blob lives at a scope the restore never reads.
The startup prefetch fires getMe, which refreshes an access token inside the
refresh margin and writes a new auth-token with no React setter. The bootstrap
then saw a changed stored token and returned without publishing anything, so a
user with valid credentials reached the login screen and only a relaunch
recovered.

- Publish the winning token instead of returning empty-handed. The epoch fence
  still lets sign-out win, proved by a new test.
- Replace deleteTombstoneIfUnchanged with a plain delete. SecureStore has no
  compare-and-delete, so the re-read and deep compare could not close the
  window they guarded.
- Delete the tombstone when it belongs to another user. The previous user's
  push token can never be unregistered without their auth, so the record only
  kept a user id and a push token in a keychain that survives app deletion.
- Render the composer without waiting for user.getMe. The gate made the new
  session screen and the session screen refuse all typing on a cold start
  without a cached identity. The draft generation fence and the composer key
  already prevent a cross-account draft.
- Apply a late draft only while the input is untouched, so text typed first
  survives.
- Key the composer on an account epoch that bumps only between two known user
  ids, so resolving identity no longer remounts and discards typed text.
- Keep the valid restored review items and drop only the invalid ones. One
  unrecognized row used to invalidate the whole array, which lost every queued
  review comment.
- Lowercase the owner and the repo in the review draft key, so one pull request
  no longer splits into two queues by route casing.
- Compute the composer render gate and the bottom bar from one value, so the
  bar no longer claims the composer's space before it exists.

Text typed before identity resolves persists on the next keystroke after it
arrives. Saving on the transition risks writing one account's text into
another's scope.
- Return one success result from both revoke procedures. Nothing read the
  difference between revoked, already revoked, and not found: the only caller
  ignores the return value, and every case showed one toast and a refetch. A
  future additive outcome would also have shown a false error on shipped
  builds.
- Keep one conditional update filtered on ownership and on an active row, and
  keep the tests that prove a caller cannot revoke a session they do not own.
- Delete mapRevokeOutcome on the client. One success toast plus the standard
  error toast is shorter than the helper.
- Say what the screen can do. Only Kilo app sign-ins create a session row, so
  editor and CLI sign-ins are not listed. Revocation stops refresh, and no auth
  path reads revoked_at, so a revoked device keeps API access until its access
  token expires after one hour.
The module flag in read-cache and the React state in auth-context carried the
same fact to two consumers of one event, and three call sites kept them in sync
by hand. Any new sign-out path could set one and forget the other.

- Add src/lib/auth/sign-out-state.ts as the single source. It lives under auth
  because the fact is auth's, and it imports nothing, so there is no cycle.
- Read it in React through useSyncExternalStore instead of mirroring it in
  state, and delete the two setters and the useState.
- Leave the auth epoch, signOutTeardownActive, and isSignedOutReference alone.
  They gate different windows.

The write-path fence still runs last, immediately before the key-value write.
pnpm matches minimumReleaseAgeExclude by package name, with no version bound,
so each of the nine entries permanently exempted every future release of that
package from the supply-chain maturity gate. The comment justified one urgent
SDK 57 patch bump, not a standing exemption.

The pinned versions have now aged past the gate: removing all nine installs
clean and leaves the lockfile unchanged. A later bump to a fresh patch will wait
for the gate, which is what the gate is for.
The delete, the key regeneration, and the recovery reopen sat outside the inner
try, so a failure in any of them escaped without a Sentry report. The open memo
keeps a failure, so one silent escape rejected every drafts and read-cache
caller for the rest of the install with nothing in Sentry.

Move the whole recovery inside the try and close the reopened handle only when
it exists. The PRAGMA-key seam now reports once, like the probe seam.
@iscekic
iscekic merged commit 8b78785 into main Aug 17, 2026
69 checks passed
@iscekic
iscekic deleted the audit-w3a-account-persistence-089a branch August 17, 2026 13:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-ready The PR is ready for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants