@polyguard/sdk 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sdk.esm.js +11231 -0
- package/dist/sdk.global.js +11256 -0
- package/dist/sdk.js +10647 -10695
- package/package.json +15 -10
- package/src/PolyguardWebsocketClientImpl.js +40 -179
- package/src/__tests__/PolyguardClient.test.js +65 -0
- package/src/__tests__/PolyguardWebsocketClientImpl.test.js +574 -0
- package/src/__tests__/helpers/fixtures.js +27 -0
- package/src/__tests__/helpers/mockWebSocket.js +66 -0
- package/src/browser.js +9 -0
- package/src/messageHandler.js +77 -0
- package/src/ticketService.js +24 -0
- package/src/ui.js +82 -0
- package/vitest.config.js +10 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { showSpinner, renderMobileButton, renderQRCode } from './ui.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handle a WebSocket message event.
|
|
5
|
+
* @param {MessageEvent} event
|
|
6
|
+
* @param {Object} ctx - Context object with references from the verify() closure:
|
|
7
|
+
* { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt }
|
|
8
|
+
*/
|
|
9
|
+
export function handleWebSocketMessage(event, ctx) {
|
|
10
|
+
const { ws, qrDiv, isTargetMode, cleanup, returnError, resolve, rawJwt } = ctx;
|
|
11
|
+
|
|
12
|
+
let data;
|
|
13
|
+
try {
|
|
14
|
+
data = JSON.parse(event.data);
|
|
15
|
+
} catch (e) {
|
|
16
|
+
console.error('Invalid message format from server', e);
|
|
17
|
+
returnError('Invalid message format from server');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!data) {
|
|
22
|
+
console.error('Unknown message type from server', data);
|
|
23
|
+
returnError('Unknown message type from server');
|
|
24
|
+
ws.close();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Backend-initiated latency measurement
|
|
29
|
+
if (data.type === 'ping' && typeof data.seq !== 'undefined') {
|
|
30
|
+
ws.send(JSON.stringify({ type: 'pong', seq: data.seq }));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (data.url) {
|
|
35
|
+
window.location.assign(data.url);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (data.qr_url) {
|
|
40
|
+
const pcre = data.qr_url.match(/pcre=([^&]*)/);
|
|
41
|
+
console.log('pcre', pcre);
|
|
42
|
+
if (pcre) {
|
|
43
|
+
ws.send(JSON.stringify({ type: 'pong', seq: pcre[1] }));
|
|
44
|
+
}
|
|
45
|
+
if (qrDiv) {
|
|
46
|
+
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
|
|
47
|
+
if (isMobile) {
|
|
48
|
+
renderMobileButton(qrDiv, data.qr_url, isTargetMode);
|
|
49
|
+
} else {
|
|
50
|
+
renderQRCode(qrDiv, data.qr_url);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (data.jwt) {
|
|
57
|
+
cleanup();
|
|
58
|
+
ws.close();
|
|
59
|
+
resolve(rawJwt ? data : data.jwt);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (data.status) {
|
|
64
|
+
if (qrDiv) showSpinner(qrDiv);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (data.error) {
|
|
69
|
+
returnError(data.error);
|
|
70
|
+
ws.close();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.error('Unknown message type from server', data);
|
|
75
|
+
returnError('Unknown message type from server');
|
|
76
|
+
ws.close();
|
|
77
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function fetchTicket({ apiServer, appId, link_uuid, requiredProofs, scanType }) {
|
|
2
|
+
const ticketUrl = link_uuid
|
|
3
|
+
? `https://${apiServer}/v2/ticket/${appId}/${link_uuid}`
|
|
4
|
+
: `https://${apiServer}/v2/ticket/${appId}`;
|
|
5
|
+
|
|
6
|
+
const ticketRes = await fetch(ticketUrl, {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
body: JSON.stringify({ requiredProofs, scanType }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
if (!ticketRes.ok) {
|
|
13
|
+
throw new Error('Failed to get ticket');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ticketData = await ticketRes.json();
|
|
17
|
+
const ticket = ticketData.ticket;
|
|
18
|
+
|
|
19
|
+
if (!ticket) {
|
|
20
|
+
throw new Error('No ticket returned from server');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return ticket;
|
|
24
|
+
}
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import QRCode from 'qrcode';
|
|
2
|
+
|
|
3
|
+
// Animated spinner SVG
|
|
4
|
+
export const LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="none">
|
|
5
|
+
<style>
|
|
6
|
+
.spinner-circle {
|
|
7
|
+
animation: spin 1s linear infinite;
|
|
8
|
+
transform-origin: 100px 100px;
|
|
9
|
+
}
|
|
10
|
+
@keyframes spin {
|
|
11
|
+
from { transform: rotate(0deg); }
|
|
12
|
+
to { transform: rotate(360deg); }
|
|
13
|
+
}
|
|
14
|
+
</style>
|
|
15
|
+
<circle cx="100" cy="100" r="80" stroke="#e0e0e0" stroke-width="8" fill="none" />
|
|
16
|
+
<circle cx="100" cy="100" r="80" stroke="#407796" stroke-width="8" fill="none"
|
|
17
|
+
stroke-dasharray="251.2" stroke-dashoffset="188.4"
|
|
18
|
+
class="spinner-circle" stroke-linecap="round" />
|
|
19
|
+
</svg>`;
|
|
20
|
+
|
|
21
|
+
export function buildModal() {
|
|
22
|
+
const modal = document.createElement('div');
|
|
23
|
+
modal.style.position = 'fixed';
|
|
24
|
+
modal.style.top = '0';
|
|
25
|
+
modal.style.left = '0';
|
|
26
|
+
modal.style.width = '100vw';
|
|
27
|
+
modal.style.height = '100vh';
|
|
28
|
+
modal.style.background = 'rgba(0,0,0,0.45)';
|
|
29
|
+
modal.style.display = 'flex';
|
|
30
|
+
modal.style.alignItems = 'flex-start';
|
|
31
|
+
modal.style.justifyContent = 'center';
|
|
32
|
+
modal.style.overflowY = 'auto';
|
|
33
|
+
modal.style.paddingTop = '24px';
|
|
34
|
+
modal.innerHTML = `
|
|
35
|
+
<div id="polyguard-modal-content" style="background: #fff; color: #222; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.18); padding: 32px 24px 24px 24px; max-width: 340px; width: 100%; text-align: center; position: relative; font-family: 'IBM Plex Sans', 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
|
|
36
|
+
<button id="polyguard-modal-close" style="position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 22px; color: #222; cursor: pointer; z-index: 2;">×</button>
|
|
37
|
+
<h2 style="margin-top: 0; font-size: 1.3rem; font-weight: 700; color: #222;">Identity Verification</h2>
|
|
38
|
+
<div style="font-size: 13px; color: #888; margin-bottom: 12px; font-weight: 500;">Powered by Polyguard</div>
|
|
39
|
+
<div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 16px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 12px;"></div>
|
|
40
|
+
<div style="margin-bottom: 12px; font-weight: 600; color: #222;">Scan this QR code to verify your identity.</div>
|
|
41
|
+
<div style="display: flex; justify-content: center; gap: 12px; font-size: 12px; color: #5a6c7d; margin-bottom: 12px; flex-wrap: wrap;">
|
|
42
|
+
<span>🔒 Encrypted</span>
|
|
43
|
+
<span>📱 On-device</span>
|
|
44
|
+
<span>⏲ Expires</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div id="polyguard-error" style="color: #b31d28; font-size: 14px; margin-bottom: 8px; display: none;"></div>
|
|
47
|
+
<button id="polyguard-modal-cancel" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer; margin-top: 8px; width: 100%; max-width: 240px;">Cancel</button>
|
|
48
|
+
</div>
|
|
49
|
+
`;
|
|
50
|
+
return modal;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function showSpinner(qrDiv) {
|
|
54
|
+
qrDiv.innerHTML = LOADING_SPINNER;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function renderMobileButton(qrDiv, qrUrl, isTargetMode) {
|
|
58
|
+
qrDiv.innerHTML = `<button id="polyguard-open-app-button" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer;">Open Polyguard App</button>`;
|
|
59
|
+
qrDiv.style.background = 'transparent';
|
|
60
|
+
qrDiv.querySelector('#polyguard-open-app-button').onclick = () => window.location.assign(qrUrl);
|
|
61
|
+
|
|
62
|
+
// Update surrounding text only in modal mode
|
|
63
|
+
if (!isTargetMode) {
|
|
64
|
+
const instructionText = qrDiv.nextElementSibling;
|
|
65
|
+
if (instructionText) {
|
|
66
|
+
instructionText.textContent = 'Tap the button to verify with the Polyguard app.';
|
|
67
|
+
}
|
|
68
|
+
const instructionList = instructionText.nextElementSibling;
|
|
69
|
+
if (instructionList && instructionList.children[1]) {
|
|
70
|
+
instructionList.children[1].textContent = 'If you do not have the Polyguard app, you will be redirected to download it.';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function renderQRCode(qrDiv, qrUrl) {
|
|
76
|
+
const startTime = Date.now();
|
|
77
|
+
console.log('time before qr code', startTime);
|
|
78
|
+
QRCode.toString(qrUrl, { type: 'svg' }, (err, svg) => {
|
|
79
|
+
if (!err) qrDiv.innerHTML = svg;
|
|
80
|
+
});
|
|
81
|
+
console.log('time to generate qr code', Date.now() - startTime);
|
|
82
|
+
}
|