fix(qr): accept a one-frame invitation; release v5.9.1
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s

The scanner waited for four frames when shown a single one. Its chunk assembler
was written for SB1, which the generator always cuts into exactly four frames,
and its fallback branch claims any non-JSON string longer than 100 characters —
which a 151-character SBQ2 invitation is. A complete invitation was filed as
chunk 1 of 4, and the scan never finished.

SBQ2 payloads are now recognised as complete before any assembly runs, in both
the text and raw-byte forms, and the hard-coded frame count is marked as
belonging to SB1 so it is not read as a general rule.
This commit is contained in:
lockbitchat committed 2026-08-06 23:32:20 -04:00
1 parent 808fd99b73
commit 556727eb6f
13 files changed
+179 -43

No files matched your search

+13
View File
@@ -1,5 +1,18 @@
# Changelog
## v5.9.1 — Scanning a one-frame invitation
### Fixed
- The QR scanner waited for four frames when shown a single one. Its chunk
assembler was written for SB1, which the generator always cuts into exactly
four frames, and its fallback branch claimed any non-JSON string longer than
100 characters — which a 151-character SBQ2 invitation is. A complete
invitation was filed as chunk 1 of 4 and the scan never finished. SBQ2 payloads
are now recognised as complete before any assembly runs, in both the text and
raw-byte forms.
## v5.9.0 — The invitation is now one small QR code
The connection descriptor moves to SBQ2 and the key material moves onto the
+1 -1
View File
@@ -9,7 +9,7 @@
No accounts. No servers storing your messages. No installation required.
[![License: MIT](https://img.shields.io/badge/License-MIT-f0892a.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-5.9.0-3ecf8e.svg)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-5.9.1-3ecf8e.svg)](CHANGELOG.md)
[![PWA](https://img.shields.io/badge/PWA-installable-3ecf8e.svg)](#install-as-an-app)
[![Encryption](https://img.shields.io/badge/crypto-ECDH%20P--384%20%C2%B7%20AES--256--GCM-blue.svg)](#security-model)
[![Forward secrecy](https://img.shields.io/badge/forward%20secrecy-Double%20Ratchet-3ecf8e.svg)](#forward-secrecy)
+1 -1
View File
@@ -21266,7 +21266,7 @@ var SecureMasterKeyManager = class {
var import_NotificationIntegration = __toESM(require_NotificationIntegration());
// package.json
var version = "5.9.0";
var version = "5.9.1";
// src/components/ui/Header.jsx
var APP_VERSION = `v${version}`;
+1 -1
View File
@@ -1,7 +1,7 @@
{
"version": 3,
"sources": ["../src/notifications/SecureNotificationManager.js", "../src/notifications/NotificationIntegration.js", "../node_modules/dompurify/src/utils.ts", "../node_modules/dompurify/src/tags.ts", "../node_modules/dompurify/src/attrs.ts", "../node_modules/dompurify/src/regexp.ts", "../node_modules/dompurify/src/purify.ts", "../src/crypto/EnhancedSecureCryptoUtils.js", "../src/transfer/EnhancedSecureFileTransfer.js", "../src/network/webrtc/config.js", "../src/network/webrtc/sdp.js", "../src/network/webrtc/audio.js", "../src/network/webrtc/video.js", "../src/network/webrtc/adaptation/metrics.js", "../src/network/webrtc/adaptation/controller.js", "../src/crypto/DoubleRatchet.js", "../src/network/descriptor/sbq2.js", "../src/network/descriptor/keyexchange.js", "../src/network/EnhancedSecureWebRTCManager.js", "../src/scripts/app-boot.js", "../package.json", "../src/components/ui/Header.jsx", "../src/components/ui/DownloadApps.jsx", "../src/components/ui/BecomePartner.jsx", "../src/components/ui/UniqueFeatureSlider.jsx", "../src/components/ui/Roadmap.jsx", "../src/components/ui/CommunityCTA.jsx", "../src/components/ui/FileTransfer.jsx", "../src/network/iceServers.js", "../src/components/ui/IceServerSettings.jsx", "../src/components/ui/CallUI.jsx"],
"sourcesContent": ["/**\n * Secure and Reliable Notification Manager for P2P WebRTC Chat\n * Follows best practices: OWASP, MDN, Chrome DevRel\n * \n * @version 1.0.0\n * @author SecureBit Team\n * @license MIT\n */\n\nclass SecureChatNotificationManager {\n constructor(config = {}) {\n // Safely read Notification permission (iOS Safari may not define Notification)\n this.permission = (typeof Notification !== 'undefined' && Notification && typeof Notification.permission === 'string')\n ? Notification.permission\n : 'denied';\n this.isTabActive = this.checkTabActive(); // Initialize with proper check\n this.unreadCount = 0;\n this.originalTitle = document.title;\n this.notificationQueue = [];\n this.maxQueueSize = config.maxQueueSize || 5;\n this.rateLimitMs = config.rateLimitMs || 2000; // Spam protection\n this.lastNotificationTime = 0;\n this.trustedOrigins = config.trustedOrigins || [];\n \n // Secure context flag\n this.isSecureContext = window.isSecureContext;\n \n // Cross-browser compatibility for Page Visibility API\n this.hidden = this.getHiddenProperty();\n this.visibilityChange = this.getVisibilityChangeEvent();\n \n this.initVisibilityTracking();\n this.initSecurityChecks();\n }\n\n /**\n * Initialize security checks and validation\n * @private\n */\n initSecurityChecks() {\n // Security checks are performed silently\n }\n\n /**\n * Get hidden property name for cross-browser compatibility\n * @returns {string} Hidden property name\n * @private\n */\n getHiddenProperty() {\n if (typeof document.hidden !== \"undefined\") {\n return \"hidden\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msHidden\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitHidden\";\n }\n return \"hidden\"; // fallback\n }\n\n /**\n * Get visibility change event name for cross-browser compatibility\n * @returns {string} Visibility change event name\n * @private\n */\n getVisibilityChangeEvent() {\n if (typeof document.hidden !== \"undefined\") {\n return \"visibilitychange\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msvisibilitychange\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitvisibilitychange\";\n }\n return \"visibilitychange\"; // fallback\n }\n\n /**\n * Check if tab is currently active using multiple methods\n * @returns {boolean} True if tab is active\n * @private\n */\n checkTabActive() {\n // Primary method: Page Visibility API\n if (this.hidden && typeof document[this.hidden] !== \"undefined\") {\n return !document[this.hidden];\n }\n \n // Fallback method: document.hasFocus()\n if (typeof document.hasFocus === \"function\") {\n return document.hasFocus();\n }\n \n // Ultimate fallback: assume active\n return true;\n }\n\n /**\n * Initialize page visibility tracking (Page Visibility API)\n * @private\n */\n initVisibilityTracking() {\n // Primary method: Page Visibility API with cross-browser support\n if (typeof document.addEventListener !== \"undefined\" && typeof document[this.hidden] !== \"undefined\") {\n document.addEventListener(this.visibilityChange, () => {\n this.isTabActive = this.checkTabActive();\n \n if (this.isTabActive) {\n this.resetUnreadCount();\n this.clearNotificationQueue();\n }\n });\n }\n\n // Fallback method: Window focus/blur events\n window.addEventListener('focus', () => {\n this.isTabActive = this.checkTabActive();\n if (this.isTabActive) {\n this.resetUnreadCount();\n }\n });\n\n window.addEventListener('blur', () => {\n this.isTabActive = this.checkTabActive();\n });\n\n // Page unload cleanup\n window.addEventListener('beforeunload', () => {\n this.clearNotificationQueue();\n });\n }\n\n /**\n * Request notification permission (BEST PRACTICE: Only call in response to user action)\n * Never call on page load!\n * @returns {Promise<boolean>} Permission granted status\n */\n async requestPermission() {\n // Secure context check\n if (!this.isSecureContext || !('Notification' in window)) {\n return false;\n }\n\n if (this.permission === 'granted') {\n return true;\n }\n\n if (this.permission === 'denied') {\n return false;\n }\n\n try {\n this.permission = await Notification.requestPermission();\n return this.permission === 'granted';\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Update page title with unread count\n * @private\n */\n updateTitle() {\n if (this.unreadCount > 0) {\n document.title = `(${this.unreadCount}) ${this.originalTitle}`;\n } else {\n document.title = this.originalTitle;\n }\n }\n\n /**\n Line truncated
"sourcesContent": ["/**\n * Secure and Reliable Notification Manager for P2P WebRTC Chat\n * Follows best practices: OWASP, MDN, Chrome DevRel\n * \n * @version 1.0.0\n * @author SecureBit Team\n * @license MIT\n */\n\nclass SecureChatNotificationManager {\n constructor(config = {}) {\n // Safely read Notification permission (iOS Safari may not define Notification)\n this.permission = (typeof Notification !== 'undefined' && Notification && typeof Notification.permission === 'string')\n ? Notification.permission\n : 'denied';\n this.isTabActive = this.checkTabActive(); // Initialize with proper check\n this.unreadCount = 0;\n this.originalTitle = document.title;\n this.notificationQueue = [];\n this.maxQueueSize = config.maxQueueSize || 5;\n this.rateLimitMs = config.rateLimitMs || 2000; // Spam protection\n this.lastNotificationTime = 0;\n this.trustedOrigins = config.trustedOrigins || [];\n \n // Secure context flag\n this.isSecureContext = window.isSecureContext;\n \n // Cross-browser compatibility for Page Visibility API\n this.hidden = this.getHiddenProperty();\n this.visibilityChange = this.getVisibilityChangeEvent();\n \n this.initVisibilityTracking();\n this.initSecurityChecks();\n }\n\n /**\n * Initialize security checks and validation\n * @private\n */\n initSecurityChecks() {\n // Security checks are performed silently\n }\n\n /**\n * Get hidden property name for cross-browser compatibility\n * @returns {string} Hidden property name\n * @private\n */\n getHiddenProperty() {\n if (typeof document.hidden !== \"undefined\") {\n return \"hidden\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msHidden\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitHidden\";\n }\n return \"hidden\"; // fallback\n }\n\n /**\n * Get visibility change event name for cross-browser compatibility\n * @returns {string} Visibility change event name\n * @private\n */\n getVisibilityChangeEvent() {\n if (typeof document.hidden !== \"undefined\") {\n return \"visibilitychange\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msvisibilitychange\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitvisibilitychange\";\n }\n return \"visibilitychange\"; // fallback\n }\n\n /**\n * Check if tab is currently active using multiple methods\n * @returns {boolean} True if tab is active\n * @private\n */\n checkTabActive() {\n // Primary method: Page Visibility API\n if (this.hidden && typeof document[this.hidden] !== \"undefined\") {\n return !document[this.hidden];\n }\n \n // Fallback method: document.hasFocus()\n if (typeof document.hasFocus === \"function\") {\n return document.hasFocus();\n }\n \n // Ultimate fallback: assume active\n return true;\n }\n\n /**\n * Initialize page visibility tracking (Page Visibility API)\n * @private\n */\n initVisibilityTracking() {\n // Primary method: Page Visibility API with cross-browser support\n if (typeof document.addEventListener !== \"undefined\" && typeof document[this.hidden] !== \"undefined\") {\n document.addEventListener(this.visibilityChange, () => {\n this.isTabActive = this.checkTabActive();\n \n if (this.isTabActive) {\n this.resetUnreadCount();\n this.clearNotificationQueue();\n }\n });\n }\n\n // Fallback method: Window focus/blur events\n window.addEventListener('focus', () => {\n this.isTabActive = this.checkTabActive();\n if (this.isTabActive) {\n this.resetUnreadCount();\n }\n });\n\n window.addEventListener('blur', () => {\n this.isTabActive = this.checkTabActive();\n });\n\n // Page unload cleanup\n window.addEventListener('beforeunload', () => {\n this.clearNotificationQueue();\n });\n }\n\n /**\n * Request notification permission (BEST PRACTICE: Only call in response to user action)\n * Never call on page load!\n * @returns {Promise<boolean>} Permission granted status\n */\n async requestPermission() {\n // Secure context check\n if (!this.isSecureContext || !('Notification' in window)) {\n return false;\n }\n\n if (this.permission === 'granted') {\n return true;\n }\n\n if (this.permission === 'denied') {\n return false;\n }\n\n try {\n this.permission = await Notification.requestPermission();\n return this.permission === 'granted';\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Update page title with unread count\n * @private\n */\n updateTitle() {\n if (this.unreadCount > 0) {\n document.title = `(${this.unreadCount}) ${this.originalTitle}`;\n } else {\n document.title = this.originalTitle;\n }\n }\n\n /**\n Line truncated
"mappings": "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 truncated
"names": ["text", "NotificationIntegration", "entries", "Object", "setPrototypeOf", "isFrozen", "getPrototypeOf", "getOwnPropertyDescriptor", "freeze", "seal", "create", "_ref", "Reflect", "apply", "construct", "x", "func", "thisArg", "_len", "arguments", "length", "args", "Array", "_key", "Func", "_len2", "_key2", "arrayForEach", "unapply", "prototype", "forEach", "arrayLastIndexOf", "lastIndexOf", "arrayPop", "pop", "arrayPush", "push", "arraySplice", "splice", "arrayIsArray", "isArray", "stringToLowerCase", "String", "toLowerCase", "stringToString", "toString", "stringMatch", "match", "stringReplace", "replace", "stringIndexOf", "indexOf", "stringTrim", "trim", "numberToString", "Number", "booleanToString", "Boolean", "bigintToString", "BigInt", "symbolToString", "Symbol", "objectHasOwnProperty", "hasOwnProperty", "objectToString", "regExpTest", "RegExp", "test", "typeErrorCreate", "unconstruct", "TypeError", "lastIndex", "_len3", "_key3", "_len4", "_key4", "addToSet", "set", "array", "transformCaseFunc", "l", "element", "lcElement", "cleanArray", "index", "isPropertyExist", "clone", "object", "newObject", "_ref2", "_ref3", "_slicedToArray", "property", "value", "constructor", "stringifyValue", "valueAsRecord", "valueToString", "lookupGetter", "stringified", "prop", "desc", "get", "fallbackValue", "isRegex", "_unused", "html", "svg", "svgFilters", "svgDisallowed", "mathMl", "mathMlDisallowed", "text", "xml", "MUSTACHE_EXPR", "ERB_EXPR", "TMPLIT_EXPR", "DATA_ATTR", "ARIA_ATTR", "IS_ALLOWED_URI", "IS_SCRIPT_OR_DATA", "ATTR_WHITESPACE", "DOCTYPE_NAME", "CUSTOM_ELEMENT", "ELEMENT_MARKUP_PROBE", "COMMENT_MARKUP_PROBE", "FALLBACK_TAG_CLOSE", "SELF_CLOSING_TAG", "NODE_TYPE", "attribute", "cdataSection", "entityReference", "entityNode", "processingInstruction", "comment", "document", "documentType", "documentFragment", "notation", "getGlobal", "window", "_createTrustedTypesPolicy", "trustedTypes", "purifyHostElement", "createPolicy", "suffix", "ATTR_NAME", "hasAttribute", "getAttribute", "policyName", "createHTML", "createScriptURL", "scriptUrl", "_", "console", "warn", "_createHooksMap", "afterSanitizeAttributes", "afterSanitizeElements", "afterSanitizeShadowDOM", "beforeSanitizeAttributes", "beforeSanitizeElements", "beforeSanitizeShadowDOM", "uponSanitizeAttribute", "uponSanitizeElement", "uponSanitizeShadowNode", "_resolveSetOption", "cfg", "key", "fallback", "options", "base", "transform", "createDOMPurify", "undefined", "DOMPurify", "root", "version", "VERSION", "removed", "nodeType", "Element", "isSupported", "originalDocument", "currentScript", "DocumentFragment", "HTMLTemplateElement", "Node", "NodeFilter", "_window$NamedNodeMap", "NamedNodeMap", "MozNamedAttrMap", "HTMLFormElement", "DOMParser", "ElementPrototype", "cloneNode", "remove", "getNextSibling", "getChildNodes", "getParentNode", "getShadowRoot", "getAttributes", "getNodeType", "getNodeName", "template", "createElement", "content", "ownerDocument", "trustedTypesPolicy", "emptyHTML", "defaultTrustedTypesPolicy", "defaultTrustedTypesPolicyResolved", "IN_TRUSTED_TYPES_POLICY", "_assertNotInTrustedTypesPolicy", "_createTrustedHTML", "_createTrustedScriptURL", "_getDefaultTrustedTypesPolicy", "_document", "implementation", "createNodeIterator", "createDocumentFragment", "getElementsByTagName", "importNode", "hooks", "createHTMLDocument", "EXPRESSIONS", "ALLOWED_TAGS", "DEFAULT_ALLOWED_TAGS", "TAGS", "ALLOWED_ATTR", "DEFAULT_ALLOWED_ATTR", "ATTRS", "CUSTOM_ELEMENT_HANDLING", "tagNameCheck", "writable", "configurable", "enumerable", "attributeNameCheck", "allowCustomizedBuiltInElements", "FORBID_TAGS", "FORBID_ATTR", "EXTRA_ELEMENT_HANDLING", "tagCheck", "attributeCheck", "ALLOW_ARIA_ATTR", "ALLOW_DATA_ATTR", "ALLOW_UNKNOWN_PROTOCOLS", "ALLOW_SELF_CLOSE_IN_ATTR", "SAFE_FOR_TEMPLATES", "SAFE_FOR_XML", "WHOLE_DOCUMENT", "SET_CONFIG", "FORCE_BODY", "RETURN_DOM", "RETURN_DOM_FRAGMENT", "RETURN_TRUSTED_TYPE", "SANITIZE_DOM", "SANITIZE_NAMED_PROPS", "SANITIZE_NAMED_PROPS_PREFIX", "KEEP_CONTENT", "IN_PLACE", "USE_PROFILES", "FORBID_CONTENTS", "DEFAULT_FORBID_CONTENTS", "DATA_URI_TAGS", "DEFAULT_DATA_URI_TAGS", "URI_SAFE_ATTRIBUTES", "DEFAULT_URI_SAFE_ATTRIBUTES", "MATHML_NAMESPACE", "SVG_NAMESPACE", "HTML_NAMESPACE", "NAMESPACE", "IS_EMPTY_INPUT", "ALLOWED_NAMESPACES", "DEFAULT_ALLOWED_NAMESPACES", "DEFAULT_MATHML_TEXT_INTEGRATION_POINTS", "MATHML_TEXT_INTEGRATION_POINTS", "DEFAULT_HTML_INTEGRATION_POINTS", "HTML_INTEGRATION_POINTS", "COMMON_SVG_AND_HTML_ELEMENTS", "PARSER_MEDIA_TYPE", "SUPPORTED_PARSER_MEDIA_TYPES", "DEFAULT_PARSER_MEDIA_TYPE", "CONFIG", "formElement", "isRegexOrFunction", "testValue", "Function", "_parseConfig", "ALLOWED_URI_REGEXP", "customElementHandling", "ADD_TAGS", "ADD_ATTR", "ADD_URI_SAFE_ATTR", "ADD_FORBID_CONTENTS", "table", "tbody", "TRUSTED_TYPES_POLICY", "previousTrustedTypesPolicy", "error", "ALL_SVG_TAGS", "ALL_MATHML_TAGS", "_checkSvgNamespace", "tagName", "parent", "parentTagName", "namespaceURI", "_checkMathMlNamespaceLine truncated
}
Vendored
+22 -2
View File
@@ -4231,14 +4231,31 @@ var EnhancedSecureP2PChat = () => {
try {
console.log("QR Code scanned:", scannedData.substring(0, 100) + "...");
console.log("Current buffer state:", qrChunksBufferRef.current);
if (scannedData.startsWith("SB2:") || scannedData.charCodeAt(0) === 2) {
qrChunksBufferRef.current = { id: null, total: 0, seen: /* @__PURE__ */ new Set(), items: [] };
if (showOfferStep) {
setAnswerInput(scannedData);
} else {
setOfferInput(scannedData);
}
setMessages((prev) => [...prev, {
message: "Invitation captured.",
type: "success"
}]);
setShowQRScannerModal(false);
return Promise.resolve(true);
}
if (scannedData.startsWith("SB1:bin:") || qrChunksBufferRef.current && qrChunksBufferRef.current.id) {
console.log("Binary chunk detected:", scannedData.substring(0, 50) + "...");
if (!qrChunksBufferRef.current.id) {
console.log("Initializing buffer for binary chunks");
qrChunksBufferRef.current = {
id: `bin_${Date.now()}`,
// SB1 payloads are split into exactly four
// frames by the generator above. SBQ2 never
// reaches here — it is one frame and is
// handled at the top of this function.
total: 4,
// We expect 4 chunks
seen: /* @__PURE__ */ new Set(),
items: [],
lastUpdateMs: Date.now()
@@ -4296,8 +4313,11 @@ var EnhancedSecureP2PChat = () => {
console.log("Initializing buffer for potential binary chunks");
qrChunksBufferRef.current = {
id: `bin_${Date.now()}`,
// SB1 payloads are split into exactly four
// frames by the generator above. SBQ2 never
// reaches here — it is one frame and is
// handled at the top of this function.
total: 4,
// We expect 4 chunks
seen: /* @__PURE__ */ new Set(),
items: [],
lastUpdateMs: Date.now()
+2 -2
View File
@@ -1,7 +1,7 @@
{
"version": 3,
"sources": ["../src/utils/debugWindowHooks.js", "../src/network/iceSettingsStore.js", "../src/state/sessionsStore.js", "../src/app.jsx"],
"sourcesContent": ["function isSecureBitDebugEnabled(targetWindow = globalThis.window) {\n return targetWindow?.SECUREBIT_DEBUG === true;\n}\n\nfunction installDebugWindowHooks({\n targetWindow = globalThis.window,\n webrtcManagerRef,\n onClearData,\n clearConsole = () => {\n if (typeof console.clear === 'function') {\n console.clear();\n }\n }\n}) {\n if (!isSecureBitDebugEnabled(targetWindow)) {\n return () => {};\n }\n\n targetWindow.forceCleanup = () => {\n onClearData();\n if (webrtcManagerRef.current) {\n webrtcManagerRef.current.disconnect();\n }\n };\n targetWindow.clearLogs = clearConsole;\n targetWindow.webrtcManagerRef = webrtcManagerRef;\n\n return () => {\n delete targetWindow.forceCleanup;\n delete targetWindow.clearLogs;\n delete targetWindow.webrtcManagerRef;\n };\n}\n\nexport { installDebugWindowHooks, isSecureBitDebugEnabled };\n", "// Persistent, at-rest-encrypted storage for the user's custom ICE (STUN/TURN)\n// configuration. Persistence is OPTIONAL: the UI only calls saveIceSettings when\n// the user explicitly opts in (\"Save on this device\"). Session-only use never\n// touches this store \u2014 the settings live in React state and vanish on reload.\n//\n// At-rest protection model:\n// - A non-extractable AES-GCM device key is generated once and kept in\n// IndexedDB. It can never be exported back into JS, so a copy of the\n// on-disk database is useless without executing code in this exact origin.\n// - The settings blob (which may contain TURN credentials) is encrypted with\n// that key before being written.\n// This protects against disk/profile inspection. It does NOT protect against a\n// live code-execution compromise of the page (consistent with the project's\n// stated threat model \u2014 see SECURITY.md). Credentials are never persisted in\n// plaintext, and the user can delete them at any time via clearIceSettings().\n\nconst DB_NAME = 'securebit-net';\nconst DB_VERSION = 1;\nconst STORE = 'kv';\nconst KEY_RECORD = 'ice-device-key';\nconst SETTINGS_RECORD = 'ice-settings';\nconst SETTINGS_VERSION = 1;\n\nfunction isSupported() {\n return typeof indexedDB !== 'undefined' &&\n typeof crypto !== 'undefined' &&\n !!crypto.subtle;\n}\n\nfunction openDb() {\n return new Promise((resolve, reject) => {\n let request;\n try {\n request = indexedDB.open(DB_NAME, DB_VERSION);\n } catch (error) {\n reject(error);\n return;\n }\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(STORE)) {\n db.createObjectStore(STORE);\n }\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\nfunction idbGet(db, key) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readonly');\n const req = tx.objectStore(STORE).get(key);\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n}\n\nfunction idbPut(db, key, value) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readwrite');\n tx.objectStore(STORE).put(value, key);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nfunction idbDelete(db, key) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readwrite');\n tx.objectStore(STORE).delete(key);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nasync function getOrCreateDeviceKey(db) {\n const existing = await idbGet(db, KEY_RECORD);\n if (existing instanceof CryptoKey) {\n return existing;\n }\n const key = await crypto.subtle.generateKey(\n { name: 'AES-GCM', length: 256 },\n false, // non-extractable\n ['encrypt', 'decrypt']\n );\n await idbPut(db, KEY_RECORD, key);\n return key;\n}\n\n/**\n * Persist custom ICE settings, encrypted at rest.\n * @param {{ servers: Array, privacyMode: string }} settings\n */\nexport async function saveIceSettings(settings) {\n if (!isSupported()) throw new Error('Persistent storage is not available in this browser');\n\n const db = await openDb();\n const key = await getOrCreateDeviceKey(db);\n\n const payload = JSON.stringify({\n version: SETTINGS_VERSION,\n servers: Array.isArray(settings?.servers) ? settings.servers : [],\n privacyMode: settings?.privacyMode === 'relay-only' ? 'relay-only' : 'standard'\n });\n\n const iv = crypto.getRandomValues(new Uint8Array(12));\n const ciphertext = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv },\n keLine truncated
"mappings": "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 truncated
"sourcesContent": ["function isSecureBitDebugEnabled(targetWindow = globalThis.window) {\n return targetWindow?.SECUREBIT_DEBUG === true;\n}\n\nfunction installDebugWindowHooks({\n targetWindow = globalThis.window,\n webrtcManagerRef,\n onClearData,\n clearConsole = () => {\n if (typeof console.clear === 'function') {\n console.clear();\n }\n }\n}) {\n if (!isSecureBitDebugEnabled(targetWindow)) {\n return () => {};\n }\n\n targetWindow.forceCleanup = () => {\n onClearData();\n if (webrtcManagerRef.current) {\n webrtcManagerRef.current.disconnect();\n }\n };\n targetWindow.clearLogs = clearConsole;\n targetWindow.webrtcManagerRef = webrtcManagerRef;\n\n return () => {\n delete targetWindow.forceCleanup;\n delete targetWindow.clearLogs;\n delete targetWindow.webrtcManagerRef;\n };\n}\n\nexport { installDebugWindowHooks, isSecureBitDebugEnabled };\n", "// Persistent, at-rest-encrypted storage for the user's custom ICE (STUN/TURN)\n// configuration. Persistence is OPTIONAL: the UI only calls saveIceSettings when\n// the user explicitly opts in (\"Save on this device\"). Session-only use never\n// touches this store \u2014 the settings live in React state and vanish on reload.\n//\n// At-rest protection model:\n// - A non-extractable AES-GCM device key is generated once and kept in\n// IndexedDB. It can never be exported back into JS, so a copy of the\n// on-disk database is useless without executing code in this exact origin.\n// - The settings blob (which may contain TURN credentials) is encrypted with\n// that key before being written.\n// This protects against disk/profile inspection. It does NOT protect against a\n// live code-execution compromise of the page (consistent with the project's\n// stated threat model \u2014 see SECURITY.md). Credentials are never persisted in\n// plaintext, and the user can delete them at any time via clearIceSettings().\n\nconst DB_NAME = 'securebit-net';\nconst DB_VERSION = 1;\nconst STORE = 'kv';\nconst KEY_RECORD = 'ice-device-key';\nconst SETTINGS_RECORD = 'ice-settings';\nconst SETTINGS_VERSION = 1;\n\nfunction isSupported() {\n return typeof indexedDB !== 'undefined' &&\n typeof crypto !== 'undefined' &&\n !!crypto.subtle;\n}\n\nfunction openDb() {\n return new Promise((resolve, reject) => {\n let request;\n try {\n request = indexedDB.open(DB_NAME, DB_VERSION);\n } catch (error) {\n reject(error);\n return;\n }\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(STORE)) {\n db.createObjectStore(STORE);\n }\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\nfunction idbGet(db, key) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readonly');\n const req = tx.objectStore(STORE).get(key);\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n}\n\nfunction idbPut(db, key, value) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readwrite');\n tx.objectStore(STORE).put(value, key);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nfunction idbDelete(db, key) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readwrite');\n tx.objectStore(STORE).delete(key);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nasync function getOrCreateDeviceKey(db) {\n const existing = await idbGet(db, KEY_RECORD);\n if (existing instanceof CryptoKey) {\n return existing;\n }\n const key = await crypto.subtle.generateKey(\n { name: 'AES-GCM', length: 256 },\n false, // non-extractable\n ['encrypt', 'decrypt']\n );\n await idbPut(db, KEY_RECORD, key);\n return key;\n}\n\n/**\n * Persist custom ICE settings, encrypted at rest.\n * @param {{ servers: Array, privacyMode: string }} settings\n */\nexport async function saveIceSettings(settings) {\n if (!isSupported()) throw new Error('Persistent storage is not available in this browser');\n\n const db = await openDb();\n const key = await getOrCreateDeviceKey(db);\n\n const payload = JSON.stringify({\n version: SETTINGS_VERSION,\n servers: Array.isArray(settings?.servers) ? settings.servers : [],\n privacyMode: settings?.privacyMode === 'relay-only' ? 'relay-only' : 'standard'\n });\n\n const iv = crypto.getRandomValues(new Uint8Array(12));\n const ciphertext = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv },\n keLine truncated
"mappings": "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 truncated
"names": ["o", "setMessages", "addMessageWithAutoScroll", "updateMessageStatus", "patchMessageById", "patchMessageByFileId", "setConnectionStatus", "setKeyFingerprint", "setVerificationCode", "setIsVerified", "setLocalVerificationConfirmed", "setRemoteVerificationConfirmed", "setBothVerificationsConfirmed", "setShowVerification", "setShowOfferStep", "setShowAnswerStep", "setShowQRCode", "setQrCodeUrl", "setOfferData", "setAnswerData", "setOfferInput", "setAnswerInput", "setPendingIncomingFiles", "opts", "offerData", "payload", "UpdateCheckerWrapper"]
}
+2 -2
View File
@@ -7,7 +7,7 @@ this document describes.
| | |
| --- | --- |
| Release | v5.9.0 |
| Release | v5.9.1 |
| Protocol version | 4.1 |
| Ratchet wire version | 1 |
@@ -238,5 +238,5 @@ worse than one that reports nothing.
## Scope
This describes the browser implementation as it stands in v5.9.0. It is not a
This describes the browser implementation as it stands in v5.9.1. It is not a
substitute for independent cryptographic review.
+22 -22
View File
@@ -24,7 +24,7 @@
<!-- PWA Manifest -->
<link rel="manifest" href="./manifest.json">
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1786056807121">
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1786072827999">
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -90,7 +90,7 @@
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/8.3__iPad_Mini_portrait.png">
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1786056807121">
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1786072827999">
<link rel="apple-touch-icon" sizes="57x57" href="./logo/icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="./logo/icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="./logo/icon-72x72.png">
@@ -99,7 +99,7 @@
<link rel="apple-touch-icon" sizes="120x120" href="./logo/icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="./logo/icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="./logo/icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1786056807121">
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1786072827999">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -183,7 +183,7 @@
<!-- Render-blocking JS is deferred: classic deferred scripts and module scripts
both execute in document order after parsing, so React still runs before the
app modules below, but the parser / first paint is no longer blocked. -->
<script defer src="config/ice-servers.js?v=1786056807121"></script>
<script defer src="config/ice-servers.js?v=1786072827999"></script>
<script defer src="libs/react/react.production.min.js"></script>
<script defer src="libs/react-dom/react-dom.production.min.js"></script>
<!-- Prism syntax highlighting (vendored, offline). Tokenizes code as TEXT only —
@@ -191,8 +191,8 @@
Its CSS is loaded async via load-async-css.js (not paint-critical). -->
<script defer src="libs/prism/prism.js"></script>
<!-- Critical, paint-defining CSS stays render-blocking (avoids FOUC / layout shift). -->
<link rel="stylesheet" href="assets/tailwind.css?v=1786056807121">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1786056807121">
<link rel="stylesheet" href="assets/tailwind.css?v=1786072827999">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1786072827999">
<!-- Preload only the fonts needed for first paint. fa-solid covers the bulk of UI
icons; fa-regular/fa-brands are loaded on demand by their CSS (rarely on the
first screen). Inter latin 400/700 cover body text and headings/buttons. -->
@@ -200,31 +200,31 @@
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-700.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/assets/fonts/inter/inter.css">
<link rel="stylesheet" href="src/styles/main.css?v=1786056807121">
<link rel="stylesheet" href="src/styles/animations.css?v=1786056807121">
<link rel="stylesheet" href="src/styles/components.css?v=1786056807121">
<link rel="stylesheet" href="src/styles/main.css?v=1786072827999">
<link rel="stylesheet" href="src/styles/animations.css?v=1786072827999">
<link rel="stylesheet" href="src/styles/components.css?v=1786072827999">
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="src/scripts/load-async-css.js?v=1786056807121"></script>
<script defer src="src/scripts/load-async-css.js?v=1786072827999"></script>
<noscript>
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
<link rel="stylesheet" href="libs/prism/prism.css">
</noscript>
<script defer src="src/scripts/fa-check.js?v=1786056807121"></script>
<script defer src="src/scripts/fa-check.js?v=1786072827999"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="src/utils/updateManager.js?v=1786056807121"></script>
<script type="module" src="src/components/UpdateChecker.jsx?v=1786056807121"></script>
<script type="module" src="dist/qr-local.js?v=1786056807121"></script>
<script type="module" src="src/components/QRScanner.js?v=1786056807121"></script>
<script defer src="src/utils/updateManager.js?v=1786072827999"></script>
<script type="module" src="src/components/UpdateChecker.jsx?v=1786072827999"></script>
<script type="module" src="dist/qr-local.js?v=1786072827999"></script>
<script type="module" src="src/components/QRScanner.js?v=1786072827999"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="dist/app-boot.js?v=1786056807121"></script>
<script type="module" src="dist/app.js?v=1786056807121"></script>
<script type="module" src="dist/app-boot.js?v=1786072827999"></script>
<script type="module" src="dist/app.js?v=1786072827999"></script>
<script defer src="src/scripts/pwa-register.js?v=1786056807121"></script>
<script src="./src/pwa/install-prompt.js?v=1786056807121" type="module"></script>
<script src="./src/pwa/pwa-manager.js?v=1786056807121" type="module"></script>
<script defer src="./src/scripts/pwa-offline-test.js?v=1786056807121"></script>
<link rel="stylesheet" href="./src/styles/pwa.css?v=1786056807121">
<script defer src="src/scripts/pwa-register.js?v=1786072827999"></script>
<script src="./src/pwa/install-prompt.js?v=1786072827999" type="module"></script>
<script src="./src/pwa/pwa-manager.js?v=1786072827999" type="module"></script>
<script defer src="./src/scripts/pwa-offline-test.js?v=1786072827999"></script>
<link rel="stylesheet" href="./src/styles/pwa.css?v=1786072827999">
</body>
</html>
+7 -7
View File
@@ -1,10 +1,10 @@
{
"version": "1786056807121",
"buildVersion": "1786056807121",
"appVersion": "5.9.0",
"buildTime": "2026-08-06T22:53:27.174Z",
"buildId": "1786056807121-7754f6b",
"gitHash": "7754f6b",
"version": "1786072827999",
"buildVersion": "1786072827999",
"appVersion": "5.9.1",
"buildTime": "2026-08-07T03:20:28.043Z",
"buildId": "1786072827999-808fd99",
"gitHash": "808fd99",
"generated": true,
"generatedAt": "2026-08-06T22:53:27.175Z"
"generatedAt": "2026-08-07T03:20:28.044Z"
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "securebit-chat",
"version": "5.9.0",
"version": "5.9.1",
"description": "Secure P2P Communication Application with End-to-End Encryption",
"main": "index.html",
"scripts": {
@@ -11,7 +11,7 @@
"dev": "npm run build && python -m http.server 8000",
"watch": "npx tailwindcss -i src/styles/tw-input.css -o assets/tailwind.css --watch",
"serve": "npx http-server -p 8000",
"test": "node tests/sas-verification.test.mjs && node tests/verification-gate.test.mjs && node tests/inbound-frame-authentication.test.mjs && node tests/control-frame-authorization.test.mjs && node tests/security-level-shape.test.mjs && node tests/desktop-download-links.test.mjs && node tests/file-transfer-consent.test.mjs && node tests/incoming-message-sanitization.test.mjs && node tests/outgoing-message-integrity.test.mjs && node tests/secure-chat-features.test.mjs && node tests/notification-meta-forwarding.test.mjs && node tests/notification-ephemeral-privacy.test.mjs && node tests/key-derivation-compat.test.mjs && node tests/key-exchange-e2e.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/voice-auto-accept.test.mjs && node tests/legacy-offer-purge.test.mjs && node tests/webrtc-privacy-mode.test.mjs && node tests/indexeddb-metadata-encryption.test.mjs && node tests/disconnect-cleanup.test.mjs && node tests/timer-lifecycle.test.mjs && node tests/file-transfer-cleanup.test.mjs && node tests/file-transfer-ui-cleanup.test.mjs && node tests/file-transfer-callback-propagation.test.mjs && node tests/debug-window-hooks.test.mjs && node tests/inbound-message-rate-limit.test.mjs && node tests/file-transfer-chunk-rate-limit.test.mjs && node tests/ice-servers-validation.test.mjs && node tests/sessions-reducer.test.mjs && node tests/webrtc-sdp.test.mjs && node tests/webrtc-video.test.mjs && node tests/webrtc-adaptation.test.mjs && node tests/session-recovery.test.mjs && node tests/qr-zip-bomb.test.mjs && node tests/ice-gathering-patience.test.mjs && node tests/version-consistency.test.mjs && node tests/double-ratchet.test.mjs && node tests/ratchet-integration.test.mjs && node tests/descriptor-sbq2.test.mjs && node tests/sbq2-key-exchange.test.mjs"
"test": "node tests/sas-verification.test.mjs && node tests/verification-gate.test.mjs && node tests/inbound-frame-authentication.test.mjs && node tests/control-frame-authorization.test.mjs && node tests/security-level-shape.test.mjs && node tests/desktop-download-links.test.mjs && node tests/file-transfer-consent.test.mjs && node tests/incoming-message-sanitization.test.mjs && node tests/outgoing-message-integrity.test.mjs && node tests/secure-chat-features.test.mjs && node tests/notification-meta-forwarding.test.mjs && node tests/notification-ephemeral-privacy.test.mjs && node tests/key-derivation-compat.test.mjs && node tests/key-exchange-e2e.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/voice-auto-accept.test.mjs && node tests/legacy-offer-purge.test.mjs && node tests/webrtc-privacy-mode.test.mjs && node tests/indexeddb-metadata-encryption.test.mjs && node tests/disconnect-cleanup.test.mjs && node tests/timer-lifecycle.test.mjs && node tests/file-transfer-cleanup.test.mjs && node tests/file-transfer-ui-cleanup.test.mjs && node tests/file-transfer-callback-propagation.test.mjs && node tests/debug-window-hooks.test.mjs && node tests/inbound-message-rate-limit.test.mjs && node tests/file-transfer-chunk-rate-limit.test.mjs && node tests/ice-servers-validation.test.mjs && node tests/sessions-reducer.test.mjs && node tests/webrtc-sdp.test.mjs && node tests/webrtc-video.test.mjs && node tests/webrtc-adaptation.test.mjs && node tests/session-recovery.test.mjs && node tests/qr-zip-bomb.test.mjs && node tests/ice-gathering-patience.test.mjs && node tests/version-consistency.test.mjs && node tests/double-ratchet.test.mjs && node tests/ratchet-integration.test.mjs && node tests/descriptor-sbq2.test.mjs && node tests/sbq2-key-exchange.test.mjs && node tests/qr-scan-single-frame.test.mjs"
},
"keywords": [
"p2p",
+34 -2
View File
@@ -4230,6 +4230,30 @@ import {
try {
console.log('QR Code scanned:', scannedData.substring(0, 100) + '...');
console.log('Current buffer state:', qrChunksBufferRef.current);
// An SBQ2 invitation is ONE frame, always. It has to be
// recognised before any chunk logic runs: the assembler
// below assumes a fixed four-frame split (that is what
// an SB1 payload is cut into) and the fallback branch
// claims every non-JSON string over 100 characters — a
// 151-character SB2: code matches, is filed as chunk 1
// of 4, and the scanner waits for three frames that do
// not exist.
if (scannedData.startsWith('SB2:') || scannedData.charCodeAt(0) === 0x02) {
qrChunksBufferRef.current = { id: null, total: 0, seen: new Set(), items: [] };
if (showOfferStep) {
setAnswerInput(scannedData);
} else {
setOfferInput(scannedData);
}
setMessages(prev => [...prev, {
message: 'Invitation captured.',
type: 'success'
}]);
setShowQRScannerModal(false);
return Promise.resolve(true);
}
// Check if this is a binary chunk (starts with SB1:bin: or is a raw binary chunk)
if (scannedData.startsWith('SB1:bin:') || (qrChunksBufferRef.current && qrChunksBufferRef.current.id)) {
@@ -4241,7 +4265,11 @@ import {
// Initialize buffer for binary chunks
qrChunksBufferRef.current = {
id: `bin_${Date.now()}`,
total: 4, // We expect 4 chunks
// SB1 payloads are split into exactly four
// frames by the generator above. SBQ2 never
// reaches here — it is one frame and is
// handled at the top of this function.
total: 4,
seen: new Set(),
items: [],
lastUpdateMs: Date.now()
@@ -4321,7 +4349,11 @@ import {
console.log('Initializing buffer for potential binary chunks');
qrChunksBufferRef.current = {
id: `bin_${Date.now()}`,
total: 4, // We expect 4 chunks
// SB1 payloads are split into exactly four
// frames by the generator above. SBQ2 never
// reaches here — it is one frame and is
// handled at the top of this function.
total: 4,
seen: new Set(),
items: [],
lastUpdateMs: Date.now()
+1 -1
View File
@@ -11,7 +11,7 @@ let DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.7.56';
// Build stamp — rewritten by scripts/post-build.js on every release so this file's
// bytes change each deploy. That is what makes the browser detect a new Service Worker,
// reinstall it, drop stale caches and (via controllerchange) prompt the page to update.
const SW_BUILD_VERSION = '1786056807121';
const SW_BUILD_VERSION = '1786072827999';
// Load version from meta.json on install
async function getAppVersion() {
+71
View File
@@ -0,0 +1,71 @@
// An SBQ2 invitation is one QR frame. The scanner must treat it as complete on
// sight.
//
// This test exists because of a real bug. The chunk assembler in handleQRScan
// was written for SB1, which the generator always cuts into exactly four frames,
// so the expected count is hard-coded to 4. Its fallback branch claims *any*
// non-JSON string longer than 100 characters — and an `SB2:` payload is 151 to
// 170 characters. A single, complete invitation was therefore filed as chunk 1
// of 4, and the scanner sat waiting for three frames that do not exist.
//
// The assertions are structural rather than behavioural: handleQRScan lives
// inside a 5,900-line JSX component and cannot be invoked in isolation, but the
// ordering that makes it correct can still be pinned.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const src = readFileSync(new URL('../src/app.jsx', import.meta.url), 'utf8');
const scanIdx = src.indexOf('const handleQRScan = async (scannedData) => {');
assert.notEqual(scanIdx, -1, 'handleQRScan must exist in src/app.jsx');
// Everything from the start of the handler to the end of the file is enough:
// what matters is the order of the branches inside it.
const body = src.slice(scanIdx);
const sbq2Idx = body.indexOf("scannedData.startsWith('SB2:')");
assert.notEqual(sbq2Idx, -1, 'handleQRScan must recognise an SB2 payload');
// The raw-byte form must be recognised too — the two families never collide,
// since any SB1 payload begins with ASCII 'S'.
assert.ok(/scannedData\.charCodeAt\(0\) === 0x02/.test(body.slice(sbq2Idx, sbq2Idx + 400)),
'the raw-byte SBQ2 form must be recognised alongside the text form');
// The SBQ2 branch must come before BOTH chunk-assembly branches: the explicit
// SB1:bin: one and the "long non-JSON string" fallback that actually caught it.
const binChunkIdx = body.indexOf("scannedData.startsWith('SB1:bin:')");
const lengthHeuristicIdx = body.indexOf('scannedData.length > 100');
assert.notEqual(binChunkIdx, -1, 'the SB1 chunk branch must still exist');
assert.notEqual(lengthHeuristicIdx, -1, 'the long-string fallback must still exist');
assert.ok(sbq2Idx < binChunkIdx,
'SBQ2 must be handled before the SB1:bin chunk assembler');
assert.ok(sbq2Idx < lengthHeuristicIdx,
'SBQ2 must be handled before the "long non-JSON string" fallback, which a ' +
'151-character SB2 payload otherwise matches');
// The branch must terminate the scan rather than fall through into assembly.
const sbq2Branch = body.slice(sbq2Idx, binChunkIdx);
assert.ok(/setShowQRScannerModal\(false\)/.test(sbq2Branch),
'capturing a single-frame invitation must close the scanner');
assert.ok(/return Promise\.resolve\(true\)/.test(sbq2Branch),
'the SBQ2 branch must report completion, not "waiting for more"');
assert.ok(/qrChunksBufferRef\.current = \{ id: null/.test(sbq2Branch),
'any partial chunk buffer must be cleared, so a stray earlier frame cannot ' +
'be spliced onto a complete invitation');
// Both destinations must be fed, because the same scanner serves both steps.
assert.ok(/setAnswerInput\(scannedData\)/.test(sbq2Branch) &&
/setOfferInput\(scannedData\)/.test(sbq2Branch),
'the scanned code must reach the answer field on the offering side and the ' +
'offer field on the joining side');
// The hard-coded four must stay explicitly tied to SB1, so it is not read as a
// general rule the next time the format changes.
for (const m of body.matchAll(/total: 4,/g)) {
const context = body.slice(Math.max(0, m.index - 400), m.index);
assert.ok(/SB1/.test(context),
'a hard-coded frame count must say which format it belongs to');
}
console.log('qr-scan-single-frame: all assertions passed');