@polyguard/sdk 1.4.2 → 1.5.1

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.
Files changed (116) hide show
  1. package/LICENSE +200 -0
  2. package/README.md +328 -0
  3. package/dist/sdk.esm.js +16 -4
  4. package/dist/sdk.js +16 -4
  5. package/dist/server.esm.js +83 -0
  6. package/package.json +14 -3
  7. package/dist/sdk.global.js +0 -11256
  8. package/scripts/regenerate-client.sh +0 -59
  9. package/src/PolyguardClient.js +0 -25
  10. package/src/PolyguardWebsocketClientImpl.js +0 -183
  11. package/src/__tests__/PolyguardClient.test.js +0 -65
  12. package/src/__tests__/PolyguardWebsocketClientImpl.test.js +0 -574
  13. package/src/__tests__/helpers/fixtures.js +0 -27
  14. package/src/__tests__/helpers/mockWebSocket.js +0 -66
  15. package/src/__tests__/sidebar.test.js +0 -289
  16. package/src/browser.js +0 -9
  17. package/src/generated/.babelrc +0 -33
  18. package/src/generated/.openapi-generator/FILES +0 -63
  19. package/src/generated/.openapi-generator/VERSION +0 -1
  20. package/src/generated/.openapi-generator-ignore +0 -23
  21. package/src/generated/.travis.yml +0 -5
  22. package/src/generated/README.md +0 -211
  23. package/src/generated/docs/ApiControllersPslStirCallRequest.md +0 -18
  24. package/src/generated/docs/ApiModelsApiCallModelsCallRequest.md +0 -12
  25. package/src/generated/docs/AppId.md +0 -8
  26. package/src/generated/docs/AppleApi.md +0 -88
  27. package/src/generated/docs/AuthApi.md +0 -1464
  28. package/src/generated/docs/BlockingApi.md +0 -208
  29. package/src/generated/docs/CallsApi.md +0 -140
  30. package/src/generated/docs/Certainty.md +0 -8
  31. package/src/generated/docs/CreateLinkRequest.md +0 -12
  32. package/src/generated/docs/DefaultApi.md +0 -128
  33. package/src/generated/docs/HTTPValidationError.md +0 -9
  34. package/src/generated/docs/InviteRequestModel.md +0 -10
  35. package/src/generated/docs/JWTRequest.md +0 -11
  36. package/src/generated/docs/LinksApi.md +0 -162
  37. package/src/generated/docs/NumberVerification.md +0 -10
  38. package/src/generated/docs/SdkApi.md +0 -54
  39. package/src/generated/docs/SecureLinksApi.md +0 -614
  40. package/src/generated/docs/StartAttestationRequest.md +0 -9
  41. package/src/generated/docs/StartMeetingRequest.md +0 -9
  42. package/src/generated/docs/StirApi.md +0 -52
  43. package/src/generated/docs/TwilioApi.md +0 -138
  44. package/src/generated/docs/UsersApi.md +0 -362
  45. package/src/generated/docs/ValidationError.md +0 -11
  46. package/src/generated/docs/ValidationErrorLocInner.md +0 -8
  47. package/src/generated/docs/VeriffApi.md +0 -188
  48. package/src/generated/docs/VeriffSessionRequest.md +0 -10
  49. package/src/generated/docs/VerifyRequest.md +0 -10
  50. package/src/generated/docs/WellKnownApi.md +0 -128
  51. package/src/generated/docs/ZoomApi.md +0 -848
  52. package/src/generated/git_push.sh +0 -57
  53. package/src/generated/mocha.opts +0 -1
  54. package/src/generated/package.json +0 -46
  55. package/src/generated/src/ApiClient.js +0 -677
  56. package/src/generated/src/api/AppleApi.js +0 -109
  57. package/src/generated/src/api/AuthApi.js +0 -1512
  58. package/src/generated/src/api/BlockingApi.js +0 -217
  59. package/src/generated/src/api/CallsApi.js +0 -164
  60. package/src/generated/src/api/DefaultApi.js +0 -145
  61. package/src/generated/src/api/LinksApi.js +0 -195
  62. package/src/generated/src/api/SdkApi.js +0 -81
  63. package/src/generated/src/api/SecureLinksApi.js +0 -670
  64. package/src/generated/src/api/StirApi.js +0 -80
  65. package/src/generated/src/api/TwilioApi.js +0 -158
  66. package/src/generated/src/api/UsersApi.js +0 -375
  67. package/src/generated/src/api/VeriffApi.js +0 -209
  68. package/src/generated/src/api/WellKnownApi.js +0 -145
  69. package/src/generated/src/api/ZoomApi.js +0 -823
  70. package/src/generated/src/index.js +0 -244
  71. package/src/generated/src/model/ApiControllersPslStirCallRequest.js +0 -211
  72. package/src/generated/src/model/ApiModelsApiCallModelsCallRequest.js +0 -119
  73. package/src/generated/src/model/CreateLinkRequest.js +0 -131
  74. package/src/generated/src/model/HTTPValidationError.js +0 -94
  75. package/src/generated/src/model/InviteRequestModel.js +0 -109
  76. package/src/generated/src/model/JWTRequest.js +0 -113
  77. package/src/generated/src/model/NumberVerification.js +0 -107
  78. package/src/generated/src/model/StartAttestationRequest.js +0 -95
  79. package/src/generated/src/model/StartMeetingRequest.js +0 -95
  80. package/src/generated/src/model/ValidationError.js +0 -130
  81. package/src/generated/src/model/VeriffSessionRequest.js +0 -107
  82. package/src/generated/src/model/VerifyRequest.js +0 -109
  83. package/src/generated/test/api/AppleApi.spec.js +0 -73
  84. package/src/generated/test/api/AuthApi.spec.js +0 -353
  85. package/src/generated/test/api/BlockingApi.spec.js +0 -103
  86. package/src/generated/test/api/CallsApi.spec.js +0 -83
  87. package/src/generated/test/api/DefaultApi.spec.js +0 -73
  88. package/src/generated/test/api/LinksApi.spec.js +0 -83
  89. package/src/generated/test/api/SdkApi.spec.js +0 -63
  90. package/src/generated/test/api/SecureLinksApi.spec.js +0 -143
  91. package/src/generated/test/api/StirApi.spec.js +0 -63
  92. package/src/generated/test/api/TwilioApi.spec.js +0 -83
  93. package/src/generated/test/api/UsersApi.spec.js +0 -133
  94. package/src/generated/test/api/VeriffApi.spec.js +0 -93
  95. package/src/generated/test/api/WellKnownApi.spec.js +0 -83
  96. package/src/generated/test/api/ZoomApi.spec.js +0 -213
  97. package/src/generated/test/model/ApiControllersPslStirCallRequest.spec.js +0 -119
  98. package/src/generated/test/model/ApiModelsApiCallModelsCallRequest.spec.js +0 -83
  99. package/src/generated/test/model/AppId.spec.js +0 -59
  100. package/src/generated/test/model/Certainty.spec.js +0 -59
  101. package/src/generated/test/model/CreateLinkRequest.spec.js +0 -83
  102. package/src/generated/test/model/HTTPValidationError.spec.js +0 -65
  103. package/src/generated/test/model/InviteRequestModel.spec.js +0 -71
  104. package/src/generated/test/model/JWTRequest.spec.js +0 -77
  105. package/src/generated/test/model/NumberVerification.spec.js +0 -71
  106. package/src/generated/test/model/StartAttestationRequest.spec.js +0 -65
  107. package/src/generated/test/model/StartMeetingRequest.spec.js +0 -65
  108. package/src/generated/test/model/ValidationError.spec.js +0 -77
  109. package/src/generated/test/model/ValidationErrorLocInner.spec.js +0 -59
  110. package/src/generated/test/model/VeriffSessionRequest.spec.js +0 -71
  111. package/src/generated/test/model/VerifyRequest.spec.js +0 -71
  112. package/src/index.js +0 -16
  113. package/src/messageHandler.js +0 -121
  114. package/src/ticketService.js +0 -24
  115. package/src/ui.js +0 -88
  116. package/vitest.config.js +0 -10
