From a421bb5d413ff3753e87f17f68dfdc94cbc46185 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Sun, 2 Nov 2025 09:46:54 +0000 Subject: [PATCH] fix(test-app): remove aud claim from JWT to resolve server validation error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the aud (audience) claim from JWT payloads. The server's did-jwt verification requires an audience option when aud is present, but the server isn't configured to validate it, causing "JWT audience is required but your app address has not been configured" errors. Changes: - Removed aud claim from JWT payload in generateEndorserJWT() - Updated key derivation to User Zero's specific path (m/84737769'/0'/0'/0') - Added public key verification against expected User Zero key - Enhanced JWT diagnostics logging throughout - Added alarm deduplication optimization (prevent duplicate alarms for same time) Verified: JWT validation now passes (token length 360→333 chars, no audience error). New error is API parameter validation (afterId required - separate issue). --- .../DailyNotificationScheduler.java | 51 +++++- .../test/TestNativeFetcher.java | 35 +++- .../scripts/find-user-zero-key.js | 66 +++++++ .../src/config/test-user-zero.ts | 166 +++++++++++++++--- .../src/views/HomeView.vue | 23 ++- 5 files changed, 304 insertions(+), 37 deletions(-) create mode 100644 test-apps/daily-notification-test/scripts/find-user-zero-key.js diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java index 37072f0..abdfe7a 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java @@ -36,6 +36,10 @@ public class DailyNotificationScheduler { private final AlarmManager alarmManager; private final ConcurrentHashMap scheduledAlarms; + // Track scheduled times to prevent duplicate alarms for same time + // Maps scheduledTime (ms) -> notificationId that has alarm scheduled + private final ConcurrentHashMap scheduledTimeToId; + // PendingIntent management private PendingIntentManager pendingIntentManager; @@ -55,6 +59,7 @@ public class DailyNotificationScheduler { this.context = context; this.alarmManager = alarmManager; this.scheduledAlarms = new ConcurrentHashMap<>(); + this.scheduledTimeToId = new ConcurrentHashMap<>(); this.pendingIntentManager = new PendingIntentManager(context); Log.d(TAG, "DailyNotificationScheduler initialized with PendingIntentManager"); @@ -120,6 +125,36 @@ public class DailyNotificationScheduler { Log.d(TAG, "Phase 3: Cancelling existing alarm for notification: " + content.getId()); cancelNotification(content.getId()); + // Get scheduled time and check for duplicate alarms at same time + long triggerTime = content.getScheduledTime(); + long toleranceMs = 60 * 1000; // 1 minute tolerance for DST/clock adjustments + + // Cancel any existing alarm for the same scheduled time (within tolerance) + // This prevents multiple notifications scheduled for same time from creating duplicate alarms + // Check all scheduled times to find any within tolerance + java.util.List duplicateIds = new java.util.ArrayList<>(); + for (java.util.Map.Entry entry : scheduledTimeToId.entrySet()) { + Long scheduledTime = entry.getKey(); + String existingId = entry.getValue(); + + // Skip if it's the same notification ID or time difference is too large + if (existingId.equals(content.getId()) || + Math.abs(scheduledTime - triggerTime) > toleranceMs) { + continue; + } + + // Found an alarm scheduled for a time very close to this one + duplicateIds.add(existingId); + } + + // Cancel any duplicate alarms found + for (String duplicateId : duplicateIds) { + Log.w(TAG, "Phase 3: Cancelling duplicate alarm for time " + + formatTime(triggerTime) + " (existing ID: " + duplicateId + + ", new ID: " + content.getId() + ")"); + cancelNotification(duplicateId); + } + // Create intent for the notification Intent intent = new Intent(context, DailyNotificationReceiver.class); intent.setAction(ACTION_NOTIFICATION); @@ -143,8 +178,10 @@ public class DailyNotificationScheduler { // Store the pending intent scheduledAlarms.put(content.getId(), pendingIntent); + // Track scheduled time to notification ID mapping + scheduledTimeToId.put(triggerTime, content.getId()); + // Schedule the alarm - long triggerTime = content.getScheduledTime(); boolean scheduled = scheduleAlarm(pendingIntent, triggerTime); if (scheduled) { @@ -421,6 +458,17 @@ public class DailyNotificationScheduler { pendingIntentManager.cancelAlarm(pendingIntent); pendingIntent.cancel(); Log.d(TAG, "Cancelled existing alarm for notification: " + notificationId); + + // Remove from time-to-ID mapping by finding and removing the entry + java.util.List timesToRemove = new java.util.ArrayList<>(); + for (java.util.Map.Entry entry : scheduledTimeToId.entrySet()) { + if (entry.getValue().equals(notificationId)) { + timesToRemove.add(entry.getKey()); + } + } + for (Long time : timesToRemove) { + scheduledTimeToId.remove(time); + } } else { Log.d(TAG, "No existing alarm found to cancel for notification: " + notificationId); } @@ -441,6 +489,7 @@ public class DailyNotificationScheduler { } scheduledAlarms.clear(); + scheduledTimeToId.clear(); Log.i(TAG, "All notifications cancelled"); } catch (Exception e) { diff --git a/test-apps/daily-notification-test/android/app/src/main/java/com/timesafari/dailynotification/test/TestNativeFetcher.java b/test-apps/daily-notification-test/android/app/src/main/java/com/timesafari/dailynotification/test/TestNativeFetcher.java index 32486a6..c9ba55b 100644 --- a/test-apps/daily-notification-test/android/app/src/main/java/com/timesafari/dailynotification/test/TestNativeFetcher.java +++ b/test-apps/daily-notification-test/android/app/src/main/java/com/timesafari/dailynotification/test/TestNativeFetcher.java @@ -95,8 +95,26 @@ public class TestNativeFetcher implements NativeNotificationContentFetcher { this.apiBaseUrl = apiBaseUrl; this.activeDid = activeDid; this.jwtToken = jwtToken; - Log.i(TAG, "TestNativeFetcher: Configured with API: " + apiBaseUrl + - ", ActiveDID: " + (activeDid != null ? activeDid.substring(0, Math.min(20, activeDid.length())) + "..." : "null")); + + // Enhanced logging for JWT diagnostic purposes + Log.i(TAG, "TestNativeFetcher: Configured with API: " + apiBaseUrl); + if (activeDid != null) { + Log.i(TAG, "TestNativeFetcher: ActiveDID: " + activeDid.substring(0, Math.min(30, activeDid.length())) + + (activeDid.length() > 30 ? "..." : "")); + } else { + Log.w(TAG, "TestNativeFetcher: ActiveDID is NULL"); + } + + if (jwtToken != null) { + Log.i(TAG, "TestNativeFetcher: JWT token received - Length: " + jwtToken.length() + " chars"); + // Log first and last 10 chars for verification (not full token for security) + String tokenPreview = jwtToken.length() > 20 + ? jwtToken.substring(0, 10) + "..." + jwtToken.substring(jwtToken.length() - 10) + : jwtToken.substring(0, Math.min(jwtToken.length(), 20)) + "..."; + Log.d(TAG, "TestNativeFetcher: JWT preview: " + tokenPreview); + } else { + Log.e(TAG, "TestNativeFetcher: JWT token is NULL - API calls will fail"); + } } @Override @@ -141,6 +159,19 @@ public class TestNativeFetcher implements NativeNotificationContentFetcher { connection.setReadTimeout(READ_TIMEOUT_MS); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/json"); + + // Diagnostic logging for JWT usage + if (jwtToken != null) { + String jwtPreview = jwtToken.length() > 20 + ? jwtToken.substring(0, 10) + "..." + jwtToken.substring(jwtToken.length() - 10) + : jwtToken; + Log.d(TAG, "TestNativeFetcher: Using JWT for API call - Length: " + jwtToken.length() + + ", Preview: " + jwtPreview + ", ActiveDID: " + + (activeDid != null ? activeDid.substring(0, Math.min(30, activeDid.length())) + "..." : "null")); + } else { + Log.e(TAG, "TestNativeFetcher: JWT token is NULL when making API call!"); + } + connection.setRequestProperty("Authorization", "Bearer " + jwtToken); connection.setDoOutput(true); diff --git a/test-apps/daily-notification-test/scripts/find-user-zero-key.js b/test-apps/daily-notification-test/scripts/find-user-zero-key.js new file mode 100644 index 0000000..7cfca39 --- /dev/null +++ b/test-apps/daily-notification-test/scripts/find-user-zero-key.js @@ -0,0 +1,66 @@ +/** + * Script to find the correct derivation path for User Zero's DID + * + * User Zero DID: did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F + * Target address: 0x0000694B58C2cC69658993A90D3840C560f2F51F + */ + +import { HDNodeWallet, Mnemonic } from 'ethers'; + +const seedPhrase = "rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage"; +const targetAddress = "0x0000694B58C2cC69658993A90D3840C560f2F51F".toLowerCase(); + +console.log('Target address:', targetAddress); +console.log('Searching for matching derivation path...\n'); + +const mnemonic = Mnemonic.fromPhrase(seedPhrase); +let found = false; + +// Try standard Ethereum derivation paths +for (let account = 0; account < 50; account++) { + try { + const path = `m/44'/60'/0'/0/${account}`; + const wallet = HDNodeWallet.fromPhrase(mnemonic.phrase, path); + const address = wallet.address.toLowerCase(); + + if (address === targetAddress) { + console.log(`✅✅✅ MATCH FOUND: ${path}`); + console.log('Address:', wallet.address); + console.log('Private key (hex, no 0x prefix):', wallet.privateKey.slice(2)); + console.log('\nUse this path in generateEndorserJWT():'); + console.log(`const wallet = HDNodeWallet.fromPhrase(mnemonic.phrase, "${path}");`); + found = true; + break; + } + + if (account < 10) { + console.log(`Path ${path}: ${address}`); + } + } catch (e) { + console.error(`Error with path m/44'/60'/0'/0/${account}:`, e.message); + } +} + +// Also try default path (no account index specified) +if (!found) { + try { + const wallet = HDNodeWallet.fromMnemonic(mnemonic); + const address = wallet.address.toLowerCase(); + console.log(`\nDefault path (fromMnemonic): ${address}`); + if (address === targetAddress) { + console.log('✅✅✅ MATCH FOUND: Default path (fromMnemonic)'); + found = true; + } + } catch (e) { + console.error('Error with default path:', e.message); + } +} + +if (!found) { + console.log('\n❌ No match found in first 50 account indices'); + console.log('Possible causes:'); + console.log('1. Different derivation path (not m/44\'/60\'/0\'/0/X)'); + console.log('2. Wrong seed phrase'); + console.log('3. User Zero registered with different key source'); +} + diff --git a/test-apps/daily-notification-test/src/config/test-user-zero.ts b/test-apps/daily-notification-test/src/config/test-user-zero.ts index 244943e..80e302b 100644 --- a/test-apps/daily-notification-test/src/config/test-user-zero.ts +++ b/test-apps/daily-notification-test/src/config/test-user-zero.ts @@ -11,8 +11,8 @@ // Lazy import logger to avoid ES module issues when loaded by Capacitor CLI (CommonJS) // Logger is only used inside functions, not at module scope -let logger: { error: (...args: unknown[]) => void; custom: (emoji: string, ...args: unknown[]) => void } | null = null; -const getLogger = async (): Promise<{ error: (...args: unknown[]) => void; custom: (emoji: string, ...args: unknown[]) => void } | null> => { +let logger: { error: (...args: unknown[]) => void; custom: (emoji: string, ...args: unknown[]) => void; info: (...args: unknown[]) => void } | null = null; +const getLogger = async (): Promise<{ error: (...args: unknown[]) => void; custom: (emoji: string, ...args: unknown[]) => void; info: (...args: unknown[]) => void } | null> => { if (!logger) { const loggerModule = await import('../lib/logger'); logger = loggerModule.logger; @@ -25,7 +25,14 @@ export const TEST_USER_ZERO_CONFIG = { identity: { did: "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F", name: "User Zero", - seedPhrase: "rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage" + seedPhrase: "rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage", + // User Zero's known public key for verification + // Public Key (hex): 03b231e73413f1d36b3327763ad9a44bb81fc91480a14977ab3cfd32df0564af1d + // Public Key (base64): A7Ix5zQT8dNrMyd2OtmkS7gfyRSAoUl3qzz9Mt8FZK8d + // Derivation Path: m/84737769'/0'/0'/0' + expectedPublicKeyHex: "03b231e73413f1d36b3327763ad9a44bb81fc91480a14977ab3cfd32df0564af1d", + expectedPublicKeyBase64: "A7Ix5zQT8dNrMyd2OtmkS7gfyRSAoUl3qzz9Mt8FZK8d", + derivationPath: "m/84737769'/0'/0'/0'" }, // API Configuration @@ -226,10 +233,15 @@ export const MOCK_STARRED_PROJECTS_RESPONSE = { /** * Generate ES256K signed JWT token for User Zero using DID-based signing * - * This function mimics TimeSafari's createEndorserJwtForKey() function, - * using did-jwt library with ES256K algorithm (DID-based signing). + * This function generates a JWT token signed with User Zero's private key, + * using User Zero's DID and seed phrase from TEST_USER_ZERO_CONFIG. * - * @returns Promise ES256K signed JWT token + * Uses TimeSafari's approach (mimics createEndorserJwtForKey() function): + * - did-jwt library with ES256K algorithm (DID-based signing) + * - Ethereum private key derived from User Zero's seed phrase + * - JWT payload includes User Zero's DID as issuer and subject + * + * @returns Promise ES256K signed JWT token for User Zero */ export async function generateEndorserJWT(): Promise { try { @@ -237,44 +249,148 @@ export async function generateEndorserJWT(): Promise { const { createJWT, SimpleSigner } = await import('did-jwt'); const { HDNodeWallet, Mnemonic } = await import('ethers'); - // Derive Ethereum private key from seed phrase - // Using the same derivation as TimeSafari (m/44'/60'/0'/0/0 - Ethereum standard) - const mnemonic = Mnemonic.fromPhrase(TEST_USER_ZERO_CONFIG.identity.seedPhrase); - const wallet = HDNodeWallet.fromMnemonic(mnemonic); + // Derive Ethereum private key from User Zero's seed phrase + // Using User Zero's specific derivation path: m/84737769'/0'/0'/0' + // This is the path used when User Zero was registered on the server + // NOTE: Must use fromMnemonic() with the Mnemonic object, not fromPhrase() with phrase string + const userZeroSeedPhrase = TEST_USER_ZERO_CONFIG.identity.seedPhrase; + const mnemonic = Mnemonic.fromPhrase(userZeroSeedPhrase); + const derivationPath = "m/84737769'/0'/0'/0'"; + const wallet = HDNodeWallet.fromMnemonic(mnemonic, derivationPath); const privateKeyHex = wallet.privateKey.slice(2); // Remove '0x' prefix - // Create signer for ES256K (Ethereum secp256k1 curve) + // Extract public key from wallet in compressed format + // ethers.js wallet.signingKey.compressedPublicKey provides the compressed format + // Compressed format: 33 bytes (0x02 or 0x03 prefix + 32-byte x-coordinate) + const signingKey = wallet.signingKey; + const compressedPublicKey = signingKey.compressedPublicKey; // This should be the compressed public key + const publicKeyHexRaw = compressedPublicKey ? compressedPublicKey.slice(2).toLowerCase() : null; + + // Fallback: if compressed public key not available, use first 66 hex chars from uncompressed + // Note: This is a workaround - we should have compressedPublicKey from signingKey + const publicKeyHexFinal = publicKeyHexRaw || wallet.publicKey.slice(2).substring(0, 66).toLowerCase(); + + // Verify derived address matches User Zero's DID + const derivedAddress = wallet.address.toLowerCase(); + const expectedAddress = TEST_USER_ZERO_CONFIG.identity.did.replace('did:ethr:', '').toLowerCase(); + const logger = await getLogger(); + + // Verify public key matches expected (if provided) + const expectedPublicKeyHex = TEST_USER_ZERO_CONFIG.identity.expectedPublicKeyHex; + + // Comprehensive verification logging + if (derivedAddress !== expectedAddress) { + logger.error('⚠️ Key derivation address mismatch!', { + derivedAddress: derivedAddress, + expectedAddress: expectedAddress, + derivationPath: derivationPath, + note: 'JWT signing may fail if server expects different key' + }); + logger.error('⚠️ CRITICAL: Derived address does not match User Zero DID!', { + derived: derivedAddress, + expected: expectedAddress, + path: derivationPath, + warning: 'JWT verification will likely fail on server' + }); + } else { + logger.custom('✅', 'Key derivation verified - address matches User Zero DID'); + } + + // Verify public key if expected value is provided + if (expectedPublicKeyHex && publicKeyHexFinal) { + // Compare the derived public key with expected (both should be compressed format) + const publicKeyMatchesActual = publicKeyHexFinal.toLowerCase() === expectedPublicKeyHex.toLowerCase(); + + if (!publicKeyMatchesActual) { + logger.error('⚠️ CRITICAL: Derived public key does not match expected!', { + derivedPublicKey: publicKeyHexFinal, + expectedPublicKey: expectedPublicKeyHex, + derivationPath: derivationPath, + warning: 'Server will not recognize this public key - JWT verification will fail' + }); + logger.error('⚠️ Public Key Mismatch Details:', { + derived: publicKeyHexFinal.substring(0, 10) + '...' + publicKeyHexFinal.substring(publicKeyHexFinal.length - 10), + expected: expectedPublicKeyHex.substring(0, 10) + '...' + expectedPublicKeyHex.substring(expectedPublicKeyHex.length - 10), + derivedLength: publicKeyHexFinal.length, + expectedLength: expectedPublicKeyHex.length, + derivedPrefix: publicKeyHexFinal.substring(0, 2), // Should be 02 or 03 + expectedPrefix: expectedPublicKeyHex.substring(0, 2), + note: 'Verify derivation path and seed phrase are correct' + }); + } else { + logger.custom('✅', 'Public key verified - matches expected User Zero public key'); + logger.info('✅ Public Key Verification:', { + derived: publicKeyHexFinal.substring(0, 20) + '...' + publicKeyHexFinal.substring(publicKeyHexFinal.length - 20), + expected: expectedPublicKeyHex.substring(0, 20) + '...' + expectedPublicKeyHex.substring(expectedPublicKeyHex.length - 20), + match: true + }); + } + } + + // Log comprehensive key derivation diagnostics + logger.info('🔐 JWT Key Derivation: Complete diagnostics', { + address: derivedAddress, + addressMatch: derivedAddress === expectedAddress, + derivationPath: derivationPath, + publicKeyHex: publicKeyHexFinal ? (publicKeyHexFinal.substring(0, 20) + '...' + publicKeyHexFinal.substring(publicKeyHexFinal.length - 20)) : 'N/A', + expectedPublicKeyHex: expectedPublicKeyHex ? (expectedPublicKeyHex.substring(0, 20) + '...' + expectedPublicKeyHex.substring(expectedPublicKeyHex.length - 20)) : 'N/A', + publicKeyMatch: expectedPublicKeyHex && publicKeyHexFinal ? (publicKeyHexFinal.toLowerCase() === expectedPublicKeyHex.toLowerCase()) : null, + publicKeyLength: publicKeyHexFinal?.length || 0, + expectedPublicKeyLength: expectedPublicKeyHex?.length || 0, + hasCompressedPublicKey: !!compressedPublicKey + }); + + // Create signer for ES256K (Ethereum secp256k1 curve) using User Zero's private key const signer = SimpleSigner(privateKeyHex); - // Create JWT payload with standard claims + // Create JWT payload with User Zero's DID as issuer and subject const nowEpoch = Math.floor(Date.now() / 1000); const expiresIn = TEST_USER_ZERO_CONFIG.api.jwtExpirationMinutes * 60; + const userZeroDid = TEST_USER_ZERO_CONFIG.identity.did; const payload = { - // Standard JWT claims + // Standard JWT claims - all using User Zero's DID iat: nowEpoch, exp: nowEpoch + expiresIn, - iss: TEST_USER_ZERO_CONFIG.identity.did, - sub: TEST_USER_ZERO_CONFIG.identity.did, - // Additional claims that endorser-ch might expect - aud: "endorser-ch" + iss: userZeroDid, // User Zero's DID as issuer + sub: userZeroDid, // User Zero's DID as subject + // NOTE: aud (audience) claim is omitted because server's did-jwt verifyJWT() + // requires audience option when aud is present, but server isn't configured + // to validate it. Server will reject JWTs with aud claim until it adds support. }; - // Create ES256K signed JWT (ES256K is the default algorithm for did-jwt) + // Create ES256K signed JWT using User Zero's DID and private key + // ES256K is the default algorithm for did-jwt + // NOTE: sub claim is included in payload; aud is omitted because server's + // did-jwt verifyJWT() requires audience option when aud is present, but + // server isn't configured to validate it const jwt = await createJWT(payload, { - issuer: TEST_USER_ZERO_CONFIG.identity.did, - signer: signer, + issuer: userZeroDid, // User Zero's DID (overwrites payload.iss) + signer: signer, // User Zero's private key signer expiresIn: expiresIn }); - const log = await getLogger(); - log.custom("🔐", "JWT generated - Algorithm: ES256K, DID:", TEST_USER_ZERO_CONFIG.identity.did.substring(0, 30) + "..."); - log.custom("🔐", "JWT length:", jwt.length, "characters"); + logger.custom("🔐", "JWT generated for User Zero - Algorithm: ES256K, DID:", userZeroDid.substring(0, 30) + "..."); + logger.custom("🔐", "JWT length:", jwt.length, "characters"); + + // Enhanced diagnostic logging + logger.info('🔐 JWT Generation Diagnostics:', { + algorithm: 'ES256K', + issuerDid: userZeroDid, + issuerAddress: userZeroDid.replace('did:ethr:', '').toLowerCase(), + derivedAddress: derivedAddress, + derivationPath: derivationPath, + addressMatch: derivedAddress === expectedAddress, + jwtLength: jwt.length, + jwtPreview: jwt.substring(0, 20) + '...' + jwt.substring(jwt.length - 20), + expiresIn: expiresIn, + nowEpoch: nowEpoch + }); return jwt; } catch (error) { - const log = await getLogger(); - log.error('Failed to generate ES256K JWT:', error); + const logger = await getLogger(); + logger.error('Failed to generate ES256K JWT:', error); throw new Error(`JWT generation failed: ${error instanceof Error ? error.message : String(error)}`); } } diff --git a/test-apps/daily-notification-test/src/views/HomeView.vue b/test-apps/daily-notification-test/src/views/HomeView.vue index bd2718c..72b1550 100644 --- a/test-apps/daily-notification-test/src/views/HomeView.vue +++ b/test-apps/daily-notification-test/src/views/HomeView.vue @@ -450,7 +450,9 @@ const configureNativeFetcher = async (): Promise => { try { console.log('🚀 HomeView: Starting native fetcher configuration...') - logger.info('Configuring native fetcher from HomeView...') + console.log('👤 HomeView: Using User Zero identity:', TEST_USER_ZERO_CONFIG.identity.name) + console.log('🔑 HomeView: User Zero DID:', TEST_USER_ZERO_CONFIG.identity.did) + logger.info('Configuring native fetcher from HomeView using User Zero identity...') // Get API server URL const apiBaseUrl = TEST_USER_ZERO_CONFIG.getApiServerUrl() @@ -465,22 +467,25 @@ const configureNativeFetcher = async (): Promise => { return } - console.log('🔧 HomeView: Generating ES256K JWT token...') - // Generate JWT token for authentication + console.log('🔧 HomeView: Generating ES256K JWT token for User Zero...') + // Generate JWT token for authentication using User Zero's DID and seed phrase + // This uses TEST_USER_ZERO_CONFIG.identity.did and TEST_USER_ZERO_CONFIG.identity.seedPhrase const jwtToken = await generateEndorserJWT() - console.log('✅ HomeView: JWT token generated, length:', jwtToken.length) + console.log('✅ HomeView: JWT token generated for User Zero, length:', jwtToken.length) - console.log('🔧 HomeView: Calling configureNativeFetcher with:', { + console.log('🔧 HomeView: Configuring native fetcher with User Zero credentials:', { apiBaseUrl, - activeDid: TEST_USER_ZERO_CONFIG.identity.did.substring(0, 30) + '...', + activeDid: TEST_USER_ZERO_CONFIG.identity.did, + userZeroName: TEST_USER_ZERO_CONFIG.identity.name, jwtTokenLength: jwtToken.length }) - // Configure native fetcher with credentials + // Configure native fetcher with User Zero's credentials + // This passes User Zero's DID and JWT token (signed with User Zero's private key) await DailyNotification.configureNativeFetcher({ apiBaseUrl: apiBaseUrl, - activeDid: TEST_USER_ZERO_CONFIG.identity.did, - jwtToken: jwtToken + activeDid: TEST_USER_ZERO_CONFIG.identity.did, // User Zero's DID + jwtToken: jwtToken // JWT signed with User Zero's private key derived from seed phrase }) console.log('✅ HomeView: Native fetcher configured successfully!')