@polyguard/sdk 1.2.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sdk.esm.js +154 -115
- package/dist/sdk.js +154 -115
- package/package.json +1 -1
- package/src/PolyguardWebsocketClientImpl.js +40 -179
- package/src/messageHandler.js +77 -0
- package/src/ticketService.js +24 -0
- package/src/ui.js +82 -0
package/dist/sdk.esm.js
CHANGED
|
@@ -10837,7 +10837,7 @@ var ReconnectingWebSocket = (
|
|
|
10837
10837
|
);
|
|
10838
10838
|
var reconnecting_websocket_mjs_default = ReconnectingWebSocket;
|
|
10839
10839
|
|
|
10840
|
-
// src/
|
|
10840
|
+
// src/ui.js
|
|
10841
10841
|
var import_qrcode = __toESM(require_browser(), 1);
|
|
10842
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>
|
|
@@ -10851,10 +10851,149 @@ var LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="200" heigh
|
|
|
10851
10851
|
}
|
|
10852
10852
|
</style>
|
|
10853
10853
|
<circle cx="100" cy="100" r="80" stroke="#e0e0e0" stroke-width="8" fill="none" />
|
|
10854
|
-
<circle cx="100" cy="100" r="80" stroke="#
|
|
10855
|
-
stroke-dasharray="251.2" stroke-dashoffset="188.4"
|
|
10854
|
+
<circle cx="100" cy="100" r="80" stroke="#407796" stroke-width="8" fill="none"
|
|
10855
|
+
stroke-dasharray="251.2" stroke-dashoffset="188.4"
|
|
10856
10856
|
class="spinner-circle" stroke-linecap="round" />
|
|
10857
10857
|
</svg>`;
|
|
10858
|
+
function buildModal() {
|
|
10859
|
+
const modal = document.createElement("div");
|
|
10860
|
+
modal.style.position = "fixed";
|
|
10861
|
+
modal.style.top = "0";
|
|
10862
|
+
modal.style.left = "0";
|
|
10863
|
+
modal.style.width = "100vw";
|
|
10864
|
+
modal.style.height = "100vh";
|
|
10865
|
+
modal.style.background = "rgba(0,0,0,0.45)";
|
|
10866
|
+
modal.style.display = "flex";
|
|
10867
|
+
modal.style.alignItems = "flex-start";
|
|
10868
|
+
modal.style.justifyContent = "center";
|
|
10869
|
+
modal.style.overflowY = "auto";
|
|
10870
|
+
modal.style.paddingTop = "24px";
|
|
10871
|
+
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: 32px 24px 24px 24px; max-width: 340px; 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
|
+
<button id="polyguard-modal-close" style="position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 22px; color: #222; cursor: pointer; z-index: 2;">×</button>
|
|
10874
|
+
<h2 style="margin-top: 0; font-size: 1.3rem; font-weight: 700; color: #222;">Identity Verification</h2>
|
|
10875
|
+
<div style="font-size: 13px; color: #888; margin-bottom: 12px; font-weight: 500;">Powered by Polyguard</div>
|
|
10876
|
+
<div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 16px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 12px;"></div>
|
|
10877
|
+
<div style="margin-bottom: 12px; font-weight: 600; color: #222;">Scan this QR code to verify your identity.</div>
|
|
10878
|
+
<div style="display: flex; justify-content: center; gap: 12px; font-size: 12px; color: #5a6c7d; margin-bottom: 12px; flex-wrap: wrap;">
|
|
10879
|
+
<span>🔒 Encrypted</span>
|
|
10880
|
+
<span>📱 On-device</span>
|
|
10881
|
+
<span>⏲ Expires</span>
|
|
10882
|
+
</div>
|
|
10883
|
+
<div id="polyguard-error" style="color: #b31d28; font-size: 14px; margin-bottom: 8px; display: none;"></div>
|
|
10884
|
+
<button id="polyguard-modal-cancel" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer; margin-top: 8px; width: 100%; max-width: 240px;">Cancel</button>
|
|
10885
|
+
</div>
|
|
10886
|
+
`;
|
|
10887
|
+
return modal;
|
|
10888
|
+
}
|
|
10889
|
+
function showSpinner(qrDiv) {
|
|
10890
|
+
qrDiv.innerHTML = LOADING_SPINNER;
|
|
10891
|
+
}
|
|
10892
|
+
function renderMobileButton(qrDiv, qrUrl, isTargetMode) {
|
|
10893
|
+
qrDiv.innerHTML = `<button id="polyguard-open-app-button" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer;">Open Polyguard App</button>`;
|
|
10894
|
+
qrDiv.style.background = "transparent";
|
|
10895
|
+
qrDiv.querySelector("#polyguard-open-app-button").onclick = () => window.location.assign(qrUrl);
|
|
10896
|
+
if (!isTargetMode) {
|
|
10897
|
+
const instructionText = qrDiv.nextElementSibling;
|
|
10898
|
+
if (instructionText) {
|
|
10899
|
+
instructionText.textContent = "Tap the button to verify with the Polyguard app.";
|
|
10900
|
+
}
|
|
10901
|
+
const instructionList = instructionText.nextElementSibling;
|
|
10902
|
+
if (instructionList && instructionList.children[1]) {
|
|
10903
|
+
instructionList.children[1].textContent = "If you do not have the Polyguard app, you will be redirected to download it.";
|
|
10904
|
+
}
|
|
10905
|
+
}
|
|
10906
|
+
}
|
|
10907
|
+
function renderQRCode(qrDiv, qrUrl) {
|
|
10908
|
+
const startTime = Date.now();
|
|
10909
|
+
console.log("time before qr code", startTime);
|
|
10910
|
+
import_qrcode.default.toString(qrUrl, { type: "svg" }, (err, svg) => {
|
|
10911
|
+
if (!err) qrDiv.innerHTML = svg;
|
|
10912
|
+
});
|
|
10913
|
+
console.log("time to generate qr code", Date.now() - startTime);
|
|
10914
|
+
}
|
|
10915
|
+
|
|
10916
|
+
// src/ticketService.js
|
|
10917
|
+
async function fetchTicket({ apiServer, appId, link_uuid, requiredProofs, scanType }) {
|
|
10918
|
+
const ticketUrl = link_uuid ? `https://${apiServer}/v2/ticket/${appId}/${link_uuid}` : `https://${apiServer}/v2/ticket/${appId}`;
|
|
10919
|
+
const ticketRes = await fetch(ticketUrl, {
|
|
10920
|
+
method: "POST",
|
|
10921
|
+
headers: { "Content-Type": "application/json" },
|
|
10922
|
+
body: JSON.stringify({ requiredProofs, scanType })
|
|
10923
|
+
});
|
|
10924
|
+
if (!ticketRes.ok) {
|
|
10925
|
+
throw new Error("Failed to get ticket");
|
|
10926
|
+
}
|
|
10927
|
+
const ticketData = await ticketRes.json();
|
|
10928
|
+
const ticket = ticketData.ticket;
|
|
10929
|
+
if (!ticket) {
|
|
10930
|
+
throw new Error("No ticket returned from server");
|
|
10931
|
+
}
|
|
10932
|
+
return ticket;
|
|
10933
|
+
}
|
|
10934
|
+
|
|
10935
|
+
// src/messageHandler.js
|
|
10936
|
+
function handleWebSocketMessage(event, ctx) {
|
|
10937
|
+
const { ws, qrDiv, isTargetMode, cleanup, returnError, resolve, rawJwt } = ctx;
|
|
10938
|
+
let data;
|
|
10939
|
+
try {
|
|
10940
|
+
data = JSON.parse(event.data);
|
|
10941
|
+
} catch (e) {
|
|
10942
|
+
console.error("Invalid message format from server", e);
|
|
10943
|
+
returnError("Invalid message format from server");
|
|
10944
|
+
return;
|
|
10945
|
+
}
|
|
10946
|
+
if (!data) {
|
|
10947
|
+
console.error("Unknown message type from server", data);
|
|
10948
|
+
returnError("Unknown message type from server");
|
|
10949
|
+
ws.close();
|
|
10950
|
+
return;
|
|
10951
|
+
}
|
|
10952
|
+
if (data.type === "ping" && typeof data.seq !== "undefined") {
|
|
10953
|
+
ws.send(JSON.stringify({ type: "pong", seq: data.seq }));
|
|
10954
|
+
return;
|
|
10955
|
+
}
|
|
10956
|
+
if (data.url) {
|
|
10957
|
+
window.location.assign(data.url);
|
|
10958
|
+
return;
|
|
10959
|
+
}
|
|
10960
|
+
if (data.qr_url) {
|
|
10961
|
+
const pcre = data.qr_url.match(/pcre=([^&]*)/);
|
|
10962
|
+
console.log("pcre", pcre);
|
|
10963
|
+
if (pcre) {
|
|
10964
|
+
ws.send(JSON.stringify({ type: "pong", seq: pcre[1] }));
|
|
10965
|
+
}
|
|
10966
|
+
if (qrDiv) {
|
|
10967
|
+
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
|
|
10968
|
+
if (isMobile) {
|
|
10969
|
+
renderMobileButton(qrDiv, data.qr_url, isTargetMode);
|
|
10970
|
+
} else {
|
|
10971
|
+
renderQRCode(qrDiv, data.qr_url);
|
|
10972
|
+
}
|
|
10973
|
+
}
|
|
10974
|
+
return;
|
|
10975
|
+
}
|
|
10976
|
+
if (data.jwt) {
|
|
10977
|
+
cleanup();
|
|
10978
|
+
ws.close();
|
|
10979
|
+
resolve(rawJwt ? data : data.jwt);
|
|
10980
|
+
return;
|
|
10981
|
+
}
|
|
10982
|
+
if (data.status) {
|
|
10983
|
+
if (qrDiv) showSpinner(qrDiv);
|
|
10984
|
+
return;
|
|
10985
|
+
}
|
|
10986
|
+
if (data.error) {
|
|
10987
|
+
returnError(data.error);
|
|
10988
|
+
ws.close();
|
|
10989
|
+
return;
|
|
10990
|
+
}
|
|
10991
|
+
console.error("Unknown message type from server", data);
|
|
10992
|
+
returnError("Unknown message type from server");
|
|
10993
|
+
ws.close();
|
|
10994
|
+
}
|
|
10995
|
+
|
|
10996
|
+
// src/PolyguardWebsocketClientImpl.js
|
|
10858
10997
|
var PolyguardWebsocketClientImpl = class {
|
|
10859
10998
|
constructor(params = {}) {
|
|
10860
10999
|
this.apiClient = new ApiClient_default();
|
|
@@ -10884,35 +11023,7 @@ var PolyguardWebsocketClientImpl = class {
|
|
|
10884
11023
|
this.link_uuid = link_uuid;
|
|
10885
11024
|
}
|
|
10886
11025
|
buildModal() {
|
|
10887
|
-
|
|
10888
|
-
modal.style.position = "fixed";
|
|
10889
|
-
modal.style.top = "0";
|
|
10890
|
-
modal.style.left = "0";
|
|
10891
|
-
modal.style.width = "100vw";
|
|
10892
|
-
modal.style.height = "100vh";
|
|
10893
|
-
modal.style.background = "rgba(0,0,0,0.45)";
|
|
10894
|
-
modal.style.display = "flex";
|
|
10895
|
-
modal.style.alignItems = "flex-start";
|
|
10896
|
-
modal.style.justifyContent = "center";
|
|
10897
|
-
modal.style.overflowY = "auto";
|
|
10898
|
-
modal.style.paddingTop = "24px";
|
|
10899
|
-
modal.innerHTML = `
|
|
10900
|
-
<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: 32px 24px 24px 24px; max-width: 340px; width: 100%; text-align: center; position: relative; font-family: 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
|
|
10901
|
-
<button id="polyguard-modal-close" style="position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 22px; color: #222; cursor: pointer; z-index: 2;">×</button>
|
|
10902
|
-
<h2 style="margin-top: 0; font-size: 1.5rem; font-weight: 700; color: #222;">Quick Identity Verification</h2>
|
|
10903
|
-
<div style="font-size: 15px; color: #888; margin-bottom: 12px; font-weight: 500;">Powered by Polyguard</div>
|
|
10904
|
-
<div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 16px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 12px;"></div>
|
|
10905
|
-
<div style="margin-bottom: 12px; font-weight: 600; color: #222;">Scan this QR code to verify your identity.</div>
|
|
10906
|
-
<ul style="text-align: left; margin: 0 0 16px 0; padding-left: 20px; font-size: 14px; color: #444;">
|
|
10907
|
-
<li>We use the Polyguard service to verify your identity.</li>
|
|
10908
|
-
<li>If you do not have the Polyguard app, the QR code will redirect you to download it from the App Store or Google Play.</li>
|
|
10909
|
-
<li>Your credentials remain private on your device.</li>
|
|
10910
|
-
</ul>
|
|
10911
|
-
<div id="polyguard-error" style="color: #b31d28; font-size: 14px; margin-bottom: 8px; display: none;"></div>
|
|
10912
|
-
<button id="polyguard-modal-cancel" style="background: #7be7c2; color: #222; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer; margin-top: 8px; width: 100%; max-width: 240px;">Cancel</button>
|
|
10913
|
-
</div>
|
|
10914
|
-
`;
|
|
10915
|
-
return modal;
|
|
11026
|
+
return buildModal();
|
|
10916
11027
|
}
|
|
10917
11028
|
async verify(target = null, rawJwt = false) {
|
|
10918
11029
|
let modal = null;
|
|
@@ -10925,14 +11036,14 @@ var PolyguardWebsocketClientImpl = class {
|
|
|
10925
11036
|
throw new Error(`Target element with ID '${target}' not found`);
|
|
10926
11037
|
}
|
|
10927
11038
|
} else {
|
|
10928
|
-
modal =
|
|
11039
|
+
modal = buildModal();
|
|
10929
11040
|
}
|
|
10930
11041
|
if (!isTargetMode) {
|
|
10931
11042
|
document.body.appendChild(modal);
|
|
10932
11043
|
qrDiv = modal.querySelector("#polyguard-qr");
|
|
10933
11044
|
}
|
|
10934
11045
|
if (qrDiv) {
|
|
10935
|
-
qrDiv
|
|
11046
|
+
showSpinner(qrDiv);
|
|
10936
11047
|
}
|
|
10937
11048
|
function cleanup() {
|
|
10938
11049
|
if (isTargetMode) {
|
|
@@ -10982,22 +11093,13 @@ var PolyguardWebsocketClientImpl = class {
|
|
|
10982
11093
|
try {
|
|
10983
11094
|
clearError();
|
|
10984
11095
|
const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
|
|
10985
|
-
const
|
|
10986
|
-
|
|
10987
|
-
|
|
10988
|
-
|
|
10989
|
-
|
|
11096
|
+
const newTicket = await fetchTicket({
|
|
11097
|
+
apiServer: this.apiServer,
|
|
11098
|
+
appId: this.appId,
|
|
11099
|
+
link_uuid: this.link_uuid,
|
|
11100
|
+
requiredProofs: this.requiredProofs,
|
|
11101
|
+
scanType: this.scanType
|
|
10990
11102
|
});
|
|
10991
|
-
if (!ticketRes.ok) {
|
|
10992
|
-
returnError("Failed to get ticket");
|
|
10993
|
-
return;
|
|
10994
|
-
}
|
|
10995
|
-
const ticketData = await ticketRes.json();
|
|
10996
|
-
const newTicket = ticketData.ticket;
|
|
10997
|
-
if (!newTicket) {
|
|
10998
|
-
returnError("No ticket returned from server");
|
|
10999
|
-
return;
|
|
11000
|
-
}
|
|
11001
11103
|
const wsUrl = `${wsProtocol}://${this.apiServer}/v2/realtime/${newTicket}`;
|
|
11002
11104
|
const options = {
|
|
11003
11105
|
maxRetries: 10,
|
|
@@ -11007,71 +11109,8 @@ var PolyguardWebsocketClientImpl = class {
|
|
|
11007
11109
|
reconnectionDelayGrowFactor: 1.5
|
|
11008
11110
|
};
|
|
11009
11111
|
ws = new reconnecting_websocket_mjs_default(wsUrl, [], options);
|
|
11010
|
-
ws
|
|
11011
|
-
|
|
11012
|
-
const data = JSON.parse(event.data);
|
|
11013
|
-
if (data && data.type === "ping" && typeof data.seq !== "undefined") {
|
|
11014
|
-
ws.send(JSON.stringify({ type: "pong", seq: data.seq }));
|
|
11015
|
-
return;
|
|
11016
|
-
}
|
|
11017
|
-
if (data && data.url) {
|
|
11018
|
-
window.location.assign(data.url);
|
|
11019
|
-
return;
|
|
11020
|
-
} else if (data && data.qr_url) {
|
|
11021
|
-
const pcre = data.qr_url.match(/pcre=([^&]*)/);
|
|
11022
|
-
console.log("pcre", pcre);
|
|
11023
|
-
if (pcre) {
|
|
11024
|
-
ws.send(JSON.stringify({ type: "pong", seq: pcre[1] }));
|
|
11025
|
-
}
|
|
11026
|
-
if (!qrDiv) return;
|
|
11027
|
-
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
|
|
11028
|
-
if (isMobile) {
|
|
11029
|
-
qrDiv.innerHTML = `<button id="polyguard-open-app-button" style="background: #7be7c2; color: #222; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer;">Open Polyguard App</button>`;
|
|
11030
|
-
qrDiv.style.background = "transparent";
|
|
11031
|
-
qrDiv.querySelector("#polyguard-open-app-button").onclick = () => window.location.assign(data.qr_url);
|
|
11032
|
-
if (!isTargetMode) {
|
|
11033
|
-
const instructionText = qrDiv.nextElementSibling;
|
|
11034
|
-
if (instructionText) {
|
|
11035
|
-
instructionText.textContent = "Tap the button to verify with the Polyguard app.";
|
|
11036
|
-
}
|
|
11037
|
-
const instructionList = instructionText.nextElementSibling;
|
|
11038
|
-
if (instructionList && instructionList.children[1]) {
|
|
11039
|
-
instructionList.children[1].textContent = "If you do not have the Polyguard app, you will be redirected to download it.";
|
|
11040
|
-
}
|
|
11041
|
-
}
|
|
11042
|
-
} else {
|
|
11043
|
-
const startTime = Date.now();
|
|
11044
|
-
console.log("time before qr code", startTime);
|
|
11045
|
-
import_qrcode.default.toString(data.qr_url, { type: "svg" }, (err, svg) => {
|
|
11046
|
-
if (!err) qrDiv.innerHTML = svg;
|
|
11047
|
-
});
|
|
11048
|
-
console.log("time to generate qr code", Date.now() - startTime);
|
|
11049
|
-
}
|
|
11050
|
-
return;
|
|
11051
|
-
} else if (data && data.jwt) {
|
|
11052
|
-
cleanup();
|
|
11053
|
-
ws.close();
|
|
11054
|
-
resolve(rawJwt ? data : data.jwt);
|
|
11055
|
-
return;
|
|
11056
|
-
} else if (data && data.status) {
|
|
11057
|
-
if (!qrDiv) return;
|
|
11058
|
-
qrDiv.innerHTML = LOADING_SPINNER;
|
|
11059
|
-
return;
|
|
11060
|
-
} else if (data && data.error) {
|
|
11061
|
-
returnError(data.error);
|
|
11062
|
-
ws.close();
|
|
11063
|
-
return;
|
|
11064
|
-
} else {
|
|
11065
|
-
console.error("Unknown message type from server", data);
|
|
11066
|
-
returnError(`Unknown message type from server`);
|
|
11067
|
-
ws.close();
|
|
11068
|
-
return;
|
|
11069
|
-
}
|
|
11070
|
-
} catch (e) {
|
|
11071
|
-
console.error("Invalid message format from server", e);
|
|
11072
|
-
returnError("Invalid message format from server");
|
|
11073
|
-
}
|
|
11074
|
-
});
|
|
11112
|
+
const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt };
|
|
11113
|
+
ws.addEventListener("message", (event) => handleWebSocketMessage(event, ctx));
|
|
11075
11114
|
ws.addEventListener("error", () => {
|
|
11076
11115
|
console.error("WebSocket error");
|
|
11077
11116
|
returnError("WebSocket error");
|
|
@@ -11080,7 +11119,7 @@ var PolyguardWebsocketClientImpl = class {
|
|
|
11080
11119
|
if (!closed) cleanup();
|
|
11081
11120
|
});
|
|
11082
11121
|
} catch (err) {
|
|
11083
|
-
returnError("Failed to connect to WebSocket");
|
|
11122
|
+
returnError(err.message === "Failed to get ticket" || err.message === "No ticket returned from server" ? err.message : "Failed to connect to WebSocket");
|
|
11084
11123
|
}
|
|
11085
11124
|
});
|
|
11086
11125
|
}
|
package/dist/sdk.js
CHANGED
|
@@ -10870,7 +10870,7 @@ var Polyguard = (() => {
|
|
|
10870
10870
|
}
|
|
10871
10871
|
};
|
|
10872
10872
|
|
|
10873
|
-
// src/
|
|
10873
|
+
// src/ui.js
|
|
10874
10874
|
var import_qrcode = __toESM(require_browser(), 1);
|
|
10875
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>
|
|
@@ -10884,10 +10884,149 @@ var Polyguard = (() => {
|
|
|
10884
10884
|
}
|
|
10885
10885
|
</style>
|
|
10886
10886
|
<circle cx="100" cy="100" r="80" stroke="#e0e0e0" stroke-width="8" fill="none" />
|
|
10887
|
-
<circle cx="100" cy="100" r="80" stroke="#
|
|
10888
|
-
stroke-dasharray="251.2" stroke-dashoffset="188.4"
|
|
10887
|
+
<circle cx="100" cy="100" r="80" stroke="#407796" stroke-width="8" fill="none"
|
|
10888
|
+
stroke-dasharray="251.2" stroke-dashoffset="188.4"
|
|
10889
10889
|
class="spinner-circle" stroke-linecap="round" />
|
|
10890
10890
|
</svg>`;
|
|
10891
|
+
function buildModal() {
|
|
10892
|
+
const modal = document.createElement("div");
|
|
10893
|
+
modal.style.position = "fixed";
|
|
10894
|
+
modal.style.top = "0";
|
|
10895
|
+
modal.style.left = "0";
|
|
10896
|
+
modal.style.width = "100vw";
|
|
10897
|
+
modal.style.height = "100vh";
|
|
10898
|
+
modal.style.background = "rgba(0,0,0,0.45)";
|
|
10899
|
+
modal.style.display = "flex";
|
|
10900
|
+
modal.style.alignItems = "flex-start";
|
|
10901
|
+
modal.style.justifyContent = "center";
|
|
10902
|
+
modal.style.overflowY = "auto";
|
|
10903
|
+
modal.style.paddingTop = "24px";
|
|
10904
|
+
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: 32px 24px 24px 24px; max-width: 340px; 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
|
+
<button id="polyguard-modal-close" style="position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 22px; color: #222; cursor: pointer; z-index: 2;">×</button>
|
|
10907
|
+
<h2 style="margin-top: 0; font-size: 1.3rem; font-weight: 700; color: #222;">Identity Verification</h2>
|
|
10908
|
+
<div style="font-size: 13px; color: #888; margin-bottom: 12px; font-weight: 500;">Powered by Polyguard</div>
|
|
10909
|
+
<div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 16px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 12px;"></div>
|
|
10910
|
+
<div style="margin-bottom: 12px; font-weight: 600; color: #222;">Scan this QR code to verify your identity.</div>
|
|
10911
|
+
<div style="display: flex; justify-content: center; gap: 12px; font-size: 12px; color: #5a6c7d; margin-bottom: 12px; flex-wrap: wrap;">
|
|
10912
|
+
<span>🔒 Encrypted</span>
|
|
10913
|
+
<span>📱 On-device</span>
|
|
10914
|
+
<span>⏲ Expires</span>
|
|
10915
|
+
</div>
|
|
10916
|
+
<div id="polyguard-error" style="color: #b31d28; font-size: 14px; margin-bottom: 8px; display: none;"></div>
|
|
10917
|
+
<button id="polyguard-modal-cancel" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer; margin-top: 8px; width: 100%; max-width: 240px;">Cancel</button>
|
|
10918
|
+
</div>
|
|
10919
|
+
`;
|
|
10920
|
+
return modal;
|
|
10921
|
+
}
|
|
10922
|
+
function showSpinner(qrDiv) {
|
|
10923
|
+
qrDiv.innerHTML = LOADING_SPINNER;
|
|
10924
|
+
}
|
|
10925
|
+
function renderMobileButton(qrDiv, qrUrl, isTargetMode) {
|
|
10926
|
+
qrDiv.innerHTML = `<button id="polyguard-open-app-button" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer;">Open Polyguard App</button>`;
|
|
10927
|
+
qrDiv.style.background = "transparent";
|
|
10928
|
+
qrDiv.querySelector("#polyguard-open-app-button").onclick = () => window.location.assign(qrUrl);
|
|
10929
|
+
if (!isTargetMode) {
|
|
10930
|
+
const instructionText = qrDiv.nextElementSibling;
|
|
10931
|
+
if (instructionText) {
|
|
10932
|
+
instructionText.textContent = "Tap the button to verify with the Polyguard app.";
|
|
10933
|
+
}
|
|
10934
|
+
const instructionList = instructionText.nextElementSibling;
|
|
10935
|
+
if (instructionList && instructionList.children[1]) {
|
|
10936
|
+
instructionList.children[1].textContent = "If you do not have the Polyguard app, you will be redirected to download it.";
|
|
10937
|
+
}
|
|
10938
|
+
}
|
|
10939
|
+
}
|
|
10940
|
+
function renderQRCode(qrDiv, qrUrl) {
|
|
10941
|
+
const startTime = Date.now();
|
|
10942
|
+
console.log("time before qr code", startTime);
|
|
10943
|
+
import_qrcode.default.toString(qrUrl, { type: "svg" }, (err, svg) => {
|
|
10944
|
+
if (!err) qrDiv.innerHTML = svg;
|
|
10945
|
+
});
|
|
10946
|
+
console.log("time to generate qr code", Date.now() - startTime);
|
|
10947
|
+
}
|
|
10948
|
+
|
|
10949
|
+
// src/ticketService.js
|
|
10950
|
+
async function fetchTicket({ apiServer, appId, link_uuid, requiredProofs, scanType }) {
|
|
10951
|
+
const ticketUrl = link_uuid ? `https://${apiServer}/v2/ticket/${appId}/${link_uuid}` : `https://${apiServer}/v2/ticket/${appId}`;
|
|
10952
|
+
const ticketRes = await fetch(ticketUrl, {
|
|
10953
|
+
method: "POST",
|
|
10954
|
+
headers: { "Content-Type": "application/json" },
|
|
10955
|
+
body: JSON.stringify({ requiredProofs, scanType })
|
|
10956
|
+
});
|
|
10957
|
+
if (!ticketRes.ok) {
|
|
10958
|
+
throw new Error("Failed to get ticket");
|
|
10959
|
+
}
|
|
10960
|
+
const ticketData = await ticketRes.json();
|
|
10961
|
+
const ticket = ticketData.ticket;
|
|
10962
|
+
if (!ticket) {
|
|
10963
|
+
throw new Error("No ticket returned from server");
|
|
10964
|
+
}
|
|
10965
|
+
return ticket;
|
|
10966
|
+
}
|
|
10967
|
+
|
|
10968
|
+
// src/messageHandler.js
|
|
10969
|
+
function handleWebSocketMessage(event, ctx) {
|
|
10970
|
+
const { ws, qrDiv, isTargetMode, cleanup, returnError, resolve, rawJwt } = ctx;
|
|
10971
|
+
let data;
|
|
10972
|
+
try {
|
|
10973
|
+
data = JSON.parse(event.data);
|
|
10974
|
+
} catch (e) {
|
|
10975
|
+
console.error("Invalid message format from server", e);
|
|
10976
|
+
returnError("Invalid message format from server");
|
|
10977
|
+
return;
|
|
10978
|
+
}
|
|
10979
|
+
if (!data) {
|
|
10980
|
+
console.error("Unknown message type from server", data);
|
|
10981
|
+
returnError("Unknown message type from server");
|
|
10982
|
+
ws.close();
|
|
10983
|
+
return;
|
|
10984
|
+
}
|
|
10985
|
+
if (data.type === "ping" && typeof data.seq !== "undefined") {
|
|
10986
|
+
ws.send(JSON.stringify({ type: "pong", seq: data.seq }));
|
|
10987
|
+
return;
|
|
10988
|
+
}
|
|
10989
|
+
if (data.url) {
|
|
10990
|
+
window.location.assign(data.url);
|
|
10991
|
+
return;
|
|
10992
|
+
}
|
|
10993
|
+
if (data.qr_url) {
|
|
10994
|
+
const pcre = data.qr_url.match(/pcre=([^&]*)/);
|
|
10995
|
+
console.log("pcre", pcre);
|
|
10996
|
+
if (pcre) {
|
|
10997
|
+
ws.send(JSON.stringify({ type: "pong", seq: pcre[1] }));
|
|
10998
|
+
}
|
|
10999
|
+
if (qrDiv) {
|
|
11000
|
+
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
|
|
11001
|
+
if (isMobile) {
|
|
11002
|
+
renderMobileButton(qrDiv, data.qr_url, isTargetMode);
|
|
11003
|
+
} else {
|
|
11004
|
+
renderQRCode(qrDiv, data.qr_url);
|
|
11005
|
+
}
|
|
11006
|
+
}
|
|
11007
|
+
return;
|
|
11008
|
+
}
|
|
11009
|
+
if (data.jwt) {
|
|
11010
|
+
cleanup();
|
|
11011
|
+
ws.close();
|
|
11012
|
+
resolve(rawJwt ? data : data.jwt);
|
|
11013
|
+
return;
|
|
11014
|
+
}
|
|
11015
|
+
if (data.status) {
|
|
11016
|
+
if (qrDiv) showSpinner(qrDiv);
|
|
11017
|
+
return;
|
|
11018
|
+
}
|
|
11019
|
+
if (data.error) {
|
|
11020
|
+
returnError(data.error);
|
|
11021
|
+
ws.close();
|
|
11022
|
+
return;
|
|
11023
|
+
}
|
|
11024
|
+
console.error("Unknown message type from server", data);
|
|
11025
|
+
returnError("Unknown message type from server");
|
|
11026
|
+
ws.close();
|
|
11027
|
+
}
|
|
11028
|
+
|
|
11029
|
+
// src/PolyguardWebsocketClientImpl.js
|
|
10891
11030
|
var PolyguardWebsocketClientImpl = class {
|
|
10892
11031
|
constructor(params = {}) {
|
|
10893
11032
|
this.apiClient = new ApiClient_default();
|
|
@@ -10917,35 +11056,7 @@ var Polyguard = (() => {
|
|
|
10917
11056
|
this.link_uuid = link_uuid;
|
|
10918
11057
|
}
|
|
10919
11058
|
buildModal() {
|
|
10920
|
-
|
|
10921
|
-
modal.style.position = "fixed";
|
|
10922
|
-
modal.style.top = "0";
|
|
10923
|
-
modal.style.left = "0";
|
|
10924
|
-
modal.style.width = "100vw";
|
|
10925
|
-
modal.style.height = "100vh";
|
|
10926
|
-
modal.style.background = "rgba(0,0,0,0.45)";
|
|
10927
|
-
modal.style.display = "flex";
|
|
10928
|
-
modal.style.alignItems = "flex-start";
|
|
10929
|
-
modal.style.justifyContent = "center";
|
|
10930
|
-
modal.style.overflowY = "auto";
|
|
10931
|
-
modal.style.paddingTop = "24px";
|
|
10932
|
-
modal.innerHTML = `
|
|
10933
|
-
<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: 32px 24px 24px 24px; max-width: 340px; width: 100%; text-align: center; position: relative; font-family: 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
|
|
10934
|
-
<button id="polyguard-modal-close" style="position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 22px; color: #222; cursor: pointer; z-index: 2;">×</button>
|
|
10935
|
-
<h2 style="margin-top: 0; font-size: 1.5rem; font-weight: 700; color: #222;">Quick Identity Verification</h2>
|
|
10936
|
-
<div style="font-size: 15px; color: #888; margin-bottom: 12px; font-weight: 500;">Powered by Polyguard</div>
|
|
10937
|
-
<div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 16px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 12px;"></div>
|
|
10938
|
-
<div style="margin-bottom: 12px; font-weight: 600; color: #222;">Scan this QR code to verify your identity.</div>
|
|
10939
|
-
<ul style="text-align: left; margin: 0 0 16px 0; padding-left: 20px; font-size: 14px; color: #444;">
|
|
10940
|
-
<li>We use the Polyguard service to verify your identity.</li>
|
|
10941
|
-
<li>If you do not have the Polyguard app, the QR code will redirect you to download it from the App Store or Google Play.</li>
|
|
10942
|
-
<li>Your credentials remain private on your device.</li>
|
|
10943
|
-
</ul>
|
|
10944
|
-
<div id="polyguard-error" style="color: #b31d28; font-size: 14px; margin-bottom: 8px; display: none;"></div>
|
|
10945
|
-
<button id="polyguard-modal-cancel" style="background: #7be7c2; color: #222; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer; margin-top: 8px; width: 100%; max-width: 240px;">Cancel</button>
|
|
10946
|
-
</div>
|
|
10947
|
-
`;
|
|
10948
|
-
return modal;
|
|
11059
|
+
return buildModal();
|
|
10949
11060
|
}
|
|
10950
11061
|
async verify(target = null, rawJwt = false) {
|
|
10951
11062
|
let modal = null;
|
|
@@ -10958,14 +11069,14 @@ var Polyguard = (() => {
|
|
|
10958
11069
|
throw new Error(`Target element with ID '${target}' not found`);
|
|
10959
11070
|
}
|
|
10960
11071
|
} else {
|
|
10961
|
-
modal =
|
|
11072
|
+
modal = buildModal();
|
|
10962
11073
|
}
|
|
10963
11074
|
if (!isTargetMode) {
|
|
10964
11075
|
document.body.appendChild(modal);
|
|
10965
11076
|
qrDiv = modal.querySelector("#polyguard-qr");
|
|
10966
11077
|
}
|
|
10967
11078
|
if (qrDiv) {
|
|
10968
|
-
qrDiv
|
|
11079
|
+
showSpinner(qrDiv);
|
|
10969
11080
|
}
|
|
10970
11081
|
function cleanup() {
|
|
10971
11082
|
if (isTargetMode) {
|
|
@@ -11015,22 +11126,13 @@ var Polyguard = (() => {
|
|
|
11015
11126
|
try {
|
|
11016
11127
|
clearError();
|
|
11017
11128
|
const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
|
|
11018
|
-
const
|
|
11019
|
-
|
|
11020
|
-
|
|
11021
|
-
|
|
11022
|
-
|
|
11129
|
+
const newTicket = await fetchTicket({
|
|
11130
|
+
apiServer: this.apiServer,
|
|
11131
|
+
appId: this.appId,
|
|
11132
|
+
link_uuid: this.link_uuid,
|
|
11133
|
+
requiredProofs: this.requiredProofs,
|
|
11134
|
+
scanType: this.scanType
|
|
11023
11135
|
});
|
|
11024
|
-
if (!ticketRes.ok) {
|
|
11025
|
-
returnError("Failed to get ticket");
|
|
11026
|
-
return;
|
|
11027
|
-
}
|
|
11028
|
-
const ticketData = await ticketRes.json();
|
|
11029
|
-
const newTicket = ticketData.ticket;
|
|
11030
|
-
if (!newTicket) {
|
|
11031
|
-
returnError("No ticket returned from server");
|
|
11032
|
-
return;
|
|
11033
|
-
}
|
|
11034
11136
|
const wsUrl = `${wsProtocol}://${this.apiServer}/v2/realtime/${newTicket}`;
|
|
11035
11137
|
const options = {
|
|
11036
11138
|
maxRetries: 10,
|
|
@@ -11040,71 +11142,8 @@ var Polyguard = (() => {
|
|
|
11040
11142
|
reconnectionDelayGrowFactor: 1.5
|
|
11041
11143
|
};
|
|
11042
11144
|
ws = new reconnecting_websocket_mjs_default(wsUrl, [], options);
|
|
11043
|
-
ws
|
|
11044
|
-
|
|
11045
|
-
const data = JSON.parse(event.data);
|
|
11046
|
-
if (data && data.type === "ping" && typeof data.seq !== "undefined") {
|
|
11047
|
-
ws.send(JSON.stringify({ type: "pong", seq: data.seq }));
|
|
11048
|
-
return;
|
|
11049
|
-
}
|
|
11050
|
-
if (data && data.url) {
|
|
11051
|
-
window.location.assign(data.url);
|
|
11052
|
-
return;
|
|
11053
|
-
} else if (data && data.qr_url) {
|
|
11054
|
-
const pcre = data.qr_url.match(/pcre=([^&]*)/);
|
|
11055
|
-
console.log("pcre", pcre);
|
|
11056
|
-
if (pcre) {
|
|
11057
|
-
ws.send(JSON.stringify({ type: "pong", seq: pcre[1] }));
|
|
11058
|
-
}
|
|
11059
|
-
if (!qrDiv) return;
|
|
11060
|
-
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
|
|
11061
|
-
if (isMobile) {
|
|
11062
|
-
qrDiv.innerHTML = `<button id="polyguard-open-app-button" style="background: #7be7c2; color: #222; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer;">Open Polyguard App</button>`;
|
|
11063
|
-
qrDiv.style.background = "transparent";
|
|
11064
|
-
qrDiv.querySelector("#polyguard-open-app-button").onclick = () => window.location.assign(data.qr_url);
|
|
11065
|
-
if (!isTargetMode) {
|
|
11066
|
-
const instructionText = qrDiv.nextElementSibling;
|
|
11067
|
-
if (instructionText) {
|
|
11068
|
-
instructionText.textContent = "Tap the button to verify with the Polyguard app.";
|
|
11069
|
-
}
|
|
11070
|
-
const instructionList = instructionText.nextElementSibling;
|
|
11071
|
-
if (instructionList && instructionList.children[1]) {
|
|
11072
|
-
instructionList.children[1].textContent = "If you do not have the Polyguard app, you will be redirected to download it.";
|
|
11073
|
-
}
|
|
11074
|
-
}
|
|
11075
|
-
} else {
|
|
11076
|
-
const startTime = Date.now();
|
|
11077
|
-
console.log("time before qr code", startTime);
|
|
11078
|
-
import_qrcode.default.toString(data.qr_url, { type: "svg" }, (err, svg) => {
|
|
11079
|
-
if (!err) qrDiv.innerHTML = svg;
|
|
11080
|
-
});
|
|
11081
|
-
console.log("time to generate qr code", Date.now() - startTime);
|
|
11082
|
-
}
|
|
11083
|
-
return;
|
|
11084
|
-
} else if (data && data.jwt) {
|
|
11085
|
-
cleanup();
|
|
11086
|
-
ws.close();
|
|
11087
|
-
resolve(rawJwt ? data : data.jwt);
|
|
11088
|
-
return;
|
|
11089
|
-
} else if (data && data.status) {
|
|
11090
|
-
if (!qrDiv) return;
|
|
11091
|
-
qrDiv.innerHTML = LOADING_SPINNER;
|
|
11092
|
-
return;
|
|
11093
|
-
} else if (data && data.error) {
|
|
11094
|
-
returnError(data.error);
|
|
11095
|
-
ws.close();
|
|
11096
|
-
return;
|
|
11097
|
-
} else {
|
|
11098
|
-
console.error("Unknown message type from server", data);
|
|
11099
|
-
returnError(`Unknown message type from server`);
|
|
11100
|
-
ws.close();
|
|
11101
|
-
return;
|
|
11102
|
-
}
|
|
11103
|
-
} catch (e) {
|
|
11104
|
-
console.error("Invalid message format from server", e);
|
|
11105
|
-
returnError("Invalid message format from server");
|
|
11106
|
-
}
|
|
11107
|
-
});
|
|
11145
|
+
const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt };
|
|
11146
|
+
ws.addEventListener("message", (event) => handleWebSocketMessage(event, ctx));
|
|
11108
11147
|
ws.addEventListener("error", () => {
|
|
11109
11148
|
console.error("WebSocket error");
|
|
11110
11149
|
returnError("WebSocket error");
|
|
@@ -11113,7 +11152,7 @@ var Polyguard = (() => {
|
|
|
11113
11152
|
if (!closed) cleanup();
|
|
11114
11153
|
});
|
|
11115
11154
|
} catch (err) {
|
|
11116
|
-
returnError("Failed to connect to WebSocket");
|
|
11155
|
+
returnError(err.message === "Failed to get ticket" || err.message === "No ticket returned from server" ? err.message : "Failed to connect to WebSocket");
|
|
11117
11156
|
}
|
|
11118
11157
|
});
|
|
11119
11158
|
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
import ReconnectingWebSocket from 'reconnecting-websocket';
|
|
2
2
|
import * as PolyguardApi from './generated/src';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="none">
|
|
7
|
-
<style>
|
|
8
|
-
.spinner-circle {
|
|
9
|
-
animation: spin 1s linear infinite;
|
|
10
|
-
transform-origin: 100px 100px;
|
|
11
|
-
}
|
|
12
|
-
@keyframes spin {
|
|
13
|
-
from { transform: rotate(0deg); }
|
|
14
|
-
to { transform: rotate(360deg); }
|
|
15
|
-
}
|
|
16
|
-
</style>
|
|
17
|
-
<circle cx="100" cy="100" r="80" stroke="#e0e0e0" stroke-width="8" fill="none" />
|
|
18
|
-
<circle cx="100" cy="100" r="80" stroke="#7be7c2" stroke-width="8" fill="none"
|
|
19
|
-
stroke-dasharray="251.2" stroke-dashoffset="188.4"
|
|
20
|
-
class="spinner-circle" stroke-linecap="round" />
|
|
21
|
-
</svg>`;
|
|
3
|
+
import { buildModal, showSpinner } from './ui.js';
|
|
4
|
+
import { fetchTicket } from './ticketService.js';
|
|
5
|
+
import { handleWebSocketMessage } from './messageHandler.js';
|
|
22
6
|
|
|
23
7
|
// Implementation class for websocket integration
|
|
24
8
|
export class PolyguardWebsocketClientImpl {
|
|
@@ -53,71 +37,35 @@ export class PolyguardWebsocketClientImpl {
|
|
|
53
37
|
}
|
|
54
38
|
|
|
55
39
|
buildModal() {
|
|
56
|
-
|
|
57
|
-
modal.style.position = 'fixed';
|
|
58
|
-
modal.style.top = '0';
|
|
59
|
-
modal.style.left = '0';
|
|
60
|
-
modal.style.width = '100vw';
|
|
61
|
-
modal.style.height = '100vh';
|
|
62
|
-
modal.style.background = 'rgba(0,0,0,0.45)';
|
|
63
|
-
modal.style.display = 'flex';
|
|
64
|
-
modal.style.alignItems = 'flex-start';
|
|
65
|
-
modal.style.justifyContent = 'center';
|
|
66
|
-
modal.style.overflowY = 'auto';
|
|
67
|
-
modal.style.paddingTop = '24px';
|
|
68
|
-
modal.innerHTML = `
|
|
69
|
-
<div id="polyguard-modal-content" style="background: #fff; color: #222; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.18); padding: 32px 24px 24px 24px; max-width: 340px; width: 100%; text-align: center; position: relative; font-family: 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
|
|
70
|
-
<button id="polyguard-modal-close" style="position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 22px; color: #222; cursor: pointer; z-index: 2;">×</button>
|
|
71
|
-
<h2 style="margin-top: 0; font-size: 1.5rem; font-weight: 700; color: #222;">Quick Identity Verification</h2>
|
|
72
|
-
<div style="font-size: 15px; color: #888; margin-bottom: 12px; font-weight: 500;">Powered by Polyguard</div>
|
|
73
|
-
<div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 16px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 12px;"></div>
|
|
74
|
-
<div style="margin-bottom: 12px; font-weight: 600; color: #222;">Scan this QR code to verify your identity.</div>
|
|
75
|
-
<ul style="text-align: left; margin: 0 0 16px 0; padding-left: 20px; font-size: 14px; color: #444;">
|
|
76
|
-
<li>We use the Polyguard service to verify your identity.</li>
|
|
77
|
-
<li>If you do not have the Polyguard app, the QR code will redirect you to download it from the App Store or Google Play.</li>
|
|
78
|
-
<li>Your credentials remain private on your device.</li>
|
|
79
|
-
</ul>
|
|
80
|
-
<div id="polyguard-error" style="color: #b31d28; font-size: 14px; margin-bottom: 8px; display: none;"></div>
|
|
81
|
-
<button id="polyguard-modal-cancel" style="background: #7be7c2; color: #222; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer; margin-top: 8px; width: 100%; max-width: 240px;">Cancel</button>
|
|
82
|
-
</div>
|
|
83
|
-
`;
|
|
84
|
-
return modal;
|
|
40
|
+
return buildModal();
|
|
85
41
|
}
|
|
86
42
|
|
|
87
43
|
async verify(target = null, rawJwt = false) {
|
|
88
|
-
// Only websocket integration is supported for modal
|
|
89
44
|
let modal = null;
|
|
90
45
|
let qrDiv = null;
|
|
91
46
|
let isTargetMode = false;
|
|
92
|
-
|
|
47
|
+
|
|
93
48
|
if (target) {
|
|
94
|
-
// Target mode: render QR code in specified element
|
|
95
49
|
isTargetMode = true;
|
|
96
50
|
qrDiv = document.getElementById(target);
|
|
97
51
|
if (!qrDiv) {
|
|
98
52
|
throw new Error(`Target element with ID '${target}' not found`);
|
|
99
53
|
}
|
|
100
54
|
} else {
|
|
101
|
-
|
|
102
|
-
modal = this.buildModal();
|
|
55
|
+
modal = buildModal();
|
|
103
56
|
}
|
|
104
|
-
|
|
57
|
+
|
|
105
58
|
if (!isTargetMode) {
|
|
106
|
-
// Show modal immediately with spinner
|
|
107
59
|
document.body.appendChild(modal);
|
|
108
|
-
|
|
109
|
-
// Initialize QR div with spinner
|
|
110
60
|
qrDiv = modal.querySelector('#polyguard-qr');
|
|
111
61
|
}
|
|
112
|
-
|
|
62
|
+
|
|
113
63
|
if (qrDiv) {
|
|
114
|
-
qrDiv
|
|
64
|
+
showSpinner(qrDiv);
|
|
115
65
|
}
|
|
116
|
-
|
|
117
|
-
// Helper to cleanup modal or target
|
|
66
|
+
|
|
118
67
|
function cleanup() {
|
|
119
68
|
if (isTargetMode) {
|
|
120
|
-
// Clear target element content
|
|
121
69
|
if (qrDiv) {
|
|
122
70
|
qrDiv.innerHTML = '';
|
|
123
71
|
}
|
|
@@ -125,28 +73,27 @@ export class PolyguardWebsocketClientImpl {
|
|
|
125
73
|
modal.parentNode.removeChild(modal);
|
|
126
74
|
}
|
|
127
75
|
}
|
|
128
|
-
|
|
76
|
+
|
|
129
77
|
return new Promise(async (resolve, reject) => {
|
|
130
78
|
let ws = null;
|
|
131
79
|
let closed = false;
|
|
132
|
-
|
|
80
|
+
|
|
133
81
|
function returnError(msg, score = "OFFLINE") {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
82
|
+
if (isTargetMode) {
|
|
83
|
+
console.error('Polyguard Error:', msg);
|
|
84
|
+
cleanup();
|
|
85
|
+
resolve({presence: { score: score, msg: msg }});
|
|
86
|
+
} else {
|
|
87
|
+
const errDiv = modal.querySelector('#polyguard-error');
|
|
88
|
+
if (errDiv) {
|
|
89
|
+
errDiv.textContent = msg;
|
|
90
|
+
errDiv.style.display = 'block';
|
|
91
|
+
}
|
|
92
|
+
setTimeout(() => {
|
|
137
93
|
cleanup();
|
|
138
94
|
resolve({presence: { score: score, msg: msg }});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (errDiv) {
|
|
142
|
-
errDiv.textContent = msg;
|
|
143
|
-
errDiv.style.display = 'block';
|
|
144
|
-
}
|
|
145
|
-
setTimeout(() => {
|
|
146
|
-
cleanup();
|
|
147
|
-
resolve({presence: { score: score, msg: msg }});
|
|
148
|
-
}, 1250);
|
|
149
|
-
}
|
|
95
|
+
}, 1250);
|
|
96
|
+
}
|
|
150
97
|
}
|
|
151
98
|
function clearError() {
|
|
152
99
|
if (!isTargetMode) {
|
|
@@ -154,42 +101,30 @@ export class PolyguardWebsocketClientImpl {
|
|
|
154
101
|
if (errDiv) errDiv.style.display = 'none';
|
|
155
102
|
}
|
|
156
103
|
}
|
|
157
|
-
// Close/cancel handler
|
|
158
104
|
function handleClose() {
|
|
159
105
|
closed = true;
|
|
160
106
|
if (ws) ws.close();
|
|
161
107
|
cleanup();
|
|
162
108
|
reject(new Error('User cancelled'));
|
|
163
109
|
}
|
|
164
|
-
|
|
165
|
-
// Set up close/cancel handlers (only in modal mode)
|
|
110
|
+
|
|
166
111
|
if (!isTargetMode) {
|
|
167
112
|
modal.querySelector('#polyguard-modal-close').onclick = handleClose;
|
|
168
113
|
modal.querySelector('#polyguard-modal-cancel').onclick = handleClose;
|
|
169
114
|
}
|
|
170
|
-
|
|
171
|
-
// Start ticket/ws flow
|
|
115
|
+
|
|
172
116
|
try {
|
|
173
117
|
clearError();
|
|
174
118
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
119
|
+
|
|
120
|
+
const newTicket = await fetchTicket({
|
|
121
|
+
apiServer: this.apiServer,
|
|
122
|
+
appId: this.appId,
|
|
123
|
+
link_uuid: this.link_uuid,
|
|
124
|
+
requiredProofs: this.requiredProofs,
|
|
125
|
+
scanType: this.scanType,
|
|
182
126
|
});
|
|
183
|
-
|
|
184
|
-
returnError('Failed to get ticket');
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const ticketData = await ticketRes.json();
|
|
188
|
-
const newTicket = ticketData.ticket;
|
|
189
|
-
if (!newTicket) {
|
|
190
|
-
returnError('No ticket returned from server');
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
127
|
+
|
|
193
128
|
const wsUrl = `${wsProtocol}://${this.apiServer}/v2/realtime/${newTicket}`;
|
|
194
129
|
const options = {
|
|
195
130
|
maxRetries: 10,
|
|
@@ -199,86 +134,10 @@ export class PolyguardWebsocketClientImpl {
|
|
|
199
134
|
reconnectionDelayGrowFactor: 1.5,
|
|
200
135
|
};
|
|
201
136
|
ws = new ReconnectingWebSocket(wsUrl, [], options);
|
|
202
|
-
ws.addEventListener('message', (event) => {
|
|
203
|
-
try {
|
|
204
|
-
const data = JSON.parse(event.data);
|
|
205
|
-
// --- BEGIN: Backend-initiated latency measurement ---
|
|
206
|
-
if (data && data.type === 'ping' && typeof data.seq !== 'undefined') {
|
|
207
|
-
ws.send(JSON.stringify({ type: 'pong', seq: data.seq }));
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
// --- END: Backend-initiated latency measurement ---
|
|
211
|
-
if (data && data.url) {
|
|
212
|
-
window.location.assign(data.url);
|
|
213
|
-
return;
|
|
214
|
-
} else if (data && data.qr_url) {
|
|
215
|
-
// Replace spinner with QR code content
|
|
216
|
-
|
|
217
|
-
const pcre = data.qr_url.match(/pcre=([^&]*)/);
|
|
218
|
-
console.log('pcre', pcre);
|
|
219
|
-
if (pcre) {
|
|
220
|
-
ws.send(JSON.stringify({ type: 'pong', seq: pcre[1] }));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (!qrDiv) return;
|
|
224
|
-
|
|
225
|
-
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
|
|
226
137
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
qrDiv.innerHTML = `<button id="polyguard-open-app-button" style="background: #7be7c2; color: #222; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer;">Open Polyguard App</button>`;
|
|
230
|
-
qrDiv.style.background = 'transparent';
|
|
231
|
-
qrDiv.querySelector('#polyguard-open-app-button').onclick = () => window.location.assign(data.qr_url);
|
|
138
|
+
const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt };
|
|
139
|
+
ws.addEventListener('message', (event) => handleWebSocketMessage(event, ctx));
|
|
232
140
|
|
|
233
|
-
// Update surrounding text only in modal mode
|
|
234
|
-
if (!isTargetMode) {
|
|
235
|
-
const instructionText = qrDiv.nextElementSibling;
|
|
236
|
-
if (instructionText) {
|
|
237
|
-
instructionText.textContent = 'Tap the button to verify with the Polyguard app.';
|
|
238
|
-
}
|
|
239
|
-
const instructionList = instructionText.nextElementSibling;
|
|
240
|
-
if (instructionList && instructionList.children[1]) {
|
|
241
|
-
instructionList.children[1].textContent = 'If you do not have the Polyguard app, you will be redirected to download it.';
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
} else {
|
|
245
|
-
const startTime = Date.now();
|
|
246
|
-
console.log('time before qr code', startTime);
|
|
247
|
-
// For desktop, display the QR code
|
|
248
|
-
QRCode.toString(data.qr_url, { type: 'svg' }, (err, svg) => {
|
|
249
|
-
if (!err) qrDiv.innerHTML = svg;
|
|
250
|
-
});
|
|
251
|
-
console.log('time to generate qr code', Date.now() - startTime);
|
|
252
|
-
}
|
|
253
|
-
return;
|
|
254
|
-
} else if (data && data.jwt) {
|
|
255
|
-
cleanup();
|
|
256
|
-
ws.close();
|
|
257
|
-
resolve(rawJwt ? data : data.jwt);
|
|
258
|
-
return;
|
|
259
|
-
} else if (data && data.status) {
|
|
260
|
-
// ignore
|
|
261
|
-
// generate a spinner in svg and set the qr code to it
|
|
262
|
-
if (!qrDiv) return;
|
|
263
|
-
qrDiv.innerHTML = LOADING_SPINNER;
|
|
264
|
-
return;
|
|
265
|
-
|
|
266
|
-
} else if (data && data.error) {
|
|
267
|
-
returnError(data.error);
|
|
268
|
-
ws.close();
|
|
269
|
-
return;
|
|
270
|
-
} else {
|
|
271
|
-
console.error('Unknown message type from server', data);
|
|
272
|
-
returnError(`Unknown message type from server`);
|
|
273
|
-
ws.close();
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
} catch (e) {
|
|
277
|
-
console.error('Invalid message format from server', e);
|
|
278
|
-
returnError('Invalid message format from server');
|
|
279
|
-
// ws.close();
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
141
|
ws.addEventListener('error', () => {
|
|
283
142
|
console.error('WebSocket error');
|
|
284
143
|
returnError('WebSocket error');
|
|
@@ -287,7 +146,9 @@ export class PolyguardWebsocketClientImpl {
|
|
|
287
146
|
if (!closed) cleanup();
|
|
288
147
|
});
|
|
289
148
|
} catch (err) {
|
|
290
|
-
returnError('Failed to
|
|
149
|
+
returnError(err.message === 'Failed to get ticket' || err.message === 'No ticket returned from server'
|
|
150
|
+
? err.message
|
|
151
|
+
: 'Failed to connect to WebSocket');
|
|
291
152
|
}
|
|
292
153
|
});
|
|
293
154
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { showSpinner, renderMobileButton, renderQRCode } from './ui.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handle a WebSocket message event.
|
|
5
|
+
* @param {MessageEvent} event
|
|
6
|
+
* @param {Object} ctx - Context object with references from the verify() closure:
|
|
7
|
+
* { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt }
|
|
8
|
+
*/
|
|
9
|
+
export function handleWebSocketMessage(event, ctx) {
|
|
10
|
+
const { ws, qrDiv, isTargetMode, cleanup, returnError, resolve, rawJwt } = ctx;
|
|
11
|
+
|
|
12
|
+
let data;
|
|
13
|
+
try {
|
|
14
|
+
data = JSON.parse(event.data);
|
|
15
|
+
} catch (e) {
|
|
16
|
+
console.error('Invalid message format from server', e);
|
|
17
|
+
returnError('Invalid message format from server');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!data) {
|
|
22
|
+
console.error('Unknown message type from server', data);
|
|
23
|
+
returnError('Unknown message type from server');
|
|
24
|
+
ws.close();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Backend-initiated latency measurement
|
|
29
|
+
if (data.type === 'ping' && typeof data.seq !== 'undefined') {
|
|
30
|
+
ws.send(JSON.stringify({ type: 'pong', seq: data.seq }));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (data.url) {
|
|
35
|
+
window.location.assign(data.url);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (data.qr_url) {
|
|
40
|
+
const pcre = data.qr_url.match(/pcre=([^&]*)/);
|
|
41
|
+
console.log('pcre', pcre);
|
|
42
|
+
if (pcre) {
|
|
43
|
+
ws.send(JSON.stringify({ type: 'pong', seq: pcre[1] }));
|
|
44
|
+
}
|
|
45
|
+
if (qrDiv) {
|
|
46
|
+
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
|
|
47
|
+
if (isMobile) {
|
|
48
|
+
renderMobileButton(qrDiv, data.qr_url, isTargetMode);
|
|
49
|
+
} else {
|
|
50
|
+
renderQRCode(qrDiv, data.qr_url);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (data.jwt) {
|
|
57
|
+
cleanup();
|
|
58
|
+
ws.close();
|
|
59
|
+
resolve(rawJwt ? data : data.jwt);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (data.status) {
|
|
64
|
+
if (qrDiv) showSpinner(qrDiv);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (data.error) {
|
|
69
|
+
returnError(data.error);
|
|
70
|
+
ws.close();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.error('Unknown message type from server', data);
|
|
75
|
+
returnError('Unknown message type from server');
|
|
76
|
+
ws.close();
|
|
77
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function fetchTicket({ apiServer, appId, link_uuid, requiredProofs, scanType }) {
|
|
2
|
+
const ticketUrl = link_uuid
|
|
3
|
+
? `https://${apiServer}/v2/ticket/${appId}/${link_uuid}`
|
|
4
|
+
: `https://${apiServer}/v2/ticket/${appId}`;
|
|
5
|
+
|
|
6
|
+
const ticketRes = await fetch(ticketUrl, {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
body: JSON.stringify({ requiredProofs, scanType }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
if (!ticketRes.ok) {
|
|
13
|
+
throw new Error('Failed to get ticket');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ticketData = await ticketRes.json();
|
|
17
|
+
const ticket = ticketData.ticket;
|
|
18
|
+
|
|
19
|
+
if (!ticket) {
|
|
20
|
+
throw new Error('No ticket returned from server');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return ticket;
|
|
24
|
+
}
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import QRCode from 'qrcode';
|
|
2
|
+
|
|
3
|
+
// Animated spinner SVG
|
|
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
|
+
<style>
|
|
6
|
+
.spinner-circle {
|
|
7
|
+
animation: spin 1s linear infinite;
|
|
8
|
+
transform-origin: 100px 100px;
|
|
9
|
+
}
|
|
10
|
+
@keyframes spin {
|
|
11
|
+
from { transform: rotate(0deg); }
|
|
12
|
+
to { transform: rotate(360deg); }
|
|
13
|
+
}
|
|
14
|
+
</style>
|
|
15
|
+
<circle cx="100" cy="100" r="80" stroke="#e0e0e0" stroke-width="8" fill="none" />
|
|
16
|
+
<circle cx="100" cy="100" r="80" stroke="#407796" stroke-width="8" fill="none"
|
|
17
|
+
stroke-dasharray="251.2" stroke-dashoffset="188.4"
|
|
18
|
+
class="spinner-circle" stroke-linecap="round" />
|
|
19
|
+
</svg>`;
|
|
20
|
+
|
|
21
|
+
export function buildModal() {
|
|
22
|
+
const modal = document.createElement('div');
|
|
23
|
+
modal.style.position = 'fixed';
|
|
24
|
+
modal.style.top = '0';
|
|
25
|
+
modal.style.left = '0';
|
|
26
|
+
modal.style.width = '100vw';
|
|
27
|
+
modal.style.height = '100vh';
|
|
28
|
+
modal.style.background = 'rgba(0,0,0,0.45)';
|
|
29
|
+
modal.style.display = 'flex';
|
|
30
|
+
modal.style.alignItems = 'flex-start';
|
|
31
|
+
modal.style.justifyContent = 'center';
|
|
32
|
+
modal.style.overflowY = 'auto';
|
|
33
|
+
modal.style.paddingTop = '24px';
|
|
34
|
+
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: 32px 24px 24px 24px; max-width: 340px; 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
|
+
<button id="polyguard-modal-close" style="position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 22px; color: #222; cursor: pointer; z-index: 2;">×</button>
|
|
37
|
+
<h2 style="margin-top: 0; font-size: 1.3rem; font-weight: 700; color: #222;">Identity Verification</h2>
|
|
38
|
+
<div style="font-size: 13px; color: #888; margin-bottom: 12px; font-weight: 500;">Powered by Polyguard</div>
|
|
39
|
+
<div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 16px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 12px;"></div>
|
|
40
|
+
<div style="margin-bottom: 12px; font-weight: 600; color: #222;">Scan this QR code to verify your identity.</div>
|
|
41
|
+
<div style="display: flex; justify-content: center; gap: 12px; font-size: 12px; color: #5a6c7d; margin-bottom: 12px; flex-wrap: wrap;">
|
|
42
|
+
<span>🔒 Encrypted</span>
|
|
43
|
+
<span>📱 On-device</span>
|
|
44
|
+
<span>⏲ Expires</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div id="polyguard-error" style="color: #b31d28; font-size: 14px; margin-bottom: 8px; display: none;"></div>
|
|
47
|
+
<button id="polyguard-modal-cancel" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer; margin-top: 8px; width: 100%; max-width: 240px;">Cancel</button>
|
|
48
|
+
</div>
|
|
49
|
+
`;
|
|
50
|
+
return modal;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function showSpinner(qrDiv) {
|
|
54
|
+
qrDiv.innerHTML = LOADING_SPINNER;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function renderMobileButton(qrDiv, qrUrl, isTargetMode) {
|
|
58
|
+
qrDiv.innerHTML = `<button id="polyguard-open-app-button" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer;">Open Polyguard App</button>`;
|
|
59
|
+
qrDiv.style.background = 'transparent';
|
|
60
|
+
qrDiv.querySelector('#polyguard-open-app-button').onclick = () => window.location.assign(qrUrl);
|
|
61
|
+
|
|
62
|
+
// Update surrounding text only in modal mode
|
|
63
|
+
if (!isTargetMode) {
|
|
64
|
+
const instructionText = qrDiv.nextElementSibling;
|
|
65
|
+
if (instructionText) {
|
|
66
|
+
instructionText.textContent = 'Tap the button to verify with the Polyguard app.';
|
|
67
|
+
}
|
|
68
|
+
const instructionList = instructionText.nextElementSibling;
|
|
69
|
+
if (instructionList && instructionList.children[1]) {
|
|
70
|
+
instructionList.children[1].textContent = 'If you do not have the Polyguard app, you will be redirected to download it.';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function renderQRCode(qrDiv, qrUrl) {
|
|
76
|
+
const startTime = Date.now();
|
|
77
|
+
console.log('time before qr code', startTime);
|
|
78
|
+
QRCode.toString(qrUrl, { type: 'svg' }, (err, svg) => {
|
|
79
|
+
if (!err) qrDiv.innerHTML = svg;
|
|
80
|
+
});
|
|
81
|
+
console.log('time to generate qr code', Date.now() - startTime);
|
|
82
|
+
}
|