Optimize JSON and QR codes

- Replaced original JSON with minimized binary format (gzip + base64).
- Adjusted rendering and QR code generation for compatibility.
- Reduced payload size for improved efficiency.
This commit is contained in:
lockbitchat
2025-10-05 06:21:14 -04:00
parent ec04bebf22
commit d2830b9c46
8 changed files with 824 additions and 212 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

349
dist/app.js vendored
View File

@@ -489,7 +489,7 @@ var EnhancedConnectionSetup = ({
}, "Creating a secure channel") }, "Creating a secure channel")
]), ]),
// Step 1 // Step 1
React.createElement("div", { !showAnswerStep && React.createElement("div", {
key: "step1", key: "step1",
className: "card-minimal rounded-xl p-6" className: "card-minimal rounded-xl p-6"
}, [ }, [
@@ -510,16 +510,16 @@ var EnhancedConnectionSetup = ({
key: "description", key: "description",
className: "text-secondary text-sm mb-4" className: "text-secondary text-sm mb-4"
}, "Creating cryptographically strong keys and codes to protect against attacks"), }, "Creating cryptographically strong keys and codes to protect against attacks"),
React.createElement("button", { !showOfferStep && React.createElement("button", {
key: "create-btn", key: "create-btn",
onClick: onCreateOffer, onClick: onCreateOffer,
disabled: connectionStatus === "connecting" || showOfferStep, disabled: connectionStatus === "connecting",
className: `w-full btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed` className: `w-full btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed`
}, [ }, [
React.createElement("i", { React.createElement("i", {
className: "fas fa-shield-alt mr-2" className: "fas fa-shield-alt mr-2"
}), }),
showOfferStep ? "Keys created \u2713" : "Create secure keys" "Create secure keys"
]), ]),
showOfferStep && React.createElement("div", { showOfferStep && React.createElement("div", {
key: "offer-result", key: "offer-result",
@@ -542,46 +542,29 @@ var EnhancedConnectionSetup = ({
key: "offer-data", key: "offer-data",
className: "space-y-3" className: "space-y-3"
}, [ }, [
React.createElement("textarea", { // Raw JSON hidden intentionally; users copy compressed string or use QR
key: "textarea",
value: typeof offerData === "object" ? JSON.stringify(offerData, null, 2) : offerData,
readOnly: true,
rows: 8,
className: "w-full p-3 bg-custom-bg border border-gray-500/20 rounded-lg font-mono text-xs text-secondary resize-none custom-scrollbar"
}),
React.createElement("div", { React.createElement("div", {
key: "buttons", key: "buttons",
className: "flex gap-2" className: "flex gap-2"
}, [ }, [
React.createElement(EnhancedCopyButton, { React.createElement(EnhancedCopyButton, {
key: "copy", key: "copy",
text: typeof offerData === "object" ? JSON.stringify(offerData, null, 2) : offerData, text: (() => {
className: "flex-1 px-3 py-2 bg-orange-500/10 hover:bg-orange-500/20 text-orange-400 border border-orange-500/20 rounded text-sm font-medium"
}, "Copy invitation code"),
React.createElement("button", {
key: "qr-toggle",
onClick: async () => {
const next = !showQRCode;
setShowQRCode(next);
if (next) {
try { try {
const payload = typeof offerData === "object" ? JSON.stringify(offerData) : offerData; const min = typeof offerData === "object" ? JSON.stringify(offerData) : offerData || "";
if (payload && payload.length) { if (typeof window.encodeBinaryToPrefixed === "function") {
await generateQRCode(payload); return window.encodeBinaryToPrefixed(min);
} }
} catch (e2) { if (typeof window.compressToPrefixedGzip === "function") {
console.warn("QR regenerate on toggle failed:", e2); return window.compressToPrefixedGzip(min);
} }
return min;
} catch {
return typeof offerData === "object" ? JSON.stringify(offerData) : offerData || "";
} }
}, })(),
className: "px-3 py-2 bg-blue-500/10 hover:bg-blue-500/20 text-blue-400 border border-blue-500/20 rounded text-sm font-medium transition-all duration-200" className: "flex-1 px-3 py-2 bg-orange-500/10 hover:bg-orange-500/20 text-orange-400 border border-orange-500/20 rounded text-sm font-medium"
}, [ }, "Copy invitation code")
React.createElement("i", {
key: "icon",
className: showQRCode ? "fas fa-eye-slash mr-1" : "fas fa-qrcode mr-1"
}),
showQRCode ? "Hide QR" : "Show QR"
])
]), ]),
showQRCode && qrCodeUrl && React.createElement("div", { showQRCode && qrCodeUrl && React.createElement("div", {
key: "qr-container", key: "qr-container",
@@ -785,8 +768,7 @@ var EnhancedConnectionSetup = ({
className: "text-xl font-semibold text-primary mb-2" className: "text-xl font-semibold text-primary mb-2"
}, "Joining the secure channel") }, "Joining the secure channel")
]), ]),
// Step 1 showAnswerStep ? null : React.createElement("div", {
React.createElement("div", {
key: "step1", key: "step1",
className: "card-minimal rounded-xl p-6" className: "card-minimal rounded-xl p-6"
}, [ }, [
@@ -934,16 +916,23 @@ var EnhancedConnectionSetup = ({
key: "answer-data", key: "answer-data",
className: "space-y-3 mb-4" className: "space-y-3 mb-4"
}, [ }, [
React.createElement("textarea", { // Raw JSON hidden intentionally; users copy compressed string or use QR
key: "textarea",
value: typeof answerData === "object" ? JSON.stringify(answerData, null, 2) : answerData,
readOnly: true,
rows: 6,
className: "w-full p-3 bg-custom-bg border border-green-500/20 rounded-lg font-mono text-xs text-secondary resize-none custom-scrollbar"
}),
React.createElement(EnhancedCopyButton, { React.createElement(EnhancedCopyButton, {
key: "copy", key: "copy",
text: typeof answerData === "object" ? JSON.stringify(answerData, null, 2) : answerData, text: (() => {
try {
const min = typeof answerData === "object" ? JSON.stringify(answerData) : answerData || "";
if (typeof window.encodeBinaryToPrefixed === "function") {
return window.encodeBinaryToPrefixed(min);
}
if (typeof window.compressToPrefixedGzip === "function") {
return window.compressToPrefixedGzip(min);
}
return min;
} catch {
return typeof answerData === "object" ? JSON.stringify(answerData) : answerData || "";
}
})(),
className: "w-full px-3 py-2 bg-green-500/10 hover:bg-green-500/20 text-green-400 border border-green-500/20 rounded text-sm font-medium" className: "w-full px-3 py-2 bg-green-500/10 hover:bg-green-500/20 text-green-400 border border-green-500/20 rounded text-sm font-medium"
}, "Copy response code") }, "Copy response code")
]), ]),
@@ -1823,6 +1812,7 @@ var EnhancedSecureP2PChat = () => {
return templateOffer; return templateOffer;
}; };
const MAX_QR_LEN = 800; const MAX_QR_LEN = 800;
const BIN_MAX_QR_LEN = 400;
const [qrFramesTotal, setQrFramesTotal] = React.useState(0); const [qrFramesTotal, setQrFramesTotal] = React.useState(0);
const [qrFrameIndex, setQrFrameIndex] = React.useState(0); const [qrFrameIndex, setQrFrameIndex] = React.useState(0);
const [qrManualMode, setQrManualMode] = React.useState(false); const [qrManualMode, setQrManualMode] = React.useState(false);
@@ -1839,6 +1829,29 @@ var EnhancedSecureP2PChat = () => {
setQrFramesTotal(0); setQrFramesTotal(0);
setQrManualMode(false); setQrManualMode(false);
}; };
const renderCurrent = async () => {
const { chunks, idx } = qrAnimationRef.current || {};
if (!chunks || !chunks.length) return;
const current = chunks[idx % chunks.length];
try {
const isDesktop = typeof window !== "undefined" && (window.innerWidth || 0) >= 1024;
const QR_SIZE = isDesktop ? 720 : 512;
const url = await (window.generateQRCode ? window.generateQRCode(current, { errorCorrectionLevel: "M", margin: 2, size: QR_SIZE }) : Promise.resolve(""));
if (url) setQrCodeUrl(url);
} catch (e2) {
console.warn("Animated QR render error (current):", e2);
}
setQrFrameIndex((qrAnimationRef.current?.idx || 0) % (qrAnimationRef.current?.chunks?.length || 1) + 1);
};
const renderAndAdvance = async () => {
await renderCurrent();
const len = qrAnimationRef.current?.chunks?.length || 0;
if (len > 0) {
const nextIdx = ((qrAnimationRef.current?.idx || 0) + 1) % len;
qrAnimationRef.current.idx = nextIdx;
setQrFrameIndex(nextIdx + 1);
}
};
const toggleQrManualMode = () => { const toggleQrManualMode = () => {
const newManualMode = !qrManualMode; const newManualMode = !qrManualMode;
setQrManualMode(newManualMode); setQrManualMode(newManualMode);
@@ -1849,39 +1862,65 @@ var EnhancedSecureP2PChat = () => {
} }
console.log("QR Manual mode enabled - auto-scroll stopped"); console.log("QR Manual mode enabled - auto-scroll stopped");
} else { } else {
if (qrAnimationRef.current.chunks.length > 1 && qrAnimationRef.current.active) { if (qrAnimationRef.current.chunks.length > 1) {
const intervalMs = 4e3; const intervalMs = 3e3;
qrAnimationRef.current.timer = setInterval(renderNext, intervalMs); qrAnimationRef.current.active = true;
clearInterval(qrAnimationRef.current.timer);
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
} }
console.log("QR Manual mode disabled - auto-scroll resumed"); console.log("QR Manual mode disabled - auto-scroll resumed");
} }
}; };
const nextQrFrame = () => { const nextQrFrame = async () => {
console.log("\u{1F3AE} nextQrFrame called, qrFramesTotal:", qrFramesTotal, "qrAnimationRef.current:", qrAnimationRef.current); console.log("\u{1F3AE} nextQrFrame called, qrFramesTotal:", qrFramesTotal, "qrAnimationRef.current:", qrAnimationRef.current);
if (qrAnimationRef.current.chunks.length > 1) { if (qrAnimationRef.current.chunks.length > 1) {
const nextIdx = (qrAnimationRef.current.idx + 1) % qrAnimationRef.current.chunks.length; const nextIdx = (qrAnimationRef.current.idx + 1) % qrAnimationRef.current.chunks.length;
qrAnimationRef.current.idx = nextIdx; qrAnimationRef.current.idx = nextIdx;
setQrFrameIndex(nextIdx + 1); setQrFrameIndex(nextIdx + 1);
console.log("\u{1F3AE} Next frame index:", nextIdx + 1); console.log("\u{1F3AE} Next frame index:", nextIdx + 1);
renderNext(); try {
clearInterval(qrAnimationRef.current.timer);
} catch {
}
qrAnimationRef.current.timer = null;
await renderCurrent();
if (!qrManualMode && qrAnimationRef.current.chunks.length > 1) {
const intervalMs = 3e3;
qrAnimationRef.current.active = true;
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
} else {
qrAnimationRef.current.active = false;
}
} else { } else {
console.log("\u{1F3AE} No multiple frames to navigate"); console.log("\u{1F3AE} No multiple frames to navigate");
} }
}; };
const prevQrFrame = () => { const prevQrFrame = async () => {
console.log("\u{1F3AE} prevQrFrame called, qrFramesTotal:", qrFramesTotal, "qrAnimationRef.current:", qrAnimationRef.current); console.log("\u{1F3AE} prevQrFrame called, qrFramesTotal:", qrFramesTotal, "qrAnimationRef.current:", qrAnimationRef.current);
if (qrAnimationRef.current.chunks.length > 1) { if (qrAnimationRef.current.chunks.length > 1) {
const prevIdx = (qrAnimationRef.current.idx - 1 + qrAnimationRef.current.chunks.length) % qrAnimationRef.current.chunks.length; const prevIdx = (qrAnimationRef.current.idx - 1 + qrAnimationRef.current.chunks.length) % qrAnimationRef.current.chunks.length;
qrAnimationRef.current.idx = prevIdx; qrAnimationRef.current.idx = prevIdx;
setQrFrameIndex(prevIdx + 1); setQrFrameIndex(prevIdx + 1);
console.log("\u{1F3AE} Previous frame index:", prevIdx + 1); console.log("\u{1F3AE} Previous frame index:", prevIdx + 1);
renderNext(); try {
clearInterval(qrAnimationRef.current.timer);
} catch {
}
qrAnimationRef.current.timer = null;
await renderCurrent();
if (!qrManualMode && qrAnimationRef.current.chunks.length > 1) {
const intervalMs = 3e3;
qrAnimationRef.current.active = true;
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
} else {
qrAnimationRef.current.active = false;
}
} else { } else {
console.log("\u{1F3AE} No multiple frames to navigate"); console.log("\u{1F3AE} No multiple frames to navigate");
} }
}; };
const qrChunksBufferRef = React.useRef({ id: null, total: 0, seen: /* @__PURE__ */ new Set(), items: [] }); const qrChunksBufferRef = React.useRef({ id: null, total: 0, seen: /* @__PURE__ */ new Set(), items: [] });
const generateQRCode2 = async (data) => { const generateQRCode = async (data) => {
try { try {
const originalSize = typeof data === "string" ? data.length : JSON.stringify(data).length; const originalSize = typeof data === "string" ? data.length : JSON.stringify(data).length;
const payload = typeof data === "string" ? data : JSON.stringify(data); const payload = typeof data === "string" ? data : JSON.stringify(data);
@@ -1889,14 +1928,32 @@ var EnhancedSecureP2PChat = () => {
const QR_SIZE = isDesktop ? 720 : 512; const QR_SIZE = isDesktop ? 720 : 512;
if (payload.length <= MAX_QR_LEN) { if (payload.length <= MAX_QR_LEN) {
if (!window.generateQRCode) throw new Error("QR code generator unavailable"); if (!window.generateQRCode) throw new Error("QR code generator unavailable");
stopQrAnimation(); try {
if (qrAnimationRef.current && qrAnimationRef.current.timer) {
clearInterval(qrAnimationRef.current.timer);
}
} catch {
}
qrAnimationRef.current = { timer: null, chunks: [], idx: 0, active: false };
setQrFrameIndex(0);
setQrFramesTotal(0);
setQrManualMode(false);
const qrDataUrl = await window.generateQRCode(payload, { errorCorrectionLevel: "M", size: QR_SIZE, margin: 2 }); const qrDataUrl = await window.generateQRCode(payload, { errorCorrectionLevel: "M", size: QR_SIZE, margin: 2 });
setQrCodeUrl(qrDataUrl); setQrCodeUrl(qrDataUrl);
setQrFramesTotal(1); setQrFramesTotal(1);
setQrFrameIndex(1); setQrFrameIndex(1);
return; return;
} }
stopQrAnimation(); try {
if (qrAnimationRef.current && qrAnimationRef.current.timer) {
clearInterval(qrAnimationRef.current.timer);
}
} catch {
}
qrAnimationRef.current = { timer: null, chunks: [], idx: 0, active: false };
setQrFrameIndex(0);
setQrFramesTotal(0);
setQrManualMode(false);
const id = `raw_${Date.now()}_${Math.random().toString(36).slice(2)}`; const id = `raw_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const TARGET_CHUNKS = 10; const TARGET_CHUNKS = 10;
const FRAME_MAX = Math.max(200, Math.floor(payload.length / TARGET_CHUNKS)); const FRAME_MAX = Math.max(200, Math.floor(payload.length / TARGET_CHUNKS));
@@ -1921,26 +1978,11 @@ var EnhancedSecureP2PChat = () => {
setQrFramesTotal(rawChunks.length); setQrFramesTotal(rawChunks.length);
setQrFrameIndex(1); setQrFrameIndex(1);
const EC_OPTS = { errorCorrectionLevel: "M", margin: 2, size: QR_SIZE }; const EC_OPTS = { errorCorrectionLevel: "M", margin: 2, size: QR_SIZE };
const renderNext2 = async () => { await renderNext();
const { chunks, idx, active } = qrAnimationRef.current;
if (!active || !chunks.length) return;
const current = chunks[idx % chunks.length];
try {
const url = await window.generateQRCode(current, EC_OPTS);
setQrCodeUrl(url);
} catch (e2) {
console.warn("Animated QR render error (raw):", e2);
}
const nextIdx = (idx + 1) % chunks.length;
qrAnimationRef.current.idx = nextIdx;
setQrFrameIndex(nextIdx + 1);
};
await renderNext2();
if (!qrManualMode) { if (!qrManualMode) {
const ua = typeof navigator !== "undefined" && navigator.userAgent ? navigator.userAgent : "";
const isIOS = /iPhone|iPad|iPod/i.test(ua);
const intervalMs = 4e3; const intervalMs = 4e3;
qrAnimationRef.current.timer = setInterval(renderNext2, intervalMs); qrAnimationRef.current.active = true;
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
} }
return; return;
} catch (error) { } catch (error) {
@@ -2018,7 +2060,18 @@ var EnhancedSecureP2PChat = () => {
}; };
const handleQRScan = async (scannedData) => { const handleQRScan = async (scannedData) => {
try { try {
const parsedData = JSON.parse(scannedData); let parsedData;
if (typeof window.decodeAnyPayload === "function") {
const any = window.decodeAnyPayload(scannedData);
if (typeof any === "string") {
parsedData = JSON.parse(any);
} else {
parsedData = any;
}
} else {
const maybeDecompressed = typeof window.decompressIfNeeded === "function" ? window.decompressIfNeeded(scannedData) : scannedData;
parsedData = JSON.parse(maybeDecompressed);
}
if (parsedData.hdr && parsedData.body) { if (parsedData.hdr && parsedData.body) {
const { hdr } = parsedData; const { hdr } = parsedData;
if (!qrChunksBufferRef.current.id || qrChunksBufferRef.current.id !== hdr.id) { if (!qrChunksBufferRef.current.id || qrChunksBufferRef.current.id !== hdr.id) {
@@ -2064,6 +2117,34 @@ var EnhancedSecureP2PChat = () => {
console.warn("RAW multi-frame reconstruction failed:", e2); console.warn("RAW multi-frame reconstruction failed:", e2);
return Promise.resolve(false); return Promise.resolve(false);
} }
} else if (hdr.rt === "bin") {
try {
const parts = qrChunksBufferRef.current.items.map((s) => JSON.parse(s)).sort((a, b) => (a.hdr.seq || 0) - (b.hdr.seq || 0)).map((p) => p.body || "");
const fullText = parts.join("");
let payloadObj;
if (typeof window.decodeAnyPayload === "function") {
const any = window.decodeAnyPayload(fullText);
payloadObj = typeof any === "string" ? JSON.parse(any) : any;
} else {
payloadObj = JSON.parse(fullText);
}
if (showOfferStep) {
setAnswerInput(JSON.stringify(payloadObj, null, 2));
} else {
setOfferInput(JSON.stringify(payloadObj, null, 2));
}
setMessages((prev) => [...prev, { message: "All frames captured. BIN payload reconstructed.", type: "success" }]);
try {
document.dispatchEvent(new CustomEvent("qr-scan-complete", { detail: { id: hdr.id } }));
} catch {
}
qrChunksBufferRef.current = { id: null, total: 0, seen: /* @__PURE__ */ new Set(), items: [] };
setShowQRScannerModal(false);
return Promise.resolve(true);
} catch (e2) {
console.warn("BIN multi-frame reconstruction failed:", e2);
return Promise.resolve(false);
}
} else if (window.receiveAndProcess) { } else if (window.receiveAndProcess) {
try { try {
const results = await window.receiveAndProcess(qrChunksBufferRef.current.items); const results = await window.receiveAndProcess(qrChunksBufferRef.current.items);
@@ -2130,9 +2211,9 @@ var EnhancedSecureP2PChat = () => {
return false; return false;
} }
} else { } else {
if (!parsedData.sdp) { if (!parsedData.sdp && parsedData.type === "enhanced_secure_offer") {
setMessages((prev) => [...prev, { setMessages((prev) => [...prev, {
message: "QR code contains compressed data (SDP removed). Please use copy/paste for full data.", message: "Compressed QR may omit SDP for brevity. Use copy/paste if connection fails.",
type: "warning" type: "warning"
}]); }]);
} }
@@ -2174,7 +2255,50 @@ var EnhancedSecureP2PChat = () => {
setOfferData(offer); setOfferData(offer);
setShowOfferStep(true); setShowOfferStep(true);
const offerString = typeof offer === "object" ? JSON.stringify(offer) : offer; const offerString = typeof offer === "object" ? JSON.stringify(offer) : offer;
await generateQRCode2(offerString); try {
if (typeof window.encodeBinaryToPrefixed === "function") {
const bin = window.encodeBinaryToPrefixed(offerString);
const id = `bin_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const TARGET_CHUNKS = 10;
let FRAME_MAX = Math.max(200, Math.floor(bin.length / TARGET_CHUNKS));
if (FRAME_MAX <= 0) FRAME_MAX = 200;
let total = Math.ceil(bin.length / FRAME_MAX);
if (total < 2) {
total = 2;
FRAME_MAX = Math.ceil(bin.length / 2) || 1;
}
const chunks = [];
for (let i = 0; i < total; i++) {
const seq = i + 1;
const part = bin.slice(i * FRAME_MAX, (i + 1) * FRAME_MAX);
chunks.push(JSON.stringify({ hdr: { v: 1, id, seq, total, rt: "bin" }, body: part }));
}
const isDesktop = typeof window !== "undefined" && (window.innerWidth || 0) >= 1024;
const QR_SIZE = isDesktop ? 720 : 512;
if (window.generateQRCode && chunks.length > 0) {
const firstUrl = await window.generateQRCode(chunks[0], { errorCorrectionLevel: "M", size: QR_SIZE, margin: 2 });
if (firstUrl) setQrCodeUrl(firstUrl);
}
try {
if (qrAnimationRef.current && qrAnimationRef.current.timer) {
clearInterval(qrAnimationRef.current.timer);
}
} catch {
}
qrAnimationRef.current = { timer: null, chunks, idx: 0, active: true };
setQrFramesTotal(chunks.length);
setQrFrameIndex(1);
setQrManualMode(false);
const intervalMs = 3e3;
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
try {
setShowQRCode(true);
} catch {
}
}
} catch (e2) {
console.warn("Offer QR precompute failed:", e2);
}
const existingMessages = messages.filter( const existingMessages = messages.filter(
(m) => m.type === "system" && (m.message.includes("Secure invitation created") || m.message.includes("Send the encrypted code")) (m) => m.type === "system" && (m.message.includes("Secure invitation created") || m.message.includes("Send the encrypted code"))
); );
@@ -2224,7 +2348,13 @@ var EnhancedSecureP2PChat = () => {
}]); }]);
let offer; let offer;
try { try {
offer = JSON.parse(offerInput.trim()); if (typeof window.decodeAnyPayload === "function") {
const any = window.decodeAnyPayload(offerInput.trim());
offer = typeof any === "string" ? JSON.parse(any) : any;
} else {
const rawText = typeof window.decompressIfNeeded === "function" ? window.decompressIfNeeded(offerInput.trim()) : offerInput.trim();
offer = JSON.parse(rawText);
}
} catch (parseError) { } catch (parseError) {
throw new Error(`Invalid invitation format: ${parseError.message}`); throw new Error(`Invalid invitation format: ${parseError.message}`);
} }
@@ -2239,7 +2369,62 @@ var EnhancedSecureP2PChat = () => {
setAnswerData(answer); setAnswerData(answer);
setShowAnswerStep(true); setShowAnswerStep(true);
const answerString = typeof answer === "object" ? JSON.stringify(answer) : answer; const answerString = typeof answer === "object" ? JSON.stringify(answer) : answer;
await generateQRCode2(answerString); try {
if (typeof window.encodeBinaryToPrefixed === "function") {
const bin = window.encodeBinaryToPrefixed(answerString);
const id = `ans_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const TARGET_CHUNKS = 10;
let FRAME_MAX = Math.max(200, Math.floor(bin.length / TARGET_CHUNKS));
if (FRAME_MAX <= 0) FRAME_MAX = 200;
let total = Math.ceil(bin.length / FRAME_MAX);
if (total < 2) {
total = 2;
FRAME_MAX = Math.ceil(bin.length / 2) || 1;
}
const chunks = [];
for (let i = 0; i < total; i++) {
const seq = i + 1;
const part = bin.slice(i * FRAME_MAX, (i + 1) * FRAME_MAX);
chunks.push(JSON.stringify({ hdr: { v: 1, id, seq, total, rt: "bin" }, body: part }));
}
const isDesktop = typeof window !== "undefined" && (window.innerWidth || 0) >= 1024;
const QR_SIZE = isDesktop ? 720 : 512;
if (window.generateQRCode && chunks.length > 0) {
const firstUrl = await window.generateQRCode(chunks[0], { errorCorrectionLevel: "M", size: QR_SIZE, margin: 2 });
if (firstUrl) setQrCodeUrl(firstUrl);
}
try {
if (qrAnimationRef.current && qrAnimationRef.current.timer) {
clearInterval(qrAnimationRef.current.timer);
}
} catch {
}
qrAnimationRef.current = { timer: null, chunks, idx: 0, active: true };
setQrFramesTotal(chunks.length);
setQrFrameIndex(1);
setQrManualMode(false);
const intervalMs = 3e3;
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
try {
setShowQRCode(true);
} catch {
}
} else {
let url = "";
if (typeof window.generateCompressedQRCode === "function") {
url = await window.generateCompressedQRCode(answerString);
} else {
url = await generateQRCode(answerString);
}
if (url) setQrCodeUrl(url);
try {
setShowQRCode(true);
} catch {
}
}
} catch (e2) {
console.warn("Answer QR generation failed:", e2);
}
if (e.target.value.trim().length > 0) { if (e.target.value.trim().length > 0) {
if (typeof markAnswerCreated === "function") { if (typeof markAnswerCreated === "function") {
markAnswerCreated(); markAnswerCreated();
@@ -2304,7 +2489,13 @@ var EnhancedSecureP2PChat = () => {
}]); }]);
let answer; let answer;
try { try {
answer = JSON.parse(answerInput.trim()); if (typeof window.decodeAnyPayload === "function") {
const anyAnswer = window.decodeAnyPayload(answerInput.trim());
answer = typeof anyAnswer === "string" ? JSON.parse(anyAnswer) : anyAnswer;
} else {
const rawText = typeof window.decompressIfNeeded === "function" ? window.decompressIfNeeded(answerInput.trim()) : answerInput.trim();
answer = JSON.parse(rawText);
}
} catch (parseError) { } catch (parseError) {
throw new Error(`Invalid response format: ${parseError.message}`); throw new Error(`Invalid response format: ${parseError.message}`);
} }

6
dist/app.js.map vendored

File diff suppressed because one or more lines are too long

141
dist/qr-local.js vendored
View File

@@ -860,7 +860,7 @@ var require_reed_solomon_encoder = __commonJS({
this.degree = degree; this.degree = degree;
this.genPoly = Polynomial.generateECPolynomial(this.degree); this.genPoly = Polynomial.generateECPolynomial(this.degree);
}; };
ReedSolomonEncoder.prototype.encode = function encode2(data) { ReedSolomonEncoder.prototype.encode = function encode3(data) {
if (!this.genPoly) { if (!this.genPoly) {
throw new Error("Encoder not initialized"); throw new Error("Encoder not initialized");
} }
@@ -27551,7 +27551,7 @@ var require_cbor = __commonJS({
(function(global2, undefined2) { (function(global2, undefined2) {
"use strict"; "use strict";
var POW_2_24 = Math.pow(2, -24), POW_2_32 = Math.pow(2, 32), POW_2_53 = Math.pow(2, 53); var POW_2_24 = Math.pow(2, -24), POW_2_32 = Math.pow(2, 32), POW_2_53 = Math.pow(2, 53);
function encode2(value) { function encode3(value) {
var data = new ArrayBuffer(256); var data = new ArrayBuffer(256);
var dataView = new DataView(data); var dataView = new DataView(data);
var lastLength; var lastLength;
@@ -27694,7 +27694,7 @@ var require_cbor = __commonJS({
retView.setUint8(i, dataView.getUint8(i)); retView.setUint8(i, dataView.getUint8(i));
return ret; return ret;
} }
function decode2(data, tagger, simpleValue) { function decode3(data, tagger, simpleValue) {
var dataView = new DataView(data); var dataView = new DataView(data);
var offset = 0; var offset = 0;
if (typeof tagger !== "function") if (typeof tagger !== "function")
@@ -27890,7 +27890,7 @@ var require_cbor = __commonJS({
throw "Remaining bytes"; throw "Remaining bytes";
return ret; return ret;
} }
var obj = { encode: encode2, decode: decode2 }; var obj = { encode: encode3, decode: decode3 };
if (typeof define === "function" && define.amd) if (typeof define === "function" && define.amd)
define("cbor/cbor", obj); define("cbor/cbor", obj);
else if (typeof module !== "undefined" && module.exports) else if (typeof module !== "undefined" && module.exports)
@@ -31900,9 +31900,6 @@ var Html5QrcodeScanner = (function() {
return Html5QrcodeScanner2; return Html5QrcodeScanner2;
})(); })();
// src/crypto/cose-qr.js
var cbor = __toESM(require_cbor());
// node_modules/pako/dist/pako.esm.mjs // node_modules/pako/dist/pako.esm.mjs
var Z_FIXED$1 = 4; var Z_FIXED$1 = 4;
var Z_BINARY = 0; var Z_BINARY = 0;
@@ -36076,9 +36073,15 @@ var inflate_1$1 = {
var { Deflate, deflate, deflateRaw, gzip } = deflate_1$1; var { Deflate, deflate, deflateRaw, gzip } = deflate_1$1;
var { Inflate, inflate, inflateRaw, ungzip } = inflate_1$1; var { Inflate, inflate, inflateRaw, ungzip } = inflate_1$1;
var deflate_1 = deflate; var deflate_1 = deflate;
var gzip_1 = gzip;
var inflate_1 = inflate; var inflate_1 = inflate;
var ungzip_1 = ungzip;
// src/scripts/qr-local.js
var cbor2 = __toESM(require_cbor());
// src/crypto/cose-qr.js // src/crypto/cose-qr.js
var cbor = __toESM(require_cbor());
var base64 = __toESM(require_base64_js()); var base64 = __toESM(require_base64_js());
function toBase64Url(uint8) { function toBase64Url(uint8) {
let b64 = base64.fromByteArray(uint8); let b64 = base64.fromByteArray(uint8);
@@ -36421,12 +36424,83 @@ window.packSecurePayload = packSecurePayload;
window.receiveAndProcess = receiveAndProcess; window.receiveAndProcess = receiveAndProcess;
// src/scripts/qr-local.js // src/scripts/qr-local.js
var COMPRESSION_PREFIX = "SB1:gz:";
var BINARY_PREFIX = "SB1:bin:";
function uint8ToBase64(bytes) {
let binary = "";
const chunkSize = 32768;
for (let i = 0; i < bytes.length; i += chunkSize) {
const chunk = bytes.subarray(i, i + chunkSize);
binary += String.fromCharCode.apply(null, chunk);
}
return btoa(binary);
}
function base64ToUint8(b64) {
const binary = atob(b64);
const len = binary.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
return bytes;
}
function compressStringToBase64Gzip(text) {
const utf8 = new TextEncoder().encode(text);
const gz = gzip_1(utf8);
return uint8ToBase64(gz);
}
function decompressBase64GzipToString(b64) {
const gz = base64ToUint8(b64);
const out = ungzip_1(gz);
return new TextDecoder().decode(out);
}
async function generateQRCode(text, opts = {}) { async function generateQRCode(text, opts = {}) {
const size = opts.size || 512; const size = opts.size || 512;
const margin = opts.margin ?? 2; const margin = opts.margin ?? 2;
const errorCorrectionLevel = opts.errorCorrectionLevel || "M"; const errorCorrectionLevel = opts.errorCorrectionLevel || "M";
return await QRCode.toDataURL(text, { width: size, margin, errorCorrectionLevel }); return await QRCode.toDataURL(text, { width: size, margin, errorCorrectionLevel });
} }
async function generateCompressedQRCode(text, opts = {}) {
try {
const compressedB64 = compressStringToBase64Gzip(text);
const payload = COMPRESSION_PREFIX + compressedB64;
return await generateQRCode(payload, opts);
} catch (e) {
console.warn("generateCompressedQRCode failed, falling back to plain:", e?.message || e);
return await generateQRCode(text, opts);
}
}
function base64ToBase64Url(b64) {
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
function base64UrlToBase64(b64url) {
let b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
const pad = b64.length % 4;
if (pad) b64 += "=".repeat(4 - pad);
return b64;
}
function encodeObjectToBinaryBase64Url(obj) {
const cborBytes = cbor2.encode(obj);
const compressed = deflate_1(new Uint8Array(cborBytes));
const b64 = uint8ToBase64(compressed);
return base64ToBase64Url(b64);
}
function decodeBinaryBase64UrlToObject(b64url) {
const b64 = base64UrlToBase64(b64url);
const compressed = base64ToUint8(b64);
const decompressed = inflate_1(compressed);
const ab = decompressed.buffer.slice(decompressed.byteOffset, decompressed.byteOffset + decompressed.byteLength);
return cbor2.decode(ab);
}
async function generateBinaryQRCodeFromObject(obj, opts = {}) {
try {
const b64url = encodeObjectToBinaryBase64Url(obj);
const payload = BINARY_PREFIX + b64url;
return await generateQRCode(payload, opts);
} catch (e) {
console.warn("generateBinaryQRCodeFromObject failed, falling back to JSON compressed:", e?.message || e);
const text = JSON.stringify(obj);
return await generateCompressedQRCode(text, opts);
}
}
async function generateCOSEQRCode(data, senderKey = null, recipientKey = null) { async function generateCOSEQRCode(data, senderKey = null, recipientKey = null) {
try { try {
console.log("\u{1F510} Generating COSE-based QR code..."); console.log("\u{1F510} Generating COSE-based QR code...");
@@ -36443,11 +36517,62 @@ async function generateCOSEQRCode(data, senderKey = null, recipientKey = null) {
} }
} }
window.generateQRCode = generateQRCode; window.generateQRCode = generateQRCode;
window.generateCompressedQRCode = generateCompressedQRCode;
window.generateBinaryQRCodeFromObject = generateBinaryQRCodeFromObject;
window.generateCOSEQRCode = generateCOSEQRCode; window.generateCOSEQRCode = generateCOSEQRCode;
window.Html5Qrcode = Html5Qrcode; window.Html5Qrcode = Html5Qrcode;
window.packSecurePayload = packSecurePayload; window.packSecurePayload = packSecurePayload;
window.receiveAndProcess = receiveAndProcess; window.receiveAndProcess = receiveAndProcess;
console.log("QR libraries loaded: generateQRCode, generateCOSEQRCode, Html5Qrcode, COSE functions"); window.decompressIfNeeded = function(scannedText) {
try {
if (typeof scannedText === "string" && scannedText.startsWith(COMPRESSION_PREFIX)) {
const b64 = scannedText.slice(COMPRESSION_PREFIX.length);
return decompressBase64GzipToString(b64);
}
} catch (e) {
console.warn("decompressIfNeeded failed:", e?.message || e);
}
return scannedText;
};
window.compressToPrefixedGzip = function(text) {
try {
const payload = String(text || "");
const compressedB64 = compressStringToBase64Gzip(payload);
return COMPRESSION_PREFIX + compressedB64;
} catch (e) {
console.warn("compressToPrefixedGzip failed:", e?.message || e);
return String(text || "");
}
};
window.encodeBinaryToPrefixed = function(objOrJson) {
try {
const obj = typeof objOrJson === "string" ? JSON.parse(objOrJson) : objOrJson;
const b64url = encodeObjectToBinaryBase64Url(obj);
return BINARY_PREFIX + b64url;
} catch (e) {
console.warn("encodeBinaryToPrefixed failed:", e?.message || e);
return typeof objOrJson === "string" ? objOrJson : JSON.stringify(objOrJson);
}
};
window.decodeAnyPayload = function(scannedText) {
try {
if (typeof scannedText === "string") {
if (scannedText.startsWith(BINARY_PREFIX)) {
const b64url = scannedText.slice(BINARY_PREFIX.length);
return decodeBinaryBase64UrlToObject(b64url);
}
if (scannedText.startsWith(COMPRESSION_PREFIX)) {
const s = window.decompressIfNeeded(scannedText);
return s;
}
return scannedText;
}
} catch (e) {
console.warn("decodeAnyPayload failed:", e?.message || e);
}
return scannedText;
};
console.log("QR libraries loaded: generateQRCode, generateCompressedQRCode, generateBinaryQRCodeFromObject, Html5Qrcode, COSE functions");
/*! Bundled license information: /*! Bundled license information:
pako/dist/pako.esm.mjs: pako/dist/pako.esm.mjs:

File diff suppressed because one or more lines are too long

View File

@@ -517,7 +517,7 @@
]), ]),
// Step 1 // Step 1
React.createElement('div', { !showAnswerStep && React.createElement('div', {
key: 'step1', key: 'step1',
className: "card-minimal rounded-xl p-6" className: "card-minimal rounded-xl p-6"
}, [ }, [
@@ -538,16 +538,16 @@
key: 'description', key: 'description',
className: "text-secondary text-sm mb-4" className: "text-secondary text-sm mb-4"
}, "Creating cryptographically strong keys and codes to protect against attacks"), }, "Creating cryptographically strong keys and codes to protect against attacks"),
React.createElement('button', { !showOfferStep && React.createElement('button', {
key: 'create-btn', key: 'create-btn',
onClick: onCreateOffer, onClick: onCreateOffer,
disabled: connectionStatus === 'connecting' || showOfferStep, disabled: connectionStatus === 'connecting',
className: `w-full btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed` className: `w-full btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed`
}, [ }, [
React.createElement('i', { React.createElement('i', {
className: 'fas fa-shield-alt mr-2' className: 'fas fa-shield-alt mr-2'
}), }),
showOfferStep ? 'Keys created ✓' : 'Create secure keys' 'Create secure keys'
]), ]),
showOfferStep && React.createElement('div', { showOfferStep && React.createElement('div', {
@@ -571,46 +571,27 @@
key: 'offer-data', key: 'offer-data',
className: "space-y-3" className: "space-y-3"
}, [ }, [
React.createElement('textarea', { // Raw JSON hidden intentionally; users copy compressed string or use QR
key: 'textarea',
value: typeof offerData === 'object' ? JSON.stringify(offerData, null, 2) : offerData,
readOnly: true,
rows: 8,
className: "w-full p-3 bg-custom-bg border border-gray-500/20 rounded-lg font-mono text-xs text-secondary resize-none custom-scrollbar"
}),
React.createElement('div', { React.createElement('div', {
key: 'buttons', key: 'buttons',
className: "flex gap-2" className: "flex gap-2"
}, [ }, [
React.createElement(EnhancedCopyButton, { React.createElement(EnhancedCopyButton, {
key: 'copy', key: 'copy',
text: typeof offerData === 'object' ? JSON.stringify(offerData, null, 2) : offerData, text: (() => {
className: "flex-1 px-3 py-2 bg-orange-500/10 hover:bg-orange-500/20 text-orange-400 border border-orange-500/20 rounded text-sm font-medium"
}, 'Copy invitation code'),
React.createElement('button', {
key: 'qr-toggle',
onClick: async () => {
const next = !showQRCode;
setShowQRCode(next);
if (next) {
try { try {
const payload = typeof offerData === 'object' ? JSON.stringify(offerData) : offerData; const min = typeof offerData === 'object' ? JSON.stringify(offerData) : (offerData || '');
if (payload && payload.length) { if (typeof window.encodeBinaryToPrefixed === 'function') {
await generateQRCode(payload); return window.encodeBinaryToPrefixed(min);
} }
} catch (e) { if (typeof window.compressToPrefixedGzip === 'function') {
console.warn('QR regenerate on toggle failed:', e); return window.compressToPrefixedGzip(min);
} }
} return min;
}, } catch { return typeof offerData === 'object' ? JSON.stringify(offerData) : (offerData || ''); }
className: "px-3 py-2 bg-blue-500/10 hover:bg-blue-500/20 text-blue-400 border border-blue-500/20 rounded text-sm font-medium transition-all duration-200" })(),
}, [ className: "flex-1 px-3 py-2 bg-orange-500/10 hover:bg-orange-500/20 text-orange-400 border border-orange-500/20 rounded text-sm font-medium"
React.createElement('i', { }, 'Copy invitation code')
key: 'icon',
className: showQRCode ? 'fas fa-eye-slash mr-1' : 'fas fa-qrcode mr-1'
}),
showQRCode ? 'Hide QR' : 'Show QR'
])
]), ]),
showQRCode && qrCodeUrl && React.createElement('div', { showQRCode && qrCodeUrl && React.createElement('div', {
key: 'qr-container', key: 'qr-container',
@@ -827,8 +808,7 @@
}, 'Joining the secure channel') }, 'Joining the secure channel')
]), ]),
// Step 1 (showAnswerStep ? null : React.createElement('div', {
React.createElement('div', {
key: 'step1', key: 'step1',
className: "card-minimal rounded-xl p-6" className: "card-minimal rounded-xl p-6"
}, [ }, [
@@ -854,13 +834,11 @@
value: offerInput, value: offerInput,
onChange: (e) => { onChange: (e) => {
setOfferInput(e.target.value); setOfferInput(e.target.value);
// Mark answer as created when user manually enters data
if (e.target.value.trim().length > 0) { if (e.target.value.trim().length > 0) {
if (typeof markAnswerCreated === 'function') { if (typeof markAnswerCreated === 'function') {
markAnswerCreated(); markAnswerCreated();
} }
} }
}, },
rows: 8, rows: 8,
placeholder: "Paste the encrypted invitation code or scan QR code...", placeholder: "Paste the encrypted invitation code or scan QR code...",
@@ -908,7 +886,6 @@
React.createElement('button', { React.createElement('button', {
key: 'open-scanner', key: 'open-scanner',
onClick: () => { onClick: () => {
if (typeof setShowQRScannerModal === 'function') { if (typeof setShowQRScannerModal === 'function') {
setShowQRScannerModal(true); setShowQRScannerModal(true);
} else { } else {
@@ -931,7 +908,6 @@
const testData = '{"type":"test","message":"Hello QR Scanner!"}'; const testData = '{"type":"test","message":"Hello QR Scanner!"}';
const qrUrl = await window.generateQRCode(testData); const qrUrl = await window.generateQRCode(testData);
console.log('Test QR code generated:', qrUrl); console.log('Test QR code generated:', qrUrl);
// Open QR code in new tab for testing
const newWindow = window.open(); const newWindow = window.open();
newWindow.document.write(`<img src="${qrUrl}" style="width: 300px; height: 300px;">`); newWindow.document.write(`<img src="${qrUrl}" style="width: 300px; height: 300px;">`);
} }
@@ -944,7 +920,7 @@
className: "px-3 py-1 bg-gray-600/20 hover:bg-gray-600/30 text-gray-300 border border-gray-500/20 rounded text-xs font-medium transition-all duration-200" className: "px-3 py-1 bg-gray-600/20 hover:bg-gray-600/30 text-gray-300 border border-gray-500/20 rounded text-xs font-medium transition-all duration-200"
}, 'Close Scanner') }, 'Close Scanner')
]) ])
]), ])),
// Step 2 // Step 2
showAnswerStep && React.createElement('div', { showAnswerStep && React.createElement('div', {
@@ -981,16 +957,21 @@
key: 'answer-data', key: 'answer-data',
className: "space-y-3 mb-4" className: "space-y-3 mb-4"
}, [ }, [
React.createElement('textarea', { // Raw JSON hidden intentionally; users copy compressed string or use QR
key: 'textarea',
value: typeof answerData === 'object' ? JSON.stringify(answerData, null, 2) : answerData,
readOnly: true,
rows: 6,
className: "w-full p-3 bg-custom-bg border border-green-500/20 rounded-lg font-mono text-xs text-secondary resize-none custom-scrollbar"
}),
React.createElement(EnhancedCopyButton, { React.createElement(EnhancedCopyButton, {
key: 'copy', key: 'copy',
text: typeof answerData === 'object' ? JSON.stringify(answerData, null, 2) : answerData, text: (() => {
try {
const min = typeof answerData === 'object' ? JSON.stringify(answerData) : (answerData || '');
if (typeof window.encodeBinaryToPrefixed === 'function') {
return window.encodeBinaryToPrefixed(min);
}
if (typeof window.compressToPrefixedGzip === 'function') {
return window.compressToPrefixedGzip(min);
}
return min;
} catch { return typeof answerData === 'object' ? JSON.stringify(answerData) : (answerData || ''); }
})(),
className: "w-full px-3 py-2 bg-green-500/10 hover:bg-green-500/20 text-green-400 border border-green-500/20 rounded text-sm font-medium" className: "w-full px-3 py-2 bg-green-500/10 hover:bg-green-500/20 text-green-400 border border-green-500/20 rounded text-sm font-medium"
}, 'Copy response code') }, 'Copy response code')
]), ]),
@@ -2067,8 +2048,9 @@
return templateOffer; return templateOffer;
}; };
// Conservative QR payload limit (characters). Adjust per error correction level. // Conservative QR payload limits (characters). Adjust per error correction level.
const MAX_QR_LEN = 800; const MAX_QR_LEN = 800; // for JSON/plain/gzip
const BIN_MAX_QR_LEN = 400; // stricter for SB1:bin to improve scan reliability
const [qrFramesTotal, setQrFramesTotal] = React.useState(0); const [qrFramesTotal, setQrFramesTotal] = React.useState(0);
const [qrFrameIndex, setQrFrameIndex] = React.useState(0); const [qrFrameIndex, setQrFrameIndex] = React.useState(0);
const [qrManualMode, setQrManualMode] = React.useState(false); const [qrManualMode, setQrManualMode] = React.useState(false);
@@ -2083,6 +2065,33 @@
setQrManualMode(false); setQrManualMode(false);
}; };
// Render frame at current index (no index mutation)
const renderCurrent = async () => {
const { chunks, idx } = qrAnimationRef.current || {};
if (!chunks || !chunks.length) return;
const current = chunks[idx % chunks.length];
try {
const isDesktop = (typeof window !== 'undefined') && ((window.innerWidth || 0) >= 1024);
const QR_SIZE = isDesktop ? 720 : 512;
const url = await (window.generateQRCode ? window.generateQRCode(current, { errorCorrectionLevel: 'M', margin: 2, size: QR_SIZE }) : Promise.resolve(''));
if (url) setQrCodeUrl(url);
} catch (e) {
console.warn('Animated QR render error (current):', e);
}
setQrFrameIndex(((qrAnimationRef.current?.idx || 0) % (qrAnimationRef.current?.chunks?.length || 1)) + 1);
};
// Render current frame, then advance index by 1
const renderAndAdvance = async () => {
await renderCurrent();
const len = qrAnimationRef.current?.chunks?.length || 0;
if (len > 0) {
const nextIdx = ((qrAnimationRef.current?.idx || 0) + 1) % len;
qrAnimationRef.current.idx = nextIdx;
setQrFrameIndex(nextIdx + 1);
}
};
const toggleQrManualMode = () => { const toggleQrManualMode = () => {
const newManualMode = !qrManualMode; const newManualMode = !qrManualMode;
setQrManualMode(newManualMode); setQrManualMode(newManualMode);
@@ -2095,35 +2104,57 @@
} }
console.log('QR Manual mode enabled - auto-scroll stopped'); console.log('QR Manual mode enabled - auto-scroll stopped');
} else { } else {
if (qrAnimationRef.current.chunks.length > 1 && qrAnimationRef.current.active) { if (qrAnimationRef.current.chunks.length > 1) {
const intervalMs = 4000; const intervalMs = 3000;
qrAnimationRef.current.timer = setInterval(renderNext, intervalMs); qrAnimationRef.current.active = true;
clearInterval(qrAnimationRef.current.timer);
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
} }
console.log('QR Manual mode disabled - auto-scroll resumed'); console.log('QR Manual mode disabled - auto-scroll resumed');
} }
}; };
const nextQrFrame = () => { const nextQrFrame = async () => {
console.log('🎮 nextQrFrame called, qrFramesTotal:', qrFramesTotal, 'qrAnimationRef.current:', qrAnimationRef.current); console.log('🎮 nextQrFrame called, qrFramesTotal:', qrFramesTotal, 'qrAnimationRef.current:', qrAnimationRef.current);
if (qrAnimationRef.current.chunks.length > 1) { if (qrAnimationRef.current.chunks.length > 1) {
const nextIdx = (qrAnimationRef.current.idx + 1) % qrAnimationRef.current.chunks.length; const nextIdx = (qrAnimationRef.current.idx + 1) % qrAnimationRef.current.chunks.length;
qrAnimationRef.current.idx = nextIdx; qrAnimationRef.current.idx = nextIdx;
setQrFrameIndex(nextIdx + 1); setQrFrameIndex(nextIdx + 1);
console.log('🎮 Next frame index:', nextIdx + 1); console.log('🎮 Next frame index:', nextIdx + 1);
renderNext(); // Ensure auto-advance timer runs in manual mode too
try { clearInterval(qrAnimationRef.current.timer); } catch {}
qrAnimationRef.current.timer = null;
await renderCurrent();
// If not in manual mode, restart auto timer
if (!qrManualMode && qrAnimationRef.current.chunks.length > 1) {
const intervalMs = 3000;
qrAnimationRef.current.active = true;
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
} else {
qrAnimationRef.current.active = false;
}
} else { } else {
console.log('🎮 No multiple frames to navigate'); console.log('🎮 No multiple frames to navigate');
} }
}; };
const prevQrFrame = () => { const prevQrFrame = async () => {
console.log('🎮 prevQrFrame called, qrFramesTotal:', qrFramesTotal, 'qrAnimationRef.current:', qrAnimationRef.current); console.log('🎮 prevQrFrame called, qrFramesTotal:', qrFramesTotal, 'qrAnimationRef.current:', qrAnimationRef.current);
if (qrAnimationRef.current.chunks.length > 1) { if (qrAnimationRef.current.chunks.length > 1) {
const prevIdx = (qrAnimationRef.current.idx - 1 + qrAnimationRef.current.chunks.length) % qrAnimationRef.current.chunks.length; const prevIdx = (qrAnimationRef.current.idx - 1 + qrAnimationRef.current.chunks.length) % qrAnimationRef.current.chunks.length;
qrAnimationRef.current.idx = prevIdx; qrAnimationRef.current.idx = prevIdx;
setQrFrameIndex(prevIdx + 1); setQrFrameIndex(prevIdx + 1);
console.log('🎮 Previous frame index:', prevIdx + 1); console.log('🎮 Previous frame index:', prevIdx + 1);
renderNext(); try { clearInterval(qrAnimationRef.current.timer); } catch {}
qrAnimationRef.current.timer = null;
await renderCurrent();
if (!qrManualMode && qrAnimationRef.current.chunks.length > 1) {
const intervalMs = 3000;
qrAnimationRef.current.active = true;
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
} else {
qrAnimationRef.current.active = false;
}
} else { } else {
console.log('🎮 No multiple frames to navigate'); console.log('🎮 No multiple frames to navigate');
} }
@@ -2141,7 +2172,11 @@
const QR_SIZE = isDesktop ? 720 : 512; const QR_SIZE = isDesktop ? 720 : 512;
if (payload.length <= MAX_QR_LEN) { if (payload.length <= MAX_QR_LEN) {
if (!window.generateQRCode) throw new Error('QR code generator unavailable'); if (!window.generateQRCode) throw new Error('QR code generator unavailable');
stopQrAnimation(); try { if (qrAnimationRef.current && qrAnimationRef.current.timer) { clearInterval(qrAnimationRef.current.timer); } } catch {}
qrAnimationRef.current = { timer: null, chunks: [], idx: 0, active: false };
setQrFrameIndex(0);
setQrFramesTotal(0);
setQrManualMode(false);
const qrDataUrl = await window.generateQRCode(payload, { errorCorrectionLevel: 'M', size: QR_SIZE, margin: 2 }); const qrDataUrl = await window.generateQRCode(payload, { errorCorrectionLevel: 'M', size: QR_SIZE, margin: 2 });
setQrCodeUrl(qrDataUrl); setQrCodeUrl(qrDataUrl);
setQrFramesTotal(1); setQrFramesTotal(1);
@@ -2149,7 +2184,11 @@
return; return;
} }
stopQrAnimation(); try { if (qrAnimationRef.current && qrAnimationRef.current.timer) { clearInterval(qrAnimationRef.current.timer); } } catch {}
qrAnimationRef.current = { timer: null, chunks: [], idx: 0, active: false };
setQrFrameIndex(0);
setQrFramesTotal(0);
setQrManualMode(false);
const id = `raw_${Date.now()}_${Math.random().toString(36).slice(2)}`; const id = `raw_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const TARGET_CHUNKS = 10; const TARGET_CHUNKS = 10;
@@ -2175,27 +2214,12 @@
setQrFramesTotal(rawChunks.length); setQrFramesTotal(rawChunks.length);
setQrFrameIndex(1); setQrFrameIndex(1);
const EC_OPTS = { errorCorrectionLevel: 'M', margin: 2, size: QR_SIZE }; const EC_OPTS = { errorCorrectionLevel: 'M', margin: 2, size: QR_SIZE };
const renderNext = async () => {
const { chunks, idx, active } = qrAnimationRef.current;
if (!active || !chunks.length) return;
const current = chunks[idx % chunks.length];
try {
const url = await window.generateQRCode(current, EC_OPTS);
setQrCodeUrl(url);
} catch (e) {
console.warn('Animated QR render error (raw):', e);
}
const nextIdx = (idx + 1) % chunks.length;
qrAnimationRef.current.idx = nextIdx;
setQrFrameIndex(nextIdx + 1);
};
await renderNext(); await renderNext();
if (!qrManualMode) { if (!qrManualMode) {
const ua = (typeof navigator !== 'undefined' && navigator.userAgent) ? navigator.userAgent : '';
const isIOS = /iPhone|iPad|iPod/i.test(ua);
const intervalMs = 4000; // 4 seconds per frame for better readability const intervalMs = 4000; // 4 seconds per frame for better readability
qrAnimationRef.current.timer = setInterval(renderNext, intervalMs); qrAnimationRef.current.active = true;
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
} }
return; return;
} catch (error) { } catch (error) {
@@ -2279,11 +2303,21 @@
const handleQRScan = async (scannedData) => { const handleQRScan = async (scannedData) => {
try { try {
// Prefer binary (CBOR) decode, else gzip JSON, else raw JSON
let parsedData;
if (typeof window.decodeAnyPayload === 'function') {
const any = window.decodeAnyPayload(scannedData);
if (typeof any === 'string') {
parsedData = JSON.parse(any);
} else {
parsedData = any; // object from binary
}
} else {
const maybeDecompressed = (typeof window.decompressIfNeeded === 'function') ? window.decompressIfNeeded(scannedData) : scannedData;
parsedData = JSON.parse(maybeDecompressed);
}
// Try to parse as JSON first // QR with hdr/body: COSE or RAW/BIN animated frames
const parsedData = JSON.parse(scannedData);
// QR with hdr/body: COSE or RAW animated frames
if (parsedData.hdr && parsedData.body) { if (parsedData.hdr && parsedData.body) {
const { hdr } = parsedData; const { hdr } = parsedData;
// Initialize/rotate buffer by id // Initialize/rotate buffer by id
@@ -2309,7 +2343,7 @@
// Explicitly keep scanner open // Explicitly keep scanner open
return Promise.resolve(false); return Promise.resolve(false);
} }
// Completed: decide RAW vs COSE // Completed: decide RAW vs BIN vs COSE
if (hdr.rt === 'raw') { if (hdr.rt === 'raw') {
try { try {
// Sort by seq and concatenate bodies // Sort by seq and concatenate bodies
@@ -2334,6 +2368,34 @@
console.warn('RAW multi-frame reconstruction failed:', e); console.warn('RAW multi-frame reconstruction failed:', e);
return Promise.resolve(false); return Promise.resolve(false);
} }
} else if (hdr.rt === 'bin') {
try {
const parts = qrChunksBufferRef.current.items
.map(s => JSON.parse(s))
.sort((a, b) => (a.hdr.seq || 0) - (b.hdr.seq || 0))
.map(p => p.body || '');
const fullText = parts.join(''); // SB1:bin:...
let payloadObj;
if (typeof window.decodeAnyPayload === 'function') {
const any = window.decodeAnyPayload(fullText);
payloadObj = (typeof any === 'string') ? JSON.parse(any) : any;
} else {
payloadObj = JSON.parse(fullText);
}
if (showOfferStep) {
setAnswerInput(JSON.stringify(payloadObj, null, 2));
} else {
setOfferInput(JSON.stringify(payloadObj, null, 2));
}
setMessages(prev => [...prev, { message: 'All frames captured. BIN payload reconstructed.', type: 'success' }]);
try { document.dispatchEvent(new CustomEvent('qr-scan-complete', { detail: { id: hdr.id } })); } catch {}
qrChunksBufferRef.current = { id: null, total: 0, seen: new Set(), items: [] };
setShowQRScannerModal(false);
return Promise.resolve(true);
} catch (e) {
console.warn('BIN multi-frame reconstruction failed:', e);
return Promise.resolve(false);
}
} else if (window.receiveAndProcess) { } else if (window.receiveAndProcess) {
try { try {
const results = await window.receiveAndProcess(qrChunksBufferRef.current.items); const results = await window.receiveAndProcess(qrChunksBufferRef.current.items);
@@ -2409,10 +2471,10 @@
return false; return false;
} }
} else { } else {
// Check if this is compressed data (missing SDP) // If payload was compressed, it's already decompressed above; keep legacy warning only when clearly incomplete
if (!parsedData.sdp) { if (!parsedData.sdp && parsedData.type === 'enhanced_secure_offer') {
setMessages(prev => [...prev, { setMessages(prev => [...prev, {
message: 'QR code contains compressed data (SDP removed). Please use copy/paste for full data.', message: 'Compressed QR may omit SDP for brevity. Use copy/paste if connection fails.',
type: 'warning' type: 'warning'
}]); }]);
} }
@@ -2467,10 +2529,46 @@
setOfferData(offer); setOfferData(offer);
setShowOfferStep(true); setShowOfferStep(true);
// Generate QR code for the offer data // Do not auto-generate single QR; prepare animated binary frames when user opens QR
// Use compact JSON (no pretty-printing) to reduce size
const offerString = typeof offer === 'object' ? JSON.stringify(offer) : offer; const offerString = typeof offer === 'object' ? JSON.stringify(offer) : offer;
await generateQRCode(offerString); try {
if (typeof window.encodeBinaryToPrefixed === 'function') {
const bin = window.encodeBinaryToPrefixed(offerString);
// Precompute frames to be ready instantly on show
const id = `bin_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const TARGET_CHUNKS = 10;
let FRAME_MAX = Math.max(200, Math.floor(bin.length / TARGET_CHUNKS));
if (FRAME_MAX <= 0) FRAME_MAX = 200;
let total = Math.ceil(bin.length / FRAME_MAX);
if (total < 2) { total = 2; FRAME_MAX = Math.ceil(bin.length / 2) || 1; }
const chunks = [];
for (let i = 0; i < total; i++) {
const seq = i + 1;
const part = bin.slice(i * FRAME_MAX, (i + 1) * FRAME_MAX);
chunks.push(JSON.stringify({ hdr: { v: 1, id, seq, total, rt: 'bin' }, body: part }));
}
// Seed first frame and start auto-advance immediately
const isDesktop = (typeof window !== 'undefined') && ((window.innerWidth || 0) >= 1024);
const QR_SIZE = isDesktop ? 720 : 512;
if (window.generateQRCode && chunks.length > 0) {
const firstUrl = await window.generateQRCode(chunks[0], { errorCorrectionLevel: 'M', size: QR_SIZE, margin: 2 });
if (firstUrl) setQrCodeUrl(firstUrl);
}
// Store precomputed chunks to ref, ready for animation
try { if (qrAnimationRef.current && qrAnimationRef.current.timer) { clearInterval(qrAnimationRef.current.timer); } } catch {}
qrAnimationRef.current = { timer: null, chunks, idx: 0, active: true };
setQrFramesTotal(chunks.length);
setQrFrameIndex(1);
setQrManualMode(false);
// Start auto-advance loop for Offer immediately
const intervalMs = 3000;
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
// Show QR immediately for Offer flow
try { setShowQRCode(true); } catch {}
}
} catch (e) {
console.warn('Offer QR precompute failed:', e);
}
const existingMessages = messages.filter(m => const existingMessages = messages.filter(m =>
m.type === 'system' && m.type === 'system' &&
@@ -2530,8 +2628,14 @@
let offer; let offer;
try { try {
// Parse the offer data directly (no decryption needed with SAS) // Prefer binary decode first, then gzip JSON
offer = JSON.parse(offerInput.trim()); if (typeof window.decodeAnyPayload === 'function') {
const any = window.decodeAnyPayload(offerInput.trim());
offer = (typeof any === 'string') ? JSON.parse(any) : any;
} else {
const rawText = (typeof window.decompressIfNeeded === 'function') ? window.decompressIfNeeded(offerInput.trim()) : offerInput.trim();
offer = JSON.parse(rawText);
}
} catch (parseError) { } catch (parseError) {
throw new Error(`Invalid invitation format: ${parseError.message}`); throw new Error(`Invalid invitation format: ${parseError.message}`);
} }
@@ -2552,10 +2656,51 @@
setAnswerData(answer); setAnswerData(answer);
setShowAnswerStep(true); setShowAnswerStep(true);
// Generate QR code for the answer data // Answer QR: precompute binary frames and start animation immediately
const answerString = typeof answer === 'object' ? JSON.stringify(answer) : answer; const answerString = typeof answer === 'object' ? JSON.stringify(answer) : answer;
try {
await generateQRCode(answerString); if (typeof window.encodeBinaryToPrefixed === 'function') {
const bin = window.encodeBinaryToPrefixed(answerString);
const id = `ans_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const TARGET_CHUNKS = 10;
let FRAME_MAX = Math.max(200, Math.floor(bin.length / TARGET_CHUNKS));
if (FRAME_MAX <= 0) FRAME_MAX = 200;
let total = Math.ceil(bin.length / FRAME_MAX);
if (total < 2) { total = 2; FRAME_MAX = Math.ceil(bin.length / 2) || 1; }
const chunks = [];
for (let i = 0; i < total; i++) {
const seq = i + 1;
const part = bin.slice(i * FRAME_MAX, (i + 1) * FRAME_MAX);
chunks.push(JSON.stringify({ hdr: { v: 1, id, seq, total, rt: 'bin' }, body: part }));
}
const isDesktop = (typeof window !== 'undefined') && ((window.innerWidth || 0) >= 1024);
const QR_SIZE = isDesktop ? 720 : 512;
if (window.generateQRCode && chunks.length > 0) {
const firstUrl = await window.generateQRCode(chunks[0], { errorCorrectionLevel: 'M', size: QR_SIZE, margin: 2 });
if (firstUrl) setQrCodeUrl(firstUrl);
}
try { if (qrAnimationRef.current && qrAnimationRef.current.timer) { clearInterval(qrAnimationRef.current.timer); } } catch {}
qrAnimationRef.current = { timer: null, chunks, idx: 0, active: true };
setQrFramesTotal(chunks.length);
setQrFrameIndex(1);
setQrManualMode(false);
const intervalMs = 3000;
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
try { setShowQRCode(true); } catch {}
} else {
// Fallback: single QR compressed or plain
let url = '';
if (typeof window.generateCompressedQRCode === 'function') {
url = await window.generateCompressedQRCode(answerString);
} else {
url = await generateQRCode(answerString);
}
if (url) setQrCodeUrl(url);
try { setShowQRCode(true); } catch {}
}
} catch (e) {
console.warn('Answer QR generation failed:', e);
}
// Mark answer as created for state management // Mark answer as created for state management
if (e.target.value.trim().length > 0) { if (e.target.value.trim().length > 0) {
@@ -2633,8 +2778,14 @@
let answer; let answer;
try { try {
// Parse the answer data directly (no decryption needed with SAS) // Prefer binary decode first, then gzip JSON
answer = JSON.parse(answerInput.trim()); if (typeof window.decodeAnyPayload === 'function') {
const anyAnswer = window.decodeAnyPayload(answerInput.trim());
answer = (typeof anyAnswer === 'string') ? JSON.parse(anyAnswer) : anyAnswer;
} else {
const rawText = (typeof window.decompressIfNeeded === 'function') ? window.decompressIfNeeded(answerInput.trim()) : answerInput.trim();
answer = JSON.parse(rawText);
}
} catch (parseError) { } catch (parseError) {
throw new Error(`Invalid response format: ${parseError.message}`); throw new Error(`Invalid response format: ${parseError.message}`);
} }

View File

@@ -7,8 +7,44 @@
import * as QRCode from 'qrcode'; import * as QRCode from 'qrcode';
import { Html5Qrcode } from 'html5-qrcode'; import { Html5Qrcode } from 'html5-qrcode';
import { gzip, ungzip, deflate, inflate } from 'pako';
import * as cbor from 'cbor-js';
import { packSecurePayload, receiveAndProcess } from '../crypto/cose-qr.js'; import { packSecurePayload, receiveAndProcess } from '../crypto/cose-qr.js';
// Compact payload prefix to signal gzip+base64 content
const COMPRESSION_PREFIX = 'SB1:gz:';
const BINARY_PREFIX = 'SB1:bin:'; // CBOR + deflate + base64url
function uint8ToBase64(bytes) {
let binary = '';
const chunkSize = 0x8000;
for (let i = 0; i < bytes.length; i += chunkSize) {
const chunk = bytes.subarray(i, i + chunkSize);
binary += String.fromCharCode.apply(null, chunk);
}
return btoa(binary);
}
function base64ToUint8(b64) {
const binary = atob(b64);
const len = binary.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
return bytes;
}
function compressStringToBase64Gzip(text) {
const utf8 = new TextEncoder().encode(text);
const gz = gzip(utf8);
return uint8ToBase64(gz);
}
function decompressBase64GzipToString(b64) {
const gz = base64ToUint8(b64);
const out = ungzip(gz);
return new TextDecoder().decode(out);
}
async function generateQRCode(text, opts = {}) { async function generateQRCode(text, opts = {}) {
const size = opts.size || 512; const size = opts.size || 512;
const margin = opts.margin ?? 2; const margin = opts.margin ?? 2;
@@ -16,6 +52,56 @@ async function generateQRCode(text, opts = {}) {
return await QRCode.toDataURL(text, { width: size, margin, errorCorrectionLevel }); return await QRCode.toDataURL(text, { width: size, margin, errorCorrectionLevel });
} }
// Generate QR with gzip+base64 payload and recognizable prefix for scanners
async function generateCompressedQRCode(text, opts = {}) {
try {
const compressedB64 = compressStringToBase64Gzip(text);
const payload = COMPRESSION_PREFIX + compressedB64;
return await generateQRCode(payload, opts);
} catch (e) {
console.warn('generateCompressedQRCode failed, falling back to plain:', e?.message || e);
return await generateQRCode(text, opts);
}
}
// ---- Binary (CBOR) encode/decode helpers ----
function base64ToBase64Url(b64) {
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
function base64UrlToBase64(b64url) {
let b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
const pad = b64.length % 4;
if (pad) b64 += '='.repeat(4 - pad);
return b64;
}
function encodeObjectToBinaryBase64Url(obj) {
const cborBytes = cbor.encode(obj);
const compressed = deflate(new Uint8Array(cborBytes));
const b64 = uint8ToBase64(compressed);
return base64ToBase64Url(b64);
}
function decodeBinaryBase64UrlToObject(b64url) {
const b64 = base64UrlToBase64(b64url);
const compressed = base64ToUint8(b64);
const decompressed = inflate(compressed);
const ab = decompressed.buffer.slice(decompressed.byteOffset, decompressed.byteOffset + decompressed.byteLength);
return cbor.decode(ab);
}
async function generateBinaryQRCodeFromObject(obj, opts = {}) {
try {
const b64url = encodeObjectToBinaryBase64Url(obj);
const payload = BINARY_PREFIX + b64url;
return await generateQRCode(payload, opts);
} catch (e) {
console.warn('generateBinaryQRCodeFromObject failed, falling back to JSON compressed:', e?.message || e);
const text = JSON.stringify(obj);
return await generateCompressedQRCode(text, opts);
}
}
// COSE-based QR generation for large data // COSE-based QR generation for large data
async function generateCOSEQRCode(data, senderKey = null, recipientKey = null) { async function generateCOSEQRCode(data, senderKey = null, recipientKey = null) {
try { try {
@@ -40,9 +126,68 @@ async function generateCOSEQRCode(data, senderKey = null, recipientKey = null) {
// Expose functions to global scope // Expose functions to global scope
window.generateQRCode = generateQRCode; window.generateQRCode = generateQRCode;
window.generateCompressedQRCode = generateCompressedQRCode;
window.generateBinaryQRCodeFromObject = generateBinaryQRCodeFromObject;
window.generateCOSEQRCode = generateCOSEQRCode; window.generateCOSEQRCode = generateCOSEQRCode;
window.Html5Qrcode = Html5Qrcode; window.Html5Qrcode = Html5Qrcode;
window.packSecurePayload = packSecurePayload; window.packSecurePayload = packSecurePayload;
window.receiveAndProcess = receiveAndProcess; window.receiveAndProcess = receiveAndProcess;
console.log('QR libraries loaded: generateQRCode, generateCOSEQRCode, Html5Qrcode, COSE functions'); // Expose helper to transparently decompress scanner payloads
window.decompressIfNeeded = function (scannedText) {
try {
if (typeof scannedText === 'string' && scannedText.startsWith(COMPRESSION_PREFIX)) {
const b64 = scannedText.slice(COMPRESSION_PREFIX.length);
return decompressBase64GzipToString(b64);
}
} catch (e) {
console.warn('decompressIfNeeded failed:', e?.message || e);
}
return scannedText;
};
// Expose helper to get compressed string with prefix for copy/paste flows
window.compressToPrefixedGzip = function (text) {
try {
const payload = String(text || '');
const compressedB64 = compressStringToBase64Gzip(payload);
return COMPRESSION_PREFIX + compressedB64;
} catch (e) {
console.warn('compressToPrefixedGzip failed:', e?.message || e);
return String(text || '');
}
};
// Expose helpers for binary payloads in copy/paste
window.encodeBinaryToPrefixed = function (objOrJson) {
try {
const obj = typeof objOrJson === 'string' ? JSON.parse(objOrJson) : objOrJson;
const b64url = encodeObjectToBinaryBase64Url(obj);
return BINARY_PREFIX + b64url;
} catch (e) {
console.warn('encodeBinaryToPrefixed failed:', e?.message || e);
return typeof objOrJson === 'string' ? objOrJson : JSON.stringify(objOrJson);
}
};
window.decodeAnyPayload = function (scannedText) {
try {
if (typeof scannedText === 'string') {
if (scannedText.startsWith(BINARY_PREFIX)) {
const b64url = scannedText.slice(BINARY_PREFIX.length);
return decodeBinaryBase64UrlToObject(b64url); // returns object
}
if (scannedText.startsWith(COMPRESSION_PREFIX)) {
const s = window.decompressIfNeeded(scannedText);
return s; // returns JSON string
}
// Not prefixed: return as-is
return scannedText;
}
} catch (e) {
console.warn('decodeAnyPayload failed:', e?.message || e);
}
return scannedText;
};
console.log('QR libraries loaded: generateQRCode, generateCompressedQRCode, generateBinaryQRCodeFromObject, Html5Qrcode, COSE functions');