@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
package/package.json
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polyguard/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "dist/sdk.js",
|
|
6
|
-
"module": "dist/sdk.js",
|
|
7
|
-
"browser": "dist/sdk.js",
|
|
5
|
+
"main": "dist/sdk.esm.js",
|
|
6
|
+
"module": "dist/sdk.esm.js",
|
|
7
|
+
"browser": "dist/sdk.esm.js",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"import": "./dist/sdk.js",
|
|
11
|
-
"require": "./dist/sdk.js",
|
|
12
|
-
"default": "./dist/sdk.js"
|
|
10
|
+
"import": "./dist/sdk.esm.js",
|
|
11
|
+
"require": "./dist/sdk.esm.js",
|
|
12
|
+
"default": "./dist/sdk.esm.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
-
"build": "
|
|
16
|
+
"build": "npm run build:esm && npm run build:iife",
|
|
17
|
+
"build:esm": "esbuild src/index.js --bundle --format=esm --outfile=dist/sdk.esm.js",
|
|
18
|
+
"build:iife": "esbuild src/browser.js --bundle --format=iife --global-name=Polyguard --outfile=dist/sdk.js",
|
|
17
19
|
"prepare": "npm run build",
|
|
18
|
-
"test": "
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
19
22
|
"regenerate-client": "bash scripts/regenerate-client.sh"
|
|
20
23
|
},
|
|
21
24
|
"keywords": [],
|
|
@@ -28,7 +31,9 @@
|
|
|
28
31
|
"superagent": "^10.3.0"
|
|
29
32
|
},
|
|
30
33
|
"devDependencies": {
|
|
31
|
-
"esbuild": "^0.25.0"
|
|
34
|
+
"esbuild": "^0.25.0",
|
|
35
|
+
"jsdom": "^28.0.0",
|
|
36
|
+
"vitest": "^4.0.18"
|
|
32
37
|
},
|
|
33
38
|
"repository": {
|
|
34
39
|
"type": "git",
|
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
import ReconnectingWebSocket from 'reconnecting-websocket';
|
|
2
2
|
import * as PolyguardApi from './generated/src';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="none">
|
|
7
|
-
<style>
|
|
8
|
-
.spinner-circle {
|
|
9
|
-
animation: spin 1s linear infinite;
|
|
10
|
-
transform-origin: 100px 100px;
|
|
11
|
-
}
|
|
12
|
-
@keyframes spin {
|
|
13
|
-
from { transform: rotate(0deg); }
|
|
14
|
-
to { transform: rotate(360deg); }
|
|
15
|
-
}
|
|
16
|
-
</style>
|
|
17
|
-
<circle cx="100" cy="100" r="80" stroke="#e0e0e0" stroke-width="8" fill="none" />
|
|
18
|
-
<circle cx="100" cy="100" r="80" stroke="#7be7c2" stroke-width="8" fill="none"
|
|
19
|
-
stroke-dasharray="251.2" stroke-dashoffset="188.4"
|
|
20
|
-
class="spinner-circle" stroke-linecap="round" />
|
|
21
|
-
</svg>`;
|
|
3
|
+
import { buildModal, showSpinner } from './ui.js';
|
|
4
|
+
import { fetchTicket } from './ticketService.js';
|
|
5
|
+
import { handleWebSocketMessage } from './messageHandler.js';
|
|
22
6
|
|
|
23
7
|
// Implementation class for websocket integration
|
|
24
8
|
export class PolyguardWebsocketClientImpl {
|
|
@@ -53,71 +37,35 @@ export class PolyguardWebsocketClientImpl {
|
|
|
53
37
|
}
|
|
54
38
|
|
|
55
39
|
buildModal() {
|
|
56
|
-
|
|
57
|
-
modal.style.position = 'fixed';
|
|
58
|
-
modal.style.top = '0';
|
|
59
|
-
modal.style.left = '0';
|
|
60
|
-
modal.style.width = '100vw';
|
|
61
|
-
modal.style.height = '100vh';
|
|
62
|
-
modal.style.background = 'rgba(0,0,0,0.45)';
|
|
63
|
-
modal.style.display = 'flex';
|
|
64
|
-
modal.style.alignItems = 'flex-start';
|
|
65
|
-
modal.style.justifyContent = 'center';
|
|
66
|
-
modal.style.overflowY = 'auto';
|
|
67
|
-
modal.style.paddingTop = '24px';
|
|
68
|
-
modal.innerHTML = `
|
|
69
|
-
<div id="polyguard-modal-content" style="background: #fff; 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: 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
|
|
70
|
-
<button id="polyguard-modal-close" style="position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 22px; cursor: pointer; z-index: 2;">×</button>
|
|
71
|
-
<h2 style="margin-top: 0; font-size: 1.5rem; font-weight: 700;">Quick Identity Verification</h2>
|
|
72
|
-
<div style="font-size: 15px; color: #888; margin-bottom: 12px; font-weight: 500;">Powered by Polyguard</div>
|
|
73
|
-
<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>
|
|
74
|
-
<div style="margin-bottom: 12px; font-weight: 600;">Scan this QR code to verify your identity.</div>
|
|
75
|
-
<ul style="text-align: left; margin: 0 0 16px 0; padding-left: 20px; font-size: 14px; color: #444;">
|
|
76
|
-
<li>We use the Polyguard service to verify your identity.</li>
|
|
77
|
-
<li>If you do not have the Polyguard app, the QR code will redirect you to download it from the App Store or Google Play.</li>
|
|
78
|
-
<li>Your credentials remain private on your device.</li>
|
|
79
|
-
</ul>
|
|
80
|
-
<div id="polyguard-error" style="color: #b31d28; font-size: 14px; margin-bottom: 8px; display: none;"></div>
|
|
81
|
-
<button id="polyguard-modal-cancel" style="background: #7be7c2; color: #222; 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>
|
|
82
|
-
</div>
|
|
83
|
-
`;
|
|
84
|
-
return modal;
|
|
40
|
+
return buildModal();
|
|
85
41
|
}
|
|
86
42
|
|
|
87
43
|
async verify(target = null, rawJwt = false) {
|
|
88
|
-
// Only websocket integration is supported for modal
|
|
89
44
|
let modal = null;
|
|
90
45
|
let qrDiv = null;
|
|
91
46
|
let isTargetMode = false;
|
|
92
|
-
|
|
47
|
+
|
|
93
48
|
if (target) {
|
|
94
|
-
// Target mode: render QR code in specified element
|
|
95
49
|
isTargetMode = true;
|
|
96
50
|
qrDiv = document.getElementById(target);
|
|
97
51
|
if (!qrDiv) {
|
|
98
52
|
throw new Error(`Target element with ID '${target}' not found`);
|
|
99
53
|
}
|
|
100
54
|
} else {
|
|
101
|
-
|
|
102
|
-
modal = this.buildModal();
|
|
55
|
+
modal = buildModal();
|
|
103
56
|
}
|
|
104
|
-
|
|
57
|
+
|
|
105
58
|
if (!isTargetMode) {
|
|
106
|
-
// Show modal immediately with spinner
|
|
107
59
|
document.body.appendChild(modal);
|
|
108
|
-
|
|
109
|
-
// Initialize QR div with spinner
|
|
110
60
|
qrDiv = modal.querySelector('#polyguard-qr');
|
|
111
61
|
}
|
|
112
|
-
|
|
62
|
+
|
|
113
63
|
if (qrDiv) {
|
|
114
|
-
qrDiv
|
|
64
|
+
showSpinner(qrDiv);
|
|
115
65
|
}
|
|
116
|
-
|
|
117
|
-
// Helper to cleanup modal or target
|
|
66
|
+
|
|
118
67
|
function cleanup() {
|
|
119
68
|
if (isTargetMode) {
|
|
120
|
-
// Clear target element content
|
|
121
69
|
if (qrDiv) {
|
|
122
70
|
qrDiv.innerHTML = '';
|
|
123
71
|
}
|
|
@@ -125,28 +73,27 @@ export class PolyguardWebsocketClientImpl {
|
|
|
125
73
|
modal.parentNode.removeChild(modal);
|
|
126
74
|
}
|
|
127
75
|
}
|
|
128
|
-
|
|
76
|
+
|
|
129
77
|
return new Promise(async (resolve, reject) => {
|
|
130
78
|
let ws = null;
|
|
131
79
|
let closed = false;
|
|
132
|
-
|
|
80
|
+
|
|
133
81
|
function returnError(msg, score = "OFFLINE") {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
82
|
+
if (isTargetMode) {
|
|
83
|
+
console.error('Polyguard Error:', msg);
|
|
84
|
+
cleanup();
|
|
85
|
+
resolve({presence: { score: score, msg: msg }});
|
|
86
|
+
} else {
|
|
87
|
+
const errDiv = modal.querySelector('#polyguard-error');
|
|
88
|
+
if (errDiv) {
|
|
89
|
+
errDiv.textContent = msg;
|
|
90
|
+
errDiv.style.display = 'block';
|
|
91
|
+
}
|
|
92
|
+
setTimeout(() => {
|
|
137
93
|
cleanup();
|
|
138
94
|
resolve({presence: { score: score, msg: msg }});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (errDiv) {
|
|
142
|
-
errDiv.textContent = msg;
|
|
143
|
-
errDiv.style.display = 'block';
|
|
144
|
-
}
|
|
145
|
-
setTimeout(() => {
|
|
146
|
-
cleanup();
|
|
147
|
-
resolve({presence: { score: score, msg: msg }});
|
|
148
|
-
}, 1250);
|
|
149
|
-
}
|
|
95
|
+
}, 1250);
|
|
96
|
+
}
|
|
150
97
|
}
|
|
151
98
|
function clearError() {
|
|
152
99
|
if (!isTargetMode) {
|
|
@@ -154,42 +101,30 @@ export class PolyguardWebsocketClientImpl {
|
|
|
154
101
|
if (errDiv) errDiv.style.display = 'none';
|
|
155
102
|
}
|
|
156
103
|
}
|
|
157
|
-
// Close/cancel handler
|
|
158
104
|
function handleClose() {
|
|
159
105
|
closed = true;
|
|
160
106
|
if (ws) ws.close();
|
|
161
107
|
cleanup();
|
|
162
108
|
reject(new Error('User cancelled'));
|
|
163
109
|
}
|
|
164
|
-
|
|
165
|
-
// Set up close/cancel handlers (only in modal mode)
|
|
110
|
+
|
|
166
111
|
if (!isTargetMode) {
|
|
167
112
|
modal.querySelector('#polyguard-modal-close').onclick = handleClose;
|
|
168
113
|
modal.querySelector('#polyguard-modal-cancel').onclick = handleClose;
|
|
169
114
|
}
|
|
170
|
-
|
|
171
|
-
// Start ticket/ws flow
|
|
115
|
+
|
|
172
116
|
try {
|
|
173
117
|
clearError();
|
|
174
118
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
119
|
+
|
|
120
|
+
const newTicket = await fetchTicket({
|
|
121
|
+
apiServer: this.apiServer,
|
|
122
|
+
appId: this.appId,
|
|
123
|
+
link_uuid: this.link_uuid,
|
|
124
|
+
requiredProofs: this.requiredProofs,
|
|
125
|
+
scanType: this.scanType,
|
|
182
126
|
});
|
|
183
|
-
|
|
184
|
-
returnError('Failed to get ticket');
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const ticketData = await ticketRes.json();
|
|
188
|
-
const newTicket = ticketData.ticket;
|
|
189
|
-
if (!newTicket) {
|
|
190
|
-
returnError('No ticket returned from server');
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
127
|
+
|
|
193
128
|
const wsUrl = `${wsProtocol}://${this.apiServer}/v2/realtime/${newTicket}`;
|
|
194
129
|
const options = {
|
|
195
130
|
maxRetries: 10,
|
|
@@ -199,86 +134,10 @@ export class PolyguardWebsocketClientImpl {
|
|
|
199
134
|
reconnectionDelayGrowFactor: 1.5,
|
|
200
135
|
};
|
|
201
136
|
ws = new ReconnectingWebSocket(wsUrl, [], options);
|
|
202
|
-
ws.addEventListener('message', (event) => {
|
|
203
|
-
try {
|
|
204
|
-
const data = JSON.parse(event.data);
|
|
205
|
-
// --- BEGIN: Backend-initiated latency measurement ---
|
|
206
|
-
if (data && data.type === 'ping' && typeof data.seq !== 'undefined') {
|
|
207
|
-
ws.send(JSON.stringify({ type: 'pong', seq: data.seq }));
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
// --- END: Backend-initiated latency measurement ---
|
|
211
|
-
if (data && data.url) {
|
|
212
|
-
window.location.assign(data.url);
|
|
213
|
-
return;
|
|
214
|
-
} else if (data && data.qr_url) {
|
|
215
|
-
// Replace spinner with QR code content
|
|
216
|
-
|
|
217
|
-
const pcre = data.qr_url.match(/pcre=([^&]*)/);
|
|
218
|
-
console.log('pcre', pcre);
|
|
219
|
-
if (pcre) {
|
|
220
|
-
ws.send(JSON.stringify({ type: 'pong', seq: pcre[1] }));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (!qrDiv) return;
|
|
224
|
-
|
|
225
|
-
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
|
|
226
137
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
qrDiv.innerHTML = `<button id="polyguard-open-app-button" style="background: #7be7c2; color: #222; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer;">Open Polyguard App</button>`;
|
|
230
|
-
qrDiv.style.background = 'transparent';
|
|
231
|
-
qrDiv.querySelector('#polyguard-open-app-button').onclick = () => window.location.assign(data.qr_url);
|
|
138
|
+
const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt };
|
|
139
|
+
ws.addEventListener('message', (event) => handleWebSocketMessage(event, ctx));
|
|
232
140
|
|
|
233
|
-
// Update surrounding text only in modal mode
|
|
234
|
-
if (!isTargetMode) {
|
|
235
|
-
const instructionText = qrDiv.nextElementSibling;
|
|
236
|
-
if (instructionText) {
|
|
237
|
-
instructionText.textContent = 'Tap the button to verify with the Polyguard app.';
|
|
238
|
-
}
|
|
239
|
-
const instructionList = instructionText.nextElementSibling;
|
|
240
|
-
if (instructionList && instructionList.children[1]) {
|
|
241
|
-
instructionList.children[1].textContent = 'If you do not have the Polyguard app, you will be redirected to download it.';
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
} else {
|
|
245
|
-
const startTime = Date.now();
|
|
246
|
-
console.log('time before qr code', startTime);
|
|
247
|
-
// For desktop, display the QR code
|
|
248
|
-
QRCode.toString(data.qr_url, { type: 'svg' }, (err, svg) => {
|
|
249
|
-
if (!err) qrDiv.innerHTML = svg;
|
|
250
|
-
});
|
|
251
|
-
console.log('time to generate qr code', Date.now() - startTime);
|
|
252
|
-
}
|
|
253
|
-
return;
|
|
254
|
-
} else if (data && data.jwt) {
|
|
255
|
-
cleanup();
|
|
256
|
-
ws.close();
|
|
257
|
-
resolve(rawJwt ? data : data.jwt);
|
|
258
|
-
return;
|
|
259
|
-
} else if (data && data.status) {
|
|
260
|
-
// ignore
|
|
261
|
-
// generate a spinner in svg and set the qr code to it
|
|
262
|
-
if (!qrDiv) return;
|
|
263
|
-
qrDiv.innerHTML = LOADING_SPINNER;
|
|
264
|
-
return;
|
|
265
|
-
|
|
266
|
-
} else if (data && data.error) {
|
|
267
|
-
returnError(data.error);
|
|
268
|
-
ws.close();
|
|
269
|
-
return;
|
|
270
|
-
} else {
|
|
271
|
-
console.error('Unknown message type from server', data);
|
|
272
|
-
returnError(`Unknown message type from server`);
|
|
273
|
-
ws.close();
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
} catch (e) {
|
|
277
|
-
console.error('Invalid message format from server', e);
|
|
278
|
-
returnError('Invalid message format from server');
|
|
279
|
-
// ws.close();
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
141
|
ws.addEventListener('error', () => {
|
|
283
142
|
console.error('WebSocket error');
|
|
284
143
|
returnError('WebSocket error');
|
|
@@ -287,7 +146,9 @@ export class PolyguardWebsocketClientImpl {
|
|
|
287
146
|
if (!closed) cleanup();
|
|
288
147
|
});
|
|
289
148
|
} catch (err) {
|
|
290
|
-
returnError('Failed to
|
|
149
|
+
returnError(err.message === 'Failed to get ticket' || err.message === 'No ticket returned from server'
|
|
150
|
+
? err.message
|
|
151
|
+
: 'Failed to connect to WebSocket');
|
|
291
152
|
}
|
|
292
153
|
});
|
|
293
154
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { DEFAULT_PARAMS } from './helpers/fixtures.js';
|
|
3
|
+
|
|
4
|
+
// Mock the WebSocket impl so the facade tests never touch real internals
|
|
5
|
+
vi.mock('../PolyguardWebsocketClientImpl', () => {
|
|
6
|
+
class FakeImpl {
|
|
7
|
+
constructor(params = {}) {
|
|
8
|
+
this.appId = params.appId;
|
|
9
|
+
this.apiServer = params.apiServer;
|
|
10
|
+
this.integrationType = 'websocket';
|
|
11
|
+
this.blockingApi = { fake: true };
|
|
12
|
+
this.callsApi = { fake: true };
|
|
13
|
+
}
|
|
14
|
+
async verify(...args) {
|
|
15
|
+
return { args, result: 'fake-jwt' };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { PolyguardWebsocketClientImpl: FakeImpl };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Import after mock is set up
|
|
22
|
+
const { PolyguardClient } = await import('../PolyguardClient.js');
|
|
23
|
+
|
|
24
|
+
describe('PolyguardClient', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('defaults to websocket implementation', () => {
|
|
30
|
+
const client = new PolyguardClient(DEFAULT_PARAMS);
|
|
31
|
+
expect(client._impl).toBeDefined();
|
|
32
|
+
expect(client._impl.integrationType).toBe('websocket');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('explicit integrationType websocket works the same', () => {
|
|
36
|
+
const client = new PolyguardClient({ ...DEFAULT_PARAMS, integrationType: 'websocket' });
|
|
37
|
+
expect(client._impl.integrationType).toBe('websocket');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('copies properties from impl to facade', () => {
|
|
41
|
+
const client = new PolyguardClient(DEFAULT_PARAMS);
|
|
42
|
+
expect(client.appId).toBe(DEFAULT_PARAMS.appId);
|
|
43
|
+
expect(client.apiServer).toBe(DEFAULT_PARAMS.apiServer);
|
|
44
|
+
expect(client.integrationType).toBe('websocket');
|
|
45
|
+
expect(client.blockingApi).toEqual({ fake: true });
|
|
46
|
+
expect(client.callsApi).toEqual({ fake: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('verify() delegates to _impl.verify() with all args forwarded', async () => {
|
|
50
|
+
const client = new PolyguardClient(DEFAULT_PARAMS);
|
|
51
|
+
const spy = vi.spyOn(client._impl, 'verify');
|
|
52
|
+
await client.verify('my-target', true);
|
|
53
|
+
expect(spy).toHaveBeenCalledWith('my-target', true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('verify() returns the impl result', async () => {
|
|
57
|
+
const client = new PolyguardClient(DEFAULT_PARAMS);
|
|
58
|
+
const result = await client.verify('t', false);
|
|
59
|
+
expect(result).toEqual({ args: ['t', false], result: 'fake-jwt' });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('constructor with no params does not throw', () => {
|
|
63
|
+
expect(() => new PolyguardClient()).not.toThrow();
|
|
64
|
+
});
|
|
65
|
+
});
|