@polyguard/sdk 1.3.2 → 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 +34 -3
- package/dist/sdk.js +34 -3
- package/package.json +1 -1
- package/src/PolyguardWebsocketClientImpl.js +3 -2
- package/src/__tests__/sidebar.test.js +265 -0
- package/src/messageHandler.js +39 -0
- package/src/ui.js +2 -1
package/dist/sdk.esm.js
CHANGED
|
@@ -10868,8 +10868,9 @@ 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: 312px; 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;">×</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>
|
|
@@ -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
|
@@ -10901,8 +10901,9 @@ 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: 312px; 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;">×</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>
|
|
@@ -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
|
@@ -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
|
+
});
|
package/src/messageHandler.js
CHANGED
|
@@ -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
|
@@ -31,8 +31,9 @@ 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: 312px; 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;">×</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>
|