@@ -1,574 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { buildFakeJwt, DEFAULT_PARAMS, mockTicketResponse } from './helpers/fixtures.js';
3
- import { MockReconnectingWebSocket } from './helpers/mockWebSocket.js';
4
-
5
- // ---- Module mocks (using vi.hoisted to avoid hoisting issues) ----
6
-
7
- const API_CLASS_NAMES = [
8
- 'BlockingApi', 'CallsApi', 'DefaultApi', 'LinksApi', 'SdkApi',
9
- 'SecureLinksApi', 'StirApi', 'TwilioApi', 'UsersApi', 'VeriffApi',
10
- 'WellKnownApi', 'ZoomApi',
11
- ];
12
-
13
- // Use vi.hoisted so these are available when vi.mock factories run (which are hoisted)
14
- const { MockApiClient, generatedMock, mockQRCode } = vi.hoisted(() => {
15
- class MockApiClient {
16
- constructor() {
17
- this.basePath = '';
18
- this.authentications = { bearerAuth: { apiKey: null } };
19
- }
20
- }
21
-
22
- const generatedMock = { ApiClient: MockApiClient };
23
- const apiClassNames = [
24
- 'BlockingApi', 'CallsApi', 'DefaultApi', 'LinksApi', 'SdkApi',
25
- 'SecureLinksApi', 'StirApi', 'TwilioApi', 'UsersApi', 'VeriffApi',
26
- 'WellKnownApi', 'ZoomApi',
27
- ];
28
- for (const name of apiClassNames) {
29
- generatedMock[name] = class {
30
- constructor(apiClient) { this._apiClient = apiClient; }
31
- };
32
- }
33
-
34
- const mockQRCode = {
35
- toString: null, // will be set per-test via vi.fn()
36
- };
37
-
38
- return { MockApiClient, generatedMock, mockQRCode };
39
- });
40
-
41
- vi.mock('../generated/src', () => generatedMock);
42
-
43
- vi.mock('reconnecting-websocket', async () => {
44
- // Import the actual mock class at runtime (not hoisted)
45
- const { MockReconnectingWebSocket } = await import('./helpers/mockWebSocket.js');
46
- return { default: MockReconnectingWebSocket };
47
- });
48
-
49
- vi.mock('qrcode', () => ({
50
- default: mockQRCode,
51
- }));
52
-
53
- // Import after mocks
54
- const { PolyguardWebsocketClientImpl } = await import('../PolyguardWebsocketClientImpl.js');
55
-
56
- // ---- Helpers ----
57
-
58
- /** Flush microtasks/timers to let async code settle */
59
- async function flushMicrotasks() {
60
- // Use a real setTimeout(0) to flush the microtask queue
61
- await new Promise(resolve => setTimeout(resolve, 0));
62
- }
63
-
64
- /** Set up fetch mock that returns a valid ticket response */
65
- function stubFetch(ticket = 'test-ticket-123', ok = true) {
66
- const mock = vi.fn().mockResolvedValue(mockTicketResponse(ticket, ok));
67
- vi.stubGlobal('fetch', mock);
68
- return mock;
69
- }
70
-
71
- /** Set window.location.protocol */
72
- function setProtocol(protocol) {
73
- Object.defineProperty(window, 'location', {
74
- value: { protocol, assign: vi.fn(), href: '' },
75
- writable: true,
76
- configurable: true,
77
- });
78
- }
79
-
80
- /** Set navigator.userAgent */
81
- function setUserAgent(ua) {
82
- Object.defineProperty(navigator, 'userAgent', {
83
- value: ua,
84
- writable: true,
85
- configurable: true,
86
- });
87
- }
88
-
89
- /**
90
- * Start verify and wait for the WebSocket to be created.
91
- * Returns { promise, ws }.
92
- */
93
- async function startVerifyAndGetWs(client, target = null, rawJwt = false) {
94
- const promise = client.verify(target, rawJwt);
95
- // Flush to let the async executor run through fetch + WS creation
96
- await flushMicrotasks();
97
- const ws = MockReconnectingWebSocket.latest();
98
- return { promise, ws };
99
- }
100
-
101
- // ---- Tests ----
102
-
103
- describe('PolyguardWebsocketClientImpl', () => {
104
- beforeEach(() => {
105
- vi.clearAllMocks();
106
- MockReconnectingWebSocket.reset();
107
- setProtocol('https:');
108
- setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)');
109
- stubFetch();
110
- // Reset QRCode mock
111
- mockQRCode.toString = vi.fn((data, opts, cb) => cb(null, '<svg>mock-qr</svg>'));
112
- });
113
-
114
- afterEach(() => {
115
- vi.restoreAllMocks();
116
- document.body.innerHTML = '';
117
- });
118
-
119
- // ---------- constructor ----------
120
- describe('constructor', () => {
121
- it('defaults requiredProofs to ["Full Name"]', () => {
122
- const client = new PolyguardWebsocketClientImpl({ appId: 'x', apiServer: 'y' });
123
- expect(client.requiredProofs).toEqual(['Full Name']);
124
- });
125
-
126
- it('defaults scanType to "single"', () => {
127
- const client = new PolyguardWebsocketClientImpl({ appId: 'x', apiServer: 'y' });
128
- expect(client.scanType).toBe('single');
129
- });
130
-
131
- it('stores custom requiredProofs', () => {
132
- const client = new PolyguardWebsocketClientImpl({ ...DEFAULT_PARAMS });
133
- expect(client.requiredProofs).toEqual(['Full Name', 'Email']);
134
- });
135
-
136
- it('stores all params correctly', () => {
137
- const params = {
138
- ...DEFAULT_PARAMS,
139
- redirectUrl: 'https://example.com/redirect',
140
- callbackUrl: 'https://example.com/callback',
141
- cookieName: 'pg_token',
142
- link_uuid: 'link-abc',
143
- };
144
- const client = new PolyguardWebsocketClientImpl(params);
145
- expect(client.appId).toBe(params.appId);
146
- expect(client.apiServer).toBe(params.apiServer);
147
- expect(client.scanType).toBe('single');
148
- expect(client.redirectUrl).toBe(params.redirectUrl);
149
- expect(client.callbackUrl).toBe(params.callbackUrl);
150
- expect(client.cookieName).toBe(params.cookieName);
151
- expect(client.link_uuid).toBe(params.link_uuid);
152
- });
153
-
154
- it('creates an ApiClient instance', () => {
155
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
156
- expect(client.apiClient).toBeDefined();
157
- expect(client.apiClient.authentications).toBeDefined();
158
- });
159
-
160
- it('sets basePath when baseUrl is provided', () => {
161
- const client = new PolyguardWebsocketClientImpl({ ...DEFAULT_PARAMS, baseUrl: 'https://custom.api' });
162
- expect(client.apiClient.basePath).toBe('https://custom.api');
163
- });
164
-
165
- it('sets apiKey when provided', () => {
166
- const client = new PolyguardWebsocketClientImpl({ ...DEFAULT_PARAMS, apiKey: 'my-key' });
167
- expect(client.apiClient.authentications.bearerAuth.apiKey).toBe('my-key');
168
- });
169
-
170
- it('sets integrationType to websocket', () => {
171
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
172
- expect(client.integrationType).toBe('websocket');
173
- });
174
-
175
- it('instantiates all 12 API service classes as lowercase-first properties', () => {
176
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
177
- for (const name of API_CLASS_NAMES) {
178
- const propName = name.charAt(0).toLowerCase() + name.slice(1);
179
- expect(client[propName]).toBeDefined();
180
- expect(client[propName]._apiClient).toBe(client.apiClient);
181
- }
182
- });
183
-
184
- it('constructor with no params does not throw', () => {
185
- expect(() => new PolyguardWebsocketClientImpl()).not.toThrow();
186
- });
187
- });
188
-
189
- // ---------- buildModal ----------
190
- describe('buildModal', () => {
191
- let client;
192
- let modal;
193
-
194
- beforeEach(() => {
195
- client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
196
- modal = client.buildModal();
197
- });
198
-
199
- it('returns a div element', () => {
200
- expect(modal.tagName).toBe('DIV');
201
- });
202
-
203
- it('is a fixed-position fullscreen overlay', () => {
204
- expect(modal.style.position).toBe('fixed');
205
- expect(modal.style.top).toBe('0px');
206
- expect(modal.style.left).toBe('0px');
207
- expect(modal.style.width).toBe('100vw');
208
- expect(modal.style.height).toBe('100vh');
209
- });
210
-
211
- it('contains close button', () => {
212
- expect(modal.querySelector('#polyguard-modal-close')).not.toBeNull();
213
- });
214
-
215
- it('contains QR div', () => {
216
- expect(modal.querySelector('#polyguard-qr')).not.toBeNull();
217
- });
218
-
219
- it('contains cancel button', () => {
220
- expect(modal.querySelector('#polyguard-modal-cancel')).not.toBeNull();
221
- });
222
-
223
- it('contains error div that is initially hidden', () => {
224
- const errDiv = modal.querySelector('#polyguard-error');
225
- expect(errDiv).not.toBeNull();
226
- expect(errDiv.style.display).toBe('none');
227
- });
228
-
229
- it('contains modal content wrapper', () => {
230
- expect(modal.querySelector('#polyguard-modal-content')).not.toBeNull();
231
- });
232
- });
233
-
234
- // ---------- verify - ticket fetch ----------
235
- describe('verify - ticket fetch', () => {
236
- it('builds ticket URL without link_uuid', async () => {
237
- const fetchMock = stubFetch();
238
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
239
- const { promise, ws } = await startVerifyAndGetWs(client);
240
- expect(fetchMock).toHaveBeenCalled();
241
- const url = fetchMock.mock.calls[0][0];
242
- expect(url).toBe(`https://${DEFAULT_PARAMS.apiServer}/v2/ticket/${DEFAULT_PARAMS.appId}`);
243
- if (ws) ws.simulateMessage({ jwt: 'cleanup' });
244
- await promise.catch(() => {});
245
- });
246
-
247
- it('builds ticket URL with link_uuid', async () => {
248
- const fetchMock = stubFetch();
249
- const client = new PolyguardWebsocketClientImpl({ ...DEFAULT_PARAMS, link_uuid: 'link-123' });
250
- const { promise, ws } = await startVerifyAndGetWs(client);
251
- expect(fetchMock).toHaveBeenCalled();
252
- const url = fetchMock.mock.calls[0][0];
253
- expect(url).toBe(`https://${DEFAULT_PARAMS.apiServer}/v2/ticket/${DEFAULT_PARAMS.appId}/link-123`);
254
- if (ws) ws.simulateMessage({ jwt: 'cleanup' });
255
- await promise.catch(() => {});
256
- });
257
-
258
- it('sends POST with requiredProofs and scanType in body', async () => {
259
- const fetchMock = stubFetch();
260
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
261
- const { promise, ws } = await startVerifyAndGetWs(client);
262
- expect(fetchMock).toHaveBeenCalled();
263
- const opts = fetchMock.mock.calls[0][1];
264
- expect(opts.method).toBe('POST');
265
- expect(opts.headers['Content-Type']).toBe('application/json');
266
- const body = JSON.parse(opts.body);
267
- expect(body.requiredProofs).toEqual(DEFAULT_PARAMS.requiredProofs);
268
- expect(body.scanType).toBe(DEFAULT_PARAMS.scanType);
269
- if (ws) ws.simulateMessage({ jwt: 'cleanup' });
270
- await promise.catch(() => {});
271
- });
272
-
273
- it('resolves with error presence on non-ok response', async () => {
274
- stubFetch('test-ticket', false);
275
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
276
- document.body.innerHTML = '<div id="test-target"></div>';
277
- const result = await client.verify('test-target');
278
- expect(result).toEqual({ presence: { score: 'OFFLINE', msg: 'Failed to get ticket' } });
279
- });
280
-
281
- it('resolves with error presence when no ticket in response', async () => {
282
- stubFetch(null, true);
283
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
284
- document.body.innerHTML = '<div id="test-target"></div>';
285
- const result = await client.verify('test-target');
286
- expect(result).toEqual({ presence: { score: 'OFFLINE', msg: 'No ticket returned from server' } });
287
- });
288
-
289
- it('resolves with error presence on fetch exception', async () => {
290
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Network error')));
291
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
292
- document.body.innerHTML = '<div id="test-target"></div>';
293
- const result = await client.verify('test-target');
294
- expect(result).toEqual({ presence: { score: 'OFFLINE', msg: 'Failed to connect to WebSocket' } });
295
- });
296
- });
297
-
298
- // ---------- verify - WebSocket ----------
299
- describe('verify - WebSocket', () => {
300
- let client;
301
-
302
- beforeEach(() => {
303
- stubFetch();
304
- client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
305
- });
306
-
307
- it('uses wss protocol on https', async () => {
308
- setProtocol('https:');
309
- const { promise, ws } = await startVerifyAndGetWs(client);
310
- expect(ws).toBeDefined();
311
- expect(ws.url).toMatch(/^wss:\/\//);
312
- ws.simulateMessage({ jwt: 'cleanup' });
313
- await promise.catch(() => {});
314
- });
315
-
316
- it('uses ws protocol on http', async () => {
317
- setProtocol('http:');
318
- const { promise, ws } = await startVerifyAndGetWs(client);
319
- expect(ws).toBeDefined();
320
- expect(ws.url).toMatch(/^ws:\/\//);
321
- ws.simulateMessage({ jwt: 'cleanup' });
322
- await promise.catch(() => {});
323
- });
324
-
325
- it('builds correct WebSocket URL', async () => {
326
- const { promise, ws } = await startVerifyAndGetWs(client);
327
- expect(ws).toBeDefined();
328
- expect(ws.url).toBe(`wss://${DEFAULT_PARAMS.apiServer}/v2/realtime/test-ticket-123`);
329
- ws.simulateMessage({ jwt: 'cleanup' });
330
- await promise.catch(() => {});
331
- });
332
-
333
- it('responds pong to ping with matching seq', async () => {
334
- const { promise, ws } = await startVerifyAndGetWs(client);
335
- ws.simulateMessage({ type: 'ping', seq: 42 });
336
- expect(ws.sent).toHaveLength(1);
337
- expect(JSON.parse(ws.sent[0])).toEqual({ type: 'pong', seq: 42 });
338
- ws.simulateMessage({ jwt: 'cleanup' });
339
- await promise.catch(() => {});
340
- });
341
-
342
- it('jwt message resolves promise with JWT string', async () => {
343
- const fakeJwt = buildFakeJwt({ 'Full Name': 'Alice' });
344
- const { promise, ws } = await startVerifyAndGetWs(client);
345
- ws.simulateMessage({ jwt: fakeJwt });
346
- const result = await promise;
347
- expect(result).toBe(fakeJwt);
348
- });
349
-
350
- it('jwt message with rawJwt=true resolves with full data object', async () => {
351
- const fakeJwt = buildFakeJwt({ 'Full Name': 'Alice' });
352
- const { promise, ws } = await startVerifyAndGetWs(client, null, true);
353
- ws.simulateMessage({ jwt: fakeJwt });
354
- const result = await promise;
355
- expect(result).toEqual({ jwt: fakeJwt });
356
- });
357
-
358
- it('jwt message closes WebSocket', async () => {
359
- const fakeJwt = buildFakeJwt({});
360
- const { promise, ws } = await startVerifyAndGetWs(client);
361
- ws.simulateMessage({ jwt: fakeJwt });
362
- await promise;
363
- expect(ws.readyState).toBe(3); // CLOSED
364
- });
365
-
366
- it('error message resolves with error presence', async () => {
367
- document.body.innerHTML = '<div id="test-target"></div>';
368
- const c = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
369
- const { promise, ws } = await startVerifyAndGetWs(c, 'test-target');
370
- ws.simulateMessage({ error: 'Something went wrong' });
371
- const result = await promise;
372
- expect(result).toEqual({ presence: { score: 'OFFLINE', msg: 'Something went wrong' } });
373
- });
374
-
375
- it('url message calls window.location.assign', async () => {
376
- const { promise, ws } = await startVerifyAndGetWs(client);
377
- ws.simulateMessage({ url: 'https://example.com/redirect' });
378
- expect(window.location.assign).toHaveBeenCalledWith('https://example.com/redirect');
379
- ws.simulateMessage({ jwt: 'cleanup' });
380
- await promise.catch(() => {});
381
- });
382
-
383
- it('status message resets QR div to spinner', async () => {
384
- const { promise, ws } = await startVerifyAndGetWs(client);
385
- // First send a qr_url to populate the div
386
- ws.simulateMessage({ qr_url: 'https://app.polyguard.ai/qr?pcre=abc123' });
387
- const qrDiv = document.querySelector('#polyguard-qr');
388
- expect(qrDiv.innerHTML).toContain('mock-qr');
389
- // Then status message should reset to spinner
390
- ws.simulateMessage({ status: 'pending' });
391
- expect(qrDiv.innerHTML).toContain('spinner-circle');
392
- ws.simulateMessage({ jwt: 'cleanup' });
393
- await promise.catch(() => {});
394
- });
395
-
396
- it('unknown message type resolves with error', async () => {
397
- document.body.innerHTML = '<div id="test-target"></div>';
398
- const c = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
399
- const { promise, ws } = await startVerifyAndGetWs(c, 'test-target');
400
- ws.simulateMessage({ unknown_field: true });
401
- const result = await promise;
402
- expect(result).toEqual({ presence: { score: 'OFFLINE', msg: 'Unknown message type from server' } });
403
- });
404
-
405
- it('qr_url extracts pcre param and sends pong', async () => {
406
- const { promise, ws } = await startVerifyAndGetWs(client);
407
- ws.simulateMessage({ qr_url: 'https://app.polyguard.ai/qr?pcre=xyz789&other=1' });
408
- const pongMsg = ws.sent.find(s => {
409
- const parsed = JSON.parse(s);
410
- return parsed.type === 'pong' && parsed.seq === 'xyz789';
411
- });
412
- expect(pongMsg).toBeDefined();
413
- ws.simulateMessage({ jwt: 'cleanup' });
414
- await promise.catch(() => {});
415
- });
416
- });
417
-
418
- // ---------- verify - modal vs target mode ----------
419
- describe('verify - modal vs target mode', () => {
420
- it('target mode renders into specified element', async () => {
421
- document.body.innerHTML = '<div id="my-target"></div>';
422
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
423
- const { promise, ws } = await startVerifyAndGetWs(client, 'my-target');
424
- const target = document.getElementById('my-target');
425
- expect(target.innerHTML).toContain('spinner-circle');
426
- ws.simulateMessage({ jwt: 'cleanup' });
427
- await promise.catch(() => {});
428
- });
429
-
430
- it('target mode throws if element not found', async () => {
431
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
432
- await expect(client.verify('nonexistent-id')).rejects.toThrow("Target element with ID 'nonexistent-id' not found");
433
- });
434
-
435
- it('modal mode appends to document.body', async () => {
436
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
437
- const { promise, ws } = await startVerifyAndGetWs(client);
438
- expect(document.querySelector('#polyguard-modal-content')).not.toBeNull();
439
- ws.simulateMessage({ jwt: 'cleanup' });
440
- await promise.catch(() => {});
441
- });
442
-
443
- it('close button rejects with User cancelled', async () => {
444
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
445
- const { promise } = await startVerifyAndGetWs(client);
446
- document.querySelector('#polyguard-modal-close').click();
447
- await expect(promise).rejects.toThrow('User cancelled');
448
- });
449
-
450
- it('cancel button rejects with User cancelled', async () => {
451
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
452
- const { promise } = await startVerifyAndGetWs(client);
453
- document.querySelector('#polyguard-modal-cancel').click();
454
- await expect(promise).rejects.toThrow('User cancelled');
455
- });
456
-
457
- it('jwt message cleans up modal from DOM', async () => {
458
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
459
- const { promise, ws } = await startVerifyAndGetWs(client);
460
- expect(document.querySelector('#polyguard-modal-content')).not.toBeNull();
461
- ws.simulateMessage({ jwt: buildFakeJwt({}) });
462
- await promise;
463
- expect(document.querySelector('#polyguard-modal-content')).toBeNull();
464
- });
465
- });
466
-
467
- // ---------- verify - mobile detection ----------
468
- describe('verify - mobile detection', () => {
469
- it('mobile UA renders button instead of QR code', async () => {
470
- setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148');
471
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
472
- const { promise, ws } = await startVerifyAndGetWs(client);
473
- ws.simulateMessage({ qr_url: 'https://app.polyguard.ai/qr?pcre=abc' });
474
- const btn = document.querySelector('#polyguard-open-app-button');
475
- expect(btn).not.toBeNull();
476
- expect(btn.textContent).toBe('Open Polyguard App');
477
- ws.simulateMessage({ jwt: 'cleanup' });
478
- await promise.catch(() => {});
479
- });
480
-
481
- it('desktop UA calls QRCode.toString', async () => {
482
- setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)');
483
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
484
- const { promise, ws } = await startVerifyAndGetWs(client);
485
- ws.simulateMessage({ qr_url: 'https://app.polyguard.ai/qr?pcre=abc' });
486
- expect(mockQRCode.toString).toHaveBeenCalledWith(
487
- 'https://app.polyguard.ai/qr?pcre=abc',
488
- { type: 'svg' },
489
- expect.any(Function),
490
- );
491
- ws.simulateMessage({ jwt: 'cleanup' });
492
- await promise.catch(() => {});
493
- });
494
-
495
- it('mobile button click calls window.location.assign with qr_url', async () => {
496
- setUserAgent('Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100 Mobile');
497
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
498
- const { promise, ws } = await startVerifyAndGetWs(client);
499
- ws.simulateMessage({ qr_url: 'https://app.polyguard.ai/qr?pcre=abc' });
500
- const btn = document.querySelector('#polyguard-open-app-button');
501
- btn.click();
502
- expect(window.location.assign).toHaveBeenCalledWith('https://app.polyguard.ai/qr?pcre=abc');
503
- ws.simulateMessage({ jwt: 'cleanup' });
504
- await promise.catch(() => {});
505
- });
506
-
507
- it('Android UA also detected as mobile', async () => {
508
- setUserAgent('Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36');
509
- const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
510
- const { promise, ws } = await startVerifyAndGetWs(client);
511
- ws.simulateMessage({ qr_url: 'https://app.polyguard.ai/qr?pcre=abc' });
512
- expect(document.querySelector('#polyguard-open-app-button')).not.toBeNull();
513
- ws.simulateMessage({ jwt: 'cleanup' });
514
- await promise.catch(() => {});
515
- });
516
- });
517
-
518
- // ---------- require ----------
519
- describe('require', () => {
520
- let client;
521
-
522
- beforeEach(() => {
523
- client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
524
- });
525
-
526
- it('returns true when all proofs match JWT payload', async () => {
527
- const jwt = buildFakeJwt({ 'Full Name': 'Alice Smith', 'Email': 'alice@example.com' });
528
- vi.spyOn(client, 'verify').mockResolvedValue(jwt);
529
- const result = await client.require({ 'Full Name': 'Alice Smith', 'Email': 'alice@example.com' });
530
- expect(result).toBe(true);
531
- });
532
-
533
- it('returns false on proof value mismatch', async () => {
534
- const jwt = buildFakeJwt({ 'Full Name': 'Alice Smith' });
535
- vi.spyOn(client, 'verify').mockResolvedValue(jwt);
536
- const result = await client.require({ 'Full Name': 'Bob Jones' });
537
- expect(result).toBe(false);
538
- });
539
-
540
- it('returns false when expected key is missing from JWT', async () => {
541
- const jwt = buildFakeJwt({ 'Full Name': 'Alice' });
542
- vi.spyOn(client, 'verify').mockResolvedValue(jwt);
543
- const result = await client.require({ 'Email': 'alice@example.com' });
544
- expect(result).toBe(false);
545
- });
546
-
547
- it('returns false when verify returns falsy JWT', async () => {
548
- vi.spyOn(client, 'verify').mockResolvedValue(null);
549
- const result = await client.require({ 'Full Name': 'Alice' });
550
- expect(result).toBe(false);
551
- });
552
-
553
- it('returns false when verify rejects (cancellation)', async () => {
554
- vi.spyOn(client, 'verify').mockRejectedValue(new Error('User cancelled'));
555
- const result = await client.require({ 'Full Name': 'Alice' });
556
- expect(result).toBe(false);
557
- });
558
-
559
- it('correctly decodes base64 JWT payload', async () => {
560
- const payload = { 'Full Name': 'Test User', score: 99 };
561
- const jwt = buildFakeJwt(payload);
562
- vi.spyOn(client, 'verify').mockResolvedValue(jwt);
563
- const result = await client.require({ 'Full Name': 'Test User', score: 99 });
564
- expect(result).toBe(true);
565
- });
566
-
567
- it('passes target through to verify', async () => {
568
- const jwt = buildFakeJwt({ 'Full Name': 'Alice' });
569
- const spy = vi.spyOn(client, 'verify').mockResolvedValue(jwt);
570
- await client.require({ 'Full Name': 'Alice' }, 'my-element');
571
- expect(spy).toHaveBeenCalledWith('my-element');
572
- });
573
- });
574
- });
@@ -1,27 +0,0 @@
1
- /**
2
- * Build a fake JWT with the given payload.
3
- * Structure: header.payload.signature (base64url encoded)
4
- */
5
- export function buildFakeJwt(payload = {}) {
6
- const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
7
- const body = btoa(JSON.stringify(payload));
8
- const sig = btoa('fakesignature');
9
- return `${header}.${body}.${sig}`;
10
- }
11
-
12
- export const DEFAULT_PARAMS = {
13
- appId: 'test-app-id',
14
- apiServer: 'api.test.polyguard.ai',
15
- requiredProofs: ['Full Name', 'Email'],
16
- scanType: 'single',
17
- };
18
-
19
- /**
20
- * Create a mock Response for the ticket fetch.
21
- */
22
- export function mockTicketResponse(ticket = 'test-ticket-123', ok = true) {
23
- return {
24
- ok,
25
- json: async () => (ticket ? { ticket } : {}),
26
- };
27
- }
@@ -1,66 +0,0 @@
1
- /**
2
- * Mock for reconnecting-websocket.
3
- * Captures the URL/protocols/options passed to the constructor and
4
- * exposes helpers to drive the message handler under test.
5
- */
6
- export class MockReconnectingWebSocket {
7
- static instances = [];
8
-
9
- constructor(url, protocols, options) {
10
- this.url = url;
11
- this.protocols = protocols;
12
- this.options = options;
13
- this._listeners = {};
14
- this.readyState = 1; // OPEN
15
- this.sent = [];
16
- MockReconnectingWebSocket.instances.push(this);
17
- }
18
-
19
- addEventListener(type, handler) {
20
- if (!this._listeners[type]) this._listeners[type] = [];
21
- this._listeners[type].push(handler);
22
- }
23
-
24
- removeEventListener(type, handler) {
25
- if (!this._listeners[type]) return;
26
- this._listeners[type] = this._listeners[type].filter(h => h !== handler);
27
- }
28
-
29
- send(data) {
30
- this.sent.push(data);
31
- }
32
-
33
- close() {
34
- this.readyState = 3; // CLOSED
35
- this._emit('close', {});
36
- }
37
-
38
- // --- Test helpers ---
39
-
40
- simulateMessage(data) {
41
- this._emit('message', { data: JSON.stringify(data) });
42
- }
43
-
44
- simulateError() {
45
- this._emit('error', {});
46
- }
47
-
48
- simulateClose() {
49
- this._emit('close', {});
50
- }
51
-
52
- _emit(type, event) {
53
- const handlers = this._listeners[type] || [];
54
- for (const h of handlers) {
55
- h(event);
56
- }
57
- }
58
-
59
- static reset() {
60
- MockReconnectingWebSocket.instances = [];
61
- }
62
-
63
- static latest() {
64
- return MockReconnectingWebSocket.instances[MockReconnectingWebSocket.instances.length - 1];
65
- }
66
- }