fix(qr): accept a one-frame invitation; release v5.9.1
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:
@@ -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
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
No accounts. No servers storing your messages. No installation required.
|
||||
|
||||
[](LICENSE)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](#install-as-an-app)
|
||||
[](#security-model)
|
||||
[](#forward-secrecy)
|
||||
|
||||
Vendored
+1
-1
@@ -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}`;
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+22
-2
@@ -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()
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
+2
-2
@@ -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
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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');
|
||||
Reference in New Issue
Block a user