Skip to content

Conversation

sosweetham
Copy link
Member

@sosweetham sosweetham commented Oct 7, 2025

Description of change

adds ui flows to support documents other than passport

Issue Number

closes #356

Type of change

  • New (a change which implements a new feature)
  • Update (a change which updates existing functionality)

How the change has been tested

Manual

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features

    • Added document type selection and back-side document capture with previews, retake flow, and adjusted verification step flow.
    • Added device/notification support: device registration, local notifications, platform/device ID endpoints, and server-side notification storage and APIs.
  • Bug Fixes

    • Strengthened key-manager guards, improved duplicate/resubmission handling, websocket-driven step progression, and upload error reporting.
  • Refactor

    • Split drawer lifecycle into separate initialization and open/close effects.
  • Chores

    • Bumped app version, updated build deps, increased Android build code, and added migration for notification/device fields.

@sosweetham sosweetham requested a review from coodos as a code owner October 7, 2025 18:57
Copy link
Contributor

coderabbitai bot commented Oct 7, 2025

Walkthrough

Splits Drawer lifecycle into separate init and open/close effects; adds document-type selection UI and state; implements back-side document capture, retake and upload error handling; defers selfie progression to SSE/websocket; tightens websocket typing and key-manager guards; adds notification support (client service, tauri commands, backend entities/controllers/migrations); bumps package versions and mobile config.

Changes

Cohort / File(s) Summary
Drawer lifecycle split
infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte
Creates a one-time initialization effect to instantiate CupertinoPane with its own destroy cleanup; moves present/destroy handling into a separate reactive effect that runs after init.
Verify page & flow rework
infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte
Inserts DocumentType as initial step and reorders steps (passport → selfie indices change); narrows websocketData type to `{ w3id?: string }
Document selection UI
infrastructure/eid-wallet/src/routes/(auth)/verify/steps/document-type.svelte
New component to select document type (Passport/ID Card/Driving License/Residence Permit); updates documentType store and advances verifStep.
Document capture (front/back) & retake
infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte
Adds DocBack store and multi-side capture flow for non-passport types; wraps uploads in try/catch with user-facing errors; retains camera stream for back capture; implements retake flow, camera lifecycle controls, and dynamic UI per documentType.
Selfie progression change
infrastructure/eid-wallet/src/routes/(auth)/verify/steps/selfie.svelte
Removes client-side auto-advance after final image; relies on SSE/websocket to set verifStep (comments added).
Verify stores additions
infrastructure/eid-wallet/src/routes/(auth)/verify/store.ts
Adds exported writable stores: `DocBack: string
Notification client (TAURI) & integration
infrastructure/eid-wallet/src/lib/services/NotificationService.ts, infrastructure/eid-wallet/src/lib/global/controllers/evault.ts, infrastructure/eid-wallet/src/lib/global/state.ts, infrastructure/eid-wallet/src/routes/(app)/+layout.svelte
New singleton NotificationService with device registration, permission, local notification, and check flows; GlobalState now exposes notificationService; VaultController registers device for notifications when resolving/setting vault; layout runs notification checks post-auth.
TAURI/native additions
infrastructure/eid-wallet/src-tauri/src/lib.rs, infrastructure/eid-wallet/src-tauri/Cargo.toml, infrastructure/eid-wallet/src-tauri/tauri.conf.json, infrastructure/eid-wallet/src-tauri/capabilities/mobile.json
Adds tauri plugin notification and uuid dependency; new Tauri commands get_device_id and get_platform; adds mobile notification:default permission and increments Android versionCode.
Backend notification support (provisioner)
infrastructure/evault-provisioner/src/entities/Notification.ts, .../services/NotificationService.ts, .../controllers/NotificationController.ts, .../config/database.ts, .../index.ts
Adds Notification entity, NotificationService (device registration, send/check/delivery), NotificationController routes (register/unregister/send/check/stats), and registers controller in server startup; database entities updated to include Notification.
Migrations & tests
infrastructure/evault-provisioner/src/migrations/*, infrastructure/evault-provisioner/test-notification.js, infrastructure/evault-provisioner/test-real-notifications.js
Adds migration to extend Verification (deviceId/platform/fcmToken/deviceActive) and migration to create notifications table; includes two test scripts for notification flows.
Store/UX small changes
infrastructure/eid-wallet/src/lib/fragments/IdentityCard/IdentityCard.svelte, infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte, package.json, infrastructure/eid-wallet/package.json
Changes ePassport badge text to “ID”; updates settings version label to v0.3.0.0; updates pnpm onlyBuiltDependencies and bumps infrastructure/eid-wallet package version to 0.3.0; adds @tauri-apps/plugin-notification dependency.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Drawer
  participant VerifyPage
  participant Stores as Stores (verifStep, documentType, DocFront/DocBack...)
  participant Camera
  participant NotificationSvc as NotificationService (client)
  participant Backend as Backend/SSE
  participant Provisioner

  rect rgba(200,220,255,0.12)
    note right of Drawer: Drawer init separated from open/close
    Drawer->>Drawer: onMount -> create CupertinoPane (init)
    Drawer-->>Drawer: onDestroy -> destroy pane
    Drawer->>Drawer: reactive open state -> present / destroy (post-init)
  end

  rect rgba(220,255,220,0.12)
    User->>VerifyPage: Open verification
    VerifyPage->>Stores: verifStep = 0
    VerifyPage->>User: Render DocumentType
    User->>VerifyPage: Select document type
    VerifyPage->>Stores: documentType = chosen / verifStep = 1
  end

  rect rgba(255,245,200,0.12)
    VerifyPage->>Camera: Start stream
    User->>Camera: Capture front -> upload -> Stores.DocFront
    alt documentType != passport
      User->>Camera: Capture back -> upload -> Stores.DocBack
    end
    User->>VerifyPage: Continue to selfie
    VerifyPage->>Camera: Stop stream
    Note right of VerifyPage: Await SSE/websocket to advance verifStep
  end

  rect rgba(255,220,220,0.12)
    VerifyPage->>Backend: Subscribe websocket/SSE
    Backend-->>VerifyPage: Events (progress, duplicate {w3id}, completed)
    alt Duplicate
      VerifyPage->>Stores: reset DocFront/DocBack/Selfie, set verifStep per duplicate flow
      Backend->>Provisioner: resolve eVault via w3id
    else Normal
      Backend-->>Stores: set verifStep to next step(s)
    end
  end

  rect rgba(240,240,255,0.12)
    VerifyPage->>NotificationSvc: requestPermissions / registerDevice (post-auth)
    NotificationSvc->>Provisioner: POST /api/devices/register
    Provisioner-->>NotificationSvc: registration result
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • JulienAuvo

Poem

A rabbit hops from pane to pane,
Chooses ID, snaps front and back again.
Retakes, uploads, the camera hums,
SSE whispers when the next step comes.
A tiny bell — notifications registered! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The PR contains extensive notification service integration, Tauri plugin additions, backend migrations, and provisioner controller changes that are unrelated to the document verification UI objective. Please extract the notification integration, migrations, and service additions into a separate PR so this one remains focused solely on adding multi-document verification support.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The title refers to adding support for more verification documents in the wallet, which aligns with the core change, although its phrasing is informal and could be clearer.
Linked Issues Check ✅ Passed The PR adds a decision drawer and document-type component, updates the store and verification flow to handle multiple document types, and enables users to select and proceed with their chosen document, satisfying acceptance criteria in issue #356.
Description Check ✅ Passed The description follows the repository template by including the change description, linked issue number, change type, testing approach, and a completed checklist matching the required sections.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/wallet-more-doc-support

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 and usage tips.

@sosweetham sosweetham changed the title Feat/wallet more doc support Feat/wallet more verification docs support Oct 7, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (6)
infrastructure/eid-wallet/src/routes/(auth)/verify/steps/selfie.svelte (1)

65-67: Progression via SSE/WebSocket only — add a fallback

Good to defer step advancement to server events. Consider a timeout fallback (e.g., show “Retry” or re-enable capture) if no event arrives to avoid UI dead-ends.

Confirm the event that sets verifStep=3 is always emitted after PATCH; if not, add a fallback timer.

infrastructure/eid-wallet/src/routes/(auth)/verify/store.ts (1)

11-11: documentType union looks good; consider centralizing the type

Optional: export a DocumentType type and reuse it across components to avoid union drift.

export type DocumentType = "passport" | "id" | "permit" | "dl";
export const documentType = writable<DocumentType | null>(null);
infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte (2)

51-94: Camera selection complexity (torch probing) likely unnecessary

You detect torch support but never enable it; iterating all devices with getUserMedia can be heavy and prompt repeatedly on some browsers. Consider simplifying to facingMode: "environment" and avoid per-device streams. If torch is needed, use applyConstraints when available.

// Simplified approach
async function getMainCameraStream() {
  return navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } });
}

Ensure iOS/Safari behavior meets your UX with the simpler constraint; otherwise, add torch via track.applyConstraints({ advanced: [{ torch: true }] }) when low-light is detected.


141-141: Remove debug logs or guard behind dev flag

console.log in production UI can be noisy.

- console.log("Switched to back image capture, image =", image);
+ if (import.meta.env?.DEV) console.debug("Switched to back image capture", { image });

Also applies to: 173-173

infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte (2)

272-279: Encode w3id when building the resolve URL

Use URLSearchParams to avoid malformed queries if w3id contains special characters.

-                const response = await axios.get(
-                    new URL(
-                        `resolve?w3id=${existingW3id}`,
-                        PUBLIC_REGISTRY_URL,
-                    ).toString(),
-                );
+                const url = new URL("resolve", PUBLIC_REGISTRY_URL);
+                url.searchParams.set("w3id", existingW3id);
+                const response = await axios.get(url.toString());

118-145: Close the EventSource on component destroy to prevent leaks

Persist the EventSource and close it in onDestroy.

-    import { getContext, onMount } from "svelte";
+    import { getContext, onMount, onDestroy } from "svelte";
+    let verificationEventSource: EventSource | null = null;
-        const eventSource = new EventSource(sseUrl);
+        verificationEventSource = new EventSource(sseUrl);
-        eventSource.onopen = () => {
+        verificationEventSource.onopen = () => {
@@
-        eventSource.onmessage = (e) => {
+        verificationEventSource.onmessage = (e) => {
+    onDestroy(() => {
+        verificationEventSource?.close();
+        verificationEventSource = null;
+    });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ba8dbd and 173a474.

⛔ Files ignored due to path filters (2)
  • infrastructure/eid-wallet/src-tauri/gen/android/.idea/kotlinc.xml is excluded by !**/gen/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte (2 hunks)
  • infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte (7 hunks)
  • infrastructure/eid-wallet/src/routes/(auth)/verify/steps/document-type.svelte (1 hunks)
  • infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte (3 hunks)
  • infrastructure/eid-wallet/src/routes/(auth)/verify/steps/selfie.svelte (1 hunks)
  • infrastructure/eid-wallet/src/routes/(auth)/verify/store.ts (1 hunks)
  • package.json (1 hunks)
