Skip to content

feat: iOS Notification Service Extension for rich chat notification previews - #2571

Draft
kaladivo wants to merge 6 commits into
mainfrom
feat/ios-nse-chat-notifications
Draft

feat: iOS Notification Service Extension for rich chat notification previews#2571
kaladivo wants to merge 6 commits into
mainfrom
feat/ios-nse-chat-notifications

Conversation

@kaladivo

@kaladivo kaladivo commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces the generic iOS chat notification ("you have a new message") with a real preview — sender name + decrypted message text — rendered on-device by a Notification Service Extension, the way Signal does it. Android and the foreground WebSocket flow are untouched.

How it works: the push payload only carries an opaque targetToken (vexl_nt_…). The NSE looks up the matching inbox private key in a shared keychain access group, signs a chat-service challenge, fetches the new messages read-only, decrypts them (eciesLegacy), and rewrites the notification content. On any failure — unknown token, non-secp256k1 key, network error, timeout, locked keychain — it delivers the original generic content, so the worst case is exactly today's behavior.

⚠️ Deployment ordering (load-bearing)

The chat-service change (first commit) MUST be deployed to production before any app build containing the NSE ships. The new markAsPulled: false request path is strictly read-only server-side. Against an old server the NSE request deterministically 500s before any mutation (missing client-version header hits the NOT NULL inbox-metadata write inside the transaction) — harmless generic fallback, but log noise. Do not add a client-version header to the NSE's ChatApiClient before the server ships; that would un-mask a pulled-marking message-loss race (documented in ChatApiClient.swift and contracts.ts). The server change alone is fully backward compatible and can deploy immediately.

Changes

  • chat-service / rest-api: optional markAsPulled (default true) on retrieveMessages; when false, no pulled flags and no inbox-metadata writes. "Seen" semantics stay exclusively with the JS app.
  • packages/cryptography: checked-in NSE test-vector suite (pnpm generate:nse-vectors) — eciesLegacy/GTM decrypt + ECDSA verify vectors pinned to the TS reference by a jest suite; consumed by the Swift tests.
  • apps/mobile — bridge (modules/vexl-nse-bridge): local Expo module; JS declaratively syncs vexl-token → inbox-key entries into a shared keychain group (kSecAttrAccessibleAfterFirstUnlock) and non-secret metadata (sender names, service URL, locale) into the App Group container. Cleared on logout and on logged-out startup. MMKV stays the source of truth — the bridge is purely additive.
  • apps/mobile — NSE (targets/vexl-nse + native/VexlNotificationCore): extension target via @bacons/apple-targets; testable SPM package (swift-secp256k1 + CryptoKit) with the crypto port, read-only chat client, payload parsing, and localized rendering with thread-grouping parity. 20s deadline, always-deliver guarantee.
  • JS notification flow: NSE-enriched notifications are skipped by the generic-cancel logic and dismissed only when the richer JS local notification replaces them.

Privacy

No new server-side metadata: the payload is unchanged and the NSE fetch looks identical to the existing background fetch. Message content exists only on-device; the iOS system "Show Previews" setting (default: only when unlocked) governs lock-screen exposure — no in-app setting added. Keys move into the keychain (an upgrade over plain MMKV); the App Group store holds no secrets. Session credential headers are deliberately not synced — the NSE doesn't need them.

Testing

  • repo: turbo:typecheck / turbo:lint / turbo:format — 29/29 each
  • chat-service: 73/73 tests, incl. read-only-retrieve regression tests (no pulled flags, no metadata writes, survives deletePulledMessages)
  • cryptography: 81/81 tests (vector pin suite)
  • Swift: 55/55 tests — all shared TS vectors (decrypt, ECDSA incl. high-S/tampered negatives, secp224r1 rejection), thread-id parity pinned against node crypto, end-to-end enricher test asserting markAsPulled: false + a server-verifiable signature
  • expo prebuild smoke test: NSE target, local-SPM link, and app-group entitlements verified in the generated project
  • Adversarially-verified multi-agent review: 16 confirmed findings, all fixed (incl. two criticals in the server read-only path)

Not yet verified / follow-ups

  • Full Xcode build + on-device run (bridge module Swift is parse-checked only; needs a dev build + a real push to confirm the Expo envelope end-to-end)
  • EAS: credentials sync registers the new .nse bundle ID + App Group; build image needs Xcode 16.3+ (swift-secp256k1 0.23.2). New target changes the fingerprint → new runtimeVersion.
  • Optional: dedicated hidden-previews localization string (currently reuses "New message")

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added iOS chat notification preview enrichment with localized titles/bodies and NSE “enriched” notification delivery.
    • Introduced an NSE bridge with shared app-group/keychain sync and a new notification preview category registered at startup.
    • Added iOS-side crypto and payload handling to decrypt and render chat previews.
    • Added markAsPulled support for message retrieval to enable true read-only preview behavior.
  • Bug Fixes

    • Prevent duplicate enriched previews by dismissing existing previews for the same chat before showing a new one.
    • Logout now clears shared NSE bridge data to reduce stale preview content.

kaladivo added 3 commits July 6, 2026 20:26
When markAsPulled is false the handler skips both the pulled-flag update
and the inbox metadata write, making the request strictly read-only.
Defaults to true (absent = true), so existing clients are unaffected.

This is the server-side prerequisite for the iOS Notification Service
Extension, which fetches messages to render notification previews but
must never affect seen/pulled semantics. Must be deployed before any
app build containing the NSE ships.
…tation

Deterministic vector suite (pnpm generate:nse-vectors) covering
eciesLegacy decrypt (the chat message scheme), eciesGTM decrypt,
and ECDSA verify over secp256k1 keys in the exact PrivateKeyHolder
encodings, including negative cases and a stripped-leading-zero
DER scalar key. A jest suite validates every vector against the
TS implementation so the checked-in file cannot drift.

Consumed by the Swift VexlNotificationCore tests to prove parity
of the iOS NSE crypto port.
…reviews

