@polyguard/sdk 1.3.1 → 1.4.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 CHANGED
@@ -10839,7 +10839,7 @@ var reconnecting_websocket_mjs_default = ReconnectingWebSocket;
10839
10839
 
10840
10840
  // src/ui.js
10841
10841
  var import_qrcode = __toESM(require_browser(), 1);
10842
- var LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 200 200" fill="none">
10842
+ var LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="none">
10843
10843
  <style>
10844
10844
  .spinner-circle {
10845
10845
  animation: spin 1s linear infinite;
@@ -10868,12 +10868,13 @@ function buildModal() {
10868
10868
  modal.style.justifyContent = "center";
10869
10869
  modal.style.overflowY = "auto";
10870
10870
  modal.style.paddingTop = "24px";
10871
+ modal.style.zIndex = "2147483647";
10871
10872
  modal.innerHTML = `
10872
- <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: 26px 19px 19px 19px; max-width: 272px; width: 100%; text-align: center; position: relative; font-family: 'IBM Plex Sans', 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
10873
+ <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: 26px 19px 19px 19px; max-width: 312px; width: 100%; text-align: center; position: relative; z-index: 2147483647; font-family: 'IBM Plex Sans', 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
10873
10874
  <button id="polyguard-modal-close" style="position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 18px; color: #222; cursor: pointer; z-index: 2;">&times;</button>
10874
10875
  <h2 style="margin-top: 0; font-size: 1.04rem; font-weight: 700; color: #222;">Quick Identity Verification</h2>
10875
10876
  <div style="font-size: 10px; color: #888; margin-bottom: 10px; font-weight: 500;">Powered by Polyguard</div>
10876
- <div id="polyguard-qr" style="width: 160px; height: 160px; margin: 0 auto 13px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 10px;"></div>
10877
+ <div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 13px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 10px;"></div>
10877
10878
  <div style="margin-bottom: 10px; font-weight: 600; font-size: 13px; color: #222;">Scan this QR code to verify your identity.</div>
10878
10879
  <ul style="text-align: left; margin: 0 0 13px 0; padding-left: 16px; font-size: 11px; color: #444;">
10879
10880
  <li>We use the Polyguard service to verify your identity.</li>
@@ -10979,6 +10980,35 @@ function handleWebSocketMessage(event, ctx) {
10979
10980
  return;
10980
10981
  }
10981
10982
  if (data.jwt) {
10983
+ const redirectUrl = data.redirect_url;
10984
+ const { sidebarUrl, link_uuid } = ctx;
10985
+ if (sidebarUrl && redirectUrl) {
10986
+ const hasMsftSession = document.cookie.includes("pg_msft_session");
10987
+ cleanup();
10988
+ ws.close();
10989
+ if (hasMsftSession) {
10990
+ window.location.assign(redirectUrl);
10991
+ resolve(rawJwt ? data : data.jwt);
10992
+ } else {
10993
+ const targetEl = qrDiv || document.body;
10994
+ const btn = document.createElement("button");
10995
+ btn.id = "polyguard-join-meeting";
10996
+ btn.textContent = "Join Meeting";
10997
+ btn.style.cssText = "padding:12px 24px;font-size:16px;cursor:pointer;border:none;border-radius:8px;background:#4CAF50;color:white;margin:16px auto;display:block;";
10998
+ targetEl.appendChild(btn);
10999
+ btn.addEventListener("click", () => {
11000
+ const sidebarFullUrl = link_uuid ? `${sidebarUrl}?linkUuid=${link_uuid}` : sidebarUrl;
11001
+ window.open(
11002
+ sidebarFullUrl,
11003
+ "polyguard-sidebar",
11004
+ "width=320,height=900,top=0,left=" + (window.screen.availWidth - 320)
11005
+ );
11006
+ window.location.assign(redirectUrl);
11007
+ resolve(rawJwt ? data : data.jwt);
11008
+ });
11009
+ }
11010
+ return;
11011
+ }
10982
11012
  cleanup();
10983
11013
  ws.close();
10984
11014
  resolve(rawJwt ? data : data.jwt);
@@ -11002,7 +11032,8 @@ function handleWebSocketMessage(event, ctx) {
11002
11032
  var PolyguardWebsocketClientImpl = class {
11003
11033
  constructor(params = {}) {
11004
11034
  this.apiClient = new ApiClient_default();
11005
- const { apiKey, baseUrl, appId, apiServer, requiredProofs = ["Full Name"], scanType = "single", redirectUrl, callbackUrl, cookieName, link_uuid } = params;
11035
+ const { apiKey, baseUrl, appId, apiServer, requiredProofs = ["Full Name"], scanType = "single", redirectUrl, callbackUrl, cookieName, link_uuid, sidebarUrl } = params;
11036
+ this.sidebarUrl = sidebarUrl;
11006
11037
  if (baseUrl) {
11007
11038
  this.apiClient.basePath = baseUrl;
11008
11039
  }
@@ -11114,7 +11145,7 @@ var PolyguardWebsocketClientImpl = class {
11114
11145
  reconnectionDelayGrowFactor: 1.5
11115
11146
  };
11116
11147
  ws = new reconnecting_websocket_mjs_default(wsUrl, [], options);
11117
- const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt };
11148
+ const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt, sidebarUrl: this.sidebarUrl, link_uuid: this.link_uuid };
11118
11149
  ws.addEventListener("message", (event) => handleWebSocketMessage(event, ctx));
11119
11150
  ws.addEventListener("error", () => {
11120
11151
  console.error("WebSocket error");
package/dist/sdk.js CHANGED
@@ -10872,7 +10872,7 @@ var Polyguard = (() => {
10872
10872
 
10873
10873
  // src/ui.js
10874
10874
  var import_qrcode = __toESM(require_browser(), 1);
10875
- var LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 200 200" fill="none">
10875
+ var LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="none">
10876
10876
  <style>
10877
10877
  .spinner-circle {
10878
10878
  animation: spin 1s linear infinite;
@@ -10901,12 +10901,13 @@ var Polyguard = (() => {
10901
10901
  modal.style.justifyContent = "center";
10902
10902
  modal.style.overflowY = "auto";
10903
10903
  modal.style.paddingTop = "24px";
10904
+ modal.style.zIndex = "2147483647";
10904
10905
  modal.innerHTML = `
10905
- <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: 26px 19px 19px 19px; max-width: 272px; width: 100%; text-align: center; position: relative; font-family: 'IBM Plex Sans', 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
10906
+ <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: 26px 19px 19px 19px; max-width: 312px; width: 100%; text-align: center; position: relative; z-index: 2147483647; font-family: 'IBM Plex Sans', 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
10906
10907
  <button id="polyguard-modal-close" style="position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 18px; color: #222; cursor: pointer; z-index: 2;">&times;</button>
10907
10908
  <h2 style="margin-top: 0; font-size: 1.04rem; font-weight: 700; color: #222;">Quick Identity Verification</h2>
10908
10909
  <div style="font-size: 10px; color: #888; margin-bottom: 10px; font-weight: 500;">Powered by Polyguard</div>
10909
- <div id="polyguard-qr" style="width: 160px; height: 160px; margin: 0 auto 13px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 10px;"></div>
10910
+ <div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 13px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 10px;"></div>
10910
10911
  <div style="margin-bottom: 10px; font-weight: 600; font-size: 13px; color: #222;">Scan this QR code to verify your identity.</div>
10911
10912
  <ul style="text-align: left; margin: 0 0 13px 0; padding-left: 16px; font-size: 11px; color: #444;">
10912
10913
  <li>We use the Polyguard service to verify your identity.</li>
@@ -11012,6 +11013,35 @@ var Polyguard = (() => {
11012
11013
  return;
11013
11014
  }
11014
11015
  if (data.jwt) {
11016
+ const redirectUrl = data.redirect_url;
11017
+ const { sidebarUrl, link_uuid } = ctx;
11018
+ if (sidebarUrl && redirectUrl) {
11019
+ const hasMsftSession = document.cookie.includes("pg_msft_session");
11020
+ cleanup();
11021
+ ws.close();
11022
+ if (hasMsftSession) {
11023
+ window.location.assign(redirectUrl);
11024
+ resolve(rawJwt ? data : data.jwt);
11025
+ } else {
11026
+ const targetEl = qrDiv || document.body;
11027
+ const btn = document.createElement("button");
11028
+ btn.id = "polyguard-join-meeting";
11029
+ btn.textContent = "Join Meeting";
11030
+ btn.style.cssText = "padding:12px 24px;font-size:16px;cursor:pointer;border:none;border-radius:8px;background:#4CAF50;color:white;margin:16px auto;display:block;";
11031
+ targetEl.appendChild(btn);
11032
+ btn.addEventListener("click", () => {
11033
+ const sidebarFullUrl = link_uuid ? `${sidebarUrl}?linkUuid=${link_uuid}` : sidebarUrl;
11034
+ window.open(
11035
+ sidebarFullUrl,
11036
+ "polyguard-sidebar",
11037
+ "width=320,height=900,top=0,left=" + (window.screen.availWidth - 320)
11038
+ );
11039
+ window.location.assign(redirectUrl);
11040
+ resolve(rawJwt ? data : data.jwt);
11041
+ });
11042
+ }
11043
+ return;
11044
+ }
11015
11045
  cleanup();
11016
11046
  ws.close();
11017
11047
  resolve(rawJwt ? data : data.jwt);
@@ -11035,7 +11065,8 @@ var Polyguard = (() => {
11035
11065
  var PolyguardWebsocketClientImpl = class {
11036
11066
  constructor(params = {}) {
11037
11067
  this.apiClient = new ApiClient_default();
11038
- const { apiKey, baseUrl, appId, apiServer, requiredProofs = ["Full Name"], scanType = "single", redirectUrl, callbackUrl, cookieName, link_uuid } = params;
11068
+ const { apiKey, baseUrl, appId, apiServer, requiredProofs = ["Full Name"], scanType = "single", redirectUrl, callbackUrl, cookieName, link_uuid, sidebarUrl } = params;
11069
+ this.sidebarUrl = sidebarUrl;
11039
11070
  if (baseUrl) {
11040
11071
  this.apiClient.basePath = baseUrl;
11041
11072
  }
@@ -11147,7 +11178,7 @@ var Polyguard = (() => {
11147
11178
  reconnectionDelayGrowFactor: 1.5
11148
11179
  };
11149
11180
  ws = new reconnecting_websocket_mjs_default(wsUrl, [], options);
11150
- const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt };
11181
+ const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt, sidebarUrl: this.sidebarUrl, link_uuid: this.link_uuid };
11151
11182
  ws.addEventListener("message", (event) => handleWebSocketMessage(event, ctx));
11152
11183
  ws.addEventListener("error", () => {
11153
11184
  console.error("WebSocket error");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polyguard/sdk",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "main": "dist/sdk.esm.js",
6
6
  "module": "dist/sdk.esm.js",
@@ -8,7 +8,8 @@ import { handleWebSocketMessage } from './messageHandler.js';
8
8
  export class PolyguardWebsocketClientImpl {
9
9
  constructor(params = {}) {
10
10
  this.apiClient = new PolyguardApi.ApiClient();
11
- const { apiKey, baseUrl, appId, apiServer, requiredProofs = ['Full Name'], scanType = 'single', redirectUrl, callbackUrl, cookieName, link_uuid } = params;
11
+ const { apiKey, baseUrl, appId, apiServer, requiredProofs = ['Full Name'], scanType = 'single', redirectUrl, callbackUrl, cookieName, link_uuid, sidebarUrl } = params;
12
+ this.sidebarUrl = sidebarUrl;
12
13
  if (baseUrl) {
13
14
  this.apiClient.basePath = baseUrl;
14
15
  }
@@ -135,7 +136,7 @@ export class PolyguardWebsocketClientImpl {
135
136
  };
136
137
  ws = new ReconnectingWebSocket(wsUrl, [], options);
137
138
 
138
- const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt };
139
+ const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt, sidebarUrl: this.sidebarUrl, link_uuid: this.link_uuid };
139
140
  ws.addEventListener('message', (event) => handleWebSocketMessage(event, ctx));
140
141
 
141
142
  ws.addEventListener('error', () => {
@@ -0,0 +1,265 @@
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 ----
6
+
7
+ const { MockApiClient, generatedMock, mockQRCode } = vi.hoisted(() => {
8
+ class MockApiClient {
9
+ constructor() {
10
+ this.basePath = '';
11
+ this.authentications = { bearerAuth: { apiKey: null } };
12
+ }
13
+ }
14
+
15
+ const generatedMock = { ApiClient: MockApiClient };
16
+ const apiClassNames = [
17
+ 'BlockingApi', 'CallsApi', 'DefaultApi', 'LinksApi', 'SdkApi',
18
+ 'SecureLinksApi', 'StirApi', 'TwilioApi', 'UsersApi', 'VeriffApi',
19
+ 'WellKnownApi', 'ZoomApi',
20
+ ];
21
+ for (const name of apiClassNames) {
22
+ generatedMock[name] = class {
23
+ constructor(apiClient) { this._apiClient = apiClient; }
24
+ };
25
+ }
26
+
27
+ const mockQRCode = {
28
+ toString: null,
29
+ };
30
+
31
+ return { MockApiClient, generatedMock, mockQRCode };
32
+ });
33
+
34
+ vi.mock('../generated/src', () => generatedMock);
35
+
36
+ vi.mock('reconnecting-websocket', async () => {
37
+ const { MockReconnectingWebSocket } = await import('./helpers/mockWebSocket.js');
38
+ return { default: MockReconnectingWebSocket };
39
+ });
40
+
41
+ vi.mock('qrcode', () => ({
42
+ default: mockQRCode,
43
+ }));
44
+
45
+ const { PolyguardWebsocketClientImpl } = await import('../PolyguardWebsocketClientImpl.js');
46
+
47
+ // ---- Helpers ----
48
+
49
+ async function flushMicrotasks() {
50
+ await new Promise(resolve => setTimeout(resolve, 0));
51
+ }
52
+
53
+ function stubFetch(ticket = 'test-ticket-123', ok = true) {
54
+ const mock = vi.fn().mockResolvedValue(mockTicketResponse(ticket, ok));
55
+ vi.stubGlobal('fetch', mock);
56
+ return mock;
57
+ }
58
+
59
+ function setProtocol(protocol) {
60
+ Object.defineProperty(window, 'location', {
61
+ value: { protocol, assign: vi.fn(), href: '' },
62
+ writable: true,
63
+ configurable: true,
64
+ });
65
+ }
66
+
67
+ function setUserAgent(ua) {
68
+ Object.defineProperty(navigator, 'userAgent', {
69
+ value: ua,
70
+ writable: true,
71
+ configurable: true,
72
+ });
73
+ }
74
+
75
+ async function startVerifyAndGetWs(client, target = null, rawJwt = false) {
76
+ const promise = client.verify(target, rawJwt);
77
+ await flushMicrotasks();
78
+ const ws = MockReconnectingWebSocket.latest();
79
+ return { promise, ws };
80
+ }
81
+
82
+ function setCookie(name, value) {
83
+ Object.defineProperty(document, 'cookie', {
84
+ value: `${name}=${value}; other=stuff`,
85
+ writable: true,
86
+ configurable: true,
87
+ });
88
+ }
89
+
90
+ function clearCookies() {
91
+ Object.defineProperty(document, 'cookie', {
92
+ value: '',
93
+ writable: true,
94
+ configurable: true,
95
+ });
96
+ }
97
+
98
+ // ---- Tests ----
99
+
100
+ describe('sidebarUrl feature', () => {
101
+ beforeEach(() => {
102
+ vi.clearAllMocks();
103
+ MockReconnectingWebSocket.reset();
104
+ setProtocol('https:');
105
+ setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)');
106
+ stubFetch();
107
+ mockQRCode.toString = vi.fn((data, opts, cb) => cb(null, '<svg>mock-qr</svg>'));
108
+ clearCookies();
109
+ });
110
+
111
+ afterEach(() => {
112
+ vi.restoreAllMocks();
113
+ document.body.innerHTML = '';
114
+ });
115
+
116
+ describe('constructor', () => {
117
+ it('stores sidebarUrl when provided', () => {
118
+ const client = new PolyguardWebsocketClientImpl({
119
+ ...DEFAULT_PARAMS,
120
+ sidebarUrl: 'https://teams.polyguard.ai/ms-teams/sidebar-standalone',
121
+ });
122
+ expect(client.sidebarUrl).toBe('https://teams.polyguard.ai/ms-teams/sidebar-standalone');
123
+ });
124
+
125
+ it('sidebarUrl is undefined when not provided', () => {
126
+ const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
127
+ expect(client.sidebarUrl).toBeUndefined();
128
+ });
129
+ });
130
+
131
+ describe('without sidebarUrl - existing behavior unchanged', () => {
132
+ it('jwt with redirect_url resolves normally', async () => {
133
+ const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
134
+ const fakeJwt = buildFakeJwt({ redirect_url: 'https://teams.microsoft.com/meet/123' });
135
+ const { promise, ws } = await startVerifyAndGetWs(client);
136
+ ws.simulateMessage({ jwt: fakeJwt });
137
+ const result = await promise;
138
+ expect(result).toBe(fakeJwt);
139
+ });
140
+
141
+ it('does not call window.open', async () => {
142
+ const openSpy = vi.fn();
143
+ vi.stubGlobal('open', openSpy);
144
+ const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
145
+ const fakeJwt = buildFakeJwt({ redirect_url: 'https://teams.microsoft.com/meet/123' });
146
+ const { promise, ws } = await startVerifyAndGetWs(client);
147
+ ws.simulateMessage({ jwt: fakeJwt });
148
+ await promise;
149
+ expect(openSpy).not.toHaveBeenCalled();
150
+ });
151
+ });
152
+
153
+ describe('with sidebarUrl and pg_msft_session cookie (authenticated user)', () => {
154
+ it('auto-redirects without showing a button', async () => {
155
+ setCookie('pg_msft_session', '1');
156
+ const client = new PolyguardWebsocketClientImpl({
157
+ ...DEFAULT_PARAMS,
158
+ sidebarUrl: 'https://teams.polyguard.ai/ms-teams/sidebar-standalone',
159
+ link_uuid: 'link-abc',
160
+ });
161
+ document.body.innerHTML = '<div id="test-target"></div>';
162
+ const fakeJwt = buildFakeJwt({ redirect_url: 'https://teams.microsoft.com/meet/123' });
163
+ const { promise, ws } = await startVerifyAndGetWs(client, 'test-target');
164
+ ws.simulateMessage({ jwt: fakeJwt, redirect_url: 'https://teams.microsoft.com/meet/123' });
165
+ await promise;
166
+ expect(window.location.assign).toHaveBeenCalledWith('https://teams.microsoft.com/meet/123');
167
+ });
168
+
169
+ it('does not render a join button', async () => {
170
+ setCookie('pg_msft_session', '1');
171
+ const client = new PolyguardWebsocketClientImpl({
172
+ ...DEFAULT_PARAMS,
173
+ sidebarUrl: 'https://teams.polyguard.ai/ms-teams/sidebar-standalone',
174
+ link_uuid: 'link-abc',
175
+ });
176
+ document.body.innerHTML = '<div id="test-target"></div>';
177
+ const fakeJwt = buildFakeJwt({ redirect_url: 'https://teams.microsoft.com/meet/123' });
178
+ const { promise, ws } = await startVerifyAndGetWs(client, 'test-target');
179
+ ws.simulateMessage({ jwt: fakeJwt, redirect_url: 'https://teams.microsoft.com/meet/123' });
180
+ await promise;
181
+ expect(document.querySelector('#polyguard-join-meeting')).toBeNull();
182
+ });
183
+ });
184
+
185
+ describe('with sidebarUrl and NO cookie (anonymous user)', () => {
186
+ it('renders a join button instead of auto-redirecting', async () => {
187
+ clearCookies();
188
+ const client = new PolyguardWebsocketClientImpl({
189
+ ...DEFAULT_PARAMS,
190
+ sidebarUrl: 'https://teams.polyguard.ai/ms-teams/sidebar-standalone',
191
+ link_uuid: 'link-abc',
192
+ });
193
+ document.body.innerHTML = '<div id="test-target"></div>';
194
+ const fakeJwt = buildFakeJwt({ redirect_url: 'https://teams.microsoft.com/meet/123' });
195
+ const { promise, ws } = await startVerifyAndGetWs(client, 'test-target');
196
+ ws.simulateMessage({ jwt: fakeJwt, redirect_url: 'https://teams.microsoft.com/meet/123' });
197
+ // Should not have redirected yet
198
+ expect(window.location.assign).not.toHaveBeenCalled();
199
+ // Should show join button
200
+ const btn = document.querySelector('#polyguard-join-meeting');
201
+ expect(btn).not.toBeNull();
202
+ // Resolve by clicking the button
203
+ btn.click();
204
+ await promise;
205
+ });
206
+
207
+ it('clicking join button opens sidebar popup', async () => {
208
+ clearCookies();
209
+ const openSpy = vi.fn().mockReturnValue({});
210
+ vi.stubGlobal('open', openSpy);
211
+ const client = new PolyguardWebsocketClientImpl({
212
+ ...DEFAULT_PARAMS,
213
+ sidebarUrl: 'https://teams.polyguard.ai/ms-teams/sidebar-standalone',
214
+ link_uuid: 'link-abc',
215
+ });
216
+ document.body.innerHTML = '<div id="test-target"></div>';
217
+ const fakeJwt = buildFakeJwt({ redirect_url: 'https://teams.microsoft.com/meet/123' });
218
+ const { promise, ws } = await startVerifyAndGetWs(client, 'test-target');
219
+ ws.simulateMessage({ jwt: fakeJwt, redirect_url: 'https://teams.microsoft.com/meet/123' });
220
+ const btn = document.querySelector('#polyguard-join-meeting');
221
+ btn.click();
222
+ await promise;
223
+ expect(openSpy).toHaveBeenCalledWith(
224
+ 'https://teams.polyguard.ai/ms-teams/sidebar-standalone?linkUuid=link-abc',
225
+ 'polyguard-sidebar',
226
+ expect.stringContaining('width=320')
227
+ );
228
+ });
229
+
230
+ it('clicking join button redirects to meeting', async () => {
231
+ clearCookies();
232
+ vi.stubGlobal('open', vi.fn().mockReturnValue({}));
233
+ const client = new PolyguardWebsocketClientImpl({
234
+ ...DEFAULT_PARAMS,
235
+ sidebarUrl: 'https://teams.polyguard.ai/ms-teams/sidebar-standalone',
236
+ link_uuid: 'link-abc',
237
+ });
238
+ document.body.innerHTML = '<div id="test-target"></div>';
239
+ const fakeJwt = buildFakeJwt({ redirect_url: 'https://teams.microsoft.com/meet/123' });
240
+ const { promise, ws } = await startVerifyAndGetWs(client, 'test-target');
241
+ ws.simulateMessage({ jwt: fakeJwt, redirect_url: 'https://teams.microsoft.com/meet/123' });
242
+ document.querySelector('#polyguard-join-meeting').click();
243
+ await promise;
244
+ expect(window.location.assign).toHaveBeenCalledWith('https://teams.microsoft.com/meet/123');
245
+ });
246
+ });
247
+
248
+ describe('with sidebarUrl but no redirect_url in JWT (non-meeting links)', () => {
249
+ it('resolves normally without button or redirect', async () => {
250
+ clearCookies();
251
+ const client = new PolyguardWebsocketClientImpl({
252
+ ...DEFAULT_PARAMS,
253
+ sidebarUrl: 'https://teams.polyguard.ai/ms-teams/sidebar-standalone',
254
+ link_uuid: 'link-abc',
255
+ });
256
+ const fakeJwt = buildFakeJwt({ confirmation_code: 'ABC123' });
257
+ const { promise, ws } = await startVerifyAndGetWs(client);
258
+ // No redirect_url in the message data
259
+ ws.simulateMessage({ jwt: fakeJwt });
260
+ const result = await promise;
261
+ expect(result).toBe(fakeJwt);
262
+ expect(window.location.assign).not.toHaveBeenCalled();
263
+ });
264
+ });
265
+ });
@@ -54,6 +54,45 @@ export function handleWebSocketMessage(event, ctx) {
54
54
  }
55
55
 
56
56
  if (data.jwt) {
57
+ const redirectUrl = data.redirect_url;
58
+ const { sidebarUrl, link_uuid } = ctx;
59
+
60
+ // When sidebarUrl is configured and the server provides a redirect_url,
61
+ // the SDK handles the redirect (and optionally opens a sidebar popup).
62
+ if (sidebarUrl && redirectUrl) {
63
+ const hasMsftSession = document.cookie.includes('pg_msft_session');
64
+ cleanup();
65
+ ws.close();
66
+
67
+ if (hasMsftSession) {
68
+ // Authenticated user — redirect directly, they'll see the in-Teams sidepanel
69
+ window.location.assign(redirectUrl);
70
+ resolve(rawJwt ? data : data.jwt);
71
+ } else {
72
+ // Anonymous user — show a "Join Meeting" button so the click provides
73
+ // the user gesture Chrome requires for window.open()
74
+ const targetEl = qrDiv || document.body;
75
+ const btn = document.createElement('button');
76
+ btn.id = 'polyguard-join-meeting';
77
+ btn.textContent = 'Join Meeting';
78
+ btn.style.cssText = 'padding:12px 24px;font-size:16px;cursor:pointer;border:none;border-radius:8px;background:#4CAF50;color:white;margin:16px auto;display:block;';
79
+ targetEl.appendChild(btn);
80
+ btn.addEventListener('click', () => {
81
+ const sidebarFullUrl = link_uuid
82
+ ? `${sidebarUrl}?linkUuid=${link_uuid}`
83
+ : sidebarUrl;
84
+ window.open(
85
+ sidebarFullUrl,
86
+ 'polyguard-sidebar',
87
+ 'width=320,height=900,top=0,left=' + (window.screen.availWidth - 320)
88
+ );
89
+ window.location.assign(redirectUrl);
90
+ resolve(rawJwt ? data : data.jwt);
91
+ });
92
+ }
93
+ return;
94
+ }
95
+
57
96
  cleanup();
58
97
  ws.close();
59
98
  resolve(rawJwt ? data : data.jwt);
package/src/ui.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import QRCode from 'qrcode';
2
2
 
3
3
  // Animated spinner SVG
4
- export const LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 200 200" fill="none">
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
5
  <style>
6
6
  .spinner-circle {
7
7
  animation: spin 1s linear infinite;
@@ -31,12 +31,13 @@ export function buildModal() {
31
31
  modal.style.justifyContent = 'center';
32
32
  modal.style.overflowY = 'auto';
33
33
  modal.style.paddingTop = '24px';
34
+ modal.style.zIndex = '2147483647';
34
35
  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: 26px 19px 19px 19px; max-width: 272px; 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
+ <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: 26px 19px 19px 19px; max-width: 312px; width: 100%; text-align: center; position: relative; z-index: 2147483647; font-family: 'IBM Plex Sans', 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
36
37
  <button id="polyguard-modal-close" style="position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 18px; color: #222; cursor: pointer; z-index: 2;">&times;</button>
37
38
  <h2 style="margin-top: 0; font-size: 1.04rem; font-weight: 700; color: #222;">Quick Identity Verification</h2>
38
39
  <div style="font-size: 10px; color: #888; margin-bottom: 10px; font-weight: 500;">Powered by Polyguard</div>
39
- <div id="polyguard-qr" style="width: 160px; height: 160px; margin: 0 auto 13px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 10px;"></div>
40
+ <div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 13px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 10px;"></div>
40
41
  <div style="margin-bottom: 10px; font-weight: 600; font-size: 13px; color: #222;">Scan this QR code to verify your identity.</div>
41
42
  <ul style="text-align: left; margin: 0 0 13px 0; padding-left: 16px; font-size: 11px; color: #444;">
42
43
  <li>We use the Polyguard service to verify your identity.</li>