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