Replaces the generic iOS chat notification with a real preview
(sender name + message text) decrypted on-device, Signal-style.

- targets/vexl-nse: NSE target via @bacons/apple-targets; always
  falls back to the generic content on any failure or timeout
- native/VexlNotificationCore: testable SPM package (swift-secp256k1
  + CryptoKit) implementing eciesLegacy/GTM decrypt, ECDSA challenge
  signing, bridge store readers, a read-only chat API client
  (markAsPulled: false), payload parsing and localized rendering
  with thread-grouping parity; 55 tests incl. shared TS vectors
- modules/vexl-nse-bridge: local Expo module syncing vexl-token ->
  inbox private key entries into a shared keychain access group
  (AfterFirstUnlock) and non-secret metadata into the App Group
  container; declarative replace-all sync, cleared on logout and
  on logged-out startup
- JS wiring: bridge sync on token-map/chat/session/locale changes;
  NSE-enriched notifications are dismissed before the richer JS
  local notification replaces them, and skipped by the generic
  cancel logic

Only vexl_nt_ tokens and secp256k1 inbox keys are supported; legacy
cyphers and other curves bail to the generic notification. MMKV
remains the source of truth for keys; the bridge is additive.

Requires the chat-service markAsPulled change to be deployed first.
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f7f0d392-1183-4e30-8cad-9cde380cfac1

📥 Commits

Reviewing files that changed from the base of the PR and between edf564b and b21415a.

📒 Files selected for processing (4)
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Crypto/VexlKeys.swift
  • apps/mobile/native/VexlNotificationCore/Tests/VexlNotificationCoreTests/CryptoVectorTests.swift
  • apps/mobile/src/utils/notifications/nseEnrichedNotifications.ts
  • docs/ios_nse_chat_notifications.md
✅ Files skipped from review due to trivial changes (1)
  • docs/ios_nse_chat_notifications.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/mobile/src/utils/notifications/nseEnrichedNotifications.ts
  • apps/mobile/native/VexlNotificationCore/Tests/VexlNotificationCoreTests/CryptoVectorTests.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Crypto/VexlKeys.swift

📝 Walkthrough

Walkthrough

This PR adds an optional markAsPulled path to chat-service message retrieval and builds an iOS Notification Service Extension pipeline for chat notification enrichment, including Swift crypto/storage/rendering, native bridge wiring, mobile sync hooks, test vectors, and documentation.

Changes

markAsPulled retrieval flag

Layer / File(s) Summary
Request contract and route behavior
packages/rest-api/src/services/chat/contracts.ts, packages/rest-api/src/services/chat/index.ts, apps/chat-service/src/routes/messages/retrieveMessages.ts
RetrieveMessagesRequest adds markAsPulled; the route skips inbox metadata updates and pulled-flag writes when it is false.
Helper and route tests
apps/chat-service/src/__tests__/utils/addChallengeForKey.ts, apps/chat-service/src/__tests__/routes/retrieveMessages.test.ts
The test helper now sends markAsPulled, and route tests cover the read-only path, delete behavior, and inbox metadata preservation.

iOS Notification Service Extension pipeline

