From 376feecb280c28504788c9677c6cb3cc455f00b6 Mon Sep 17 00:00:00 2001 From: Pawel Zelawski Date: Sat, 23 May 2026 10:33:53 +0200 Subject: chore: upgrade digiid-ts to v3 and stabilize dev/build scripts --- src/client/App.tsx | 103 +++++++++++++---- src/client/main.tsx | 2 +- src/client/utils.ts | 19 +++- src/server/main.ts | 316 +++++++++++++++++++++++++++++----------------------- src/server/utils.ts | 10 +- 5 files changed, 280 insertions(+), 170 deletions(-) (limited to 'src') diff --git a/src/client/App.tsx b/src/client/App.tsx index b3f0308..ad1e230 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -7,7 +7,7 @@ type UiState = 'initial' | 'waiting' | 'success' | 'failed'; // Define the structure for result data (success or failure) interface ResultData { address?: string; // Present on success - error?: string; // Present on failure + error?: string; // Present on failure addressType?: string; // Added later } @@ -30,7 +30,7 @@ function App() { } console.log(`Starting status polling for session: ${sessionId}`); - let intervalId: any = null; // Use 'any' to avoid browser/node type conflict for setInterval return type + let intervalId: ReturnType | null = null; const checkStatus = async () => { if (!sessionId) return; // Should not happen here, but type guard @@ -41,19 +41,26 @@ function App() { if (!response.ok) { // Handle specific errors like 404 (session not found/expired) if (response.status === 404) { - console.warn(`Session ${sessionId} not found or expired during polling.`); + console.warn( + `Session ${sessionId} not found or expired during polling.` + ); setError('Session expired or could not be found.'); setUiState('failed'); // Transition to failed state setResultData({ error: 'Session expired or could not be found.' }); } else { - const errorData = await response.json().catch(() => ({ message: 'Error fetching status' })); - throw new Error(errorData.message || `Server responded with ${response.status}`); + const errorData = await response + .json() + .catch(() => ({ message: 'Error fetching status' })); + throw new Error( + errorData.message || `Server responded with ${response.status}` + ); } if (intervalId) clearInterval(intervalId); // Stop polling on error return; } - const data: { status: UiState, address?: string, error?: string } = await response.json(); + const data: { status: UiState; address?: string; error?: string } = + await response.json(); console.log('Received status data:', data); // If status changed from pending, update UI and stop polling @@ -67,10 +74,12 @@ function App() { if (intervalId) clearInterval(intervalId); } // If status is still 'pending', the interval will continue - } catch (err) { console.error('Error polling status:', err); - const message = err instanceof Error ? err.message : 'An unknown error occurred during status check'; + const message = + err instanceof Error + ? err.message + : 'An unknown error occurred during status check'; setError(`Status polling failed: ${message}`); // Decide if we should stop polling or transition state on generic fetch error // For now, let's stop polling and show error, moving to failed state @@ -104,7 +113,11 @@ function App() { try { const response = await fetch('/api/digiid/start'); if (!response.ok) { - const errorData = await response.json().catch(() => ({ message: 'Failed to start session. Server responded with status: ' + response.status })); + const errorData = await response.json().catch(() => ({ + message: + 'Failed to start session. Server responded with status: ' + + response.status, + })); throw new Error(errorData.message || 'Failed to start session'); } const data = await response.json(); @@ -114,7 +127,8 @@ function App() { setUiState('waiting'); // Move to waiting state } catch (err) { console.error('Error starting Digi-ID session:', err); - const message = err instanceof Error ? err.message : 'An unknown error occurred'; + const message = + err instanceof Error ? err.message : 'An unknown error occurred'; setError(`Failed to initiate Digi-ID: ${message}`); setUiState('initial'); // Stay in initial state on error } finally { @@ -140,9 +154,20 @@ function App() { {/* --- Views based on uiState --- */} {uiState === 'initial' && (
-
)} @@ -152,7 +177,10 @@ function App() { {uiState === 'waiting' && qrCodeDataUrl && (

Scan the QR Code

-

Scan the QR code below using your Digi-ID compatible mobile wallet.

+

+ Scan the QR code below using your Digi-ID compatible mobile + wallet. +

Digi-ID QR Code

Waiting for authentication...

@@ -161,14 +189,30 @@ function App() { {uiState === 'success' && resultData?.address && (
- - - + + +

Authentication Successful!

Verified Address:

{resultData.address}

-

Address Format: {getDigiByteAddressType(resultData.address)}

+

+ Address Format: {getDigiByteAddressType(resultData.address)} +

)} @@ -177,7 +221,8 @@ function App() {

Authentication Failed

- Reason: {resultData?.error || error || 'An unknown error occurred.'} + Reason:{' '} + {resultData?.error || error || 'An unknown error occurred.'}

@@ -188,14 +233,26 @@ function App() { {/* --- Description section (Always visible below the view container) --- */}

- This application demonstrates integrating Digi-ID authentication using the digiid-ts library. + This application demonstrates integrating Digi-ID authentication using + the{' '} + + digiid-ts + {' '} + library.

- Upon successful verification, the system can identify the following DigiByte address formats: + Upon successful verification, the system can identify the following + DigiByte address formats:

  • Legacy Addresses (P2PKH) - starting with 'D'
  • -
  • Pay-to-Script-Hash Addresses (P2SH) - commonly starting with 'S'
  • +
  • + Pay-to-Script-Hash Addresses (P2SH) - commonly starting with 'S' +
  • Segregated Witness (SegWit) Addresses - starting with 'dgb1'
@@ -203,4 +260,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/client/main.tsx b/src/client/main.tsx index b895c80..0896e5e 100644 --- a/src/client/main.tsx +++ b/src/client/main.tsx @@ -7,4 +7,4 @@ ReactDOM.createRoot(document.getElementById('root')!).render( -); \ No newline at end of file +); diff --git a/src/client/utils.ts b/src/client/utils.ts index 6a95f7b..1677749 100644 --- a/src/client/utils.ts +++ b/src/client/utils.ts @@ -1,6 +1,10 @@ // Utility to determine DigiByte address type based on prefix -export type DigiByteAddressFormat = 'Legacy (P2PKH)' | 'Script (P2SH)' | 'SegWit (Bech32)' | 'Unknown'; +export type DigiByteAddressFormat = + | 'Legacy (P2PKH)' + | 'Script (P2SH)' + | 'SegWit (Bech32)' + | 'Unknown'; /** * Determines the format of a DigiByte address based on its prefix. @@ -11,21 +15,24 @@ export type DigiByteAddressFormat = 'Legacy (P2PKH)' | 'Script (P2SH)' | 'SegWit * @param address The DigiByte address string. * @returns The determined address format. */ -export function getDigiByteAddressType(address: string | undefined | null): DigiByteAddressFormat { +export function getDigiByteAddressType( + address: string | undefined | null +): DigiByteAddressFormat { if (!address) { return 'Unknown'; } if (address.startsWith('dgb1')) { return 'SegWit (Bech32)'; } - if (address.startsWith('S')) { // Common prefix for P2SH on DGB - return 'Script (P2SH)'; + if (address.startsWith('S')) { + // Common prefix for P2SH on DGB + return 'Script (P2SH)'; } if (address.startsWith('D')) { - return 'Legacy (P2PKH)'; + return 'Legacy (P2PKH)'; } // If it doesn't match known prefixes, return Unknown // Could potentially add DigiAsset checks here if they have distinct prefixes return 'Unknown'; -} \ No newline at end of file +} diff --git a/src/server/main.ts b/src/server/main.ts index 96afb0e..1ab9004 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -28,157 +28,199 @@ console.log(`Attempting to listen on port: ${PORT}`); console.log(`Configured PUBLIC_URL: ${process.env.PUBLIC_URL}`); // Endpoint to initiate the DigiID authentication flow -app.get('/api/digiid/start', (req: Request, res: Response, next: NextFunction) => { - (async () => { - try { - const sessionId = randomBytes(16).toString('hex'); - const nonce = randomBytes(16).toString('hex'); - - const publicUrl = process.env.PUBLIC_URL; - if (!publicUrl) { - console.error('PUBLIC_URL environment variable is not set.'); - return res.status(500).json({ error: 'Server configuration error: PUBLIC_URL is missing.' }); - } - - let callbackUrl: string; +app.get( + '/api/digiid/start', + (req: Request, res: Response, next: NextFunction) => { + (async () => { try { - const baseUrl = new URL(publicUrl); - callbackUrl = new URL('/api/digiid/callback', baseUrl).toString(); + const sessionId = randomBytes(16).toString('hex'); + const nonce = randomBytes(16).toString('hex'); + + const publicUrl = process.env.PUBLIC_URL; + if (!publicUrl) { + console.error('PUBLIC_URL environment variable is not set.'); + return res.status(500).json({ + error: 'Server configuration error: PUBLIC_URL is missing.', + }); + } + + let callbackUrl: string; + try { + const baseUrl = new URL(publicUrl); + callbackUrl = new URL('/api/digiid/callback', baseUrl).toString(); + } catch (error) { + console.error('Invalid PUBLIC_URL format:', publicUrl, error); + return res.status(500).json({ + error: 'Server configuration error: Invalid PUBLIC_URL format.', + }); + } + + const unsecure = callbackUrl.startsWith('http://'); + // Use the actual function from digiid-ts + const digiIdUri = generateDigiIDUri({ callbackUrl, unsecure, nonce }); + console.log( + `Generated DigiID URI: ${digiIdUri} for session ${sessionId}` + ); + + sessionStore.set(sessionId, { nonce, status: 'pending' }); + nonceToSessionMap.set(nonce, sessionId); + console.log(`Stored pending session: ${sessionId}, nonce: ${nonce}`); + + const qrCodeDataUrl = await qrcode.toDataURL(digiIdUri); + res.json({ sessionId, qrCodeDataUrl }); } catch (error) { - console.error('Invalid PUBLIC_URL format:', publicUrl, error); - return res.status(500).json({ error: 'Server configuration error: Invalid PUBLIC_URL format.' }); + console.error('Error in /api/digiid/start:', error); + // Ensure response is sent even on error + if (!res.headersSent) { + // Check if response hasn't already been sent + if (error instanceof Error) { + res + .status(400) + .json({ error: `Failed to generate URI: ${error.message}` }); + } else { + res + .status(500) + .json({ error: 'Internal server error during start' }); + } + } } + })().catch(next); + } +); - const unsecure = callbackUrl.startsWith('http://'); - // Use the actual function from digiid-ts - const digiIdUri = generateDigiIDUri({ callbackUrl, unsecure, nonce }); - console.log(`Generated DigiID URI: ${digiIdUri} for session ${sessionId}`); +// Callback endpoint for the DigiID mobile app +app.post( + '/api/digiid/callback', + (req: Request, res: Response, next: NextFunction) => { + (async () => { + const { address, uri, signature } = req.body; + + // Basic validation of received data + if (!address || !uri || !signature) { + console.warn('Callback missing required fields.', { + address, + uri, + signature, + }); + // Wallet doesn't expect a body on failure, just non-200. Status only for logging/debug. + return res.status(400).send('Missing required callback parameters.'); + } - sessionStore.set(sessionId, { nonce, status: 'pending' }); - nonceToSessionMap.set(nonce, sessionId); - console.log(`Stored pending session: ${sessionId}, nonce: ${nonce}`); + const callbackData = { address, uri, signature }; + console.log('Received callback:', callbackData); - const qrCodeDataUrl = await qrcode.toDataURL(digiIdUri); - res.json({ sessionId, qrCodeDataUrl }); + // --- Nonce Extraction and Session Lookup --- + let receivedNonce: string | null = null; + try { + // DigiID URIs need scheme replaced for standard URL parsing + const parsableUri = uri.replace(/^digiid:/, 'http:'); + const parsedUri = new URL(parsableUri); + receivedNonce = parsedUri.searchParams.get('x'); + } catch (error) { + console.warn('Error parsing received URI:', uri, error); + return res.status(400).send('Invalid URI format.'); + } - } catch (error) { - console.error('Error in /api/digiid/start:', error); - // Ensure response is sent even on error - if (!res.headersSent) { // Check if response hasn't already been sent - if (error instanceof Error) { - res.status(400).json({ error: `Failed to generate URI: ${error.message}` }); - } else { - res.status(500).json({ error: 'Internal server error during start' }); - } + if (!receivedNonce) { + console.warn('Nonce (x parameter) not found in received URI:', uri); + return res.status(400).send('Nonce not found in URI.'); } - } - })().catch(next); -}); -// Callback endpoint for the DigiID mobile app -app.post('/api/digiid/callback', (req: Request, res: Response, next: NextFunction) => { - (async () => { - const { address, uri, signature } = req.body; - - // Basic validation of received data - if (!address || !uri || !signature) { - console.warn('Callback missing required fields.', { address, uri, signature }); - // Wallet doesn't expect a body on failure, just non-200. Status only for logging/debug. - return res.status(400).send('Missing required callback parameters.'); - } - - const callbackData = { address, uri, signature }; - console.log('Received callback:', callbackData); - - // --- Nonce Extraction and Session Lookup --- - let receivedNonce: string | null = null; - try { - // DigiID URIs need scheme replaced for standard URL parsing - const parsableUri = uri.replace(/^digiid:/, 'http:'); - const parsedUri = new URL(parsableUri); - receivedNonce = parsedUri.searchParams.get('x'); - } catch (error) { - console.warn('Error parsing received URI:', uri, error); - return res.status(400).send('Invalid URI format.'); - } - - if (!receivedNonce) { - console.warn('Nonce (x parameter) not found in received URI:', uri); - return res.status(400).send('Nonce not found in URI.'); - } - - const sessionId = nonceToSessionMap.get(receivedNonce); - if (!sessionId) { - console.warn('Session not found for received nonce:', receivedNonce); - // Nonce might be expired or invalid - return res.status(404).send('Session not found or expired for this nonce.'); - } - - // Retrieve the session *before* the try/finally block for verification - const session = sessionStore.get(sessionId); - if (!session) { - console.error(`Critical: Session data missing for ${sessionId} despite nonce match.`); - if (!res.headersSent) res.status(500).send('Internal server error: Session data missing.'); + const sessionId = nonceToSessionMap.get(receivedNonce); + if (!sessionId) { + console.warn('Session not found for received nonce:', receivedNonce); + // Nonce might be expired or invalid + return res + .status(404) + .send('Session not found or expired for this nonce.'); + } + + // Retrieve the session *before* the try/finally block for verification + const session = sessionStore.get(sessionId); + if (!session) { + console.error( + `Critical: Session data missing for ${sessionId} despite nonce match.` + ); + if (!res.headersSent) + res.status(500).send('Internal server error: Session data missing.'); return; // Explicitly return void - } - if (session.status !== 'pending') { + } + if (session.status !== 'pending') { console.warn('Session already processed:', sessionId, session.status); - if (!res.headersSent) res.status(200).send('Session already processed.'); + if (!res.headersSent) + res.status(200).send('Session already processed.'); + return; // Explicitly return void + } + + // --- Verification --- + let expectedCallbackUrl: string; + try { + const publicUrl = process.env.PUBLIC_URL; + if (!publicUrl) + throw new Error( + 'PUBLIC_URL environment variable is not configured on the server.' + ); + expectedCallbackUrl = new URL( + '/api/digiid/callback', + publicUrl + ).toString(); + } catch (error) { + console.error( + 'Server configuration error constructing expected callback URL:', + error + ); + session.status = 'failed'; + session.error = 'Server configuration error preventing verification.'; + sessionStore.set(sessionId, session); + // Respond 200 OK as per protocol + if (!res.headersSent) res.status(200).send(); return; // Explicitly return void - } - - // --- Verification --- - let expectedCallbackUrl: string; - try { - const publicUrl = process.env.PUBLIC_URL; - if (!publicUrl) throw new Error('PUBLIC_URL environment variable is not configured on the server.'); - expectedCallbackUrl = new URL('/api/digiid/callback', publicUrl).toString(); - } catch (error) { - console.error('Server configuration error constructing expected callback URL:', error); - session.status = 'failed'; - session.error = 'Server configuration error preventing verification.'; - sessionStore.set(sessionId, session); - // Respond 200 OK as per protocol - if (!res.headersSent) res.status(200).send(); - return; // Explicitly return void - } - - const verifyOptions = { expectedCallbackUrl, expectedNonce: session.nonce }; - - try { - console.log('Attempting to verify callback with:', { callbackData, verifyOptions }); - await verifyDigiIDCallback(callbackData, verifyOptions); - - // Success case (no throw from verifyDigiIDCallback) - console.log(`Verification successful for session ${sessionId}, address: ${address}`); - session.status = 'success'; - session.address = address; // Store the verified address - session.error = undefined; // Clear any previous error - nonceToSessionMap.delete(session.nonce); // Clean up nonce map only on success - - } catch (error) { - // Failure case (verifyDigiIDCallback threw an error) - console.warn(`Verification failed for session ${sessionId}:`, error); - session.status = 'failed'; - if (error instanceof Error) { - session.error = error.message; - } else { - session.error = 'An unknown verification error occurred.'; } - // Optionally cleanup nonce map on failure too, depending on policy - // nonceToSessionMap.delete(session.nonce); - } finally { - // Update store and respond 200 OK in all cases (after try/catch) - sessionStore.set(sessionId, session); - console.log(`Final session state for ${sessionId}:`, session); - // Ensure response is sent if not already done (e.g. in case of unexpected error before finally) - if (!res.headersSent) { + + const verifyOptions = { + expectedCallbackUrl, + expectedNonce: session.nonce, + }; + + try { + console.log('Attempting to verify callback with:', { + callbackData, + verifyOptions, + }); + await verifyDigiIDCallback(callbackData, verifyOptions); + + // Success case (no throw from verifyDigiIDCallback) + console.log( + `Verification successful for session ${sessionId}, address: ${address}` + ); + session.status = 'success'; + session.address = address; // Store the verified address + session.error = undefined; // Clear any previous error + nonceToSessionMap.delete(session.nonce); // Clean up nonce map only on success + } catch (error) { + // Failure case (verifyDigiIDCallback threw an error) + console.warn(`Verification failed for session ${sessionId}:`, error); + session.status = 'failed'; + if (error instanceof Error) { + session.error = error.message; + } else { + session.error = 'An unknown verification error occurred.'; + } + // Optionally cleanup nonce map on failure too, depending on policy + // nonceToSessionMap.delete(session.nonce); + } finally { + // Update store and respond 200 OK in all cases (after try/catch) + sessionStore.set(sessionId, session); + console.log(`Final session state for ${sessionId}:`, session); + // Ensure response is sent if not already done (e.g. in case of unexpected error before finally) + if (!res.headersSent) { res.status(200).send(); + } + // No explicit return needed here as it's the end of the function } - // No explicit return needed here as it's the end of the function - } - })().catch(next); -}); + })().catch(next); + } +); // Endpoint to check the status of an authentication session app.get('/api/digiid/status/:sessionId', (req: Request, res: Response) => { @@ -200,4 +242,4 @@ app.get('/', (_: Request, res: Response) => { // Start the server app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); -}); \ No newline at end of file +}); diff --git a/src/server/utils.ts b/src/server/utils.ts index 0b1333f..bb614c0 100644 --- a/src/server/utils.ts +++ b/src/server/utils.ts @@ -1,6 +1,9 @@ // Simple utility to determine DigiByte address type based on prefix -export type DigiByteAddressType = 'DigiByte (DGB)' | 'DigiAsset (DGA)' | 'Unknown'; +export type DigiByteAddressType = + | 'DigiByte (DGB)' + | 'DigiAsset (DGA)' + | 'Unknown'; export function getDigiByteAddressType(address: string): DigiByteAddressType { if (address.startsWith('dgb1')) { @@ -10,11 +13,12 @@ export function getDigiByteAddressType(address: string): DigiByteAddressType { // For now, assume non-DGB is DigiAsset, but this might need refinement // depending on actual DigiAsset address formats. // If the digiid-ts library provides a helper for this, use that instead. - else if (address) { // Basic check to differentiate from empty/null + else if (address) { + // Basic check to differentiate from empty/null // Assuming DigiAssets might start differently or be the fallback // This is a placeholder assumption. // A more robust check based on DigiAsset address specification is needed. return 'DigiAsset (DGA)'; // Placeholder - ADJUST BASED ON ACTUAL DGA PREFIX } return 'Unknown'; -} \ No newline at end of file +} -- cgit v1.2.3