🧰 Additional context used
🪛 GitHub Actions: Check Code
package.json

[error] 1-1: turbo run check failed: Failed to add workspace 'rest-express'; it already exists at 'platforms/dreamSync/package.json'.

🪛 GitHub Actions: Check Format
package.json

[error] 1-1: Failed to add workspace 'rest-express' from 'platforms/dreamSync/package.json'; it already exists at 'platforms/marketplace/package.json'.

🪛 GitHub Actions: Check Lint
package.json

[error] 1-1: Turbo: Failed to add workspace 'rest-express' from 'platforms/dreamSync/package.json'; it already exists at 'platforms/marketplace/package.json'.

🔇 Additional comments (8)
infrastructure/eid-wallet/src/routes/(auth)/verify/steps/document-type.svelte (1)

1-50: LGTM — clear UX and state flow

Selecting a document type updates the store and advances to capture step. Matches store union.

infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte (2)

118-134: Good: front image upload now wrapped in try/catch with user-facing error

Error path halts loading and provides feedback. Solid improvement.


144-177: Good: back image capture/upload with error handling and state updates

Back side flow and previews align with non-passport types.

infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte (1)

30-50: Confirm Svelte 5 migration and legacy syntax support
Svelte ^5.0.0 is declared and runes ($effect, $props, $state, $derived, $bindable) are used widely, yet legacy export let remains in many *.svelte files. Ensure your build config supports both syntaxes or complete migration to runes to avoid build errors.

infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte (4)

22-22: Good: reset back-side doc and selfie on resubmission

DocBack reset complements DocFront/Selfie resets to avoid stale state. LGTM.

Also applies to: 140-141


105-105: Narrowed websocketData type is safer

Typing to { w3id?: string } | null is an improvement. Please confirm the backend always includes w3id for duplicate events.


188-191: Key manager guards and init check look good

Explicit not-initialized throws and the exists→generate flow reduce runtime surprises. LGTM.

Also applies to: 207-210, 232-236


143-144: Confirm step index change to 3

Jumping to step 3 on any message bypasses step 2. Verify this aligns with the updated step model (DocumentType=0, Passport=1, Selfie=2, Results=3) for approved/duplicate/resubmission flows.

Comment on lines +51 to 60
// Handle open/close state separately
$effect(() => {
if (!pane) return;
if (isPaneOpen) {
pane.present({ animate: true });
} else {
pane.destroy({ animate: true });
}
return () => pane.destroy();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Destroying pane on close prevents reopening (pane not re-initialized)

Calling pane.destroy() when isPaneOpen=false removes the instance; later openings won’t recreate it because the init effect doesn’t rerun. Use hide/close (non-destructive) on close, and reserve destroy for unmount.

Apply:

-        if (isPaneOpen) {
-            pane.present({ animate: true });
-        } else {
-            pane.destroy({ animate: true });
-        }
+        if (isPaneOpen) {
+            pane.present({ animate: true });
+        } else {
+            // Prefer non-destructive close
+            if (typeof (pane as any).hide === "function") {
+                (pane as any).hide({ animate: true });
+            } else {
+                // As a fallback, keep the instance; avoid destroy here
+            }
+        }

Option B: if hide isn’t available, gate destroy to unmount and re-init pane when reopening.


🌐 Web query:

Does CupertinoPane expose a non-destructive method (e.g., hide/close/dismiss) to close the pane without destroying it?

💡 Result:

Yes. CupertinoPane provides a non‑destructive hide() that removes the pane from view but keeps it in the DOM (you can check isHidden() and call present() to show it again). [1][2]

Sources:

  • GitHub docs (public methods: hide, destroy, isHidden). [1]
  • npm package docs (showing hide(), destroy(), present(), isHidden()). [2]

Replace pane.destroy() with pane.hide() on close
CupertinoPane provides hide(), which non-destructively closes the pane and lets you call present() to reopen it.
File: infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte lines 51–60

-        if (isPaneOpen) {
-            pane.present({ animate: true });
-        } else {
-            pane.destroy({ animate: true });
-        }
+        if (isPaneOpen) {
+            pane.present({ animate: true });
+        } else {
+            pane.hide({ animate: true });
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle open/close state separately
$effect(() => {
if (!pane) return;
if (isPaneOpen) {
pane.present({ animate: true });
} else {
pane.destroy({ animate: true });
}
return () => pane.destroy();
});
// Handle open/close state separately
$effect(() => {
if (!pane) return;
if (isPaneOpen) {
pane.present({ animate: true });
} else {
pane.hide({ animate: true });
}
});
🤖 Prompt for AI Agents
In infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte around lines 51 to
60, the code currently calls pane.destroy({ animate: true }) when closing;
replace that with pane.hide({ animate: true }) so the pane is closed
non-destructively and can be reopened with pane.present(), keeping the existing
null check for pane; ensure the call signature matches the CupertinoPane API
(use hide with the animate option) and remove any logic that assumes the pane is
destroyed.

Comment on lines +17 to 18
import DocumentType from "./steps/document-type.svelte";
import Passport from "./steps/passport.svelte";
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Generalize UI and stored labels for non-passport docs

Hardcoded “Passport” persists in user metadata and UI, which mislabels non-passport flows. Import documentType, add a label map, and switch labels to be document‑type aware. Also generalize the hero subtitle and alt text.

Apply:

-    import {
-        DocFront,
-        DocBack,
-        Selfie as SelfiePic,
-        reason,
-        status,
-        verifStep,
-        verificaitonId,
-    } from "./store";
+    import {
+        DocFront,
+        DocBack,
+        Selfie as SelfiePic,
+        reason,
+        status,
+        verifStep,
+        verificaitonId,
+        documentType,
+    } from "./store";
-    let hardwareKeyCheckComplete = $state(false);
+    let hardwareKeyCheckComplete = $state(false);
+    const docLabel = {
+        passport: "Passport",
+        id: "National ID",
+        permit: "Residence Permit",
+        dl: "Driver's License",
+    } as const;
-                "ID submitted": `Passport - ${person.nationality.value}`,
-                "Passport Number": document.number.value,
+                "ID submitted": `${docLabel[$documentType ?? "passport"]} - ${person.nationality.value}`,
+                "Document Number": document.number.value,
-                Get your passport ready. You’ll be directed to present your
-                passport and take a quick selfie.
+                Get your document ready. You’ll be directed to present your
+                chosen ID and take a quick selfie.
-        <img class="mx-auto mt-20" src="images/Passport.svg" alt="passport" />
+        <img class="mx-auto mt-20" src="images/Passport.svg" alt="identity document" />

Also applies to: 358-362

export const verifStep = writable(0);
export const permissionGranted = writable<boolean>(false);
export const DocFront = writable<string | null>();
export const DocBack = writable<string | null>();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

writable requires an initial value

This will fail type-checking and may break at runtime. Initialize to null.

-export const DocBack = writable<string | null>();
+export const DocBack = writable<string | null>(null);

Also align adjacent stores for consistency:

// Outside this hunk but recommended
export const DocFront = writable<string | null>(null);
export const Selfie = writable<string | null>(null);
export const status = writable<string>("");
export const reason = writable<string>("");
🤖 Prompt for AI Agents
in infrastructure/eid-wallet/src/routes/(auth)/verify/store.ts around line 6,
the DocBack writable is declared without an initial value which fails TypeScript
checks and can break at runtime; initialize it to null (writable<string |
null>(null)) and while here, make adjacent store declarations consistent by
explicitly initializing DocFront, Selfie to null and status, reason to empty
strings as shown in the comment.

Comment on lines 25 to 34
"onlyBuiltDependencies": [
"@firebase/util",
"@tailwindcss/oxide",
"bufferutil",
"core-js",
"@biomejs/biome",
"cpu-features",
"es5-ext",
"esbuild",
"msw",
"protobufjs",
"sharp",
"ssh2",
"svelte-preprocess",
"unrs-resolver"
"svelte-preprocess"
]
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

CI is failing: duplicate workspace name; fix before merge

Turbo reports a duplicate workspace "rest-express" (dreamSync vs marketplace). Rename one or adjust its package.json "name" to be unique, then update references.

Run to locate duplicates:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# List all package.json names and paths, highlight duplicates
fd -H -t f package.json \
| xargs -I '{}' sh -c 'printf "%s\t%s\n" "$(jq -r .name "{}")" "{}"' \
| sort \
| awk -F'\t' '{a[$1]=a[$1] ? a[$1]" | "$2 : $2} END{for (k in a) if (split(a[k], b, /\|/) > 1) printf "DUPLICATE\t%s\t%s\n", k, a[k]}'

Length of output: 193


Fix duplicate workspace names
CI fails due to duplicate workspaces; ensure each name in package.json is unique:

  • web3-adapter (in infrastructure/web3-adapter/package.json & services/web3-adapter/package.json)
  • rest-express (in platforms/dreamSync/package.json & platforms/marketplace/package.json)
    Rename or adjust one instance of each before merging.
🤖 Prompt for AI Agents
In package.json around lines 25 to 34, CI is failing due to duplicate workspace
package names: web3-adapter appears in infrastructure/web3-adapter/package.json
and services/web3-adapter/package.json, and rest-express appears in
platforms/dreamSync/package.json and platforms/marketplace/package.json; fix by
renaming one of each pair (or adjusting package.json "name" fields) to unique,
descriptive names (e.g., web3-adapter-infra or web3-adapter-service,
dreamSync-rest-express or marketplace-rest-express) and update any internal
references/imports or workspace globs that rely on the old names to match the
new names before merging.

Comment on lines +26 to +33
"@biomejs/biome",
"cpu-features",
"es5-ext",
"esbuild",
"msw",
"protobufjs",
"sharp",
"ssh2",
"svelte-preprocess",
"unrs-resolver"
"svelte-preprocess"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

onlyBuiltDependencies changes look fine

The additions align with the toolchain. Ensure all entries listed here are actual deps in the repo to avoid noise.

Quick check:


🏁 Script executed:

#!/bin/bash
for p in "@biomejs/biome" "es5-ext" "svelte-preprocess"; do
  rg -nP "\"$p\"" -g '**/package.json'
done

Length of output: 334


🏁 Script executed:

#!/usr/bin/env bash
python3 - << 'PYCODE'
import json
with open('package.json') as f:
    data = json.load(f)
declared = set()
for scope in ['dependencies','devDependencies','peerDependencies','optionalDependencies']:
    declared.update(data.get(scope, {}).keys())
pkgs = ["@biomejs/biome","cpu-features","es5-ext","esbuild","msw","protobufjs","ssh2","svelte-preprocess"]
missing = [p for p in pkgs if p not in declared]
print("Missing:", missing)
PYCODE

Length of output: 97


Remove or declare missing onlyBuiltDependencies entries

The following packages in onlyBuiltDependencies aren’t declared in package.json: cpu-features, es5-ext, esbuild, msw, protobufjs, ssh2, svelte-preprocess. Either add them to dependencies/devDependencies or remove them to avoid noise.

🤖 Prompt for AI Agents
In package.json around lines 26 to 33, the onlyBuiltDependencies list references
packages (cpu-features, es5-ext, esbuild, msw, protobufjs, ssh2,
svelte-preprocess) that are not declared in dependencies or devDependencies;
update package.json by either adding each referenced package to the appropriate
dependencies/devDependencies entry with sensible version specifiers (or moving
existing build-only tools to devDependencies) or remove the unused names from
onlyBuiltDependencies to keep the manifest consistent and avoid noise.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte (1)

241-242: Instruction text logic is still incorrect (duplicate issue).

This issue was flagged in the previous review but remains unfixed. The instruction text incorrectly uses $DocFront ? "back" : "photo", which:

  • Shows "photo" initially (when $DocFront is null), which is wrong for non-passport documents
  • Shows "back" after front capture for all documents, which is wrong for passports

For passports, it should always show "photo" (single-page document). For other documents, it should show "front" initially, then "back" after the front is captured.

Apply this diff to fix the instruction text:

                 <p>
                     Please place your document's
-                    {$DocFront ? "back" : "photo"}
+                    {#if $documentType === "passport"}
+                        photo
+                    {:else}
+                        {image === 1 ? "front" : "back"}
+                    {/if}
                     page within the rectangle and press the take photo button
                 </p>
🧹 Nitpick comments (1)
infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte (1)

22-23: Optional: Consider using plain reactive variables for local state.

image1Captured and image2Captured are only used within this component and don't need to be writable stores. Plain reactive variables would be simpler and more performant for local-only state.

Apply this diff to simplify the state management:

-let image1Captured = writable(false);
-let image2Captured = writable(false);
+let image1Captured = false;
+let image2Captured = false;

Then update the usages:

-            image1Captured.set(true);
+            image1Captured = true;
-            image2Captured.set(true);
+            image2Captured = true;
-    image1Captured.set(false);
-    image2Captured.set(false);
+    image1Captured = false;
+    image2Captured = false;

And in the template:

-{#if ($documentType !== "passport" && $image1Captured && $image2Captured) || ($documentType === "passport" && $image1Captured)}
+{#if ($documentType !== "passport" && image1Captured && image2Captured) || ($documentType === "passport" && image1Captured)}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 173a474 and d50e381.

📒 Files selected for processing (1)
  • infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte (3 hunks)
🔇 Additional comments (6)
infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte (6)

7-14: LGTM! Imports support multi-document verification.

The addition of DocBack and documentType imports correctly supports the extended verification flow for multiple document types with front/back capture.


180-208: LGTM! Camera lifecycle and retake flow are well-implemented.

The helper functions properly manage camera lifecycle and enable retake functionality:

  • retakeImages comprehensively resets capture state
  • stopCamera properly releases media tracks
  • continueToSelfie cleanly transitions to the next step

210-217: LGTM! Proper cleanup prevents resource leaks.

The onMount cleanup properly releases camera resources when the component unmounts, following Svelte best practices.


222-224: LGTM! Improved error display.

The styled error message block provides clear user feedback when upload failures occur.


229-239: LGTM! Dynamic heading supports multiple document types.

The heading correctly adapts to the selected document type, enhancing user clarity.


291-320: LGTM! Conditional button logic correctly handles all document types.

The button rendering logic properly handles:

  • Showing "Retake" and "Continue to Selfie" when all required photos are captured (both for non-passport, single for passport)
  • Showing appropriate capture button text during the capture process

Comment on lines +118 to +134
try {
await axios.post(
new URL(
`/verification/${$verificaitonId}/media`,
PUBLIC_PROVISIONER_URL,
).toString(),
{
img: dataUrl,
type: "document-front",
},
);
} catch (err) {
console.error("Failed to upload front image:", err);
error = "Failed to upload image. Please try again.";
loading = false;
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Good error handling, but remove debug logging.

The try-catch error handling for image uploads is excellent and prevents silent failures with user-facing error messages.

However, remove the console.log statement at line 141 for production:

             loading = false;
             image1Captured.set(true);
-
-            // Don't stop camera - keep it running for retake or back photo
             if ($documentType !== "passport") {
                 image = 2; // Go to back image capture for non-passport documents
-                console.log("Switched to back image capture, image =", image);
             }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
in infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte
around lines 118-134, remove the debug console.error("Failed to upload front
image:", err) call from the catch block; keep the existing error = "Failed to
upload image. Please try again."; loading = false; and return statements so
user-facing handling remains, or replace the console call with a production-safe
logger/reporting call if required by the app's logging strategy.

Comment on lines 154 to 177
try {
await axios.post(
new URL(
`/verification/${$verificaitonId}/media`,
PUBLIC_PROVISIONER_URL,
).toString(),
{
img: dataUrl,
type: "document-back",
},
);
} catch (err) {
console.error("Failed to upload back image:", err);
error = "Failed to upload image. Please try again.";
loading = false;
return;
}
verifStep.set(1);
loading = false;
image2Captured.set(true);
console.log("Back image captured");
// Don't stop camera - keep it running for retake option
// Camera will be stopped when navigating away or on component cleanup
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove debug logging from back image capture.

The back image capture implementation correctly mirrors the front image logic with proper error handling.

Remove the console.log statement at line 173:

             loading = false;
             image2Captured.set(true);
-            console.log("Back image captured");
             // Don't stop camera - keep it running for retake option
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
await axios.post(
new URL(
`/verification/${$verificaitonId}/media`,
PUBLIC_PROVISIONER_URL,
).toString(),
{
img: dataUrl,
type: "document-back",
},
);
} catch (err) {
console.error("Failed to upload back image:", err);
error = "Failed to upload image. Please try again.";
loading = false;
return;
}
verifStep.set(1);
loading = false;
image2Captured.set(true);
console.log("Back image captured");
// Don't stop camera - keep it running for retake option
// Camera will be stopped when navigating away or on component cleanup
}
}
try {
await axios.post(
new URL(
`/verification/${$verificaitonId}/media`,
PUBLIC_PROVISIONER_URL,
).toString(),
{
img: dataUrl,
type: "document-back",
},
);
} catch (err) {
console.error("Failed to upload back image:", err);
error = "Failed to upload image. Please try again.";
loading = false;
return;
}
loading = false;
image2Captured.set(true);
// Don't stop camera - keep it running for retake option
// Camera will be stopped when navigating away or on component cleanup
}
}
🤖 Prompt for AI Agents
infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte around
lines 154 to 177: remove the debug console.log("Back image captured") on line
173 so debug output is not left in production; keep the surrounding logic, error
handling, and state updates intact (do not alter other console.error or
functional code).

Comment on lines +263 to +264
<div class="text-center text-xs text-gray-400 mb-4 px-4">
<!-- Accepted documents: Driver's License, Residence Permit, Passport, ID Card. -->
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove commented-out code.

Line 264 contains commented-out text that should be removed to keep the codebase clean.

Apply this diff:

             <div class="text-center text-xs text-gray-400 mb-4 px-4">
-                <!-- Accepted documents: Driver's License, Residence Permit, Passport, ID Card. -->
                 {#if $documentType === "passport"}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="text-center text-xs text-gray-400 mb-4 px-4">
<!-- Accepted documents: Driver's License, Residence Permit, Passport, ID Card. -->
<div class="text-center text-xs text-gray-400 mb-4 px-4">
{#if $documentType === "passport"}
🤖 Prompt for AI Agents
in infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte
around lines 263-264, remove the commented-out HTML comment ("<!-- Accepted
documents: Driver's License, Residence Permit, Passport, ID Card. -->") so the
component doesn't contain dead/commented code; simply delete that comment line
and ensure surrounding indentation and markup remain valid.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (6)
infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte (6)

130-130: Remove debug logging for production.

The console.error statement should be removed or replaced with a production-safe logging mechanism.


141-141: Remove debug logging for production.

The console.log statement should be removed to avoid debug output in production.


166-166: Remove debug logging for production.

The console.error statement should be removed or replaced with a production-safe logging mechanism.


173-173: Remove debug logging for production.

The console.log statement should be removed to avoid debug output in production.


264-264: Remove commented-out code.

The commented-out HTML comment should be removed to keep the codebase clean.


241-242: Instruction text logic is still incorrect.

Despite the past review comment being marked as addressed, the instruction text still uses {$DocFront ? "back" : "photo"} which is incorrect. For passports, it should always show "photo" (single page). For other documents, it should show "front" or "back" based on the current capture state (the image variable), not based on whether $DocFront exists.

Apply this diff to fix the logic:

                <p>
-                    Please place your document's {$DocFront ? "back" : "photo"} page within the rectangle
+                    Please place your document's
+                    {#if $documentType === "passport"}
+                        photo
+                    {:else}
+                        {image === 1 ? "front" : "back"}
+                    {/if}
+                    page within the rectangle
                    and press the take photo button
                </p>
🧹 Nitpick comments (2)
infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte (2)

241-242: Redundant instruction text in two locations.

The component has instruction text at both lines 241-242 (brief) and lines 263-289 (detailed). The detailed version (lines 263-289) provides better context by showing the current capture state (front/back). Consider removing or simplifying the instruction at lines 241-242 to avoid confusion, or consolidate both into a single, clear instruction.

Also applies to: 263-289


1-1: Consider renaming the component file.

The component is named passport.svelte but now handles multiple document types (passport, ID card, driver's license, residence permit). Consider renaming it to something more generic like document-capture.svelte to better reflect its purpose.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 257284e and 979940a.

📒 Files selected for processing (1)
  • infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte (3 hunks)
🔇 Additional comments (3)
infrastructure/eid-wallet/src/routes/(auth)/verify/steps/passport.svelte (3)

221-225: Good error handling UI.

The styled error message block provides clear feedback to users when uploads fail. This is a significant improvement over silent failures.


291-320: Conditional button logic is well-implemented.

The button logic correctly shows different controls based on the capture state: retake/continue buttons when all required photos are captured, or a single capture button otherwise. The button text also dynamically updates based on the current capture stage.


321-375: Preview thumbnails enhance UX.

The preview implementation for front/back document images is well-designed, with clear visual indicators for captured vs. pending photos. The passport exemption from previews is also correctly handled.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte (2)

17-28: Import documentType from the store.

The documentType store is still not imported despite the past review comment. This store is needed to make user-facing labels document-type aware (as suggested in the previous review for lines 252-258).

Add the import:

 import {
     DocFront,
     DocBack,
     Selfie as SelfiePic,
     reason,
     status,
     verifStep,
     verificaitonId,
+    documentType,
 } from "./store";

247-258: Use documentType store for consistent labeling.

The code now handles different document types from the backend response (document.type.value), but it doesn't use the documentType store that captures the user's selection. This creates a potential inconsistency if the backend classification differs from what the user selected.

As suggested in the previous review, import and use the documentType store:

 "ID submitted":
-    document.type.value === "passport"
-        ? `Passport - ${document.country.value}`
-        : document.type.value === "drivers_license"
-          ? `Driving License - ${document.country.value}`
-          : `ID Card - ${document.country.value}`,
+    (() => {
+        const docLabel = {
+            passport: "Passport",
+            id: "National ID",
+            permit: "Residence Permit",
+            dl: "Driver's License",
+        } as const;
+        return `${docLabel[$documentType ?? "passport"]} - ${document.country.value}`;
+    })(),
🧹 Nitpick comments (1)
infrastructure/eid-wallet/src/lib/fragments/IdentityCard/IdentityCard.svelte (1)

94-94: Terminology update aligns with multi-document support.

The change from "DEMO PASSPORT"/"VERIFIED PASSPORT" to "DEMO ID"/"VERIFIED ID" appropriately generalizes the badge text to support multiple document types beyond passports.

Consider extracting these strings to an i18n/localization file if the application supports multiple languages.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 979940a and aec52ad.

⛔ Files ignored due to path filters (1)
  • infrastructure/eid-wallet/src-tauri/gen/android/app/build.gradle.kts is excluded by !**/gen/**
📒 Files selected for processing (5)
  • infrastructure/eid-wallet/package.json (1 hunks)
  • infrastructure/eid-wallet/src-tauri/tauri.conf.json (1 hunks)
  • infrastructure/eid-wallet/src/lib/fragments/IdentityCard/IdentityCard.svelte (2 hunks)
  • infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte (1 hunks)
  • infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte (9 hunks)
✅ Files skipped from review due to trivial changes (1)
  • infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte
🔇 Additional comments (6)
infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte (6)

138-143: LGTM!

The resubmission handling correctly resets all captured document images (front, back, and selfie) and advances to the results step. The step number change from 2 to 3 properly reflects the new step mapping that includes DocumentType selection at step 0.


188-190: LGTM!

The explicit guard after initializeKeyManager() is good defensive programming. The clear error message "Key manager not initialized" will help with debugging if initialization silently fails.


207-209: LGTM!

Consistent defensive guard with a clear error message, matching the pattern in generateApplicationKeyPair().


232-237: LGTM!

The conditional check for key existence only when keyManager is defined prevents null reference errors and aligns with the defensive programming pattern used throughout the component.


330-331: LGTM!

The subtitle text has been generalized to support multiple document types, addressing the past review concern about hardcoded "Passport" references.


362-367: LGTM!

The step mapping correctly implements the new flow with document type selection as the initial step. The sequence (0: DocumentType → 1: Passport → 2: Selfie → 3+: Results) aligns with the changes throughout the component, including the verifStep.set(3) at line 143.

"targets": "all",
"android": {
"versionCode": 2
"versionCode": 6
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep Tauri app version in sync with the release.

versionCode is bumped, but the same file still reports "version": "0.1.8", while package.json is now 0.3.0. This mismatch will surface in the built app (and any distribution metadata). Please update the Tauri version field to 0.3.0 so the Android bundle and desktop binaries advertise the intended release number.

🤖 Prompt for AI Agents
in infrastructure/eid-wallet/src-tauri/tauri.conf.json around line 29, the Tauri
config's "version" field is out of sync with package.json (file shows "version":
"0.1.8" while package.json is 0.3.0); update the "version" value to "0.3.0" so
it matches package.json/versionCode and ensure any other version fields in this
file match the new release number before committing.

let loading = $state(false);
let keyManager: KeyManager | null = $state(null);
let websocketData: any = $state(null); // Store websocket data for duplicate case
let websocketData: { w3id?: string } | null = $state(null); // Store websocket data for duplicate case
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Type narrowing conflicts with stored data.

websocketData is typed as { w3id?: string } | null, but line 137 assigns the full websocket message (which includes status, reason, person, document, and other properties). This type mismatch defeats the purpose of type safety.

Choose one of these solutions:

Option 1 (if only w3id is needed): Store only the required property.

-        websocketData = data; // Store the full websocket data
+        websocketData = data.w3id ? { w3id: data.w3id } : null;

Option 2 (if full data is needed): Define and use the complete type.

+    type WebsocketData = {
+        status: string;
+        reason: string;
+        person: Person;
+        document: Document;
+        w3id?: string;
+    };
+
-    let websocketData: { w3id?: string } | null = $state(null);
+    let websocketData: WebsocketData | null = $state(null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let websocketData: { w3id?: string } | null = $state(null); // Store websocket data for duplicate case
// in your WebSocket message handler (around line 137)
websocketData = data.w3id ? { w3id: data.w3id } : null;
Suggested change
let websocketData: { w3id?: string } | null = $state(null); // Store websocket data for duplicate case
<script lang="ts">
// … other imports
// Define the full shape of the incoming message
type WebsocketData = {
status: string;
reason: string;
person: Person;
document: Document;
w3id?: string;
};
-// Store only w3id (too narrow)
// Store the full payload with proper typing
let websocketData: WebsocketData | null = $state(null);
// … rest of your code
</script>
🤖 Prompt for AI Agents
in infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte around line
105 (and referenced assignment at line 137), websocketData is declared as {
w3id?: string } | null but later assigned the entire websocket message (which
includes status, reason, person, document, etc.), causing a type mismatch;
either restrict stored value to only the needed property (Option 1) by changing
assignments to extract and store just message.w3id and keep the current type, or
switch to Option 2 by declaring a full interface/type that matches the websocket
message shape and update websocketData’s type to that interface so the full
object can be stored safely—pick the appropriate option and update both the type
declaration and all assignments/usages accordingly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

♻️ Duplicate comments (1)
infrastructure/evault-provisioner/test-real-notifications.js (1)

12-12: Consider a stronger default secret for testing.

Similar to test-notification.js, the default value 'your-secret-here' is weak. While this is a test script, consider using a more complex default or requiring the environment variable to be set.

-const SHARED_SECRET = process.env.NOTIFICATION_SHARED_SECRET || 'your-secret-here';
+const SHARED_SECRET = process.env.NOTIFICATION_SHARED_SECRET || (() => { throw new Error('NOTIFICATION_SHARED_SECRET must be set'); })();
🧹 Nitpick comments (5)
infrastructure/evault-provisioner/test-notification.js (1)

15-15: Consider a stronger default secret for testing.

The default value 'your-secret-here' is weak. While this is a test script, consider using a more complex default or requiring the environment variable to be set to avoid accidental misuse.

-const SHARED_SECRET = process.env.NOTIFICATION_SHARED_SECRET || 'your-secret-here';
+const SHARED_SECRET = process.env.NOTIFICATION_SHARED_SECRET || (() => { throw new Error('NOTIFICATION_SHARED_SECRET must be set'); })();
infrastructure/evault-provisioner/src/migrations/1759924736417-migration.ts (1)

6-11: Consider adding indexes for device queries.

The new device-related columns are queried in NotificationService (e.g., getDevicesByEName at lines 75-78 queries by linkedEName and deviceActive). Adding indexes would improve query performance as device registrations scale.

Apply this diff to add a composite index:

     public async up(queryRunner: QueryRunner): Promise<void> {
         await queryRunner.query(`ALTER TABLE "verification" ADD "deviceId" character varying`);
         await queryRunner.query(`ALTER TABLE "verification" ADD "platform" character varying`);
         await queryRunner.query(`ALTER TABLE "verification" ADD "fcmToken" character varying`);
         await queryRunner.query(`ALTER TABLE "verification" ADD "deviceActive" boolean NOT NULL DEFAULT true`);
+        await queryRunner.query(`CREATE INDEX "IDX_verification_linkedename_deviceactive" ON "verification" ("linkedEName", "deviceActive")`);
     }

And update the down migration:

     public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`DROP INDEX "IDX_verification_linkedename_deviceactive"`);
         await queryRunner.query(`ALTER TABLE "verification" DROP COLUMN "deviceActive"`);
         await queryRunner.query(`ALTER TABLE "verification" DROP COLUMN "fcmToken"`);
         await queryRunner.query(`ALTER TABLE "verification" DROP COLUMN "platform"`);
         await queryRunner.query(`ALTER TABLE "verification" DROP COLUMN "deviceId"`);
     }
infrastructure/evault-provisioner/src/controllers/NotificationController.ts (3)

9-14: Use entity classes instead of string literals for type safety.

Using string literals "Verification" and "Notification" to retrieve repositories is less type-safe than using the entity classes directly. This makes the code more prone to typos and refactoring errors.

Apply this diff:

+import { Verification } from "../entities/Verification";
+
 export class NotificationController {
     private notificationService: NotificationService;

     constructor() {
         this.notificationService = new NotificationService(
-            AppDataSource.getRepository("Verification"),
-            AppDataSource.getRepository("Notification")
+            AppDataSource.getRepository(Verification),
+            AppDataSource.getRepository(Notification)
         );
     }

142-145: Optimize notification marking with bulk update.

Marking notifications as delivered sequentially in a loop is inefficient and creates multiple database round-trips. Consider using a bulk update operation.

Update NotificationService to add a bulk marking method:

// In NotificationService.ts
async markNotificationsAsDelivered(notificationIds: string[]): Promise<void> {
    await this.notificationRepository.update(
        { id: In(notificationIds) },
        { delivered: true, deliveredAt: new Date() }
    );
}

Then update the controller:

             // Get undelivered notifications for this eName
             const notifications = await this.notificationService.getUndeliveredNotifications(eName);
             
             // Mark notifications as delivered
-            for (const notification of notifications) {
-                await this.notificationService.markNotificationAsDelivered(notification.id);
-            }
+            if (notifications.length > 0) {
+                await this.notificationService.markNotificationsAsDelivered(
+                    notifications.map(n => n.id)
+                );
+            }

128-167: Consider adding pagination and rate limiting.

The checkNotifications endpoint could return a large number of notifications without pagination, and the notification endpoints lack rate limiting, which could lead to performance issues or abuse.

Consider:

  1. Adding pagination parameters (limit/offset or cursor-based) to checkNotifications
  2. Implementing rate limiting middleware (e.g., using express-rate-limit) on all notification endpoints
  3. Adding request size limits to prevent large payload attacks

Example for rate limiting:

import rateLimit from 'express-rate-limit';

const notificationLimiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // limit each IP to 100 requests per windowMs
    message: 'Too many requests from this IP'
});

registerRoutes(app: any) {
    app.post("/api/notifications/check", notificationLimiter, this.checkNotifications.bind(this));
    // ... other routes
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aec52ad and d4bab70.

⛔ Files ignored due to path filters (7)
  • infrastructure/eid-wallet/src-tauri/Cargo.lock is excluded by !**/*.lock
  • infrastructure/eid-wallet/src-tauri/gen/android/.idea/gradle.xml is excluded by !**/gen/**
  • infrastructure/eid-wallet/src-tauri/gen/android/app/arm64/release/app-arm64-release.aab is excluded by !**/gen/**
  • infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/AndroidManifest.xml is excluded by !**/gen/**
  • infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj is excluded by !**/gen/**
  • infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet_iOS/Info.plist is excluded by !**/gen/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • infrastructure/eid-wallet/package.json (2 hunks)
  • infrastructure/eid-wallet/src-tauri/Cargo.toml (1 hunks)
  • infrastructure/eid-wallet/src-tauri/capabilities/mobile.json (1 hunks)
  • infrastructure/eid-wallet/src-tauri/src/lib.rs (3 hunks)
  • infrastructure/eid-wallet/src/lib/global/controllers/evault.ts (5 hunks)
  • infrastructure/eid-wallet/src/lib/global/state.ts (2 hunks)
  • infrastructure/eid-wallet/src/lib/services/NotificationService.ts (1 hunks)
  • infrastructure/eid-wallet/src/routes/(app)/+layout.svelte (1 hunks)
  • infrastructure/evault-provisioner/src/config/database.ts (2 hunks)
  • infrastructure/evault-provisioner/src/controllers/NotificationController.ts (1 hunks)
  • infrastructure/evault-provisioner/src/entities/Notification.ts (1 hunks)
  • infrastructure/evault-provisioner/src/entities/Verification.ts (1 hunks)
  • infrastructure/evault-provisioner/src/index.ts (3 hunks)
  • infrastructure/evault-provisioner/src/migrations/1759924736417-migration.ts (1 hunks)
  • infrastructure/evault-provisioner/src/migrations/1759926271076-migration.ts (1 hunks)
  • infrastructure/evault-provisioner/src/services/NotificationService.ts (1 hunks)
  • infrastructure/evault-provisioner/test-notification.js (1 hunks)
  • infrastructure/evault-provisioner/test-real-notifications.js (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • infrastructure/eid-wallet/src/lib/global/state.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • infrastructure/eid-wallet/package.json
🧰 Additional context used
🧬 Code graph analysis (9)
infrastructure/evault-provisioner/test-real-notifications.js (1)
infrastructure/evault-provisioner/test-notification.js (3)
  • PROVISIONER_URL (14-14)
  • SHARED_SECRET (15-15)
  • TEST_ENAME (18-18)
infrastructure/evault-provisioner/src/entities/Notification.ts (1)
infrastructure/evault-provisioner/src/entities/Verification.ts (1)
  • Entity (9-52)
infrastructure/evault-provisioner/src/migrations/1759924736417-migration.ts (4)
infrastructure/evault-provisioner/src/migrations/1758389959600-migration.ts (1)
  • Migration1758389959600 (3-14)
infrastructure/evault-provisioner/src/migrations/1748966722767-migration.ts (1)
  • Migration1748966722767 (3-14)
infrastructure/evault-provisioner/src/migrations/1748932757644-migration.ts (2)
  • Migration1748932757644 (3-14)
  • up (6-8)
infrastructure/evault-provisioner/src/migrations/1748968097591-migration.ts (1)
  • Migration1748968097591 (3-14)
infrastructure/eid-wallet/src/lib/global/controllers/evault.ts (2)
infrastructure/evault-provisioner/src/services/NotificationService.ts (1)
  • NotificationService (25-138)
infrastructure/eid-wallet/src/lib/global/controllers/user.ts (1)
  • UserController (32-146)
infrastructure/evault-provisioner/src/services/NotificationService.ts (1)
infrastructure/eid-wallet/src/lib/services/NotificationService.ts (2)
  • DeviceRegistration (5-11)
  • NotificationPayload (13-17)
infrastructure/evault-provisioner/src/controllers/NotificationController.ts (2)
infrastructure/evault-provisioner/src/services/NotificationService.ts (1)
  • NotificationService (25-138)
infrastructure/evault-provisioner/src/config/database.ts (1)
  • AppDataSource (10-18)
infrastructure/eid-wallet/src/lib/services/NotificationService.ts (3)
infrastructure/evault-provisioner/src/services/NotificationService.ts (3)
  • DeviceRegistration (5-11)
  • NotificationPayload (13-17)
  • NotificationService (25-138)
infrastructure/evault-provisioner/src/controllers/NotificationController.ts (1)
  • sendNotification (94-126)
infrastructure/eid-wallet/src/lib/global/controllers/evault.ts (2)
  • vault (241-327)
  • vault (329-342)
infrastructure/evault-provisioner/test-notification.js (3)
infrastructure/evault-provisioner/test-real-notifications.js (3)
  • PROVISIONER_URL (11-11)
  • SHARED_SECRET (12-12)
  • TEST_ENAME (15-15)
infrastructure/evault-provisioner/src/controllers/NotificationController.ts (1)
  • getDeviceStats (169-183)
infrastructure/evault-provisioner/src/services/NotificationService.ts (1)
  • getDeviceStats (123-137)
infrastructure/evault-provisioner/src/index.ts (1)
infrastructure/evault-provisioner/src/controllers/NotificationController.ts (1)
  • NotificationController (6-184)
🔇 Additional comments (1)
infrastructure/evault-provisioner/src/migrations/1759926271076-migration.ts (1)

6-8: Enable the uuid-ossp PostgreSQL extension before running this migration.
Migration calls uuid_generate_v4(); repo scan found no "uuid-ossp" or CREATE EXTENSION references — add a prior migration or DB-init step to run: CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; (file: infrastructure/evault-provisioner/src/migrations/1759926271076-migration.ts)

Comment on lines +38 to +48
/// Gets a unique device identifier
#[tauri::command]
async fn get_device_id() -> Result<String, String> {
// Try to get device ID from environment or generate one
if let Ok(device_id) = env::var("DEVICE_ID") {
Ok(device_id)
} else {
// Generate a UUID as device ID
Ok(Uuid::new_v4().to_string())
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Device ID lacks persistence mechanism.

The function generates a new UUID every time DEVICE_ID is not set in the environment. This means each app restart will produce a different device ID, which could break device registration and notification delivery.

Consider persisting the generated device ID to storage (e.g., using Tauri's store plugin) or ensure DEVICE_ID is consistently set in the environment across restarts.

use tauri::Manager;
use std::fs;

#[tauri::command]
async fn get_device_id(app: tauri::AppHandle) -> Result<String, String> {
    let device_id_path = app.path_resolver()
        .app_data_dir()
        .ok_or("Failed to get app data dir")?
        .join("device_id");
    
    // Try to read existing device ID
    if let Ok(device_id) = fs::read_to_string(&device_id_path) {
        return Ok(device_id.trim().to_string());
    }
    
    // Generate and persist new device ID
    let device_id = Uuid::new_v4().to_string();
    fs::create_dir_all(device_id_path.parent().ok_or("Invalid path")?).map_err(|e| e.to_string())?;
    fs::write(&device_id_path, &device_id).map_err(|e| e.to_string())?;
    
    Ok(device_id)
}
🤖 Prompt for AI Agents
in infrastructure/eid-wallet/src-tauri/src/lib.rs around lines 38 to 48, the
current get_device_id generates a new UUID on every run when DEVICE_ID isn't in
the environment; change it to accept a tauri::AppHandle, resolve the app data
dir via app.path_resolver().app_data_dir(), attempt to read a "device_id" file
and return its trimmed contents if present, otherwise generate
Uuid::new_v4().to_string(), ensure the parent directory exists (create_dir_all),
write the new ID to the file, and propagate filesystem errors as String for the
Result so the device ID is persisted across restarts.

Comment on lines +285 to +286
// Register device for notifications
this.registerDeviceForNotifications(vault.ename);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing await on async call.

The call to registerDeviceForNotifications is not awaited in the non-Promise path. This means device registration will happen asynchronously without waiting for completion, potentially causing race conditions or failures that go unnoticed.

Apply this diff to add the missing await:

         // Register device for notifications
-        this.registerDeviceForNotifications(vault.ename);
+        await this.registerDeviceForNotifications(vault.ename);

However, note that adding await here requires the enclosing context to handle the async call properly. Since this is in a non-Promise setter branch, you may need to wrap this in an immediately invoked async function similar to the profile creation logic below it.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
in infrastructure/eid-wallet/src/lib/global/controllers/evault.ts around lines
285-286, the call to registerDeviceForNotifications(vault.ename) is missing an
await causing the registration to run asynchronously; change this to await the
call and because this branch is not async, wrap the awaited call in an
immediately-invoked async function (async () => { await
this.registerDeviceForNotifications(vault.ename); })().catch(err => {/*
log/handle error */}) so the registration completes and errors are surfaced,
matching the pattern used for profile creation below.

Comment on lines +26 to +27
this.provisionerEndpoint = PUBLIC_PROVISIONER_URL
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle missing provisioner endpoint

PUBLIC_PROVISIONER_URL resolves to undefined when the env var isn’t set (SvelteKit simply injects undefined). Every fetch in this service will then blow up with “Failed to parse URL from undefined…”, killing registration and notification polling. Either fail fast with a clear error or provide a real default before using it. For example:

-    this.provisionerEndpoint = PUBLIC_PROVISIONER_URL 
+    this.provisionerEndpoint = PUBLIC_PROVISIONER_URL || "http://localhost:8080";
+    if (!this.provisionerEndpoint) {
+      throw new Error("Provisioner endpoint is not configured.");
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.provisionerEndpoint = PUBLIC_PROVISIONER_URL
}
this.provisionerEndpoint = PUBLIC_PROVISIONER_URL || "http://localhost:8080";
if (!this.provisionerEndpoint) {
throw new Error("Provisioner endpoint is not configured.");
}
}
🤖 Prompt for AI Agents
in infrastructure/eid-wallet/src/lib/services/NotificationService.ts around
lines 26 to 27, PUBLIC_PROVISIONER_URL can be undefined causing fetches to
throw; update the constructor to validate the env var and either throw a clear
error immediately if it's missing or assign a safe default URL before using it;
specifically, check if PUBLIC_PROVISIONER_URL is truthy and if not throw an
Error with a descriptive message (e.g. "Missing PUBLIC_PROVISIONER_URL
environment variable") or set a documented default value, and ensure all
subsequent usages rely on this validated/normalized value.

Comment on lines +98 to +104
// Verify shared secret
if (sharedSecret !== process.env.NOTIFICATION_SHARED_SECRET) {
return res.status(401).json({
success: false,
error: "Invalid shared secret"
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use constant-time comparison for shared secret.

The equality check for sharedSecret uses !== which is vulnerable to timing attacks. An attacker could measure response times to guess the secret character by character.

Use a constant-time comparison function:

+import * as crypto from 'crypto';
+
+function constantTimeCompare(a: string, b: string): boolean {
+    if (a.length !== b.length) return false;
+    return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
+}
+
     private async sendNotification(req: Request, res: Response) {
         try {
             const { eName, notification, sharedSecret } = req.body;

             // Verify shared secret
-            if (sharedSecret !== process.env.NOTIFICATION_SHARED_SECRET) {
+            if (!constantTimeCompare(sharedSecret || '', process.env.NOTIFICATION_SHARED_SECRET || '')) {
                 return res.status(401).json({
                     success: false,
                     error: "Invalid shared secret"
                 });
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Verify shared secret
if (sharedSecret !== process.env.NOTIFICATION_SHARED_SECRET) {
return res.status(401).json({
success: false,
error: "Invalid shared secret"
});
}
import * as crypto from 'crypto';
function constantTimeCompare(a: string, b: string): boolean {
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
private async sendNotification(req: Request, res: Response) {
try {
const { eName, notification, sharedSecret } = req.body;
// Verify shared secret
if (!constantTimeCompare(sharedSecret || '', process.env.NOTIFICATION_SHARED_SECRET || '')) {
return res.status(401).json({
success: false,
error: "Invalid shared secret"
});
}
// …rest of implementation…
} catch (err) {
// error handling…
}
}
🤖 Prompt for AI Agents
in infrastructure/evault-provisioner/src/controllers/NotificationController.ts
around lines 98 to 104, replace the plain !== string comparison with a
constant-time check: import Node's crypto, compute fixed-length digests (e.g.,
SHA-256) or convert both values to Buffers of equal, known length, and use
crypto.timingSafeEqual to compare them; ensure you handle null/undefined values
safely (treat missing values as empty string or short-circuit to 401), and keep
the response logic the same when the timing-safe comparison fails.

Comment on lines +23 to +24
@Column({ nullable: true })
deliveredAt!: Date;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix type inconsistency for deliveredAt.

The deliveredAt field is marked as nullable in the decorator but uses the non-null assertion operator (!) in the type declaration. This is inconsistent. Since the field is nullable, it should be marked as optional with ?.

Apply this diff:

     @Column({ nullable: true })
-    deliveredAt!: Date;
+    deliveredAt?: Date;
🤖 Prompt for AI Agents
In infrastructure/evault-provisioner/src/entities/Notification.ts around lines
23 to 24, the deliveredAt field is decorated as nullable but declared with a
non-null assertion; change the property to be optional and allow null by
replacing the non-null assertion with an optional/nullable type (e.g.,
deliveredAt?: Date | null) so the TypeScript type matches @Column({ nullable:
true }) and update any usages to handle undefined/null accordingly.

Comment on lines +176 to +177
// Register notification routes
notificationController.registerRoutes(app);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fail closed when notification shared secret is unset.

NotificationController.sendNotification only rejects when sharedSecret !== process.env.NOTIFICATION_SHARED_SECRET. If that env var is missing (undefined), any caller omitting sharedSecret slips through because undefined === undefined. We should abort startup when the secret is absent (or otherwise guard before route registration) so the endpoint never runs without authentication.

Apply this diff near the top-level initialization to enforce the check:

 dotenv.config({ path: path.resolve(__dirname, "../../../.env") });

+if (!process.env.NOTIFICATION_SHARED_SECRET) {
+    console.error("NOTIFICATION_SHARED_SECRET must be configured before starting the provisioner API");
+    process.exit(1);
+}
+
 const app = express();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Register notification routes
notificationController.registerRoutes(app);
// Load environment variables
dotenv.config({ path: path.resolve(__dirname, "../../../.env") });
if (!process.env.NOTIFICATION_SHARED_SECRET) {
console.error("NOTIFICATION_SHARED_SECRET must be configured before starting the provisioner API");
process.exit(1);
}
const app = express();
// …rest of your initialization…
🤖 Prompt for AI Agents
In infrastructure/evault-provisioner/src/index.ts around lines 176-177, the app
registers notification routes while process.env.NOTIFICATION_SHARED_SECRET may
be undefined, allowing unauthenticated requests (because undefined ===
undefined); before registering routes (at top-level initialization) validate
that process.env.NOTIFICATION_SHARED_SECRET exists and is non-empty, and if not,
log an error and abort startup (throw or process.exit(1)) so the service never
starts without the notification shared secret.

Comment on lines +6 to +8
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "notifications" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "eName" character varying NOT NULL, "title" character varying NOT NULL, "body" character varying NOT NULL, "data" jsonb, "delivered" boolean NOT NULL DEFAULT false, "deliveredAt" TIMESTAMP, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a" PRIMARY KEY ("id"))`);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add indexes for query performance.

The notifications table lacks indexes on eName and delivered columns, which are used in the getUndeliveredNotifications query (see NotificationService.ts:105-108). Without indexes, queries will perform full table scans as notification count grows.

Apply this diff to add a composite index:

     public async up(queryRunner: QueryRunner): Promise<void> {
         await queryRunner.query(`CREATE TABLE "notifications" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "eName" character varying NOT NULL, "title" character varying NOT NULL, "body" character varying NOT NULL, "data" jsonb, "delivered" boolean NOT NULL DEFAULT false, "deliveredAt" TIMESTAMP, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_notifications_ename_delivered" ON "notifications" ("eName", "delivered")`);
     }

And update the down migration:

     public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`DROP INDEX "IDX_notifications_ename_delivered"`);
         await queryRunner.query(`DROP TABLE "notifications"`);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "notifications" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "eName" character varying NOT NULL, "title" character varying NOT NULL, "body" character varying NOT NULL, "data" jsonb, "delivered" boolean NOT NULL DEFAULT false, "deliveredAt" TIMESTAMP, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a" PRIMARY KEY ("id"))`);
}
// infrastructure/evault-provisioner/src/migrations/1759926271076-migration.ts
import { MigrationInterface, QueryRunner } from "typeorm";
export class Migration1759926271076 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "notifications" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"eName" character varying NOT NULL,
"title" character varying NOT NULL,
"body" character varying NOT NULL,
"data" jsonb,
"delivered" boolean NOT NULL DEFAULT false,
"deliveredAt" TIMESTAMP,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a" PRIMARY KEY ("id")
)
`);
// add composite index for faster lookups of undelivered notifications per eName
await queryRunner.query(`
CREATE INDEX "IDX_notifications_ename_delivered"
ON "notifications" ("eName", "delivered")
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DROP INDEX "IDX_notifications_ename_delivered"
`);
await queryRunner.query(`
DROP TABLE "notifications"
`);
}
}
🤖 Prompt for AI Agents
in infrastructure/evault-provisioner/src/migrations/1759926271076-migration.ts
around lines 6 to 8, the migration creates the notifications table but does not
add indexes used by getUndeliveredNotifications; modify the up method to create
a composite index on ( "eName", "delivered" ) to speed queries (e.g., CREATE
INDEX IDX_notifications_eName_delivered ON "notifications" ("eName",
"delivered")), and update the down method to DROP that index (e.g., DROP INDEX
IF EXISTS IDX_notifications_eName_delivered) so the migration cleanly reverts;
ensure index name matches your DB naming conventions and wrap both statements in
await queryRunner.query calls consistent with the file.

@coodos coodos merged commit 4644f3d into main Oct 9, 2025
1 of 4 checks passed
@coodos coodos deleted the feat/wallet-more-doc-support branch October 9, 2025 20:02
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.

[feature] Add UI to eid wallet to support more verification documents
2 participants