Layer / File(s) Summary
Crypto primitives and key parsing
apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Crypto/*
Swift crypto helpers, secp256k1 key parsing, ECIES decryptors, and challenge signing/verifying are added for the notification core.
Payload parsing and rendering
apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Protocol/*, apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Rendering/*, apps/mobile/native/VexlNotificationCore/scripts/generateNotificationLocalizations.mjs, apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Resources/notificationLocalizations.json, apps/mobile/native/VexlNotificationCore/Tests/VexlNotificationCoreTests/*Rendering*, *PayloadParsing*
Notification payload parsing, decrypted chat parsing, localization loading, rendered notification construction, and content enrichment are added along with their Swift tests and generated localization resource.
Bridge storage contract
apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Storage/*, apps/mobile/native/VexlNotificationCore/Tests/VexlNotificationCoreTests/BridgeContractTests.swift
Shared constants, inbox key storage, metadata storage, and bridge contract tests define the native keychain and app-group data shape.
Chat API client and notification enrichment
apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Protocol/ChatApiClient.swift, apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/NotificationEnricher.swift, apps/mobile/native/VexlNotificationCore/Tests/VexlNotificationCoreTests/EnricherTests.swift
The chat API client, notification enricher, and enricher tests implement the challenge, retrieval, decryption, candidate selection, and fallback flow.
Expo bridge module and native sync
apps/mobile/modules/vexl-nse-bridge/*
The Expo bridge module, its config and podspec, and the Swift bridge implementation sync inbox keys and metadata into shared iOS storage and clear it on demand.
NSE target wiring and app integration
apps/mobile/targets/vexl-nse/*, apps/mobile/expo-plugins/with-nse-local-spm.js, apps/mobile/app.config.ts, apps/mobile/native/VexlNotificationCore/Package.*, apps/mobile/package.json, apps/mobile/src/*, apps/mobile/.gitignore
The NSE target, Expo plugin, app config, package wiring, and mobile JS integration add the iOS notification extension, sync hooks, logout cleanup, and enriched-notification dismissal and registration.
Shared crypto vectors and parity tests
packages/cryptography/src/testVectors/*, packages/cryptography/test-vectors/nse-test-vectors.json
The cryptography package adds the NSE vector generator, schema, checked-in vectors, and parity tests for Swift and TypeScript implementations.
Notification architecture docs
docs/ios_nse_chat_notifications.md
The new documentation page describes the NSE chat notification flow, shared storage contract, crypto parity requirements, deployment ordering, and remaining release steps.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant APNs
  participant NotificationService
  participant NotificationEnricher
  participant ChatApiClient
  participant KeychainInboxKeyStore
  participant AppGroupMetadataStore

  APNs->>NotificationService: didReceive(userInfo)
  NotificationService->>NotificationEnricher: enrich(userInfo)
  NotificationEnricher->>KeychainInboxKeyStore: inboxKeyPair(forVexlToken)
  NotificationEnricher->>AppGroupMetadataStore: loadMetadata()
  NotificationEnricher->>ChatApiClient: createChallenge(publicKey)
  NotificationEnricher->>ChatApiClient: retrieveMessages(markAsPulled:false)
  ChatApiClient-->>NotificationEnricher: ServerMessage[]
  NotificationEnricher-->>NotificationService: RenderedNotification?
  NotificationService-->>APNs: deliver enriched or original content
Loading

Suggested reviewers: SamTremko

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding an iOS Notification Service Extension for rich chat notification previews.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ios-nse-chat-notifications

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

🚀 Expo preview is ready!

  • Project → vexl
  • Platforms → android, ios
  • Scheme → app.vexl.it
Android
(14d5184204b05317ff03b68e81b2618301be0170)
More info
iOS
(76f1b72b50f8fed4922ef63fbd67db9822933e11)
More info

Learn more about 𝝠 Expo Github Action

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 085b0a9cdc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/mobile/app.config.ts
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a Notification Service Extension (NSE) for iOS that intercepts incoming chat push notifications and replaces the generic "new message" alert with the decrypted sender name and message text, entirely on-device. A companion server-side change adds a markAsPulled: false read-only path to retrieveMessages so the NSE can fetch messages without claiming them, preventing a race with the JS app's deletePulledMessages call.

  • Server: retrieveMessages now conditionally skips updateInboxMetadata and the pulled-flag writes when markAsPulled: false, guarded by a deployment-ordering requirement documented in code and contracts.
  • Bridge module (vexl-nse-bridge): JS declaratively syncs inbox private keys into a shared keychain access group and non-secret metadata (sender names, service URL, locale) into an App Group container, with deduplication and explicit logout cleanup.
  • VexlNotificationCore (Swift package): re-implements eciesLegacy decrypt, secp256k1 ECDSA signing, and DER key parsing in Swift, pinned against 81 TS-generated test vectors; the NSE shell enforces a 20-second enrichment deadline and an exactly-once delivery guarantee under a per-instance lock.

Confidence Score: 5/5

Safe to merge after the deployment ordering documented in the PR description is followed (server change first, then app build).

The server change is strictly additive and fully backward-compatible. The Swift crypto primitives (eciesLegacy, ECDH, PBKDF2, AES-CTR) are all pinned against shared TypeScript test vectors, and the 55-test Swift suite covers the decrypt/sign/verify paths along with adversarial negatives. The exactly-once delivery guarantee in NotificationService is sound: the enrichmentTask is stored under the stateLock before the Task body can execute, closing the window identified in a prior review. All failure modes fall back to the existing generic notification content, so a bug in the NSE path degrades gracefully.

apps/mobile/targets/vexl-nse/generated.entitlements — committed with the staging app group identifier; worth confirming the prebuild regeneration behaviour before shipping a production build.

Important Files Changed

Filename Overview
apps/chat-service/src/routes/messages/retrieveMessages.ts Adds markAsPulled: false read-only path that skips both updateInboxMetadata and the pulled-flag writes; tests cover the regression (no metadata touch, messages survive deletePulledMessages).
apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/NotificationEnricher.swift Orchestrates the full enrichment flow (key lookup → challenge → sign → fetch → decrypt → render); returns nil on any failure, parses VexlPrivateKey once at the curve-check guard and threads it through signing and decryption.
apps/mobile/targets/vexl-nse/NotificationService.swift NSE shell with stateLock-guarded exactly-once delivery; enrichmentTask stored under the lock before the Task body runs, preventing the timeout-cancel window identified in a previous review.
apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Crypto/EciesLegacy.swift Ports eciesLegacy (AES-256-CTR + HMAC-SHA256 MAC) to Swift; MAC check precedes decryption, constant-time comparison used, trailing-null strip matches TS reference.
apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Crypto/VexlKeys.swift Minimal DER parser for PKCS#8 and SEC1 EC keys; validates secp256k1 OID in AlgorithmIdentifier (PKCS#8) or mandatory [0] parameters (SEC1), rejects unsupported curves before any network traffic.
apps/mobile/modules/vexl-nse-bridge/ios/VexlNseBridgeModule.swift Expo module that delete-all-then-rewrite-all syncs inbox keys into the shared keychain and metadata into the App Group container; also purges the legacy session-credentials keychain service.
apps/mobile/src/state/notifications/nseBridge/syncNseBridgeActionAtom.ts JS atom that assembles the sync payload (secp256k1-only keys, sender names, service URLs, locale) and hands it to the native bridge; deduplicates by JSON snapshot and purges the bridge on logged-out startup.
apps/mobile/src/utils/notifications/nseEnrichedNotifications.ts JS glue for NSE-enriched notifications: schema-validated marker check, per-chat dismiss before the JS local notification is shown, and category registration for iOS hidden previews placeholder.
apps/mobile/src/utils/notifications/cancelNewChatNotifications.ts Adds NSE-enriched-notification exclusion from cancellation while still counting them for UINotificationReceived metrics via the unfiltered systemNotificationsIds.
apps/mobile/targets/vexl-nse/generated.entitlements Committed with group.it.vexl.nextstaging.shared; should be overwritten by expo prebuild for each environment, but worth confirming regeneration behaviour before a production build.
packages/rest-api/src/services/chat/contracts.ts Adds markAsPulled with Schema.optionalWith(... {default: () => true}) to RetrieveMessagesRequest; default preserves existing app behavior, explicit false is the NSE-only path.
apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Crypto/CryptoPrimitives.swift PBKDF2-HMAC (SHA-1 and SHA-256), AES-256-CTR via CommonCrypto, raw-X ECDH over secp256k1 via swift-secp256k1; all pinned against TS test vectors in CryptoVectorTests.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant APNs as APNs
    participant NSE as NotificationService (NSE)
    participant KS as KeychainInboxKeyStore
    participant MS as AppGroupMetadataStore
    participant CS as ChatService
    participant OS as iOS

    APNs->>NSE: didReceive(request) — vexl_nt_ targetToken
    NSE->>NSE: makeEnricher(), start Task under stateLock
    NSE->>KS: inboxKeyPair(forVexlToken:)
    KS-->>NSE: InboxKeyPair (privateKey, publicKey)
    NSE->>MS: loadMetadata()
    MS-->>NSE: NseMetadata (chatServiceUrl, senderNames, locale)
    NSE->>NSE: VexlPrivateKey(pemBase64:) — curve check
    NSE->>CS: "POST /api/v1/challenges {publicKey}"
    CS-->>NSE: challenge string
    NSE->>NSE: signChallenge(challenge, privateKey) — ECDSA SHA-256
    NSE->>CS: "PUT /api/v1/inboxes/messages {markAsPulled:false}"
    CS-->>NSE: "[{message: ciphertext, senderPublicKey}]"
    NSE->>NSE: eciesLegacyDecrypt per message, pickPreviewCandidate
    NSE->>NSE: NotificationRenderer.render(message, displayName, locale)
    NSE->>NSE: RenderedNotification.apply(to: UNMutableNotificationContent)
    NSE->>OS: contentHandler(enrichedContent)

    note over NSE,OS: On any failure OR 20s timeout → contentHandler(originalContent)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant APNs as APNs
    participant NSE as NotificationService (NSE)
    participant KS as KeychainInboxKeyStore
    participant MS as AppGroupMetadataStore
    participant CS as ChatService
    participant OS as iOS

    APNs->>NSE: didReceive(request) — vexl_nt_ targetToken
    NSE->>NSE: makeEnricher(), start Task under stateLock
    NSE->>KS: inboxKeyPair(forVexlToken:)
    KS-->>NSE: InboxKeyPair (privateKey, publicKey)
    NSE->>MS: loadMetadata()
    MS-->>NSE: NseMetadata (chatServiceUrl, senderNames, locale)
    NSE->>NSE: VexlPrivateKey(pemBase64:) — curve check
    NSE->>CS: "POST /api/v1/challenges {publicKey}"
    CS-->>NSE: challenge string
    NSE->>NSE: signChallenge(challenge, privateKey) — ECDSA SHA-256
    NSE->>CS: "PUT /api/v1/inboxes/messages {markAsPulled:false}"
    CS-->>NSE: "[{message: ciphertext, senderPublicKey}]"
    NSE->>NSE: eciesLegacyDecrypt per message, pickPreviewCandidate
    NSE->>NSE: NotificationRenderer.render(message, displayName, locale)
    NSE->>NSE: RenderedNotification.apply(to: UNMutableNotificationContent)
    NSE->>OS: contentHandler(enrichedContent)

    note over NSE,OS: On any failure OR 20s timeout → contentHandler(originalContent)
Loading

Reviews (4): Last reviewed commit: "wip docs" | Re-trigger Greptile

Comment thread apps/mobile/targets/vexl-nse/NotificationService.swift
Comment thread apps/chat-service/src/__tests__/routes/retrieveMessages.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (11)
packages/rest-api/src/services/chat/contracts.ts (1)

196-202: 🗄️ Data Integrity & Integration | 🔵 Trivial

Deployment-ordering safety relies on manual sequencing.

The comment correctly documents that false = strictly read-only retrieve (used by the iOS notification service extension): messages are NOT marked pulled and inbox metadata is not updated. Absent/true = current app behavior. The doc also notes a chat-service with this field MUST be deployed before any app build containing the NSE ships. An old server silently ignores the field and marks the messages pulled, which races the app's deletePulledMessages call and can permanently delete messages the app never received.

This is purely a rollout/ops sequencing risk with no automated guard (e.g., server capability check or client-side version gating) — worth ensuring the deployment pipeline/runbook enforces this ordering before the NSE-enabled app build ships.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rest-api/src/services/chat/contracts.ts` around lines 196 - 202, The
deployment-ordering requirement for the chat-service/NSE read-only retrieve
behavior is only documented and not enforced. Update the relevant contract/docs
around the field in contracts.ts and the chat-service rollout path to add an
explicit guard such as server capability checking or client/version gating, so
NSE-enabled app builds cannot ship before the backend supports the field and the
deletePulledMessages race is prevented.
apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Rendering/NotificationLocalization.swift (1)

44-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a parity test between RenderableMessageType and the localization table.

The mapping between RenderableMessageType cases and localization keys is only enforced by convention (a comment in the generator script). If a new case is added to RenderableMessageType without updating RENDERABLE_TYPES/the JSON, entry(for:locale:) silently returns nil for that type in every locale, and NotificationRenderer.render silently drops the preview (falls back to the generic notification) with no compile-time signal.

Add a Swift test asserting NotificationLocalization.loadBundled()!.entry(for: type, locale: "en") is non-nil for every RenderableMessageType.allCases (assuming it's CaseIterable) to catch drift early.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Rendering/NotificationLocalization.swift`
around lines 44 - 54, Add a parity test to lock `RenderableMessageType` to the
bundled localization keys. In the `NotificationLocalization` test suite, verify
that `NotificationLocalization.loadBundled()!.entry(for:locale:)` returns a
non-nil result for every case in `RenderableMessageType.allCases` when using the
English locale. This should exercise the existing `entry(for:locale:)` and fail
fast if `RENDERABLE_TYPES` or the JSON drifts from the enum.
packages/cryptography/src/testVectors/generateNseTestVectors.ts (1)

77-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unbounded search loop for a leading-zero scalar.

The for (let counter = 0; ; counter++) loop has no upper bound. It's statistically safe (finds a match in ~256 iterations) and only runs in a one-off dev script, but a hard cap with a clear error would make the failure mode explicit rather than relying purely on probability.

♻️ Optional safety cap
 function createLeadingZeroScalarKey(idPrefix: string): {
   key: TestVectorKey
   holder: PrivateKeyHolder
 } {
-  for (let counter = 0; ; counter++) {
+  const maxAttempts = 100_000
+  for (let counter = 0; counter < maxAttempts; counter++) {
     const id = `${idPrefix}-${counter}`
     if (deterministicScalar(`vexl-nse-test-vectors-${id}`)[0] !== 0x00) continue
     return createDeterministicKey(id)
   }
+  throw new Error(
+    `Could not find a leading-zero scalar within ${maxAttempts} attempts`
+  )
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cryptography/src/testVectors/generateNseTestVectors.ts` around lines
77 - 86, The createLeadingZeroScalarKey helper uses an unbounded for loop to શોધ
a scalar with a leading zero, so add a fixed retry limit and fail explicitly if
no match is found. Update createLeadingZeroScalarKey to keep the existing
deterministicScalar and createDeterministicKey flow, but stop after a reasonable
maximum number of attempts and throw a clear error mentioning the idPrefix when
the cap is exceeded.
packages/cryptography/src/testVectors/nseTestVectors.test.ts (2)

74-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use Effect Array.filter instead of native .filter().

As per coding guidelines, "Prefer Effect Array helpers with pipe over native array methods like filter and map". Lines 82 and 89 use native .filter().

♻️ Proposed fix
+import {Array as Arr, pipe} from 'effect'
 ...
-      it.each(vectors.filter((vector) => vector.valid))(
+      it.each(pipe(vectors, Arr.filter((vector) => vector.valid)))(
         'vector $id decrypts to the expected plaintext',
         async (vector) => {
           expect(await runDecrypt(vector)).toEqual(vector.expectedPlaintext)
         }
       )

-      it.each(vectors.filter((vector) => !vector.valid))(
+      it.each(pipe(vectors, Arr.filter((vector) => !vector.valid)))(
         'vector $id fails to decrypt',
         async (vector) => {
           await expect(runDecrypt(vector)).rejects.toThrow()
         }
       )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cryptography/src/testVectors/nseTestVectors.test.ts` around lines 74
- 96, The test suite in eciesSuites is using native Array.filter in the two
it.each calls, which should be replaced with Effect Array helpers to match the
codebase style. Update the filtering in the describe block for the
runDecrypt/it.each vectors setup to use the Effect Array API with pipe instead
of calling .filter directly, while keeping the valid and invalid vector
selection logic unchanged.

Source: Coding guidelines


48-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use Effect Array helpers instead of native .map().

As per coding guidelines, "Prefer Effect Array helpers with pipe over native array methods like filter and map". Line 49 uses native .map().

♻️ Proposed fix
+import {Array as Arr, pipe} from 'effect'
 ...
-      const keyIds = new Set(vectorsFile.keys.map((key) => key.id))
+      const keyIds = new Set(pipe(vectorsFile.keys, Arr.map((key) => key.id)))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cryptography/src/testVectors/nseTestVectors.test.ts` around lines 48
- 58, The test in nseTestVectors.test.ts uses a native Array .map() on
vectorsFile.keys; replace that usage with the Effect Array helper style via pipe
to match the project’s coding guidelines. Update the keyIds initialization in
the it block so it derives ids from vectorsFile.keys using the appropriate
Effect Array helper instead of key => key.id with native mapping, while keeping
the rest of the vector validation logic in place.

Source: Coding guidelines

apps/mobile/native/VexlNotificationCore/Tests/VexlNotificationCoreTests/CryptoVectorTests.swift (1)

102-120: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Inconsistent error handling: try? silently swallows vector-load failures.

Every other test in this file uses try TestVectors.load() and fails loudly if the fixture can't be loaded. This test uses try?, silently falling back to an empty key string. Malformed-payload rejection doesn't strictly need real key data, but the inconsistency could mask a genuine fixture-loading regression in this test specifically.

♻️ Proposed fix
-  func testEciesLegacyRejectsMalformedPayloads() {
-    let vectors = try? TestVectors.load()
-    let key = vectors?.keys.first?.privateKeyPemBase64 ?? ""
+  func testEciesLegacyRejectsMalformedPayloads() throws {
+    let vectors = try TestVectors.load()
+    let key = try XCTUnwrap(vectors.keys.first?.privateKeyPemBase64)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/mobile/native/VexlNotificationCore/Tests/VexlNotificationCoreTests/CryptoVectorTests.swift`
around lines 102 - 120, `testEciesLegacyRejectsMalformedPayloads` is using `try?
TestVectors.load()`, which can hide fixture-loading failures by falling back to
an empty private key. Change this test to use the same loud-loading pattern as
the other tests in `CryptoVectorTests` so a bad `TestVectors.load()` call fails
the test instead of silently continuing, while keeping the malformed payload
loop and `eciesLegacyDecrypt` assertions unchanged.
apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Crypto/CryptoPrimitives.swift (1)

64-66: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Prefer throwing over precondition to preserve the graceful-bail contract.

precondition traps and crashes the NSE process on a size mismatch. Callers today pass invariant sizes (32-byte key, 16-byte counter), so this is defensive only, but converting to a thrown VexlCryptoError.aesCtrFailed keeps the extension aligned with its "fall back to generic notification, never crash" design if a future caller violates the invariant.

♻️ Optional: throw instead of trap
-  precondition(key.count == kCCKeySizeAES256)
-  precondition(counterBlock.count == kCCBlockSizeAES128)
+  guard key.count == kCCKeySizeAES256, counterBlock.count == kCCBlockSizeAES128 else {
+    throw VexlCryptoError.aesCtrFailed
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Crypto/CryptoPrimitives.swift`
around lines 64 - 66, Replace the defensive precondition checks in
aes256Ctr(key:counterBlock:data:) with thrown validation so the NSE does not
trap on bad input. Keep the existing key and counterBlock size checks, but if
either size is invalid, throw VexlCryptoError.aesCtrFailed instead of calling
precondition; preserve the current AES-CTR flow for valid inputs and update the
function’s failure path so callers can gracefully bail out.
apps/mobile/app.config.ts (1)

106-108: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Extract the shared app-group id into a single constant.

`group.${extra.packageName}.shared` is duplicated between infoPlist.VexlAppGroup and the com.apple.security.application-groups entitlement. These two must always match for the NSE's shared keychain access group to work — extracting a single appGroupId constant and reusing it in both places would eliminate the risk of future drift.

♻️ Suggested fix
+const sharedAppGroupId = `group.${extra.packageName}.shared`
 ...
-      'VexlAppGroup': `group.${extra.packageName}.shared`,
+      'VexlAppGroup': sharedAppGroupId,
 ...
-      'com.apple.security.application-groups': [
-        `group.${extra.packageName}.shared`,
-      ],
+      'com.apple.security.application-groups': [sharedAppGroupId],

Also applies to: 130-135

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/app.config.ts` around lines 106 - 108, The shared app-group
identifier is duplicated between the VexlAppGroup infoPlist entry and the
com.apple.security.application-groups entitlement, so extract it into a single
appGroupId constant in app.config.ts and reuse it in both places. Update the app
config logic around the infoPlist and ios.entitlements sections so the same
constant is referenced for both the NSE shared keychain access group and the app
group entitlement, preventing drift.
apps/mobile/expo-plugins/with-nse-local-spm.js (1)

43-46: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Idempotency check only covers package registration, not target linkage.

alreadyLinked only inspects project.rootObject.props.packageReferences; it doesn't verify the target's packageProductDependencies or frameworks build phase already include the product. A prior partial/interrupted run could leave the package referenced but not linked to VexlNSE, and this check would then skip re-linking on subsequent prebuilds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/expo-plugins/with-nse-local-spm.js` around lines 43 - 46, The
idempotency guard in withNSELocalSPM only checks
project.rootObject.props.packageReferences, so it can miss cases where VexlNSE
is not yet linked. Update the alreadyLinked logic to also verify the VexlNSE
target’s packageProductDependencies and frameworks/build phase entries for the
package product, and only return early when both the package registration and
target linkage are already present. Use the existing PACKAGE_RELATIVE_PATH and
VexlNSE target lookup in withNSELocalSPM to keep the check comprehensive.
apps/mobile/modules/vexl-nse-bridge/ios/VexlNseBridgeModule.swift (1)

71-130: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Partial-state risk in syncAll on mid-loop failure.

Deleting all keychain items for the service first, then re-adding entries one by one, means a failure partway through the loop leaves some previously-synced inbox keys permanently missing until the next successful sync trigger. Consider collecting per-entry failures and continuing, or building the full attribute set before performing the delete, to avoid transient gaps in the NSE's inbox key coverage.

♻️ Suggested approach
-    for entry in payload.keys {
-      guard !entry.vexlToken.isEmpty else {
-        continue
-      }
-      let value: [String: String] = [
-        "privateKeyPemBase64": entry.inboxPrivateKeyPemBase64,
-        "publicKeyPemBase64": entry.inboxPublicKeyPemBase64,
-      ]
-      try addKeychainItem(
-        service: NseBridgeStorage.inboxKeysService,
-        account: entry.vexlToken,
-        value: try jsonData(from: value),
-        accessGroup: appGroup
-      )
-    }
+    var firstError: Error?
+    for entry in payload.keys {
+      guard !entry.vexlToken.isEmpty else { continue }
+      let value: [String: String] = [
+        "privateKeyPemBase64": entry.inboxPrivateKeyPemBase64,
+        "publicKeyPemBase64": entry.inboxPublicKeyPemBase64,
+      ]
+      do {
+        try addKeychainItem(
+          service: NseBridgeStorage.inboxKeysService,
+          account: entry.vexlToken,
+          value: try jsonData(from: value),
+          accessGroup: appGroup
+        )
+      } catch {
+        firstError = firstError ?? error
+      }
+    }
+    if let firstError { throw firstError }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/modules/vexl-nse-bridge/ios/VexlNseBridgeModule.swift` around
lines 71 - 130, The current syncAll flow in VexlNseBridgeModule.syncAll deletes
the inbox keychain service before re-adding keys, so a failure inside the
payload.keys loop can leave the App Group keychain in a partially empty state.
Update the logic to avoid destructive overwrite until the replacement data is
fully prepared, or handle per-entry add failures while preserving existing
entries; use the syncAll, deleteAllKeychainItems, and addKeychainItem flow as
the main place to refactor the atomicity.
apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Protocol/ChatApiClient.swift (1)

82-164: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider typed Codable request bodies instead of untyped [String: Any].

createChallenge/retrieveMessages build request bodies as [String: Any] dictionaries encoded via JSONSerialization. A small Encodable request struct per endpoint would remove the risk of key-name typos (e.g. "signedChallenge", "publicKey") going undetected until a runtime request failure, and would align with the parsing side already done via typed structs like ServerMessage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Protocol/ChatApiClient.swift`
around lines 82 - 164, The request payloads in ChatApiClient’s createChallenge,
retrieveMessages, and send currently use untyped [String: Any] dictionaries,
which makes key mistakes easy to miss; replace them with small endpoint-specific
Encodable request models and update send to accept an Encodable body so JSON
encoding is type-safe. Keep the existing behavior and endpoint names the same,
but use typed request structs for fields like publicKey, signedChallenge,
markAsPulled, and challenge/signature to align with the typed ServerMessage
parsing already in place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/mobile/native/VexlNotificationCore/scripts/generateNotificationLocalizations.mjs`:
- Around line 66-70: The fallback-locale check in
generateNotificationLocalizations.mjs only verifies that result.en exists, but
it does not confirm that en-base.json covers every
notifications.<TYPE>.title/body entry for all RENDERABLE_TYPES. Update the
validation in the generation flow to inspect the English fallback object and
ensure each renderable type has both title and body before writing outputs, and
throw a clear build-time error if any key pair is missing. Use the existing
generateNotificationLocalizations script and the RENDERABLE_TYPES-driven loop to
locate and enforce this completeness check.

In
`@apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Protocol/ChatMessagePayload.swift`:
- Around line 59-84: Harden DecryptedChatMessage.parse(plaintextJson:) so the
time field only accepts real numeric JSON values and rejects booleans or other
non-timestamp values. Update the time extraction logic to mirror
NotificationPayloadParser.parseSentAt by checking the underlying NSNumber type
and explicitly excluding CFBooleanGetTypeID() before converting to Int64, while
keeping the existing nil-return failure path for invalid payloads.

In
`@apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Rendering/NotificationRenderer.swift`:
- Around line 42-48: The message rendering in NotificationRenderer currently
uses message.text directly for .message notifications, so an empty string can
bypass the localized generic body fallback. Update the .message branch in
NotificationRenderer.render so it treats empty or whitespace-only message.text
the same as nil and falls back to interpolateThem(localized.body, them: them),
while keeping the existing displayName/title behavior unchanged.

In `@apps/mobile/src/state/notifications/nseBridge/syncNseBridgeActionAtom.ts`:
- Around line 45-71: The NSE bridge sync path can race with logout and
repopulate shared keychain data after it was cleared. In
syncNseBridgeActionAtom, re-check sessionAtom immediately before calling
syncAll(payload) and only proceed if the session is still loggedIn. Use the
existing session state guard in the same action to skip the write when logout
has already switched the session to loggedOut or unset.

---

Nitpick comments:
In `@apps/mobile/app.config.ts`:
- Around line 106-108: The shared app-group identifier is duplicated between the
VexlAppGroup infoPlist entry and the com.apple.security.application-groups
entitlement, so extract it into a single appGroupId constant in app.config.ts
and reuse it in both places. Update the app config logic around the infoPlist
and ios.entitlements sections so the same constant is referenced for both the
NSE shared keychain access group and the app group entitlement, preventing
drift.

In `@apps/mobile/expo-plugins/with-nse-local-spm.js`:
- Around line 43-46: The idempotency guard in withNSELocalSPM only checks
project.rootObject.props.packageReferences, so it can miss cases where VexlNSE
is not yet linked. Update the alreadyLinked logic to also verify the VexlNSE
target’s packageProductDependencies and frameworks/build phase entries for the
package product, and only return early when both the package registration and
target linkage are already present. Use the existing PACKAGE_RELATIVE_PATH and
VexlNSE target lookup in withNSELocalSPM to keep the check comprehensive.

In `@apps/mobile/modules/vexl-nse-bridge/ios/VexlNseBridgeModule.swift`:
- Around line 71-130: The current syncAll flow in VexlNseBridgeModule.syncAll
deletes the inbox keychain service before re-adding keys, so a failure inside
the payload.keys loop can leave the App Group keychain in a partially empty
state. Update the logic to avoid destructive overwrite until the replacement
data is fully prepared, or handle per-entry add failures while preserving
existing entries; use the syncAll, deleteAllKeychainItems, and addKeychainItem
flow as the main place to refactor the atomicity.

In
`@apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Crypto/CryptoPrimitives.swift`:
- Around line 64-66: Replace the defensive precondition checks in
aes256Ctr(key:counterBlock:data:) with thrown validation so the NSE does not
trap on bad input. Keep the existing key and counterBlock size checks, but if
either size is invalid, throw VexlCryptoError.aesCtrFailed instead of calling
precondition; preserve the current AES-CTR flow for valid inputs and update the
function’s failure path so callers can gracefully bail out.

In
`@apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Protocol/ChatApiClient.swift`:
- Around line 82-164: The request payloads in ChatApiClient’s createChallenge,
retrieveMessages, and send currently use untyped [String: Any] dictionaries,
which makes key mistakes easy to miss; replace them with small endpoint-specific
Encodable request models and update send to accept an Encodable body so JSON
encoding is type-safe. Keep the existing behavior and endpoint names the same,
but use typed request structs for fields like publicKey, signedChallenge,
markAsPulled, and challenge/signature to align with the typed ServerMessage
parsing already in place.

In
`@apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Rendering/NotificationLocalization.swift`:
- Around line 44-54: Add a parity test to lock `RenderableMessageType` to the
bundled localization keys. In the `NotificationLocalization` test suite, verify
that `NotificationLocalization.loadBundled()!.entry(for:locale:)` returns a
non-nil result for every case in `RenderableMessageType.allCases` when using the
English locale. This should exercise the existing `entry(for:locale:)` and fail
fast if `RENDERABLE_TYPES` or the JSON drifts from the enum.

In
`@apps/mobile/native/VexlNotificationCore/Tests/VexlNotificationCoreTests/CryptoVectorTests.swift`:
- Around line 102-120: `testEciesLegacyRejectsMalformedPayloads` is using `try?
TestVectors.load()`, which can hide fixture-loading failures by falling back to
an empty private key. Change this test to use the same loud-loading pattern as
the other tests in `CryptoVectorTests` so a bad `TestVectors.load()` call fails
the test instead of silently continuing, while keeping the malformed payload
loop and `eciesLegacyDecrypt` assertions unchanged.

In `@packages/cryptography/src/testVectors/generateNseTestVectors.ts`:
- Around line 77-86: The createLeadingZeroScalarKey helper uses an unbounded for
loop to શોધ a scalar with a leading zero, so add a fixed retry limit and fail
explicitly if no match is found. Update createLeadingZeroScalarKey to keep the
existing deterministicScalar and createDeterministicKey flow, but stop after a
reasonable maximum number of attempts and throw a clear error mentioning the
idPrefix when the cap is exceeded.

In `@packages/cryptography/src/testVectors/nseTestVectors.test.ts`:
- Around line 74-96: The test suite in eciesSuites is using native Array.filter
in the two it.each calls, which should be replaced with Effect Array helpers to
match the codebase style. Update the filtering in the describe block for the
runDecrypt/it.each vectors setup to use the Effect Array API with pipe instead
of calling .filter directly, while keeping the valid and invalid vector
selection logic unchanged.
- Around line 48-58: The test in nseTestVectors.test.ts uses a native Array
.map() on vectorsFile.keys; replace that usage with the Effect Array helper
style via pipe to match the project’s coding guidelines. Update the keyIds
initialization in the it block so it derives ids from vectorsFile.keys using the
appropriate Effect Array helper instead of key => key.id with native mapping,
while keeping the rest of the vector validation logic in place.

In `@packages/rest-api/src/services/chat/contracts.ts`:
- Around line 196-202: The deployment-ordering requirement for the
chat-service/NSE read-only retrieve behavior is only documented and not
enforced. Update the relevant contract/docs around the field in contracts.ts and
the chat-service rollout path to add an explicit guard such as server capability
checking or client/version gating, so NSE-enabled app builds cannot ship before
the backend supports the field and the deletePulledMessages race is prevented.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1cf70735-7e68-48a9-8cd3-39aa5667186c

📥 Commits

Reviewing files that changed from the base of the PR and between 96d9756 and 085b0a9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (55)
  • apps/chat-service/src/__tests__/routes/retrieveMessages.test.ts
  • apps/chat-service/src/__tests__/utils/addChallengeForKey.ts
  • apps/chat-service/src/routes/messages/retrieveMessages.ts
  • apps/mobile/.gitignore
  • apps/mobile/app.config.ts
  • apps/mobile/expo-plugins/with-nse-local-spm.js
  • apps/mobile/modules/vexl-nse-bridge/expo-module.config.json
  • apps/mobile/modules/vexl-nse-bridge/index.ts
  • apps/mobile/modules/vexl-nse-bridge/ios/VexlNseBridge.podspec
  • apps/mobile/modules/vexl-nse-bridge/ios/VexlNseBridgeModule.swift
  • apps/mobile/native/VexlNotificationCore/.gitignore
  • apps/mobile/native/VexlNotificationCore/Package.resolved
  • apps/mobile/native/VexlNotificationCore/Package.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Crypto/ChallengeSigner.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Crypto/CryptoPrimitives.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Crypto/EciesGtm.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Crypto/EciesLegacy.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Crypto/VexlKeys.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/NotificationEnricher.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Protocol/ChatApiClient.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Protocol/ChatMessagePayload.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Protocol/NotificationPayload.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Rendering/NotificationContentApplier.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Rendering/NotificationLocalization.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Rendering/NotificationRenderer.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Resources/notificationLocalizations.json
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Storage/InboxKeyStore.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Storage/MetadataStore.swift
  • apps/mobile/native/VexlNotificationCore/Sources/VexlNotificationCore/Storage/NseBridgeConstants.swift
  • apps/mobile/native/VexlNotificationCore/Tests/VexlNotificationCoreTests/BridgeContractTests.swift
  • apps/mobile/native/VexlNotificationCore/Tests/VexlNotificationCoreTests/CryptoVectorTests.swift
  • apps/mobile/native/VexlNotificationCore/Tests/VexlNotificationCoreTests/EnricherTests.swift
  • apps/mobile/native/VexlNotificationCore/Tests/VexlNotificationCoreTests/PayloadParsingTests.swift
  • apps/mobile/native/VexlNotificationCore/Tests/VexlNotificationCoreTests/RenderingTests.swift
  • apps/mobile/native/VexlNotificationCore/Tests/VexlNotificationCoreTests/TestVectors.swift
  • apps/mobile/native/VexlNotificationCore/scripts/generateNotificationLocalizations.mjs
  • apps/mobile/package.json
  • apps/mobile/src/App.tsx
  • apps/mobile/src/state/notifications/nseBridge/syncNseBridgeActionAtom.ts
  • apps/mobile/src/state/notifications/nseBridge/useSyncNseBridge.ts
  • apps/mobile/src/state/useLogout.ts
  • apps/mobile/src/utils/notifications/cancelNewChatNotifications.ts
  • apps/mobile/src/utils/notifications/chatNotifications.ts
  • apps/mobile/src/utils/notifications/nseEnrichedNotifications.ts
  • apps/mobile/targets/vexl-nse/Info.plist
  • apps/mobile/targets/vexl-nse/NotificationService.swift
  • apps/mobile/targets/vexl-nse/expo-target.config.js
  • apps/mobile/targets/vexl-nse/generated.entitlements
  • packages/cryptography/package.json
  • packages/cryptography/src/testVectors/generateNseTestVectors.ts
  • packages/cryptography/src/testVectors/nseTestVectors.test.ts
  • packages/cryptography/src/testVectors/nseTestVectorsFile.ts
  • packages/cryptography/test-vectors/nse-test-vectors.json
  • packages/rest-api/src/services/chat/contracts.ts
  • packages/rest-api/src/services/chat/index.ts

@kaladivo
kaladivo requested a review from SamTremko July 7, 2026 06:07
kaladivo added 3 commits July 7, 2026 08:19
- declare VexlNSE extension to EAS (extra.eas.build.experimental.ios.appExtensions)
  so remote credentials cover the extension target
- create/assign the NSE enrichment Task under stateLock so the timeout
  handler can always cancel in-flight work
- parse the inbox private key once in the enricher and thread it through
  signChallenge/eciesLegacyDecrypt (was re-parsed per message)
- harden the read-only retrieve test: snapshot client_version before and
  assert it is an unchanged concrete number after
- document the bridge storage-contract ownership (NseBridgeConstants) and
  the BridgeContractTests drift guard at the CocoaPods write side
- fail localization generation loudly when the en fallback misses any
  renderable type
- reject boolean/non-numeric chat message time values (CFBoolean guard)
- treat empty/whitespace message text like nil so the body falls back to
  the localized generic string
- make NSE-enriched notification dismissal error-tolerant: a failed
  dismissNotificationAsync is reported as a warning and can no longer
  block displayLocalNotification for the conversation
- close the SEC1 curve-OID gap in VexlPrivateKey: params-less SEC1 keys
  are now rejected as unsupportedCurve (real Vexl keys are PKCS#8 or
  SEC1 with the OID present); regression tests for both forms
@kaladivo
kaladivo marked this pull request as draft July 13, 2026 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant