From 266a2bef99f0f3238bb64f6b5390d2dc502b59f4 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 11 Feb 2026 14:06:06 +0530 Subject: [PATCH 1/7] refactor: move eID wallet to wallet SDK --- docs/docs/Infrastructure/eID-Wallet.md | 3 +- infrastructure/eid-wallet/package.json | 3 +- .../.kotlin/errors/errors-1770798153274.log | 4 + .../src/lib/global/controllers/evault.ts | 186 +++------------- .../eid-wallet/src/lib/global/state.ts | 9 + .../eid-wallet/src/lib/wallet-sdk-adapter.ts | 33 +++ .../src/routes/(app)/scan-qr/scanLogic.ts | 91 ++------ .../src/routes/(auth)/onboarding/+page.svelte | 36 ++- .../src/routes/(auth)/verify/+page.svelte | 32 ++- packages/wallet-sdk/package.json | 31 +++ packages/wallet-sdk/src/auth.test.ts | 51 +++++ packages/wallet-sdk/src/auth.ts | 29 +++ packages/wallet-sdk/src/crypto-adapter.ts | 25 +++ packages/wallet-sdk/src/index.ts | 7 + packages/wallet-sdk/src/provision.test.ts | 96 ++++++++ packages/wallet-sdk/src/provision.ts | 84 +++++++ .../wallet-sdk/src/sync-public-key.test.ts | 112 ++++++++++ packages/wallet-sdk/src/sync-public-key.ts | 102 +++++++++ packages/wallet-sdk/tsconfig.build.json | 4 + packages/wallet-sdk/tsconfig.json | 18 ++ packages/wallet-sdk/vitest.config.ts | 8 + platforms/ppappt | 1 + pnpm-lock.yaml | 209 ++++++++++++++++-- 23 files changed, 880 insertions(+), 294 deletions(-) create mode 100644 infrastructure/eid-wallet/src-tauri/gen/android/buildSrc/.kotlin/errors/errors-1770798153274.log create mode 100644 infrastructure/eid-wallet/src/lib/wallet-sdk-adapter.ts create mode 100644 packages/wallet-sdk/package.json create mode 100644 packages/wallet-sdk/src/auth.test.ts create mode 100644 packages/wallet-sdk/src/auth.ts create mode 100644 packages/wallet-sdk/src/crypto-adapter.ts create mode 100644 packages/wallet-sdk/src/index.ts create mode 100644 packages/wallet-sdk/src/provision.test.ts create mode 100644 packages/wallet-sdk/src/provision.ts create mode 100644 packages/wallet-sdk/src/sync-public-key.test.ts create mode 100644 packages/wallet-sdk/src/sync-public-key.ts create mode 100644 packages/wallet-sdk/tsconfig.build.json create mode 100644 packages/wallet-sdk/tsconfig.json create mode 100644 packages/wallet-sdk/vitest.config.ts create mode 160000 platforms/ppappt diff --git a/docs/docs/Infrastructure/eID-Wallet.md b/docs/docs/Infrastructure/eID-Wallet.md index bc4db5dcd..a031651c5 100644 --- a/docs/docs/Infrastructure/eID-Wallet.md +++ b/docs/docs/Infrastructure/eID-Wallet.md @@ -292,7 +292,8 @@ The wallet creates signatures for various purposes: - **Framework**: Tauri (Rust + Web frontend) - **Frontend**: SvelteKit + TypeScript -- **Key APIs**: +- **wallet-sdk**: The wallet uses the **wallet-sdk** package for provisioning, platform authentication (signing), and public-key sync to eVault. Crypto is provided by the existing KeyService via a **CryptoAdapter** (BYOC), so hardware/software key manager behavior is unchanged. +- **Key APIs**: - iOS: LocalAuthentication (Secure Enclave) - Android: KeyStore (HSM) - Web: Web Crypto API diff --git a/infrastructure/eid-wallet/package.json b/infrastructure/eid-wallet/package.json index a6c08317c..6f0e37c36 100644 --- a/infrastructure/eid-wallet/package.json +++ b/infrastructure/eid-wallet/package.json @@ -46,7 +46,8 @@ "svelte-loading-spinners": "^0.3.6", "svelte-qrcode": "^1.0.1", "tailwind-merge": "^3.0.2", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "wallet-sdk": "workspace:*" }, "devDependencies": { "@biomejs/biome": "^1.9.4", diff --git a/infrastructure/eid-wallet/src-tauri/gen/android/buildSrc/.kotlin/errors/errors-1770798153274.log b/infrastructure/eid-wallet/src-tauri/gen/android/buildSrc/.kotlin/errors/errors-1770798153274.log new file mode 100644 index 000000000..1219b509f --- /dev/null +++ b/infrastructure/eid-wallet/src-tauri/gen/android/buildSrc/.kotlin/errors/errors-1770798153274.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.21 +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts index ff1be10ba..f6328f193 100644 --- a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts +++ b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts @@ -6,7 +6,7 @@ import { import type { Store } from "@tauri-apps/plugin-store"; import axios from "axios"; import { GraphQLClient } from "graphql-request"; -import * as jose from "jose"; +import { syncPublicKeyToEvault } from "wallet-sdk"; import NotificationService from "../../services/NotificationService"; import type { KeyService } from "./key"; import type { UserController } from "./user"; @@ -54,6 +54,7 @@ export class VaultController { #endpoint: string | null = null; #userController: UserController; #keyService: KeyService | null = null; + #walletSdkAdapter: import("wallet-sdk").CryptoAdapter | null = null; #profileCreationStatus: "idle" | "loading" | "success" | "failed" = "idle"; #notificationService: NotificationService; @@ -61,10 +62,12 @@ export class VaultController { store: Store, userController: UserController, keyService?: KeyService, + walletSdkAdapter?: import("wallet-sdk").CryptoAdapter, ) { this.#store = store; this.#userController = userController; this.#keyService = keyService || null; + this.#walletSdkAdapter = walletSdkAdapter ?? null; this.#notificationService = NotificationService.getInstance(); } @@ -87,169 +90,32 @@ export class VaultController { } /** - * Sync public key to eVault core - * Checks if public key was already saved, calls /whois, and PATCH if needed + * Sync public key to eVault core via wallet-sdk. + * SDK checks /whois and skips PATCH if current key already in certs; otherwise PATCHes /public-key. */ async syncPublicKey(eName: string): Promise { + if (!this.#walletSdkAdapter) { + console.warn( + "Wallet SDK adapter not available, cannot sync public key", + ); + return; + } + const vault = await this.vault; + if (!vault?.uri) { + console.warn("No vault URI available, cannot sync public key"); + return; + } + const isFake = await this.#userController.isFake; + const context = isFake ? "pre-verification" : "onboarding"; try { - // Note: We always check the actual source (whois endpoint) instead of relying on localStorage - // localStorage flag is only used as a hint, but we verify against the server - const savedKey = localStorage.getItem(`publicKeySaved_${eName}`); - - if (!this.#keyService) { - console.warn( - "KeyService not available, cannot sync public key", - ); - return; - } - - // Get the eVault URI - const vault = await this.vault; - if (!vault?.uri) { - console.warn("No vault URI available, cannot sync public key"); - return; - } - - // Call /whois to check if public key exists - const whoisUrl = new URL("/whois", vault.uri).toString(); - const whoisResponse = await axios.get(whoisUrl, { - headers: { - "X-ENAME": eName, - }, + await syncPublicKeyToEvault(this.#walletSdkAdapter, { + evaultUri: vault.uri, + eName, + keyId: "default", + context, + authToken: PUBLIC_EID_WALLET_TOKEN || null, + registryUrl: PUBLIC_REGISTRY_URL, }); - - // Get key binding certificates array from whois response - const keyBindingCertificates = - whoisResponse.data?.keyBindingCertificates; - - // Get current device's public key to check if it already exists - const KEY_ID = "default"; - const isFake = await this.#userController.isFake; - const context = isFake ? "pre-verification" : "onboarding"; - - let currentPublicKey: string | undefined; - try { - currentPublicKey = await this.#keyService.getPublicKey( - KEY_ID, - context, - ); - } catch (error) { - console.error( - "Failed to get current public key for comparison:", - error, - ); - // Continue to sync anyway - } - - // If we have certificates and current key, check if it already exists - if ( - keyBindingCertificates && - Array.isArray(keyBindingCertificates) && - keyBindingCertificates.length > 0 && - currentPublicKey - ) { - try { - // Get registry JWKS for JWT verification - const registryUrl = PUBLIC_REGISTRY_URL; - if (registryUrl) { - const jwksUrl = new URL( - "/.well-known/jwks.json", - registryUrl, - ).toString(); - const jwksResponse = await axios.get(jwksUrl, { - timeout: 10000, - }); - const JWKS = jose.createLocalJWKSet(jwksResponse.data); - - // Extract public keys from certificates and check if current key exists - for (const jwt of keyBindingCertificates) { - try { - const { payload } = await jose.jwtVerify( - jwt, - JWKS, - ); - - // Verify ename matches - if (payload.ename !== eName) { - continue; - } - - // Extract publicKey from JWT payload - const extractedPublicKey = - payload.publicKey as string; - if (extractedPublicKey === currentPublicKey) { - // Current device's key already exists, mark as saved - localStorage.setItem( - `publicKeySaved_${eName}`, - "true", - ); - console.log( - `Public key already exists for ${eName}`, - ); - return; - } - } catch (error) { - // JWT verification failed, try next certificate - console.warn( - "Failed to verify key binding certificate:", - error, - ); - } - } - } - } catch (error) { - console.error( - "Error checking existing public keys:", - error, - ); - // Continue to sync anyway - } - } - - // Get public key using the same method as getApplicationPublicKey() in onboarding/verify - let publicKey: string | undefined; - try { - publicKey = await this.#keyService.getPublicKey( - KEY_ID, - context, - ); - } catch (error) { - console.error( - `Failed to get public key for ${KEY_ID} with context ${context}:`, - error, - ); - return; - } - - if (!publicKey) { - console.warn( - `No public key found for ${KEY_ID} with context ${context}, cannot sync`, - ); - return; - } - - // Get authentication token from environment variable - const authToken = PUBLIC_EID_WALLET_TOKEN || null; - if (!authToken) { - console.warn( - "PUBLIC_EID_WALLET_TOKEN not set, request may fail authentication", - ); - } - - // Call PATCH /public-key to save the public key - const patchUrl = new URL("/public-key", vault.uri).toString(); - const headers: Record = { - "X-ENAME": eName, - "Content-Type": "application/json", - }; - - if (authToken) { - headers.Authorization = `Bearer ${authToken}`; - } - - await axios.patch(patchUrl, { publicKey }, { headers }); - - // Mark as saved localStorage.setItem(`publicKeySaved_${eName}`, "true"); console.log(`Public key synced successfully for ${eName}`); } catch (error) { diff --git a/infrastructure/eid-wallet/src/lib/global/state.ts b/infrastructure/eid-wallet/src/lib/global/state.ts index cbe107312..538b5f8e3 100644 --- a/infrastructure/eid-wallet/src/lib/global/state.ts +++ b/infrastructure/eid-wallet/src/lib/global/state.ts @@ -1,4 +1,6 @@ import { Store } from "@tauri-apps/plugin-store"; +import type { CryptoAdapter } from "wallet-sdk"; +import { createKeyServiceCryptoAdapter } from "../wallet-sdk-adapter"; import NotificationService from "../services/NotificationService"; import { VaultController } from "./controllers/evault"; import { KeyService } from "./controllers/key"; @@ -24,14 +26,20 @@ import { UserController } from "./controllers/user"; */ export class GlobalState { #store: Store; + #walletSdkAdapter: CryptoAdapter; securityController: SecurityController; userController: UserController; vaultController: VaultController; notificationService: NotificationService; keyService: KeyService; + get walletSdkAdapter(): CryptoAdapter { + return this.#walletSdkAdapter; + } + private constructor(store: Store, keyService: KeyService) { this.#store = store; + this.#walletSdkAdapter = createKeyServiceCryptoAdapter(keyService); this.securityController = new SecurityController(store); this.userController = new UserController(store); this.keyService = keyService; @@ -39,6 +47,7 @@ export class GlobalState { store, this.userController, keyService, + this.#walletSdkAdapter, ); this.notificationService = NotificationService.getInstance(); } diff --git a/infrastructure/eid-wallet/src/lib/wallet-sdk-adapter.ts b/infrastructure/eid-wallet/src/lib/wallet-sdk-adapter.ts new file mode 100644 index 000000000..d2db2e63c --- /dev/null +++ b/infrastructure/eid-wallet/src/lib/wallet-sdk-adapter.ts @@ -0,0 +1,33 @@ +import type { CryptoAdapter } from "wallet-sdk"; +import type { KeyService } from "$lib/global/controllers/key"; +import type { KeyServiceContext } from "$lib/global/controllers/key"; + +/** + * Adapts KeyService to wallet-sdk CryptoAdapter (BYOC). + */ +export function createKeyServiceCryptoAdapter( + keyService: KeyService, +): CryptoAdapter { + return { + async getPublicKey(keyId: string, context: string) { + return keyService.getPublicKey( + keyId, + context as KeyServiceContext, + ); + }, + async signPayload(keyId: string, context: string, payload: string) { + return keyService.signPayload( + keyId, + context as KeyServiceContext, + payload, + ); + }, + async ensureKey(keyId: string, context: string) { + const { created } = await keyService.ensureKey( + keyId, + context as KeyServiceContext, + ); + return { created }; + }, + }; +} diff --git a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts index ffef283e1..10aa8882d 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts +++ b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts @@ -11,6 +11,7 @@ import { import { openUrl } from "@tauri-apps/plugin-opener"; import axios from "axios"; import { type Writable, get, writable } from "svelte/store"; +import { authenticate } from "wallet-sdk"; import type { GlobalState } from "$lib/global"; @@ -281,44 +282,23 @@ export function createScanLogic({ authLoading.set(true); try { - const KEY_ID = "default"; const isFake = await globalState.userController.isFake; const signingContext = isFake ? "pre-verification" : "onboarding"; + const sessionPayload = get(session) as string; - console.log("=".repeat(70)); - console.log( - "🔐 [scanLogic] handleAuth - Preparing to sign payload", - ); - console.log("=".repeat(70)); - console.log(`⚠️ Using keyId: ${KEY_ID} (NOT ${vault.ename})`); - console.log(`⚠️ Using context: ${signingContext} (NOT "signing")`); - console.log( - "⚠️ This ensures we use the SAME key that was synced to eVault", - ); - console.log("=".repeat(70)); - - const { created } = await globalState.keyService.ensureKey( - KEY_ID, - signingContext, - ); - console.log( - "Key generation result for signing:", - created ? "key-generated" : "key-exists", + const { signature } = await authenticate( + globalState.walletSdkAdapter, + { + sessionId: sessionPayload, + keyId: "default", + context: signingContext, + }, ); - const w3idResult = vault.ename; - if (!w3idResult) { + if (!vault.ename) { throw new Error("Failed to get W3ID"); } - const sessionPayload = get(session) as string; - - const signature = await globalState.keyService.signPayload( - KEY_ID, - signingContext, - sessionPayload, - ); - const redirectUrl = get(redirect); if (!redirectUrl) { throw new Error( @@ -664,55 +644,28 @@ export function createScanLogic({ throw new Error("No vault available for signing"); } - // ⚠️ CRITICAL: Use the SAME keyId and context that was synced to eVault! - // The key synced to eVault uses keyId="default" with context="onboarding" or "pre-verification" - // NOT vault.ename with context="signing"! - const KEY_ID = "default"; + // Use same keyId/context as synced to eVault (default + onboarding/pre-verification) const isFake = await globalState.userController.isFake; const signingContext = isFake ? "pre-verification" : "onboarding"; - console.log("=".repeat(70)); - console.log("🔐 [scanLogic] Preparing to sign payload"); - console.log("=".repeat(70)); - console.log(`⚠️ Using keyId: ${KEY_ID} (NOT ${vault.ename})`); - console.log(`⚠️ Using context: ${signingContext} (NOT "signing")`); - console.log( - "⚠️ This ensures we use the SAME key that was synced to eVault", - ); - console.log("=".repeat(70)); - - const { created } = await globalState.keyService.ensureKey( - KEY_ID, - signingContext, - ); - console.log( - "Key generation result for signing:", - created ? "key-generated" : "key-exists", + const { signature } = await authenticate( + globalState.walletSdkAdapter, + { + sessionId: currentSigningSessionId, + keyId: "default", + context: signingContext, + }, ); - const w3idResult = vault.ename; - if (!w3idResult) { + if (!vault.ename) { throw new Error("Failed to get W3ID"); } - const messageToSign = currentSigningSessionId; - - console.log( - "🔐 Starting cryptographic signing process with KeyManager...", - ); - - const signature = await globalState.keyService.signPayload( - KEY_ID, - signingContext, - currentSigningSessionId, - ); - console.log("✅ Message signed successfully"); - const signedPayload = { sessionId: currentSigningSessionId, - signature: signature, - w3id: w3idResult, - message: messageToSign, + signature, + w3id: vault.ename, + message: currentSigningSessionId, }; const redirectUri = get(redirect); diff --git a/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte index 3d00d7877..c49032197 100644 --- a/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte @@ -11,8 +11,8 @@ import type { KeyServiceContext } from "$lib/global"; import { ButtonAction } from "$lib/ui"; import { capitalize } from "$lib/utils"; import * as falso from "@ngneat/falso"; -import axios from "axios"; import { getContext, onMount } from "svelte"; +import { provision } from "wallet-sdk"; import { Shadow } from "svelte-loading-spinners"; import { v4 as uuidv4 } from "uuid"; @@ -205,30 +205,20 @@ onMount(async () => { await initializeKeyManager(); await ensureKeyForContext(); - const entropyRes = await axios.get( - new URL("/entropy", PUBLIC_REGISTRY_URL).toString(), - ); - const registryEntropy = entropyRes.data.token; - console.log("Registry entropy:", registryEntropy); - - const provisionRes = await axios.post( - new URL("/provision", PUBLIC_PROVISIONER_URL).toString(), - { - registryEntropy, - namespace: uuidv4(), - verificationId, - publicKey: await getApplicationPublicKey(), - }, - ); - console.log("Provision response:", provisionRes.data); - - if (!provisionRes.data?.success) { - throw new Error("Invalid verification code"); - } + const result = await provision(globalState.walletSdkAdapter, { + registryUrl: PUBLIC_REGISTRY_URL, + provisionerUrl: PUBLIC_PROVISIONER_URL, + namespace: uuidv4(), + verificationId, + keyId: "default", + context: "pre-verification", + isPreVerification: true, + }); + console.log("Provision response:", result); verificationSuccess = true; - uri = provisionRes.data.uri; - ename = provisionRes.data.w3id; + uri = result.uri; + ename = result.w3id; } catch (err) { console.error("Pre-verification failed:", err); diff --git a/infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte b/infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte index 50a8e7a6a..9b8312bdf 100644 --- a/infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte @@ -16,6 +16,7 @@ import axios from "axios"; import { getContext, onDestroy, onMount } from "svelte"; import { Shadow } from "svelte-loading-spinners"; import { v4 as uuidv4 } from "uuid"; +import { provision } from "wallet-sdk"; import DocumentType from "./steps/document-type.svelte"; import Passport from "./steps/passport.svelte"; import Selfie from "./steps/selfie.svelte"; @@ -336,26 +337,21 @@ onMount(async () => { ename: existingW3id, }; } else { - // Normal flow for approved status - const { - data: { token: registryEntropy }, - } = await axios.get( - new URL("/entropy", PUBLIC_REGISTRY_URL).toString(), - ); - const { data } = await axios.post( - new URL("/provision", PUBLIC_PROVISIONER_URL).toString(), - { - registryEntropy, - namespace: uuidv4(), - verificationId: $verificaitonId, - publicKey: await getApplicationPublicKey(), - }, - ); - if (data.success === true) { + // Normal flow for approved status via wallet-sdk + const result = await provision(globalState.walletSdkAdapter, { + registryUrl: PUBLIC_REGISTRY_URL, + provisionerUrl: PUBLIC_PROVISIONER_URL, + namespace: uuidv4(), + verificationId: $verificaitonId, + keyId: "default", + context: "onboarding", + isPreVerification: false, + }); + if (result.success) { // Set vault in controller - this will trigger profile creation with retry logic globalState.vaultController.vault = { - uri: data.uri, - ename: data.w3id, + uri: result.uri, + ename: result.w3id, }; } } diff --git a/packages/wallet-sdk/package.json b/packages/wallet-sdk/package.json new file mode 100644 index 000000000..b24c8a8f2 --- /dev/null +++ b/packages/wallet-sdk/package.json @@ -0,0 +1,31 @@ +{ + "name": "wallet-sdk", + "version": "0.1.0", + "description": "BYOC wallet SDK for provisioning, auth, and public key sync", + "type": "module", + "scripts": { + "build": "tsc -p tsconfig.build.json", + "test": "vitest run", + "test:watch": "vitest", + "check-types": "tsc --noEmit" + }, + "keywords": [], + "author": "", + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": ["dist"], + "dependencies": { + "jose": "^5.2.0" + }, + "devDependencies": { + "typescript": "~5.6.2", + "vitest": "^3.0.9" + } +} diff --git a/packages/wallet-sdk/src/auth.test.ts b/packages/wallet-sdk/src/auth.test.ts new file mode 100644 index 000000000..497c84dbe --- /dev/null +++ b/packages/wallet-sdk/src/auth.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it, vi } from "vitest"; +import type { CryptoAdapter } from "./crypto-adapter.js"; +import { authenticate } from "./auth.js"; + +function createFakeAdapter(signResult = "sig123"): CryptoAdapter { + return { + async getPublicKey() { + return "zFakePublicKey"; + }, + async signPayload(_keyId, _context, payload) { + return `${signResult}:${payload}`; + }, + async ensureKey() { + return { created: false }; + }, + }; +} + +describe("authenticate", () => { + it("calls ensureKey then signPayload and returns signature", async () => { + const adapter = createFakeAdapter("mySig"); + const ensureSpy = vi.spyOn(adapter, "ensureKey"); + const signSpy = vi.spyOn(adapter, "signPayload"); + + const result = await authenticate(adapter, { + sessionId: "session-abc", + context: "onboarding", + }); + + expect(result).toEqual({ signature: "mySig:session-abc" }); + expect(ensureSpy).toHaveBeenCalledWith("default", "onboarding"); + expect(signSpy).toHaveBeenCalledWith( + "default", + "onboarding", + "session-abc", + ); + }); + + it("uses custom keyId when provided", async () => { + const adapter = createFakeAdapter(); + const ensureSpy = vi.spyOn(adapter, "ensureKey"); + + await authenticate(adapter, { + sessionId: "s", + keyId: "custom-key", + context: "pre-verification", + }); + + expect(ensureSpy).toHaveBeenCalledWith("custom-key", "pre-verification"); + }); +}); diff --git a/packages/wallet-sdk/src/auth.ts b/packages/wallet-sdk/src/auth.ts new file mode 100644 index 000000000..8e2e4022c --- /dev/null +++ b/packages/wallet-sdk/src/auth.ts @@ -0,0 +1,29 @@ +import type { CryptoAdapter } from "./crypto-adapter.js"; + +export interface AuthenticateOptions { + sessionId: string; + keyId?: string; + context: string; +} + +export interface AuthenticateResult { + signature: string; +} + +/** + * Ensure key exists and sign the session payload. Caller is responsible for POST to redirect URL or opening deeplink. + */ +export async function authenticate( + adapter: CryptoAdapter, + options: AuthenticateOptions, +): Promise { + const keyId = options.keyId ?? "default"; + + await adapter.ensureKey(keyId, options.context); + const signature = await adapter.signPayload( + keyId, + options.context, + options.sessionId, + ); + return { signature }; +} diff --git a/packages/wallet-sdk/src/crypto-adapter.ts b/packages/wallet-sdk/src/crypto-adapter.ts new file mode 100644 index 000000000..fba470ce2 --- /dev/null +++ b/packages/wallet-sdk/src/crypto-adapter.ts @@ -0,0 +1,25 @@ +/** + * Crypto adapter interface (BYOC – bring your own crypto). + * Implement this to plug your key storage (e.g. KeyService + hardware/software managers) into the SDK. + */ +export interface CryptoAdapter { + /** + * Return the public key for the given key id and context, or undefined if not found. + */ + getPublicKey(keyId: string, context: string): Promise; + + /** + * Sign a payload with the given key id and context. Must use the same key as getPublicKey. + */ + signPayload( + keyId: string, + context: string, + payload: string, + ): Promise; + + /** + * Ensure a key exists for the given key id and context (create if needed). + * Return value can be used to know if a key was created (optional). + */ + ensureKey(keyId: string, context: string): Promise<{ created: boolean }>; +} diff --git a/packages/wallet-sdk/src/index.ts b/packages/wallet-sdk/src/index.ts new file mode 100644 index 000000000..9d4578cae --- /dev/null +++ b/packages/wallet-sdk/src/index.ts @@ -0,0 +1,7 @@ +export type { CryptoAdapter } from "./crypto-adapter.js"; +export { provision } from "./provision.js"; +export type { ProvisionOptions, ProvisionResult } from "./provision.js"; +export { authenticate } from "./auth.js"; +export type { AuthenticateOptions, AuthenticateResult } from "./auth.js"; +export { syncPublicKeyToEvault } from "./sync-public-key.js"; +export type { SyncPublicKeyOptions } from "./sync-public-key.js"; diff --git a/packages/wallet-sdk/src/provision.test.ts b/packages/wallet-sdk/src/provision.test.ts new file mode 100644 index 000000000..e89718b92 --- /dev/null +++ b/packages/wallet-sdk/src/provision.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it, vi } from "vitest"; +import type { CryptoAdapter } from "./crypto-adapter.js"; +import { provision } from "./provision.js"; + +function createFakeAdapter(overrides?: { + getPublicKey?: string; +}): CryptoAdapter { + return { + async getPublicKey() { + return overrides?.getPublicKey ?? "zFakePublicKey"; + }, + async signPayload() { + return "fakeSignature"; + }, + async ensureKey() { + return { created: false }; + }, + }; +} + +describe("provision", () => { + it("calls fetch for entropy and provision with adapter public key", async () => { + const fetchMock = vi.fn(); + fetchMock + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ token: "entropy-token" }), + }) + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + success: true, + w3id: "w3id:test", + uri: "https://evault.example/vault", + }), + }); + vi.stubGlobal("fetch", fetchMock); + + const adapter = createFakeAdapter({ getPublicKey: "zMyKey" }); + const result = await provision(adapter, { + registryUrl: "https://registry.example", + provisionerUrl: "https://provisioner.example", + namespace: "ns-123", + verificationId: "v-456", + }); + + expect(result).toEqual({ + success: true, + w3id: "w3id:test", + uri: "https://evault.example/vault", + }); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + "https://registry.example/entropy", + ); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + "https://provisioner.example/provision", + expect.objectContaining({ + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + registryEntropy: "entropy-token", + namespace: "ns-123", + verificationId: "v-456", + publicKey: "zMyKey", + }), + }), + ); + + vi.unstubAllGlobals(); + }); + + it("throws when adapter returns no public key", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ token: "entropy-token" }), + })); + + const adapter = createFakeAdapter(); + vi.spyOn(adapter, "getPublicKey").mockResolvedValue(undefined); + + await expect( + provision(adapter, { + registryUrl: "https://registry.example", + provisionerUrl: "https://provisioner.example", + namespace: "ns", + verificationId: "v", + }), + ).rejects.toThrow("No public key"); + + vi.unstubAllGlobals(); + }); +}); diff --git a/packages/wallet-sdk/src/provision.ts b/packages/wallet-sdk/src/provision.ts new file mode 100644 index 000000000..582f6ae22 --- /dev/null +++ b/packages/wallet-sdk/src/provision.ts @@ -0,0 +1,84 @@ +import type { CryptoAdapter } from "./crypto-adapter.js"; + +export interface ProvisionOptions { + registryUrl: string; + provisionerUrl: string; + namespace: string; + verificationId: string; + keyId?: string; + context?: string; + isPreVerification?: boolean; +} + +export interface ProvisionResult { + success: boolean; + w3id: string; + uri: string; +} + +/** + * Provision an eVault: get entropy from registry, get public key from adapter, POST to provisioner. + */ +export async function provision( + adapter: CryptoAdapter, + options: ProvisionOptions, +): Promise { + const keyId = options.keyId ?? "default"; + const context = + options.context ?? + (options.isPreVerification ? "pre-verification" : "onboarding"); + + const entropyUrl = new URL("/entropy", options.registryUrl).toString(); + const entropyRes = await fetch(entropyUrl); + if (!entropyRes.ok) { + throw new Error( + `Failed to get entropy: ${entropyRes.status} ${entropyRes.statusText}`, + ); + } + const entropyData = (await entropyRes.json()) as { token?: string }; + const registryEntropy = entropyData.token; + if (!registryEntropy) { + throw new Error("Registry did not return entropy token"); + } + + const publicKey = await adapter.getPublicKey(keyId, context); + if (!publicKey) { + throw new Error( + `No public key for keyId=${keyId} context=${context}. Ensure key exists before provisioning.`, + ); + } + + const provisionUrl = new URL("/provision", options.provisionerUrl).toString(); + const provisionRes = await fetch(provisionUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + registryEntropy, + namespace: options.namespace, + verificationId: options.verificationId, + publicKey, + }), + }); + + if (!provisionRes.ok) { + const text = await provisionRes.text(); + throw new Error( + `Provision failed: ${provisionRes.status} ${provisionRes.statusText} ${text}`, + ); + } + + const data = (await provisionRes.json()) as { + success?: boolean; + w3id?: string; + uri?: string; + }; + if (!data.success || !data.w3id || !data.uri) { + throw new Error("Invalid provision response: missing success, w3id, or uri"); + } + + return { + success: data.success, + w3id: data.w3id, + uri: data.uri, + }; +} diff --git a/packages/wallet-sdk/src/sync-public-key.test.ts b/packages/wallet-sdk/src/sync-public-key.test.ts new file mode 100644 index 000000000..6d072b508 --- /dev/null +++ b/packages/wallet-sdk/src/sync-public-key.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it, vi } from "vitest"; +import type { CryptoAdapter } from "./crypto-adapter.js"; +import { syncPublicKeyToEvault } from "./sync-public-key.js"; + +function createFakeAdapter(publicKey = "zFakePublicKey"): CryptoAdapter { + return { + async getPublicKey() { + return publicKey; + }, + async signPayload() { + return "fakeSignature"; + }, + async ensureKey() { + return { created: false }; + }, + }; +} + +describe("syncPublicKeyToEvault", () => { + it("fetches whois then PATCHes public-key", async () => { + const fetchMock = vi.fn(); + fetchMock + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ keyBindingCertificates: [] }), + }) + .mockResolvedValueOnce({ ok: true }); + vi.stubGlobal("fetch", fetchMock); + + const adapter = createFakeAdapter("zMyKey"); + await syncPublicKeyToEvault(adapter, { + evaultUri: "https://evault.example", + eName: "ename:test", + context: "onboarding", + }); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + "https://evault.example/whois", + expect.objectContaining({ + headers: { "X-ENAME": "ename:test" }, + }), + ); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + "https://evault.example/public-key", + expect.objectContaining({ + method: "PATCH", + headers: { + "X-ENAME": "ename:test", + "Content-Type": "application/json", + }, + body: JSON.stringify({ publicKey: "zMyKey" }), + }), + ); + + vi.unstubAllGlobals(); + }); + + it("includes Authorization when authToken provided", async () => { + const fetchMock = vi.fn(); + fetchMock + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ keyBindingCertificates: [] }), + }) + .mockResolvedValueOnce({ ok: true }); + vi.stubGlobal("fetch", fetchMock); + + const adapter = createFakeAdapter(); + await syncPublicKeyToEvault(adapter, { + evaultUri: "https://evault.example", + eName: "e", + context: "onboarding", + authToken: "bearer-token", + }); + + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + "https://evault.example/public-key", + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: "Bearer bearer-token", + }), + }), + ); + + vi.unstubAllGlobals(); + }); + + it("throws when adapter returns no public key", async () => { + const adapter = createFakeAdapter(); + vi.spyOn(adapter, "getPublicKey").mockResolvedValue(undefined); + + const fetchMock = vi.fn().mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({}), + }); + vi.stubGlobal("fetch", fetchMock); + + await expect( + syncPublicKeyToEvault(adapter, { + evaultUri: "https://evault.example", + eName: "e", + context: "onboarding", + }), + ).rejects.toThrow("No public key"); + + vi.unstubAllGlobals(); + }); +}); diff --git a/packages/wallet-sdk/src/sync-public-key.ts b/packages/wallet-sdk/src/sync-public-key.ts new file mode 100644 index 000000000..62b1b20cf --- /dev/null +++ b/packages/wallet-sdk/src/sync-public-key.ts @@ -0,0 +1,102 @@ +import { createLocalJWKSet, jwtVerify } from "jose"; +import type { CryptoAdapter } from "./crypto-adapter.js"; + +export interface SyncPublicKeyOptions { + evaultUri: string; + eName: string; + keyId?: string; + context: string; + authToken?: string | null; + registryUrl?: string; +} + +/** + * Sync public key to eVault: GET /whois, optionally skip if current key already in certs, then PATCH /public-key. + * Does not touch localStorage; caller can set publicKeySaved_* if desired. + */ +export async function syncPublicKeyToEvault( + adapter: CryptoAdapter, + options: SyncPublicKeyOptions, +): Promise { + const keyId = options.keyId ?? "default"; + + const whoisUrl = new URL("/whois", options.evaultUri).toString(); + const whoisRes = await fetch(whoisUrl, { + headers: { "X-ENAME": options.eName }, + }); + if (!whoisRes.ok) { + throw new Error( + `Whois failed: ${whoisRes.status} ${whoisRes.statusText}`, + ); + } + const whoisData = (await whoisRes.json()) as { + keyBindingCertificates?: string[]; + }; + const keyBindingCertificates = whoisData?.keyBindingCertificates; + + const currentPublicKey = await adapter.getPublicKey(keyId, options.context); + if (!currentPublicKey) { + throw new Error( + `No public key for keyId=${keyId} context=${options.context}`, + ); + } + + // If we have certs and registry URL, check if current key is already present (optional verification) + if ( + keyBindingCertificates && + Array.isArray(keyBindingCertificates) && + keyBindingCertificates.length > 0 && + options.registryUrl + ) { + try { + const jwksUrl = new URL( + "/.well-known/jwks.json", + options.registryUrl, + ).toString(); + const jwksRes = await fetch(jwksUrl); + if (jwksRes.ok) { + const jwks = (await jwksRes.json()) as { keys?: unknown[] }; + const JWKS = createLocalJWKSet( + { keys: Array.isArray(jwks?.keys) ? jwks.keys : [] } as Parameters< + typeof createLocalJWKSet + >[0], + ); + for (const jwt of keyBindingCertificates) { + try { + const { payload } = await jwtVerify(jwt as string, JWKS); + if ((payload as { ename?: string }).ename !== options.eName) { + continue; + } + const extracted = (payload as { publicKey?: string }).publicKey; + if (extracted === currentPublicKey) { + return; // already synced + } + } catch { + // try next cert + } + } + } + } catch { + // continue to PATCH + } + } + + const patchUrl = new URL("/public-key", options.evaultUri).toString(); + const headers: Record = { + "X-ENAME": options.eName, + "Content-Type": "application/json", + }; + if (options.authToken) { + headers.Authorization = `Bearer ${options.authToken}`; + } + const patchRes = await fetch(patchUrl, { + method: "PATCH", + headers, + body: JSON.stringify({ publicKey: currentPublicKey }), + }); + if (!patchRes.ok) { + throw new Error( + `PATCH public-key failed: ${patchRes.status} ${patchRes.statusText}`, + ); + } +} diff --git a/packages/wallet-sdk/tsconfig.build.json b/packages/wallet-sdk/tsconfig.build.json new file mode 100644 index 000000000..8d6db0f0a --- /dev/null +++ b/packages/wallet-sdk/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] +} diff --git a/packages/wallet-sdk/tsconfig.json b/packages/wallet-sdk/tsconfig.json new file mode 100644 index 000000000..7c87788dc --- /dev/null +++ b/packages/wallet-sdk/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/wallet-sdk/vitest.config.ts b/packages/wallet-sdk/vitest.config.ts new file mode 100644 index 000000000..65b0ef66e --- /dev/null +++ b/packages/wallet-sdk/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + glob: ["**/*.{test,spec}.{ts,tsx}"], + environment: "node", + }, +}); diff --git a/platforms/ppappt b/platforms/ppappt new file mode 160000 index 000000000..c9b5b08df --- /dev/null +++ b/platforms/ppappt @@ -0,0 +1 @@ +Subproject commit c9b5b08dfdec1f3d08fe713c80b208e664ce4bfe diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 448459ae8..ec0a2f05e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -305,6 +305,9 @@ importers: uuid: specifier: ^11.1.0 version: 11.1.0 + wallet-sdk: + specifier: workspace:* + version: link:../../packages/wallet-sdk devDependencies: '@biomejs/biome': specifier: ^1.9.4 @@ -673,6 +676,19 @@ importers: packages/typescript-config: {} + packages/wallet-sdk: + dependencies: + jose: + specifier: ^5.2.0 + version: 5.10.0 + devDependencies: + typescript: + specifier: ~5.6.2 + version: 5.6.3 + vitest: + specifier: ^3.0.9 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.3)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@19.0.0(bufferutil@4.0.9))(lightningcss@1.30.2)(sass@1.96.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.1) + platforms/blabsy: dependencies: '@headlessui/react': @@ -1067,10 +1083,10 @@ importers: version: 18.3.7(@types/react@18.3.27) eslint: specifier: ^9 - version: 9.39.1(jiti@2.6.1) + version: 9.39.1(jiti@1.21.7) eslint-config-next: specifier: 15.1.2 - version: 15.1.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2) + version: 15.1.2(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2) postcss: specifier: ^8 version: 8.5.6 @@ -22500,6 +22516,11 @@ snapshots: eslint: 8.57.1 eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@1.21.7))': + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))': dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -27589,6 +27610,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/type-utils': 5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2) + '@typescript-eslint/utils': 5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2) + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.39.1(jiti@1.21.7) + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare-lite: 1.4.0 + semver: 7.7.3 + tsutils: 3.21.0(typescript@5.8.2) + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -27666,6 +27706,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2)': + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.2) + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.39.1(jiti@1.21.7) + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2)': dependencies: '@typescript-eslint/scope-manager': 5.62.0 @@ -27773,6 +27825,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2)': + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.2) + '@typescript-eslint/utils': 5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2) + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.39.1(jiti@1.21.7) + tsutils: 3.21.0(typescript@5.8.2) + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/type-utils@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2)': dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.2) @@ -27921,6 +27985,21 @@ snapshots: - supports-color - typescript + '@typescript-eslint/utils@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.1 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.2) + eslint: 9.39.1(jiti@1.21.7) + eslint-scope: 5.1.1 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + - typescript + '@typescript-eslint/utils@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) @@ -31030,19 +31109,19 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-next@15.1.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2): + eslint-config-next@15.1.2(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2): dependencies: '@next/eslint-plugin-next': 15.1.2 '@rushstack/eslint-patch': 1.15.0 - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2) - '@typescript-eslint/parser': 5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2) - eslint: 9.39.1(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2) + '@typescript-eslint/parser': 5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2) + eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@1.21.7)) optionalDependencies: typescript: 5.8.2 transitivePeerDependencies: @@ -31102,18 +31181,18 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3(supports-color@5.5.0) - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) get-tsconfig: 4.13.0 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) transitivePeerDependencies: - supports-color @@ -31158,14 +31237,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2) - eslint: 9.39.1(jiti@2.6.1) + '@typescript-eslint/parser': 5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2) + eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) transitivePeerDependencies: - supports-color @@ -31209,7 +31288,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -31218,9 +31297,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -31232,7 +31311,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2) + '@typescript-eslint/parser': 5.62.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -31286,6 +31365,25 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.1(jiti@1.21.7)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.11.0 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.1(jiti@1.21.7) + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.1(jiti@2.6.1)): dependencies: aria-query: 5.3.2 @@ -31311,6 +31409,10 @@ snapshots: dependencies: eslint: 8.57.1 + eslint-plugin-react-hooks@5.2.0(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-plugin-react-hooks@5.2.0(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -31337,6 +31439,28 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 + eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@1.21.7)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 9.39.1(jiti@1.21.7) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@2.6.1)): dependencies: array-includes: 3.1.9 @@ -31522,6 +31646,47 @@ snapshots: transitivePeerDependencies: - supports-color + eslint@9.39.1(jiti@1.21.7): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.1 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@5.5.0) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 1.21.7 + transitivePeerDependencies: + - supports-color + eslint@9.39.1(jiti@2.6.1): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) From 70b74e01ef61ca41ba3698488eb2ecbef4773160 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 11 Feb 2026 14:08:08 +0530 Subject: [PATCH 2/7] docs: wallet sdk docs --- docs/docs/Infrastructure/eID-Wallet.md | 2 +- docs/docs/Infrastructure/wallet-sdk.md | 136 +++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 docs/docs/Infrastructure/wallet-sdk.md diff --git a/docs/docs/Infrastructure/eID-Wallet.md b/docs/docs/Infrastructure/eID-Wallet.md index a031651c5..021753443 100644 --- a/docs/docs/Infrastructure/eID-Wallet.md +++ b/docs/docs/Infrastructure/eID-Wallet.md @@ -292,7 +292,7 @@ The wallet creates signatures for various purposes: - **Framework**: Tauri (Rust + Web frontend) - **Frontend**: SvelteKit + TypeScript -- **wallet-sdk**: The wallet uses the **wallet-sdk** package for provisioning, platform authentication (signing), and public-key sync to eVault. Crypto is provided by the existing KeyService via a **CryptoAdapter** (BYOC), so hardware/software key manager behavior is unchanged. +- **wallet-sdk**: The wallet uses the [wallet-sdk](/docs/Infrastructure/wallet-sdk) package for provisioning, platform authentication (signing), and public-key sync to eVault. Crypto is provided by the existing KeyService via a **CryptoAdapter** (BYOC), so hardware/software key manager behavior is unchanged. - **Key APIs**: - iOS: LocalAuthentication (Secure Enclave) - Android: KeyStore (HSM) diff --git a/docs/docs/Infrastructure/wallet-sdk.md b/docs/docs/Infrastructure/wallet-sdk.md new file mode 100644 index 000000000..d746aa7ec --- /dev/null +++ b/docs/docs/Infrastructure/wallet-sdk.md @@ -0,0 +1,136 @@ +--- +sidebar_position: 6 +--- + +# wallet-sdk + +The **wallet-sdk** is a small TypeScript package that implements the high-level flows for eVault provisioning, platform authentication (signing), and public-key sync. It is **crypto-agnostic**: you supply a **CryptoAdapter** (BYOC – bring your own crypto), and the SDK handles the HTTP and protocol steps. + +The [eID Wallet](/docs/Infrastructure/eID-Wallet) uses wallet-sdk with an adapter that delegates to its KeyService, so hardware/software key manager behavior is unchanged. + +## Overview + +- **Package**: `wallet-sdk` (workspace package under `packages/wallet-sdk/`) +- **Exports**: `CryptoAdapter` type, `provision`, `authenticate`, `syncPublicKeyToEvault`, and their option/result types +- **Dependencies**: `jose` (for JWT verification when checking existing keys during sync). Uses the global `fetch` for HTTP + +## CryptoAdapter (BYOC) + +Implement this interface to plug in your key storage (e.g. KeyService + hardware/software managers): + +```typescript +interface CryptoAdapter { + getPublicKey(keyId: string, context: string): Promise; + signPayload(keyId: string, context: string, payload: string): Promise; + ensureKey(keyId: string, context: string): Promise<{ created: boolean }>; +} +``` + +- **getPublicKey**: Return the public key for the given key id and context, or `undefined` if the key does not exist. +- **signPayload**: Sign the payload with the same key as used for `getPublicKey`. Return the signature string (encoding is up to the adapter, e.g. base64 or multibase). +- **ensureKey**: Ensure a key exists for the given key id and context; create it if needed. Return `{ created: true }` if a new key was created, `{ created: false }` otherwise. + +Contexts used by the eID Wallet include `"onboarding"`, `"pre-verification"`, and `"signing"`. The SDK does not interpret contexts; it only passes them through to the adapter. + +## API + +### provision(adapter, options) + +Provisions an eVault: fetches entropy from the Registry, gets the public key from the adapter, and POSTs to the Provisioner. + +**Flow**: + +1. `GET {registryUrl}/entropy` → obtain entropy token +2. `adapter.getPublicKey(keyId, context)` → public key (must exist; ensure key before calling if needed) +3. `POST {provisionerUrl}/provision` with `{ registryEntropy, namespace, verificationId, publicKey }` + +**Options**: `registryUrl`, `provisionerUrl`, `namespace`, `verificationId`, and optionally `keyId` (default `"default"`), `context` (default derived from `isPreVerification`), `isPreVerification`. + +**Returns**: `{ success, w3id, uri }`. Throws on HTTP or validation errors. + +**Example** (eID Wallet pre-verification): + +```typescript +const result = await provision(globalState.walletSdkAdapter, { + registryUrl: PUBLIC_REGISTRY_URL, + provisionerUrl: PUBLIC_PROVISIONER_URL, + namespace: uuidv4(), + verificationId, + keyId: "default", + context: "pre-verification", + isPreVerification: true, +}); +// result.uri, result.w3id +``` + +### authenticate(adapter, options) + +Ensures the key exists and signs the session payload. The **caller** is responsible for sending the signature to the platform (e.g. POST to redirect URL or open deeplink). + +**Flow**: + +1. `adapter.ensureKey(keyId, context)` +2. `adapter.signPayload(keyId, context, sessionId)` +3. Return `{ signature }` + +**Options**: `sessionId`, `context`, and optionally `keyId` (default `"default"`). + +**Returns**: `{ signature }`. + +**Example** (eID Wallet auth): + +```typescript +const { signature } = await authenticate(globalState.walletSdkAdapter, { + sessionId: sessionPayload, + keyId: "default", + context: isFake ? "pre-verification" : "onboarding", +}); +// Then POST to redirect URL: { ename, session, signature, appVersion } +``` + +### syncPublicKeyToEvault(adapter, options) + +Syncs the adapter’s public key to the eVault: calls `/whois`, optionally skips PATCH if the current key is already present in key-binding certificates (using Registry JWKS), then `PATCH /public-key` if needed. + +**Flow**: + +1. `GET {evaultUri}/whois` with header `X-ENAME: {eName}` +2. If `registryUrl` is provided and whois returns key-binding certificates, verify with Registry’s `/.well-known/jwks.json` and skip PATCH if the current public key is already in a valid cert for this eName +3. `adapter.getPublicKey(keyId, context)` +4. `PATCH {evaultUri}/public-key` with `{ publicKey }`, headers `X-ENAME` and optional `Authorization: Bearer {authToken}` + +The SDK does not read or write `localStorage`; the caller can set a hint (e.g. `publicKeySaved_${eName}`) after a successful sync. + +**Options**: `evaultUri`, `eName`, `context`, and optionally `keyId` (default `"default"`), `authToken`, `registryUrl` (for skip-if-present verification). + +**Example** (eID Wallet): + +```typescript +await syncPublicKeyToEvault(globalState.walletSdkAdapter, { + evaultUri: vault.uri, + eName, + keyId: "default", + context: isFake ? "pre-verification" : "onboarding", + authToken: PUBLIC_EID_WALLET_TOKEN || null, + registryUrl: PUBLIC_REGISTRY_URL, +}); +``` + +## Use in the eID Wallet + +The eID Wallet: + +1. Implements a **CryptoAdapter** by wrapping KeyService in `createKeyServiceCryptoAdapter(keyService)` (see `src/lib/wallet-sdk-adapter.ts`). +2. Exposes this adapter on GlobalState as `walletSdkAdapter` and passes it into VaultController. +3. Uses **provision** in the onboarding (pre-verification) and verify (real user) flows instead of inline entropy + provision calls. +4. Uses **authenticate** in the scan-qr auth and signing flows, then performs the POST or deeplink open in the UI. +5. Uses **syncPublicKeyToEvault** inside `VaultController.syncPublicKey(eName)` instead of inline whois + PATCH logic. + +See [eID Wallet](/docs/Infrastructure/eID-Wallet) for architecture and key manager details. + +## References + +- [eID Wallet](/docs/Infrastructure/eID-Wallet) – Consumer of wallet-sdk; KeyService and CryptoAdapter +- [Registry](/docs/Infrastructure/Registry) – Entropy and key-binding certificates +- [eVault](/docs/Infrastructure/eVault) – Whois and public-key storage +- [Links](/docs/W3DS%20Basics/Links) – Production URLs (Provisioner, Registry) From e3b025130c1cbfb75a13e0c5212b02aa0541dc96 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 11 Feb 2026 14:22:22 +0530 Subject: [PATCH 3/7] refactor: remove dead code --- .../.kotlin/errors/errors-1770798153274.log | 4 - .../src/lib/global/controllers/evault.ts | 4 +- .../src/routes/(app)/scan-qr/scanLogic.ts | 36 ++------ .../src/routes/(auth)/onboarding/+page.svelte | 69 +--------------- .../src/routes/(auth)/verify/+page.svelte | 82 ++----------------- platforms/ppappt | 1 - 6 files changed, 20 insertions(+), 176 deletions(-) delete mode 100644 infrastructure/eid-wallet/src-tauri/gen/android/buildSrc/.kotlin/errors/errors-1770798153274.log delete mode 160000 platforms/ppappt diff --git a/infrastructure/eid-wallet/src-tauri/gen/android/buildSrc/.kotlin/errors/errors-1770798153274.log b/infrastructure/eid-wallet/src-tauri/gen/android/buildSrc/.kotlin/errors/errors-1770798153274.log deleted file mode 100644 index 1219b509f..000000000 --- a/infrastructure/eid-wallet/src-tauri/gen/android/buildSrc/.kotlin/errors/errors-1770798153274.log +++ /dev/null @@ -1,4 +0,0 @@ -kotlin version: 2.0.21 -error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: - 1. Kotlin compile daemon is ready - diff --git a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts index f6328f193..8af4a4445 100644 --- a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts +++ b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts @@ -105,14 +105,12 @@ export class VaultController { console.warn("No vault URI available, cannot sync public key"); return; } - const isFake = await this.#userController.isFake; - const context = isFake ? "pre-verification" : "onboarding"; try { await syncPublicKeyToEvault(this.#walletSdkAdapter, { evaultUri: vault.uri, eName, keyId: "default", - context, + context: "onboarding", authToken: PUBLIC_EID_WALLET_TOKEN || null, registryUrl: PUBLIC_REGISTRY_URL, }); diff --git a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts index 10aa8882d..8094848e2 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts +++ b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts @@ -282,8 +282,6 @@ export function createScanLogic({ authLoading.set(true); try { - const isFake = await globalState.userController.isFake; - const signingContext = isFake ? "pre-verification" : "onboarding"; const sessionPayload = get(session) as string; const { signature } = await authenticate( @@ -291,7 +289,7 @@ export function createScanLogic({ { sessionId: sessionPayload, keyId: "default", - context: signingContext, + context: "onboarding", }, ); @@ -644,16 +642,12 @@ export function createScanLogic({ throw new Error("No vault available for signing"); } - // Use same keyId/context as synced to eVault (default + onboarding/pre-verification) - const isFake = await globalState.userController.isFake; - const signingContext = isFake ? "pre-verification" : "onboarding"; - const { signature } = await authenticate( globalState.walletSdkAdapter, { sessionId: currentSigningSessionId, keyId: "default", - context: signingContext, + context: "onboarding", }, ); @@ -784,28 +778,14 @@ export function createScanLogic({ throw new Error("No vault available for blind voting"); } - let voterPublicKey: string; - try { - const { created } = await globalState.keyService.ensureKey( - vault.ename, - "signing", - ); - console.log( - "Key generation result for blind voting:", - created ? "key-generated" : "key-exists", - ); + // Same default key via wallet-sdk adapter (no separate signing context) + await globalState.walletSdkAdapter.ensureKey("default", "onboarding"); - const w3idResult = vault.ename; - if (!w3idResult) { - throw new Error("Failed to get W3ID"); - } - voterPublicKey = w3idResult; - - console.log("🔑 Voter W3ID retrieved:", voterPublicKey); - } catch (error) { - console.error("Failed to get W3ID using KeyManager:", error); - voterPublicKey = vault.ename || "unknown_public_key"; + const voterPublicKey = vault.ename; + if (!voterPublicKey) { + throw new Error("Failed to get W3ID"); } + console.log("🔑 Voter W3ID retrieved:", voterPublicKey); const { VotingSystem } = await import("blindvote"); diff --git a/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte index c49032197..7e65053e2 100644 --- a/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte @@ -4,10 +4,8 @@ import { PUBLIC_PROVISIONER_URL, PUBLIC_REGISTRY_URL, } from "$env/static/public"; -import type { KeyManager } from "$lib/crypto"; import { Hero } from "$lib/fragments"; import { GlobalState } from "$lib/global"; -import type { KeyServiceContext } from "$lib/global"; import { ButtonAction } from "$lib/ui"; import { capitalize } from "$lib/utils"; import * as falso from "@ngneat/falso"; @@ -22,7 +20,6 @@ let loading = $state(false); let verificationId = $state(""); let demoName = $state(""); let verificationSuccess = $state(false); -let keyManager: KeyManager | null = $state(null); let showHardwareError = $state(false); let checkingHardware = $state(false); const KEY_ID = "default"; @@ -95,71 +92,13 @@ function generatePassportNumber() { return randomLetters() + randomDigits(); } -function getKeyContext(): KeyServiceContext { - return preVerified ? "pre-verification" : "onboarding"; -} - -async function initializeKeyManager() { - try { - if (!globalState) throw new Error("Global state is not defined"); - const context = getKeyContext(); - keyManager = await globalState.keyService.getManager(KEY_ID, context); - console.log(`Key manager initialized: ${keyManager.getType()}`); - return keyManager; - } catch (error) { - console.error("Failed to initialize key manager:", error); - throw error; - } -} - -async function ensureKeyForContext() { - try { - if (!globalState) throw new Error("Global state is not defined"); - const context = getKeyContext(); - const { manager, created } = await globalState.keyService.ensureKey( - KEY_ID, - context, - ); - keyManager = manager; - console.log( - "Key generation result:", - created ? "key-generated" : "key-exists", - ); - return { manager, created }; - } catch (error) { - console.error("Failed to ensure key:", error); - throw error; - } -} - -async function getApplicationPublicKey() { - try { - if (!globalState) throw new Error("Global state is not defined"); - if (!keyManager) { - await initializeKeyManager(); - } - const context = getKeyContext(); - const publicKey = await globalState.keyService.getPublicKey( - KEY_ID, - context, - ); - console.log("Public key retrieved:", publicKey); - return publicKey; - } catch (error) { - console.error("Public key retrieval failed:", error); - throw error; - } -} - const handleNext = async () => { - // Initialize keys for onboarding context before going to verify try { loading = true; if (!globalState) { globalState = getContext<() => GlobalState>("globalState")(); } - await initializeKeyManager(); - await ensureKeyForContext(); + await globalState.walletSdkAdapter.ensureKey(KEY_ID, "onboarding"); loading = false; goto("/verify"); } catch (err) { @@ -201,9 +140,7 @@ onMount(async () => { error = null; try { - // Initialize key manager for pre-verification context - await initializeKeyManager(); - await ensureKeyForContext(); + await globalState.walletSdkAdapter.ensureKey(KEY_ID, "onboarding"); const result = await provision(globalState.walletSdkAdapter, { registryUrl: PUBLIC_REGISTRY_URL, @@ -211,7 +148,7 @@ onMount(async () => { namespace: uuidv4(), verificationId, keyId: "default", - context: "pre-verification", + context: "onboarding", isPreVerification: true, }); console.log("Provision response:", result); diff --git a/infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte b/infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte index 9b8312bdf..888eca39b 100644 --- a/infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte @@ -4,10 +4,8 @@ import { PUBLIC_PROVISIONER_URL, PUBLIC_REGISTRY_URL, } from "$env/static/public"; -import type { KeyManager } from "$lib/crypto"; import { Hero } from "$lib/fragments"; import { GlobalState } from "$lib/global"; -import type { KeyServiceContext } from "$lib/global"; import { ButtonAction } from "$lib/ui"; import { capitalize } from "$lib/utils"; import { ArrowLeft01Icon } from "@hugeicons/core-free-icons"; @@ -104,7 +102,6 @@ let showVeriffModal = $state(false); let person: Person; let document: Document; let loading = $state(false); -let keyManager: KeyManager | null = $state(null); let websocketData: { w3id?: string } | null = $state(null); // Store websocket data for duplicate case let hardwareKeySupported = $state(false); let hardwareKeyCheckComplete = $state(false); @@ -112,17 +109,12 @@ const KEY_ID = "default"; async function handleVerification() { try { - // Ensure keys are initialized before starting verification - if (!keyManager) { - try { - await initializeKeyManager(); - await ensureKeyForVerification(); - } catch (keyError) { - console.error("Failed to initialize keys:", keyError); - // If key initialization fails, go back to onboarding - await goto("/onboarding"); - return; - } + try { + await globalState?.walletSdkAdapter.ensureKey(KEY_ID, "onboarding"); + } catch (keyError) { + console.error("Failed to ensure key:", keyError); + await goto("/onboarding"); + return; } const { data } = await axios.post( @@ -188,10 +180,6 @@ function closeEventStream() { } } -function getKeyContext(): KeyServiceContext { - return "verification"; -} - // Check if hardware key is supported on this device async function checkHardwareKeySupport() { try { @@ -209,57 +197,6 @@ async function checkHardwareKeySupport() { } } -// Initialize key manager for verification context -async function initializeKeyManager() { - try { - if (!globalState) throw new Error("Global state is not defined"); - const context = getKeyContext(); - keyManager = await globalState.keyService.getManager(KEY_ID, context); - console.log(`Key manager initialized: ${keyManager.getType()}`); - return keyManager; - } catch (error) { - console.error("Failed to initialize key manager:", error); - throw error; - } -} - -async function ensureKeyForVerification() { - try { - if (!globalState) throw new Error("Global state is not defined"); - const context = getKeyContext(); - const { manager, created } = await globalState.keyService.ensureKey( - KEY_ID, - context, - ); - keyManager = manager; - console.log( - "Key generation result:", - created ? "key-generated" : "key-exists", - ); - return { manager, created }; - } catch (error) { - console.error("Failed to ensure key:", error); - throw error; - } -} - -async function getApplicationPublicKey() { - if (!globalState) throw new Error("Global state is not defined"); - if (!keyManager) { - await initializeKeyManager(); - } - - try { - const context = getKeyContext(); - const res = await globalState.keyService.getPublicKey(KEY_ID, context); - console.log("Public key retrieved:", res); - return res; - } catch (e) { - console.error("Public key retrieval failed:", e); - throw e; - } -} - let handleContinue: () => Promise = $state(async () => {}); onMount(async () => { @@ -276,13 +213,10 @@ onMount(async () => { return; } - // Initialize key manager and check if default key pair exists try { - await initializeKeyManager(); - await ensureKeyForVerification(); + await globalState.walletSdkAdapter.ensureKey(KEY_ID, "onboarding"); } catch (error) { - console.error("Failed to initialize keys for verification:", error); - // If key initialization fails, redirect back to onboarding + console.error("Failed to ensure key for verification:", error); await goto("/onboarding"); return; } diff --git a/platforms/ppappt b/platforms/ppappt deleted file mode 160000 index c9b5b08df..000000000 --- a/platforms/ppappt +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c9b5b08dfdec1f3d08fe713c80b208e664ce4bfe From a7e90e87e265202ef7c83ac863afe80ff5073b7c Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 11 Feb 2026 14:42:39 +0530 Subject: [PATCH 4/7] fix: tests --- packages/wallet-sdk/package.json | 9 +- .../src/__tests__/TestCryptoAdapter.ts | 33 ++- .../wallet-sdk/src/__tests__/auth.test.ts | 163 +++--------- .../src/__tests__/provision.test.ts | 251 +++++++++--------- .../src/__tests__/sync-and-sign.test.ts | 168 ++++++------ packages/wallet-sdk/src/sync-and-sign.ts | 77 +++--- packages/wallet-sdk/tsconfig.build.json | 2 +- 7 files changed, 321 insertions(+), 382 deletions(-) diff --git a/packages/wallet-sdk/package.json b/packages/wallet-sdk/package.json index b24c8a8f2..95e70165e 100644 --- a/packages/wallet-sdk/package.json +++ b/packages/wallet-sdk/package.json @@ -7,7 +7,8 @@ "build": "tsc -p tsconfig.build.json", "test": "vitest run", "test:watch": "vitest", - "check-types": "tsc --noEmit" + "check-types": "tsc --noEmit", + "postinstall": "npm run build" }, "keywords": [], "author": "", @@ -20,7 +21,9 @@ "types": "./dist/index.d.ts" } }, - "files": ["dist"], + "files": [ + "dist" + ], "dependencies": { "jose": "^5.2.0" }, @@ -28,4 +31,4 @@ "typescript": "~5.6.2", "vitest": "^3.0.9" } -} +} \ No newline at end of file diff --git a/packages/wallet-sdk/src/__tests__/TestCryptoAdapter.ts b/packages/wallet-sdk/src/__tests__/TestCryptoAdapter.ts index 5727e16d3..731562fef 100644 --- a/packages/wallet-sdk/src/__tests__/TestCryptoAdapter.ts +++ b/packages/wallet-sdk/src/__tests__/TestCryptoAdapter.ts @@ -1,26 +1,33 @@ -import type { CryptoAdapter } from "../crypto-adapter"; +import type { CryptoAdapter } from "../crypto-adapter.js"; const TEST_PUBLIC_KEY = "test-public-key-base64-or-multibase"; const KEY_ID = "test-key-1"; +function toBase64(s: string): string { + return btoa(String.fromCharCode(...new TextEncoder().encode(s))); +} + /** * In-memory test double for CryptoAdapter. Deterministic key and fake signatures. * For use in SDK unit/integration tests only; no real crypto. */ export class TestCryptoAdapter implements CryptoAdapter { - private keys = new Map(); + private keys = new Map(); - async generateKeyPair(): Promise<{ keyId: string; publicKey: string }> { - this.keys.set(KEY_ID, TEST_PUBLIC_KEY); - return { keyId: KEY_ID, publicKey: TEST_PUBLIC_KEY }; - } + async getPublicKey(keyId: string, _context: string): Promise { + const pk = this.keys.get(keyId) ?? TEST_PUBLIC_KEY; + return pk; + } - async getPublicKey(keyId: string): Promise { - const pk = this.keys.get(keyId) ?? TEST_PUBLIC_KEY; - return pk; - } + async signPayload(keyId: string, _context: string, payload: string): Promise { + return `sig:${keyId}:${toBase64(payload)}`; + } - async sign(keyId: string, payload: string): Promise { - return `sig:${keyId}:${Buffer.from(payload, "utf-8").toString("base64")}`; - } + async ensureKey(keyId: string, _context: string): Promise<{ created: boolean }> { + if (!this.keys.has(keyId)) { + this.keys.set(keyId, TEST_PUBLIC_KEY); + return { created: true }; + } + return { created: false }; + } } diff --git a/packages/wallet-sdk/src/__tests__/auth.test.ts b/packages/wallet-sdk/src/__tests__/auth.test.ts index 27efc1480..4adb9d6a2 100644 --- a/packages/wallet-sdk/src/__tests__/auth.test.ts +++ b/packages/wallet-sdk/src/__tests__/auth.test.ts @@ -1,130 +1,37 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { authenticateToPlatform, parseAuthUri } from "../auth"; -import { TestCryptoAdapter } from "./TestCryptoAdapter"; - -describe("parseAuthUri", () => { - it("parses w3ds://auth URI", () => { - const uri = - "w3ds://auth?redirect=https%3A%2F%2Fplatform.test%2Fapi%2Fauth&session=sess-123&platform=myapp"; - const parsed = parseAuthUri(uri); - expect(parsed.redirectUrl).toBe("https://platform.test/api/auth"); - expect(parsed.sessionId).toBe("sess-123"); - expect(parsed.platform).toBe("myapp"); - }); - - it("throws on invalid URL", () => { - expect(() => parseAuthUri("not-a-url")).toThrow("Invalid auth URI"); - }); - - it("throws on wrong scheme", () => { - expect(() => parseAuthUri("https://auth?redirect=x&session=y")).toThrow( - "expected scheme w3ds://auth" - ); - }); - - it("throws when redirect missing", () => { - expect(() => - parseAuthUri("w3ds://auth?session=s1&platform=p") - ).toThrow("missing redirect"); - }); - - it("throws when session missing", () => { - expect(() => - parseAuthUri("w3ds://auth?redirect=https%3A%2F%2Fa.b%2Fc&platform=p") - ).toThrow("missing session"); - }); -}); - -describe("authenticateToPlatform", () => { - const adapter = new TestCryptoAdapter(); - - beforeEach(async () => { - await adapter.generateKeyPair(); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - }); - - it("POSTs ename, session, signature to redirect URL", async () => { - const authUri = - "w3ds://auth?redirect=https%3A%2F%2Fplatform.test%2Fapi%2Fauth&session=sess-456&platform=test"; - let capturedBody: Record = {}; - vi.stubGlobal( - "fetch", - vi.fn((input: string | URL, init?: RequestInit) => { - const url = typeof input === "string" ? input : input.toString(); - if (url === "https://platform.test/api/auth") { - capturedBody = init?.body ? JSON.parse(init.body as string) : {}; - return Promise.resolve( - new Response(JSON.stringify({ token: "jwt-123" }), { status: 200 }) - ); - } - return Promise.reject(new Error(`Unexpected: ${url}`)); - }) - ); - - const result = await authenticateToPlatform({ - cryptoAdapter: adapter, - keyId: "test-key-1", - w3id: "w3id-xyz", - authUri, - }); - - expect(result.success).toBe(true); - expect(result.token).toBe("jwt-123"); - expect(capturedBody.ename).toBe("w3id-xyz"); - expect(capturedBody.session).toBe("sess-456"); - expect(capturedBody.signature).toMatch(/^sig:test-key-1:/); - }); - - it("includes appVersion when provided", async () => { - const authUri = - "w3ds://auth?redirect=https%3A%2F%2Fp.test%2Fauth&session=s1&platform=p"; - let capturedBody: Record = {}; - vi.stubGlobal( - "fetch", - vi.fn((_input: string | URL, init?: RequestInit) => { - capturedBody = init?.body ? JSON.parse(init.body as string) : {}; - return Promise.resolve( - new Response(JSON.stringify({}), { status: 200 }) - ); - }) - ); - - await authenticateToPlatform({ - cryptoAdapter: adapter, - keyId: "test-key-1", - w3id: "w3id", - authUri, - appVersion: "1.0.0", - }); - - expect(capturedBody.appVersion).toBe("1.0.0"); - }); - - it("throws when platform returns error", async () => { - const authUri = - "w3ds://auth?redirect=https%3A%2F%2Fp.test%2Fauth&session=s1&platform=p"; - vi.stubGlobal( - "fetch", - vi.fn(() => - Promise.resolve( - new Response( - JSON.stringify({ error: "Invalid session" }), - { status: 400 } - ) - ) - ) - ); - - await expect( - authenticateToPlatform({ - cryptoAdapter: adapter, - keyId: "test-key-1", - w3id: "w3id", - authUri, - }) - ).rejects.toThrow(/Platform auth failed/); - }); +import { authenticate } from "../auth.js"; +import { TestCryptoAdapter } from "./TestCryptoAdapter.js"; + +describe("authenticate", () => { + const adapter = new TestCryptoAdapter(); + const CONTEXT = "onboarding"; + + beforeEach(async () => { + await adapter.ensureKey("default", CONTEXT); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("returns signature from adapter", async () => { + const result = await authenticate(adapter, { + sessionId: "sess-123", + keyId: "default", + context: CONTEXT, + }); + expect(result.signature).toMatch(/^sig:default:/); + const b64 = result.signature.replace(/^sig:default:/, ""); + expect(atob(b64)).toBe("sess-123"); + }); + + it("uses custom keyId when provided", async () => { + await adapter.ensureKey("test-key-1", CONTEXT); + const result = await authenticate(adapter, { + sessionId: "hello", + keyId: "test-key-1", + context: CONTEXT, + }); + expect(result.signature).toMatch(/^sig:test-key-1:/); + }); }); diff --git a/packages/wallet-sdk/src/__tests__/provision.test.ts b/packages/wallet-sdk/src/__tests__/provision.test.ts index 5ff4b2c75..77c94625f 100644 --- a/packages/wallet-sdk/src/__tests__/provision.test.ts +++ b/packages/wallet-sdk/src/__tests__/provision.test.ts @@ -1,129 +1,142 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { provision } from "../provision"; -import { TestCryptoAdapter } from "./TestCryptoAdapter"; +import { provision } from "../provision.js"; +import { TestCryptoAdapter } from "./TestCryptoAdapter.js"; describe("provision", () => { - const adapter = new TestCryptoAdapter(); - const registryUrl = "https://registry.test"; - const provisionerUrl = "https://provisioner.test"; + const adapter = new TestCryptoAdapter(); + const registryUrl = "https://registry.test"; + const provisionerUrl = "https://provisioner.test"; - beforeEach(() => { - vi.stubGlobal( - "fetch", - vi.fn((input: string | URL, init?: RequestInit) => { - const url = typeof input === "string" ? input : input.toString(); - if (url === `${registryUrl}/entropy`) { - return Promise.resolve( - new Response(JSON.stringify({ token: "mock-jwt-entropy" }), { - status: 200, - }) - ); - } - if (url === `${provisionerUrl}/provision`) { - const body = init?.body ? JSON.parse(init.body as string) : {}; - if (!body.registryEntropy || !body.namespace || !body.verificationId) { - return Promise.resolve( - new Response( - JSON.stringify({ - success: false, - message: "Missing required fields", - }), - { status: 500 } - ) - ); - } - return Promise.resolve( - new Response( - JSON.stringify({ - success: true, - w3id: "w3id-123", - uri: "https://evault.test/instance-1", - }), - { status: 200 } - ) - ); - } - return Promise.reject(new Error(`Unexpected fetch: ${url}`)); - }) - ); - }); + beforeEach(async () => { + await adapter.ensureKey("default", "onboarding"); + vi.stubGlobal( + "fetch", + vi.fn((input: string | URL, init?: RequestInit) => { + const url = typeof input === "string" ? input : input.toString(); + if (url === `${registryUrl}/entropy`) { + return Promise.resolve( + new Response(JSON.stringify({ token: "mock-jwt-entropy" }), { + status: 200, + }), + ); + } + if (url === `${provisionerUrl}/provision`) { + const body = init?.body ? JSON.parse(init.body as string) : {}; + if (!body.registryEntropy || !body.namespace || !body.verificationId) { + return Promise.resolve( + new Response( + JSON.stringify({ + success: false, + message: "Missing required fields", + }), + { status: 500 }, + ), + ); + } + return Promise.resolve( + new Response( + JSON.stringify({ + success: true, + w3id: "w3id-123", + uri: "https://evault.test/instance-1", + }), + { status: 200 }, + ), + ); + } + return Promise.reject(new Error(`Unexpected fetch: ${url}`)); + }), + ); + }); - afterEach(() => { - vi.unstubAllGlobals(); - }); + afterEach(() => { + vi.unstubAllGlobals(); + }); - it("returns w3id and uri on success", async () => { - const result = await provision({ - cryptoAdapter: adapter, - registryUrl, - provisionerUrl, - }); - expect(result.w3id).toBe("w3id-123"); - expect(result.uri).toBe("https://evault.test/instance-1"); - expect(result.keyId).toBe("test-key-1"); - }); + it("returns w3id and uri on success", async () => { + const result = await provision(adapter, { + registryUrl, + provisionerUrl, + namespace: "ns-1", + verificationId: "v-1", + }); + expect(result.w3id).toBe("w3id-123"); + expect(result.uri).toBe("https://evault.test/instance-1"); + expect(result.success).toBe(true); + }); - it("uses optional namespace and verificationId", async () => { - const result = await provision({ - cryptoAdapter: adapter, - registryUrl, - provisionerUrl, - namespace: "aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee", - verificationId: "custom-verification-id", - }); - expect(result.w3id).toBe("w3id-123"); - const fetchCall = (globalThis.fetch as ReturnType).mock - .calls as Array<[string, RequestInit]>; - const provisionCall = fetchCall.find(([url]) => url === `${provisionerUrl}/provision`); - expect(provisionCall).toBeDefined(); - const body = JSON.parse((provisionCall![1].body as string) ?? "{}"); - expect(body.namespace).toBe("aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee"); - expect(body.verificationId).toBe("custom-verification-id"); - expect(body.publicKey).toBe("test-public-key-base64-or-multibase"); - }); + it("uses optional namespace and verificationId", async () => { + const result = await provision(adapter, { + registryUrl, + provisionerUrl, + namespace: "aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee", + verificationId: "custom-verification-id", + }); + expect(result.w3id).toBe("w3id-123"); + const fetchCall = (globalThis.fetch as ReturnType).mock + .calls as Array<[string, RequestInit]>; + const provisionCall = fetchCall.find( + ([url]) => url === `${provisionerUrl}/provision`, + ); + expect(provisionCall).toBeDefined(); + const body = JSON.parse((provisionCall![1].body as string) ?? "{}"); + expect(body.namespace).toBe("aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee"); + expect(body.verificationId).toBe("custom-verification-id"); + expect(body.publicKey).toBe("test-public-key-base64-or-multibase"); + }); - it("throws when registry entropy fails", async () => { - vi.stubGlobal( - "fetch", - vi.fn((input: string | URL) => { - const url = typeof input === "string" ? input : input.toString(); - if (url === `${registryUrl}/entropy`) { - return Promise.resolve(new Response("error", { status: 500 })); - } - return Promise.reject(new Error(`Unexpected: ${url}`)); - }) - ); - await expect( - provision({ cryptoAdapter: adapter, registryUrl, provisionerUrl }) - ).rejects.toThrow(/Registry entropy failed/); - }); + it("throws when registry entropy fails", async () => { + vi.stubGlobal( + "fetch", + vi.fn((input: string | URL) => { + const url = typeof input === "string" ? input : input.toString(); + if (url === `${registryUrl}/entropy`) { + return Promise.resolve(new Response("error", { status: 500 })); + } + return Promise.reject(new Error(`Unexpected: ${url}`)); + }), + ); + await expect( + provision(adapter, { + registryUrl, + provisionerUrl, + namespace: "ns", + verificationId: "v", + }), + ).rejects.toThrow(/Failed to get entropy/); + }); - it("throws when provisioner returns failure", async () => { - vi.stubGlobal( - "fetch", - vi.fn((input: string | URL, init?: RequestInit) => { - const url = typeof input === "string" ? input : input.toString(); - if (url === `${registryUrl}/entropy`) { - return Promise.resolve( - new Response(JSON.stringify({ token: "jwt" }), { status: 200 }) - ); - } - if (url === `${provisionerUrl}/provision`) { - return Promise.resolve( - new Response( - JSON.stringify({ - success: false, - message: "verification doesn't exist", - }), - { status: 200 } - ) - ); - } - return Promise.reject(new Error(`Unexpected: ${url}`)); - }) - ); - await expect( - provision({ cryptoAdapter: adapter, registryUrl, provisionerUrl }) - ).rejects.toThrow(/Provision failed/); - }); + it("throws when provisioner returns failure", async () => { + vi.stubGlobal( + "fetch", + vi.fn((input: string | URL, init?: RequestInit) => { + const url = typeof input === "string" ? input : input.toString(); + if (url === `${registryUrl}/entropy`) { + return Promise.resolve( + new Response(JSON.stringify({ token: "jwt" }), { status: 200 }), + ); + } + if (url === `${provisionerUrl}/provision`) { + return Promise.resolve( + new Response( + JSON.stringify({ + success: false, + message: "verification doesn't exist", + }), + { status: 200 }, + ), + ); + } + return Promise.reject(new Error(`Unexpected: ${url}`)); + }), + ); + await expect( + provision(adapter, { + registryUrl, + provisionerUrl, + namespace: "ns", + verificationId: "v", + }), + ).rejects.toThrow(/Invalid provision response/); + }); }); diff --git a/packages/wallet-sdk/src/__tests__/sync-and-sign.test.ts b/packages/wallet-sdk/src/__tests__/sync-and-sign.test.ts index e82afbfe6..9b76c58b5 100644 --- a/packages/wallet-sdk/src/__tests__/sync-and-sign.test.ts +++ b/packages/wallet-sdk/src/__tests__/sync-and-sign.test.ts @@ -1,98 +1,102 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { signPayload, syncPublicKeyToEvault } from "../sync-and-sign"; -import { TestCryptoAdapter } from "./TestCryptoAdapter"; +import { signPayload, syncPublicKeyToEvault } from "../sync-and-sign.js"; +import { TestCryptoAdapter } from "./TestCryptoAdapter.js"; + +const CONTEXT = "onboarding"; describe("signPayload", () => { - const adapter = new TestCryptoAdapter(); + const adapter = new TestCryptoAdapter(); - beforeEach(async () => { - await adapter.generateKeyPair(); - }); + beforeEach(async () => { + await adapter.ensureKey("test-key-1", CONTEXT); + }); - it("returns signature from adapter", async () => { - const sig = await signPayload({ - cryptoAdapter: adapter, - keyId: "test-key-1", - payload: "hello", - }); - expect(sig).toBe( - `sig:test-key-1:${Buffer.from("hello", "utf-8").toString("base64")}` - ); - }); + it("returns signature from adapter", async () => { + const sig = await signPayload({ + cryptoAdapter: adapter, + keyId: "test-key-1", + context: CONTEXT, + payload: "hello", + }); + const expectedB64 = btoa(String.fromCharCode(...new TextEncoder().encode("hello"))); + expect(sig).toBe(`sig:test-key-1:${expectedB64}`); + }); }); describe("syncPublicKeyToEvault", () => { - const adapter = new TestCryptoAdapter(); - const evaultUrl = "https://evault.test"; - const eName = "w3id-123"; - const token = "bearer-token"; + const adapter = new TestCryptoAdapter(); + const evaultUrl = "https://evault.test"; + const eName = "w3id-123"; + const token = "bearer-token"; - beforeEach(async () => { - await adapter.generateKeyPair(); - }); + beforeEach(async () => { + await adapter.ensureKey("test-key-1", CONTEXT); + }); - afterEach(() => { - vi.unstubAllGlobals(); - }); + afterEach(() => { + vi.unstubAllGlobals(); + }); - it("PATCHes public key with X-ENAME and Authorization", async () => { - let capturedUrl: string | null = null; - let capturedInit: RequestInit | null = null; - vi.stubGlobal( - "fetch", - vi.fn((input: string | URL, init?: RequestInit) => { - capturedUrl = typeof input === "string" ? input : input.toString(); - capturedInit = init ?? null; - return Promise.resolve( - new Response( - JSON.stringify({ success: true, message: "ok" }), - { status: 200 } - ) - ); - }) - ); + it("PATCHes public key with X-ENAME and Authorization", async () => { + let capturedUrl: string | null = null; + let capturedInit: RequestInit | null = null; + vi.stubGlobal( + "fetch", + vi.fn((input: string | URL, init?: RequestInit) => { + capturedUrl = typeof input === "string" ? input : input.toString(); + capturedInit = init ?? null; + return Promise.resolve( + new Response( + JSON.stringify({ success: true, message: "ok" }), + { status: 200 }, + ), + ); + }), + ); - await syncPublicKeyToEvault({ - evaultUrl, - eName, - cryptoAdapter: adapter, - keyId: "test-key-1", - token, - }); + await syncPublicKeyToEvault({ + evaultUrl, + eName, + cryptoAdapter: adapter, + keyId: "test-key-1", + context: CONTEXT, + token, + }); - expect(capturedUrl).toBe("https://evault.test/public-key"); - expect(capturedInit?.method).toBe("PATCH"); - expect((capturedInit?.headers as Record)["X-ENAME"]).toBe( - eName - ); - expect((capturedInit?.headers as Record)["Authorization"]).toBe( - "Bearer bearer-token" - ); - const body = JSON.parse((capturedInit?.body as string) ?? "{}"); - expect(body.publicKey).toBe("test-public-key-base64-or-multibase"); - }); + expect(capturedUrl).toBe("https://evault.test/public-key"); + expect(capturedInit?.method).toBe("PATCH"); + expect((capturedInit?.headers as Record)["X-ENAME"]).toBe( + eName, + ); + expect( + (capturedInit?.headers as Record)["Authorization"], + ).toBe("Bearer bearer-token"); + const body = JSON.parse((capturedInit?.body as string) ?? "{}"); + expect(body.publicKey).toBe("test-public-key-base64-or-multibase"); + }); - it("throws when eVault returns error", async () => { - vi.stubGlobal( - "fetch", - vi.fn(() => - Promise.resolve( - new Response( - JSON.stringify({ error: "Invalid token" }), - { status: 401 } - ) - ) - ) - ); + it("throws when eVault returns error", async () => { + vi.stubGlobal( + "fetch", + vi.fn(() => + Promise.resolve( + new Response( + JSON.stringify({ error: "Invalid token" }), + { status: 401 }, + ), + ), + ), + ); - await expect( - syncPublicKeyToEvault({ - evaultUrl, - eName, - cryptoAdapter: adapter, - keyId: "test-key-1", - token, - }) - ).rejects.toThrow(/Sync public key failed/); - }); + await expect( + syncPublicKeyToEvault({ + evaultUrl, + eName, + cryptoAdapter: adapter, + keyId: "test-key-1", + context: CONTEXT, + token, + }), + ).rejects.toThrow(/Sync public key failed/); + }); }); diff --git a/packages/wallet-sdk/src/sync-and-sign.ts b/packages/wallet-sdk/src/sync-and-sign.ts index caf1d3372..5d56733ab 100644 --- a/packages/wallet-sdk/src/sync-and-sign.ts +++ b/packages/wallet-sdk/src/sync-and-sign.ts @@ -1,15 +1,17 @@ -import type { CryptoAdapter } from "./crypto-adapter"; +import type { CryptoAdapter } from "./crypto-adapter.js"; /** Options for syncing the public key to an eVault. */ export interface SyncPublicKeyToEvaultOptions { - /** Base URL of the eVault (e.g. https://evault.example.com). */ - evaultUrl: string; - /** eName (W3ID) for the identity. */ - eName: string; - cryptoAdapter: CryptoAdapter; - keyId: string; - /** Bearer token for PATCH /public-key. Required by eVault HTTP API. */ - token: string; + /** Base URL of the eVault (e.g. https://evault.example.com). */ + evaultUrl: string; + /** eName (W3ID) for the identity. */ + eName: string; + cryptoAdapter: CryptoAdapter; + keyId: string; + /** Key context (e.g. "onboarding"). */ + context: string; + /** Bearer token for PATCH /public-key. Required by eVault HTTP API. */ + token: string; } /** @@ -17,43 +19,46 @@ export interface SyncPublicKeyToEvaultOptions { * Optionally can GET /whois first to verify the eVault; we only do PATCH here. */ export async function syncPublicKeyToEvault( - options: SyncPublicKeyToEvaultOptions + options: SyncPublicKeyToEvaultOptions, ): Promise { - const { evaultUrl, eName, cryptoAdapter, keyId, token } = options; - const base = evaultUrl.replace(/\/$/, ""); - const publicKey = await cryptoAdapter.getPublicKey(keyId); + const { evaultUrl, eName, cryptoAdapter, keyId, context, token } = options; + const base = evaultUrl.replace(/\/$/, ""); + const publicKey = await cryptoAdapter.getPublicKey(keyId, context); + if (!publicKey) { + throw new Error(`No public key for keyId=${keyId} context=${context}`); + } - const res = await fetch(`${base}/public-key`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - "X-ENAME": eName, - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ publicKey }), - }); + const res = await fetch(`${base}/public-key`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + "X-ENAME": eName, + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ publicKey }), + }); - if (!res.ok) { - const body = (await res.json()) as { error?: string }; - const msg = body?.error ?? `${res.status} ${res.statusText}`; - throw new Error(`Sync public key failed: ${msg}`); - } + if (!res.ok) { + const body = (await res.json()) as { error?: string }; + const msg = body?.error ?? `${res.status} ${res.statusText}`; + throw new Error(`Sync public key failed: ${msg}`); + } } /** Options for signing a payload. */ export interface SignPayloadOptions { - cryptoAdapter: CryptoAdapter; - keyId: string; - /** Payload string to sign (e.g. session ID or message). */ - payload: string; + cryptoAdapter: CryptoAdapter; + keyId: string; + /** Key context (e.g. "onboarding"). */ + context: string; + /** Payload string to sign (e.g. session ID or message). */ + payload: string; } /** * Signs a payload using the adapter. Returns the signature (base64 or multibase per adapter). */ -export async function signPayload( - options: SignPayloadOptions -): Promise { - const { cryptoAdapter, keyId, payload } = options; - return cryptoAdapter.sign(keyId, payload); +export async function signPayload(options: SignPayloadOptions): Promise { + const { cryptoAdapter, keyId, context, payload } = options; + return cryptoAdapter.signPayload(keyId, context, payload); } diff --git a/packages/wallet-sdk/tsconfig.build.json b/packages/wallet-sdk/tsconfig.build.json index 8d6db0f0a..25b154d42 100644 --- a/packages/wallet-sdk/tsconfig.build.json +++ b/packages/wallet-sdk/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts","src/__tests__"] } From 3e830be72674243e2b085a0cfc7c73870291aabc Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 11 Feb 2026 14:52:57 +0530 Subject: [PATCH 5/7] fix: dev sandbox build --- infrastructure/dev-sandbox/src/routes/+page.svelte | 12 ++++++++++-- .../eid-wallet/src/routes/(auth)/verify/+page.svelte | 6 +++++- packages/wallet-sdk/src/index.ts | 6 ++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/infrastructure/dev-sandbox/src/routes/+page.svelte b/infrastructure/dev-sandbox/src/routes/+page.svelte index c4181634a..f81717c62 100644 --- a/infrastructure/dev-sandbox/src/routes/+page.svelte +++ b/infrastructure/dev-sandbox/src/routes/+page.svelte @@ -1,5 +1,9 @@