The 0.18.1 fix relied on an existing-device + present-stronghold-key match.
That fails for users who:
- had multiple device registrations and only retain the latest device's
private key in the local vault
- had a vault wipe / fresh OS install at some point
- have device rows that vanished server-side but keys still locally
Migration now scans conversation_keys for distinct un-migrated
recipient_device_ids visible to the user (RLS-filtered) and probes the
stronghold for each, regardless of whether the server still lists that
device. Result struct surfaces attempted/migrated/noKey/decryptFail/rpcFail
counters; SecurityCenter shows them via a new "Migration erneut ausführen"
button so users can self-diagnose without DevTools.
Also adds [crypto-migration] console.info breadcrumbs at every decision
point so a single F12 shows what happened.
PostgREST translates .eq('col', null) to `col = NULL` which is always false
in SQL. The migration silently returned zero rows -> setupNewUserIdentity
fired but re-wrapped nothing -> users could set a PIN but every send threw
'Awaiting key'. Switching to .is('col', null) emits `col IS NULL` and the
migration finally finds its work.
Also makes the migration trigger idempotent and re-fires it on:
- every successful loadOrUnlockUserKey
- AuthContext startup when the user-key is already cached
so users stuck on 0.18.0 auto-recover the moment they install 0.18.1.
PinInput: focused + active-slot now show a brand-coloured ring, glow, and
a blinking caret so users see where the next keystroke lands.
Drops the always-on 'Sprach-Channel' banner. The preview panel renders
only when peers are in the active call (1:1 and group identical).
Calls are still started via the topbar phone icon.
Migration 20260515000002 failed on prod because dropping NOT NULL on
recipient_device_id was rejected (column is part of the natural primary
key). This fix-up drops the PK, adds a synthetic row_id BIGSERIAL PK,
re-applies the NOT NULL drop, and re-runs the indexes/policies that
were skipped after the failure.
devices rows no longer carry public_key for crypto purposes. The whole
per-device key API surface (provisionNewDevice, loadDevicePrivateKey,
saveDevicePrivateKey, forgetDevicePrivateKey, restoreDeviceFromServerRecord)
is removed; registerDevice now records {name, platform} only. SQL drops the
NOT NULL on devices.public_key so future telemetry rows can omit it.
Note: SQL not applied locally - push via pnpm prod:migrate when ready.
When a conversation opens, the local client checks every accepted member
for a recipient_user_id bundle on the active key version. Members without
one get a best-effort wrap from the local conv-key handle. This closes
the legacy migration gap where peer B couldn't read because no one had
yet wrapped the new per-user conv-key for them.
Drops the manual backup-string flow; replaces it with PIN change,
recovery-code regeneration, and identity reset (all sealed via the new
user_keys table).
Replaces the per-device DeviceRecord lookup with a per-user discriminated
union (loading | needs-setup | needs-unlock | unlocked). Heartbeat block
deleted (telemetry no longer device-bound); webPush keyed by install-id.
Renames DecryptParams.ownDeviceId to ownUserId so decryptMessages actually
looks up bundles by user. Sweeps remaining OwnDeviceCtx and
loadDevicePrivateKey consumers in the desktop app to use cachedUserKey
from userIdentity. Files scheduled for deletion in later tasks
(BackupExportDialog, DeviceRestore, BackupRestoreDialog, BackupPromptBanner,
deviceBackup, DeviceRegistration) are left untouched.
apps/mobile/lib/supabase.ts imports '@chat-app/shared/supabase'. The
shared package declares this subpath in its package.json "exports"
map. The dev Metro resolver respected it; the eager exporter used by
preview/production builds defaulted to legacy resolution and 404'd on
the subpath.
unstable_enablePackageExports=true makes Metro use Node's modern
exports-aware resolver in both modes. Despite the 'unstable_' prefix,
it's the recommended setting in Expo SDK 52 monorepos.
packages/shared/src/index.ts and all sub-modules used .js extensions on
relative imports (e.g. './admin/index.js') pointing at .ts source files.
TypeScript with moduleResolution: "Bundler" doesn't need them, and
Metro's eager exporter (used for preview / production builds) reads
them literally and fails — only the dev-server Metro fell back to .ts.
Workspace typecheck remains 8/8 green; Vite and TS Bundler resolution
already accept both styles, so desktop is unaffected.
Selective public-hoist patterns weren't enough — RN's index.js imports
invariant (and nullthrows, pretty-format, regenerator-runtime, and many
more) as bare transitive deps. Each fresh EAS build was hitting a
different missing-module error.
shamefully-hoist=true is Expo's official monorepo workaround: pnpm
mirrors npm/yarn's flat layout at the workspace root, so every package's
deps are reachable via standard Node resolution. Desktop (Vite-bundled)
is unaffected.
PackageList.java generated by RN autolinking imported expo.core.ExpoModulesPackage
(old SDK <48 name). Root cause: RN CLI couldn't read expo's react-native.config.js
through the pnpm symlink (it lives at .pnpm/expo@52..../expo/react-native.config.js),
so it fell back to deriving the import path from expo's Android gradle namespace
("expo.core") + assumed class name (ExpoModulesPackage), producing a path that
doesn't exist (the actual class lives at expo.modules.ExpoModulesPackage).
Added "expo" to .npmrc public-hoist-pattern so RN's autolinking can resolve
expo's react-native.config.js via the standard node_modules lookup. That config
explicitly sets packageImportPath to the correct expo.modules.ExpoModulesPackage.
Also corrected @expo/metro-runtime from 55.0.11 (added blindly earlier) to ~4.0.1
which is the SDK-52-aligned version.
pnpm's default strict isolation hides @babel/runtime/helpers/* and other
transitive deps from Metro, which expects flat node_modules layout.
Each missing dep was a separate failed EAS build (metro-runtime,
@babel/runtime, ...) — added .npmrc with public-hoist-pattern entries
that catch the common RN/Expo families so we stop whack-a-moling them.
Also kept @babel/runtime as direct dep of @chat-app/mobile for safety.
expo-router@4 imports @expo/metro-runtime as the very first thing in its
entry-classic.js so Fast Refresh works; bundler errored without it.
Also dropped expo-router/babel from babel.config.js — its functionality
moved into babel-preset-expo in SDK 50.
Phase 0 installed expo-application/expo-image-picker/expo-dev-client at
^55.x assuming the major version matched the SDK number. It doesn't —
each expo-* package has its own major. Misaligned versions pulled in
expo-modules-core@2.2.3 which expects an API that SDK 52's gradle plugin
host doesn't provide, breaking the Gradle build with
'expo-module-gradle-plugin not found'.
`expo install --fix` aligned everything to SDK 52 baselines:
expo-application 55.0.15 -> ~6.0.2
expo-dev-client 55.0.33 -> ~5.0.20
expo-image-picker 55.0.20 -> ~16.0.6
expo-sqlite 15.0.6 -> ~15.1.4
react-native 0.76.0 -> 0.76.9
+ RN companion libs (gesture-handler, screens, async-storage)
The 2500ms timer that drifts CallState from `ended` back to `idle` was
an inline magic number. Promote to a module-level constant with a
comment explaining why the value isn't arbitrary — picked from the
post-Phase-3 quality review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Phase-3 implementer ASCII'd four strings to side-step a Windows
console encoding issue during the apply step. The TypeScript / Metro
toolchain handles non-ASCII string literals cleanly — the workaround
was unnecessary and produced ugly UI labels ("Anruf lauft", "Hoerer",
"Verbinde ..."). Restore the originals.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>