release: v4.8.20 secure chat tools — completed, fixed and polished
Completes the messaging controls from v4.8.14 and fixes the bug that made them appear broken for recipients. Fixed: - Per-message metadata was silently dropped for recipients. NotificationIntegration wrapped onMessage and deliverMessageToUI with 2-arg shims that called the originals without the 3rd argument (meta); with notifications enabled, view-once, disappearing timers and unsend all failed on the receiving side. Both wrappers now forward all arguments. Added tests/notification-meta-forwarding.test.mjs. - Chat would not open after SAS: composer props were threaded into the wrong component (EnhancedConnectionSetup vs EnhancedChatInterface) -> ReferenceError nowTick on the verified re-render. Props moved to the chat component. Changed: - Code blocks: lightweight dependency-free syntax highlighting via React nodes (no innerHTML/remote scripts); code mode expands the input; copy auto-clears the clipboard after ~30s. - View-once: configurable visible-after-open time (5s/15s/30s/1m) via meta.onceTtl. - Disappearing timer: duration picker (Off/30s/5m/1h) instead of click-cycling. - Composer toolbar moved next to "Send files"; borderless buttons, brand-orange active state; pickers open upward and are mobile-friendly. - Sender bubble background lightened to rgba(249,115,22,0.05). Removed: - Panic wipe button (disconnect already wipes keys and clears session state). Transport unchanged: per-message metadata travels inside the encrypted envelope, whitelisted/bounded by _sanitizeMessageMeta. Full suite: 19 files, all passing. Docs (README, CHANGELOG) updated; version bumped to 4.8.20.
This commit is contained in:
1 parent
cb72b9cb1c
commit
b39f9ecd2c
19 files changed
+628
-287
No files matched your search
@@ -1,5 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## v4.8.20 — Secure chat tools: completed, fixed and polished
|
||||
|
||||
Completes the messaging controls introduced in v4.8.14 and fixes the bug that made them appear broken for recipients. All per-message options travel inside the encrypted message envelope (never in the sanitized text), so message content cannot spoof or corrupt them.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Per-message metadata was silently dropped for recipients.** `NotificationIntegration` wrapped both `webrtcManager.onMessage` and `webrtcManager.deliverMessageToUI` with two-argument shims that called the originals without the third argument (`meta`). With notifications enabled, every received message lost its `meta`, so view-once, disappearing timers and unsend all failed on the recipient side. Both wrappers now forward all arguments (`...rest`). Added `tests/notification-meta-forwarding.test.mjs`.
|
||||
- **Chat would not open after SAS** (regression from the initial wiring): the composer props were threaded into the wrong component (`EnhancedConnectionSetup` instead of `EnhancedChatInterface`), throwing `ReferenceError: nowTick` on the verified-state re-render. Props are now on the chat component.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Code blocks** now include lightweight, dependency-free syntax highlighting (comments, strings, numbers, keywords) rendered via React nodes — no `innerHTML`, no remote scripts. Enabling code mode expands the input (monospace, 8 rows) for comfortable entry. Copying a block auto-clears the clipboard after ~30s.
|
||||
- **View-once** is now configurable: the sender picks how long the message stays visible after the peer opens it (5s / 15s / 30s / 1m) via `meta.onceTtl` (clamped 1s–1h).
|
||||
- **Disappearing timer** uses a duration picker (Off / 30s / 5m / 1h) instead of click-cycling.
|
||||
- **Composer toolbar** moved next to the "Send files" control; borderless buttons with the brand-orange (`accent-orange`) active state; time pickers open upward and are sized for mobile readability.
|
||||
- Sender bubble background lightened to `rgba(249, 115, 22, 0.05)`.
|
||||
|
||||
### Removed
|
||||
|
||||
- **Panic wipe** button. Disconnecting already wipes keys and clears session state, so a separate panic control was redundant.
|
||||
|
||||
## v4.8.15 — Fix: chat would not open after SAS in v4.8.14
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SecureBit.chat v4.8.15
|
||||
# SecureBit.chat v4.8.20
|
||||
|
||||
SecureBit.chat is a browser-based peer-to-peer chat application built on WebRTC and Web Crypto APIs. It is designed for direct encrypted communication, explicit peer verification, and a small operational footprint without account registration or server-side message storage.
|
||||
|
||||
@@ -15,18 +15,14 @@ SecureBit.chat uses:
|
||||
|
||||
A session is not treated as verified until both peers complete the interactive SAS flow. Each user must compare the displayed code with the peer through an out-of-band channel and enter the matching code manually. Three failed SAS attempts terminate the session.
|
||||
|
||||
## Highlights in v4.8.15
|
||||
## Highlights in v4.8.20
|
||||
|
||||
- Fix: the secure chat failed to open after SAS confirmation in v4.8.14 (a `nowTick` reference was scoped to the wrong component). The new messaging controls are now wired into the chat component correctly.
|
||||
Secure messaging controls, available from a single composer toolbar next to **Send files**. Active controls use the brand-orange accent. Every per-message option travels **inside the encrypted message envelope** (never in the sanitized text), so message content can neither spoof nor corrupt these controls.
|
||||
|
||||
The v4.8.14 messaging features:
|
||||
|
||||
- Code blocks: a composer button wraps a message in a monospace code window with a one-click copy button (clipboard auto-clears after 30s).
|
||||
- View-once messages: the recipient sees a blurred bubble that reveals on tap and is then deleted. Cooperative, like WhatsApp view-once — not screenshot-proof.
|
||||
- Disappearing messages: an optional timer (30s / 5m / 1h) auto-deletes a message on both sides with a live countdown.
|
||||
- Unsend: "delete for everyone" removes your message from the peer's chat too.
|
||||
- Panic wipe: one button clears the conversation, wipes keys and disconnects.
|
||||
- Per-message metadata travels inside the encrypted envelope (not in the sanitized text), so message content can never spoof or corrupt these controls.
|
||||
- **Code blocks.** A `Code` button sends the message as a monospace code window with lightweight syntax highlighting and a one-click **Copy** button; the clipboard auto-clears ~30s after copying so keys/commands don't linger. Enabling it also expands the input box (monospace, 8 rows) for comfortable code entry. Highlighting is built from already-sanitized text via React nodes only — no `innerHTML`, no remote scripts, no new XSS surface.
|
||||
- **View-once messages.** Pick how long the message stays visible after the peer opens it (5s / 15s / 30s / 1m). The recipient sees a blurred bubble; tapping reveals it, then it is wiped after the chosen window. Cooperative, like WhatsApp view-once — it reduces accidental lingering but is **not** screenshot-proof.
|
||||
- **Disappearing messages.** A timer picker (30s / 5m / 1h) auto-deletes the message on **both** sides, with a live countdown. The incoming timer is clamped to a safe range.
|
||||
- **Unsend (delete for everyone).** Removes your message locally and asks the peer to drop it too, over the authenticated control channel.
|
||||
|
||||
Earlier in v4.8.13:
|
||||
|
||||
|
||||
@@ -22,6 +22,6 @@ SecureBit.chat is intended for legitimate private communication, journalism, res
|
||||
|
||||
## Current release
|
||||
|
||||
- Product release: `v4.8.15`
|
||||
- Product release: `v4.8.20`
|
||||
- Protocol version: `4.1`
|
||||
- Last updated: May 17, 2026
|
||||
+1
-1
@@ -1 +1 @@
|
||||
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@mediLine truncated
|
||||
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@mediLine truncated
|
||||
Vendored
+21
-7
@@ -542,10 +542,10 @@ var require_NotificationIntegration = __commonJS({
|
||||
}
|
||||
this.originalOnMessage = this.webrtcManager.onMessage;
|
||||
this.originalOnStatusChange = this.webrtcManager.onStatusChange;
|
||||
this.webrtcManager.onMessage = (message, type) => {
|
||||
this.webrtcManager.onMessage = (message, type, ...rest) => {
|
||||
this.handleIncomingMessage(message, type);
|
||||
if (this.originalOnMessage) {
|
||||
this.originalOnMessage(message, type);
|
||||
this.originalOnMessage(message, type, ...rest);
|
||||
}
|
||||
};
|
||||
this.webrtcManager.onStatusChange = (status) => {
|
||||
@@ -556,9 +556,9 @@ var require_NotificationIntegration = __commonJS({
|
||||
};
|
||||
if (this.webrtcManager.deliverMessageToUI) {
|
||||
this.originalDeliverMessageToUI = this.webrtcManager.deliverMessageToUI.bind(this.webrtcManager);
|
||||
this.webrtcManager.deliverMessageToUI = (message, type) => {
|
||||
this.webrtcManager.deliverMessageToUI = (message, type, ...rest) => {
|
||||
this.handleIncomingMessage(message, type);
|
||||
this.originalDeliverMessageToUI(message, type);
|
||||
this.originalDeliverMessageToUI(message, type, ...rest);
|
||||
};
|
||||
}
|
||||
this.isInitialized = true;
|
||||
@@ -11086,6 +11086,10 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
||||
}
|
||||
if (meta.code === true) out.code = true;
|
||||
if (meta.once === true) out.once = true;
|
||||
if (Number.isFinite(meta.onceTtl)) {
|
||||
const onceTtl = Math.floor(meta.onceTtl);
|
||||
if (onceTtl >= 1 && onceTtl <= 3600) out.onceTtl = onceTtl;
|
||||
}
|
||||
if (Number.isFinite(meta.ttl)) {
|
||||
const ttl = Math.floor(meta.ttl);
|
||||
if (ttl >= 5 && ttl <= 86400) out.ttl = ttl;
|
||||
@@ -12148,6 +12152,16 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
||||
this._secureLog("error", "No file transfer system available for:", { errorType: parsed.type?.constructor?.name || "Unknown" });
|
||||
return;
|
||||
}
|
||||
if (parsed.type === _EnhancedSecureWebRTCManager.MESSAGE_TYPES.MESSAGE_DELETE) {
|
||||
const messageId = parsed?.data?.messageId ?? parsed?.messageId;
|
||||
if (typeof messageId === "string" && messageId) {
|
||||
try {
|
||||
this.onMessageDelete?.(messageId.slice(0, 64));
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (parsed.type && ["heartbeat", "verification", "verification_response", "verification_confirmed", "verification_both_confirmed", "sas_code", "peer_disconnect", "security_upgrade"].includes(parsed.type)) {
|
||||
this.handleSystemMessage(parsed);
|
||||
return;
|
||||
@@ -12157,7 +12171,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
||||
return;
|
||||
}
|
||||
if (this.onMessage) {
|
||||
this.deliverMessageToUI(parsed.data, "received");
|
||||
this.deliverMessageToUI(parsed.data, "received", parsed.meta);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -12252,7 +12266,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
||||
}
|
||||
if (decryptedContent && decryptedContent.type === "message" && typeof decryptedContent.data === "string") {
|
||||
if (this.onMessage) {
|
||||
this.deliverMessageToUI(decryptedContent.data, "received");
|
||||
this.deliverMessageToUI(decryptedContent.data, "received", decryptedContent.meta);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -17487,7 +17501,7 @@ Right-click or Ctrl+click to disconnect`,
|
||||
React.createElement("p", {
|
||||
key: "subtitle",
|
||||
className: "text-xs sm:text-sm text-muted hidden sm:block"
|
||||
}, "End-to-end freedom v4.8.15")
|
||||
}, "End-to-end freedom v4.8.20")
|
||||
])
|
||||
]),
|
||||
// Status and Controls - Responsive
|
||||
|
||||
Vendored
+2
-2
@@ -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/EnhancedSecureWebRTCManager.js", "../src/scripts/app-boot.js", "../src/components/ui/Header.jsx", "../src/components/ui/DownloadApps.jsx", "../src/components/ui/BecomePartner.jsx", "../src/components/ui/UniqueFeatureSlider.jsx", "../src/components/ui/SecurityFeatures.jsx", "../src/components/ui/Testimonials.jsx", "../src/components/ui/ComparisonTable.jsx", "../src/components/ui/Roadmap.jsx", "../src/components/ui/FileTransfer.jsx", "../src/network/iceServers.js", "../src/components/ui/IceServerSettings.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
|
||||
"mappings": "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 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": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AASA,QAAM,gCAAN,MAAoC;AAAA,MAClC,YAAY,SAAS,CAAC,GAAG;AAEvB,aAAK,aAAc,OAAO,iBAAiB,eAAe,gBAAgB,OAAO,aAAa,eAAe,WACzG,aAAa,aACb;AACJ,aAAK,cAAc,KAAK,eAAe;AACvC,aAAK,cAAc;AACnB,aAAK,gBAAgB,SAAS;AAC9B,aAAK,oBAAoB,CAAC;AAC1B,aAAK,eAAe,OAAO,gBAAgB;AAC3C,aAAK,cAAc,OAAO,eAAe;AACzC,aAAK,uBAAuB;AAC5B,aAAK,iBAAiB,OAAO,kBAAkB,CAAC;AAGhD,aAAK,kBAAkB,OAAO;AAG9B,aAAK,SAAS,KAAK,kBAAkB;AACrC,aAAK,mBAAmB,KAAK,yBAAyB;AAEtD,aAAK,uBAAuB;AAC5B,aAAK,mBAAmB;AAAA,MAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,qBAAqB;AAAA,MAErB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,oBAAoB;AAClB,YAAI,OAAO,SAAS,WAAW,aAAa;AAC1C,iBAAO;AAAA,QACT,WAAW,OAAO,SAAS,aAAa,aAAa;AACnD,iBAAO;AAAA,QACT,WAAW,OAAO,SAAS,iBAAiB,aAAa;AACvD,iBAAO;AAAA,QACT;AACA,eAAO;AAAA,MACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,2BAA2B;AACzB,YAAI,OAAO,SAAS,WAAW,aAAa;AAC1C,iBAAO;AAAA,QACT,WAAW,OAAO,SAAS,aAAa,aAAa;AACnD,iBAAO;AAAA,QACT,WAAW,OAAO,SAAS,iBAAiB,aAAa;AACvD,iBAAO;AAAA,QACT;AACA,eAAO;AAAA,MACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,iBAAiB;AAEf,YAAI,KAAK,UAAU,OAAO,SAAS,KAAK,MAAM,MAAM,aAAa;AAC/D,iBAAO,CAAC,SAAS,KAAK,MAAM;AAAA,QAC9B;AAGA,YAAI,OAAO,SAAS,aAAa,YAAY;AAC3C,iBAAO,SAAS,SAAS;AAAA,QAC3B;AAGA,eAAO;AAAA,MACT;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,yBAAyB;AAEvB,YAAI,OAAO,SAAS,qBAAqB,eAAe,OAAO,SAAS,KAAK,MAAM,MAAM,aAAa;AACpG,mBAAS,iBAAiB,KAAK,kBAAkB,MAAM;AACrD,iBAAK,cAAc,KAAK,eAAe;AAEvC,gBAAI,KAAK,aAAa;AACpB,mBAAK,iBAAiB;AACtB,mBAAK,uBAAuB;AAAA,YAC9B;AAAA,UACF,CAAC;AAAA,QACH;AAGA,eAAO,iBAAiB,SAAS,MAAM;AACrC,eAAK,cAAc,KAAK,eAAe;AACvC,cAAI,KAAK,aAAa;AACpB,iBAAK,iBAAiB;AAAA,UACxB;AAAA,QACF,CAAC;AAED,eAAO,iBAAiB,QAAQ,MAAM;AACpC,eAAK,cAAc,KAAK,eAAe;AAAA,QACzC,CAAC;AAGD,eAAO,iBAAiB,gBAAgB,MAAM;AAC5C,eAAK,uBAAuB;AAAA,QAC9B,CAAC;AAAA,MACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,MAAM,oBAAoB;AAExB,YAAI,CAAC,KAAK,mBAAmB,EAAE,kBAAkB,SAAS;AACxD,iBAAO;AAAA,QACT;AAEA,YAAI,KAAK,eAAe,WAAW;AACjC,iBAAO;AAAA,QACT;AAEA,YAAI,KAAK,eAAe,UAAU;AAChC,iBAAO;AAAA,QACT;AAEA,YAAI;AACF,eAAK,aAAa,MAAM,aAAa,kBAAkB;AACvD,iBAAO,KAAK,eAAe;AAAA,QAC7B,SAAS,OAAO;AACd,iBAAO;AAAA,QACT;AAAA,MACF;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,cAAc;AACZ,YAAI,KAAK,cAAc,GAAG;AACxB,mBAAS,QAAQ,IAAI,KAAK,WAAW,KAAK,KAAK,aAAa;AAAA,QAC9D,OAAO;AACL,mBAAS,QAAQ,KAAK;AAAA,QACxB;AAAA,MACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQA,aAAaA,OAAM;AACjB,YAAI,OAAOA,UAAS,UAAU;AAC5B,iBAAO;AAAA,QACT;AAGA,cAAM,MAAM,SAAS,cAAc,KAAK;AACxC,YAAI,cAAcA;AAClB,eAAO,IAAI,UACR,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,QAAQ,EACtB,UAAU,GAAG,GAAG;AAAA,MACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQA,gBAAgB,KAAK;AACnB,YAAI,CAAC,IAAK,QAAO;AAEjB,YAAI;AACF,gBAAM,YAAY,IAAI,IAAI,KAAK,OAAO,SAAS,MAAM;AAGrD,cAAI,UAAU,aAAa,YAAY,UAAU,aAAa,SAAS;AAErE,gBAAI,KAAK,eAAe,SAAS,GAAG;AAClC,oBAAM,YAAY,KAAK,eAAe;AAAA,gBAAK,YACzC,UAAU,WAAW;AAAA,cACvB;AACA,qBAAO,YAAY,UAAU,OAAO;AAAA,YACtC;AACA,mBAAO,UAAU;AAAA,UACnB;AAEA,iBAAO;AAAA,QACT,SAAS,OAAO;AACd,iBAAO;AAAA,QACT;AAAA,MACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,iBAAiB;AACf,cAAM,MAAM,KAAK,IAAI;AACrB,YAAI,MAAM,KAAK,uBAAuB,KAAK,aAAa;AACtD,iBAAO;AAAA,QACT;AACA,aAAK,uBAAuB;AAC5B,eAAO;AAAA,MACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MASA,OAAO,YAAY,SAAS,UAAU,CAAC,GAAG;AAExC,YAAI,OAAO,iBAAiB,aAAa;AACvC,iBAAO;AAAA,QACT;AAEA,aAAK,cAAc,KAAK,eAAe;AAGvC,YAAI,KAAK,aAAa;AACpB,iBAAO;AAAA,QACT;AAGA,YAAI,KAAK,eAAe,WAAW;AACjC,iBAAO;AAAA,QACT;AAGA,YAAI,CAAC,KAAK,eAAe,GAAG;AAC1B,iBAAO;AAAA,QACT;AAGA,cAAM,iBAAiB,KAAK,aAAa,cAAc,SAAS;AAChE,cAAM,cAAc,KAAK,aAAa,WAAW,EAAE;AACnD,cAAM,WAAW,KAAK,gBAAgB,QAAQ,IAAI,KAAK;AAGvD,YAAI,KAAK,kBAAkB,UAAU,KAAK,cAAc;AACtD,eAAK,uBAAuB;AAAA,QAC9B;AAEA,YAAI;AAEF,gBAAM,eAAe,IAAI;AAAA,YACvB,GAAG,cAAc;AAAA,YACjB;AAAA,cACE,MAAM,YAAY,UAAU,GAAG,GAAG;AAAA;AAAA,cAClC,MAAM;AAAA,cACN,OAAO;AAAA,cACP,KAAK,QAAQ,QAAQ,YAAY,SAAS;AAAA;AAAA,cAC1C,oBAAoB;AAAA;AAAA,cACpB,QAAQ,QAAQ,UAAU;AAAA;AAAA,cAE1B,SAAS,UAAU,UAAU,CAAC,KAAK,KAAK,GAAG,IAAI;AAAA;AAAA,cAE/C,MAAM;AAAA,gBACJ,UAAU,KAAK,aAAa,QAAQ,QAAQ;AAAA,gBAC5C,WAAW,KAAK,IAAI;AAAA;AAAA,cAEtB;AAAA,YACF;AAAA,UACF;AAGA,eAAK;AACL,eAAK,YAAY;AAGjB,eAAK,kBAAkB,KAAK,YAAY;AAGxC,uBAAa,UAAU,CAAC,UAAU;AAChC,kBAAM,eAAe;AACrB,mBAAO,MAAM;AACb,yBAAa,MAAM;AAGnB,gBAAI,OAAO,QAAQ,YAAY,YAAY;AACzC,kBAAI;AACF,wBAAQ,QAAQ,QAAQ,QAAQ;AAAA,cAClC,SAAS,OAAO;AACd,wBAAQ,MAAM,6CAA6C,KAAK;AAAA,cAClE;AAAA,YACF;AAAA,UACF;AAGA,uBAAa,UAAU,CAAC,UAAU;AAChC,oBAAQ,MAAM,+CAA+C,KAAK;AAAA,UACpE;AAGA,gBAAM,mBAAmB,KAAK,IAAI,QAAQ,aAAa,KAAM,GAAK;AAClE,qBAAW,MAAM;AACf,yBAAa,MAAM;AACnB,iBAAK,gBAAgB,YAAY;AAAA,UACnC,GAAG,gBAAgB;AAEnB,iBAAO;AAAA,QAET,SAAS,OAAO;AACd,kBAAQ,MAAM,kDAAkD,KAAK;AACrE,iBAAO;AAAA,QACT;AAAA,MACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,gBAAgB,cAAc;AAC5B,cAAM,QAAQ,KAAK,kBAAkB,QAAQ,YAAY;AACzD,YAAI,QAAQ,IAAI;AACd,eAAK,kBAAkB,OAAO,OAAO,CAAC;AAAA,QACxC;AAAA,MACF;AAAA;AAAA;AAAA;AAAA,MAKA,yBAAyB;AACvB,aAAK,kBAAkB,QAAQ,kBAAgB;AAC7C,cAAI;AACF,yBAAa,MAAM;AAAA,UACrB,SAAS,OAAO;AAAA,UAEhB;AAAA,QACF,CAAC;AACD,aAAK,oBAAoB,CAAC;AAAA,MAC5B;AAAA;AAAA;AAAA;AAAA,MAKA,mBAAmB;AACjB,aAAK,cAAc;AACnB,aAAK,YAAY;AAAA,MACnB;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,YAAY;AACV,eAAO;AAAA,UACL,YAAY,KAAK;AAAA,UACjB,aAAa,KAAK;AAAA,UAClB,aAAa,KAAK;AAAA,UACLine 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
+276
-134
@@ -204,6 +204,128 @@ var parseMessageSegments = (text) => {
|
||||
if (last < text.length) segments.push({ kind: "text", content: text.slice(last) });
|
||||
return segments.some((s) => s.kind === "code") ? segments : null;
|
||||
};
|
||||
var HL_KEYWORDS = /* @__PURE__ */ new Set([
|
||||
"const",
|
||||
"let",
|
||||
"var",
|
||||
"function",
|
||||
"return",
|
||||
"if",
|
||||
"else",
|
||||
"for",
|
||||
"while",
|
||||
"do",
|
||||
"switch",
|
||||
"case",
|
||||
"break",
|
||||
"continue",
|
||||
"class",
|
||||
"extends",
|
||||
"new",
|
||||
"this",
|
||||
"super",
|
||||
"import",
|
||||
"export",
|
||||
"from",
|
||||
"as",
|
||||
"default",
|
||||
"async",
|
||||
"await",
|
||||
"try",
|
||||
"catch",
|
||||
"finally",
|
||||
"throw",
|
||||
"typeof",
|
||||
"instanceof",
|
||||
"delete",
|
||||
"yield",
|
||||
"in",
|
||||
"of",
|
||||
"def",
|
||||
"elif",
|
||||
"lambda",
|
||||
"pass",
|
||||
"with",
|
||||
"global",
|
||||
"public",
|
||||
"private",
|
||||
"protected",
|
||||
"static",
|
||||
"final",
|
||||
"void",
|
||||
"int",
|
||||
"long",
|
||||
"float",
|
||||
"double",
|
||||
"char",
|
||||
"bool",
|
||||
"boolean",
|
||||
"string",
|
||||
"struct",
|
||||
"enum",
|
||||
"interface",
|
||||
"package",
|
||||
"func",
|
||||
"fn",
|
||||
"type",
|
||||
"where",
|
||||
"select",
|
||||
"update",
|
||||
"insert",
|
||||
"delete",
|
||||
"where",
|
||||
"and",
|
||||
"or",
|
||||
"not",
|
||||
"end",
|
||||
"then",
|
||||
"fi",
|
||||
"done",
|
||||
"echo",
|
||||
"use",
|
||||
"mut",
|
||||
"impl",
|
||||
"trait",
|
||||
"match",
|
||||
"module",
|
||||
"require"
|
||||
]);
|
||||
var HL_LITERALS = /* @__PURE__ */ new Set(["true", "false", "null", "undefined", "None", "True", "False", "nil", "NaN", "Infinity"]);
|
||||
var highlightCode = (code) => {
|
||||
const re = /(\/\/[^\n]*|#[^\n]*|\/\*[\s\S]*?\*\/|--[^\n]*)|(`(?:\\.|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|(\b\d[\d_.]*(?:[eE][+-]?\d+)?\b|\b0[xX][0-9a-fA-F]+\b)|([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
||||
const nodes = [];
|
||||
let buffer = "";
|
||||
let last = 0;
|
||||
let key = 0;
|
||||
const flush = () => {
|
||||
if (buffer) {
|
||||
nodes.push(buffer);
|
||||
buffer = "";
|
||||
}
|
||||
};
|
||||
let m;
|
||||
while ((m = re.exec(code)) !== null) {
|
||||
if (m.index > last) buffer += code.slice(last, m.index);
|
||||
last = re.lastIndex;
|
||||
let cls = null;
|
||||
if (m[1]) cls = "text-gray-500 italic";
|
||||
else if (m[2]) cls = "text-amber-300";
|
||||
else if (m[3]) cls = "text-sky-300";
|
||||
else if (m[4]) {
|
||||
if (HL_KEYWORDS.has(m[4])) cls = "text-purple-300";
|
||||
else if (HL_LITERALS.has(m[4])) cls = "text-sky-300";
|
||||
}
|
||||
if (cls) {
|
||||
flush();
|
||||
nodes.push(React.createElement("span", { key: `h${key++}`, className: cls }, m[0]));
|
||||
} else {
|
||||
buffer += m[0];
|
||||
}
|
||||
}
|
||||
if (last < code.length) buffer += code.slice(last);
|
||||
flush();
|
||||
return nodes;
|
||||
};
|
||||
var CodeBlock = ({ code, lang }) => {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const handleCopy = async () => {
|
||||
@@ -214,13 +336,13 @@ var CodeBlock = ({ code, lang }) => {
|
||||
}
|
||||
};
|
||||
return React.createElement("div", {
|
||||
className: "my-1 rounded-lg overflow-hidden border border-gray-600/40",
|
||||
style: { backgroundColor: "#1b1c1b" }
|
||||
className: "my-1 rounded-lg overflow-hidden",
|
||||
style: { backgroundColor: "#1b1c1b", border: "0 solid #e5e7eb" }
|
||||
}, [
|
||||
React.createElement("div", {
|
||||
key: "hdr",
|
||||
className: "flex items-center justify-between px-3 py-1.5 border-b border-gray-600/30",
|
||||
style: { backgroundColor: "#222322" }
|
||||
className: "flex items-center justify-between px-3 py-1.5",
|
||||
style: { backgroundColor: "#222322", border: "0 solid #e5e7eb" }
|
||||
}, [
|
||||
React.createElement("span", {
|
||||
key: "lang",
|
||||
@@ -243,7 +365,7 @@ var CodeBlock = ({ code, lang }) => {
|
||||
key: "pre",
|
||||
className: "px-3 py-2 overflow-x-auto text-xs leading-relaxed text-gray-200 custom-scrollbar",
|
||||
style: { whiteSpace: "pre", fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", margin: 0 }
|
||||
}, React.createElement("code", null, code))
|
||||
}, React.createElement("code", null, highlightCode(code)))
|
||||
]);
|
||||
};
|
||||
var MessageBody = ({ text }) => {
|
||||
@@ -266,62 +388,93 @@ var MessageBody = ({ text }) => {
|
||||
)
|
||||
);
|
||||
};
|
||||
var ChatToolbar = ({ codeMode, setCodeMode, viewOnceMode, setViewOnceMode, disappearTtl, setDisappearTtl, onPanicWipe }) => {
|
||||
const ttlCycle = [0, 30, 300, 3600];
|
||||
const ttlLabel = (s) => s === 0 ? "Off" : s >= 3600 ? `${Math.round(s / 3600)}h` : s >= 60 ? `${Math.round(s / 60)}m` : `${s}s`;
|
||||
const cycleTtl = () => {
|
||||
const i = ttlCycle.indexOf(disappearTtl);
|
||||
setDisappearTtl(ttlCycle[(i + 1) % ttlCycle.length] || 0);
|
||||
};
|
||||
const pill = (key, { active, activeClass, icon, label, title, onClick }) => React.createElement("button", {
|
||||
var ChatToolbar = ({ codeMode, setCodeMode, viewOnceMode, setViewOnceMode, viewOnceTtl, setViewOnceTtl, disappearTtl, setDisappearTtl }) => {
|
||||
const [openMenu, setOpenMenu] = React.useState(null);
|
||||
const fmt = (s) => s >= 3600 ? `${Math.round(s / 3600)}h` : s >= 60 ? `${Math.round(s / 60)}m` : `${s}s`;
|
||||
const btnClass = (active) => `inline-flex items-center gap-2 h-9 px-3 rounded-lg text-xs font-medium transition-colors duration-150 ${active ? "accent-orange bg-orange-500/10" : "text-gray-400 hover:text-gray-200 hover:bg-gray-700/40"}`;
|
||||
const pickerItem = (opt, current, onPick) => React.createElement("button", {
|
||||
key: String(opt.value),
|
||||
type: "button",
|
||||
onClick: () => {
|
||||
onPick(opt.value);
|
||||
setOpenMenu(null);
|
||||
},
|
||||
// Comfortable tap target + readable size on mobile.
|
||||
className: `w-full text-left px-4 py-3 sm:py-2.5 text-sm flex items-center gap-3 transition-colors ${current === opt.value ? "accent-orange bg-orange-500/10" : "text-gray-200 hover:bg-gray-700/50 active:bg-gray-700/60"}`
|
||||
}, [
|
||||
React.createElement("i", { key: "i", className: `${opt.icon || "far fa-clock"} w-4 text-center` }),
|
||||
React.createElement("span", { key: "l" }, opt.label)
|
||||
]);
|
||||
const picker = (options, current, onPick) => React.createElement("div", {
|
||||
className: "absolute right-0 z-50 min-w-[180px] max-w-[78vw] rounded-xl shadow-2xl overflow-hidden",
|
||||
style: { backgroundColor: "#1f201f", border: "0 solid #e5e7eb", bottom: "100%", marginBottom: "8px" }
|
||||
}, options.map((opt) => pickerItem(opt, current, onPick)));
|
||||
const labelBtn = (key, { active, icon, label, title, onClick }) => React.createElement("button", {
|
||||
key,
|
||||
type: "button",
|
||||
onClick,
|
||||
title,
|
||||
className: `inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs border transition-all duration-200 ${active ? activeClass : "text-gray-400 border-gray-600/50 hover:text-gray-200 hover:border-gray-500"}`
|
||||
"aria-pressed": !!active,
|
||||
className: btnClass(active)
|
||||
}, [
|
||||
React.createElement("i", { key: "i", className: icon }),
|
||||
label ? React.createElement("span", { key: "l" }, label) : null
|
||||
React.createElement("i", { key: "i", className: `${icon} text-[13px]` }),
|
||||
React.createElement("span", { key: "l", className: "leading-none" }, label)
|
||||
]);
|
||||
return React.createElement("div", {
|
||||
className: "flex items-center flex-wrap gap-2 pb-3"
|
||||
}, [
|
||||
pill("code", {
|
||||
return React.createElement("div", { className: "flex items-center gap-1" }, [
|
||||
// Invisible backdrop closes any open picker on outside click.
|
||||
openMenu && React.createElement("div", {
|
||||
key: "backdrop",
|
||||
className: "fixed inset-0 z-40",
|
||||
onClick: () => setOpenMenu(null)
|
||||
}),
|
||||
// Code — toggles code mode (expands the input box).
|
||||
labelBtn("code", {
|
||||
active: codeMode,
|
||||
activeClass: "text-green-400 border-green-500/40 bg-green-500/10",
|
||||
icon: "fas fa-code",
|
||||
label: "Code",
|
||||
title: "Send as a code block (with copy button)",
|
||||
title: "Send as a code block (expands the input)",
|
||||
onClick: () => setCodeMode((v) => !v)
|
||||
}),
|
||||
pill("once", {
|
||||
active: viewOnceMode,
|
||||
activeClass: "text-orange-400 border-orange-500/40 bg-orange-500/10",
|
||||
icon: "fas fa-eye-slash",
|
||||
label: "View once",
|
||||
title: "Recipient can read it once, then it is deleted (cooperative \u2014 not screenshot-proof)",
|
||||
onClick: () => setViewOnceMode((v) => !v)
|
||||
}),
|
||||
pill("ttl", {
|
||||
active: disappearTtl > 0,
|
||||
activeClass: "text-blue-400 border-blue-500/40 bg-blue-500/10",
|
||||
icon: "fas fa-stopwatch",
|
||||
label: `Timer: ${ttlLabel(disappearTtl)}`,
|
||||
title: "Disappearing messages \u2014 auto-delete on both sides",
|
||||
onClick: cycleTtl
|
||||
}),
|
||||
React.createElement("div", { key: "spacer", className: "flex-1" }),
|
||||
pill("panic", {
|
||||
active: true,
|
||||
activeClass: "text-red-400 border-red-500/40 bg-red-500/10 hover:bg-red-500/20",
|
||||
icon: "fas fa-fire-extinguisher",
|
||||
label: "Panic",
|
||||
title: "Wipe this conversation and keys, and disconnect",
|
||||
onClick: () => {
|
||||
const ok = typeof window !== "undefined" && window.confirm ? window.confirm("Panic wipe: delete all messages, wipe keys and disconnect now?") : true;
|
||||
if (ok && typeof onPanicWipe === "function") onPanicWipe();
|
||||
}
|
||||
})
|
||||
// View once — pick how long it stays after the peer opens it.
|
||||
React.createElement("div", { key: "once", className: "relative" }, [
|
||||
labelBtn("once-btn", {
|
||||
active: viewOnceMode,
|
||||
icon: "fas fa-eye-slash",
|
||||
label: viewOnceMode ? `Once \xB7 ${fmt(viewOnceTtl)}` : "View once",
|
||||
title: "View once \u2014 vanishes after the peer reads it",
|
||||
onClick: () => setOpenMenu(openMenu === "once" ? null : "once")
|
||||
}),
|
||||
openMenu === "once" && picker([
|
||||
{ value: 0, label: "Off", icon: "fas fa-ban" },
|
||||
{ value: 5, label: "5s after reading" },
|
||||
{ value: 15, label: "15s after reading" },
|
||||
{ value: 30, label: "30s after reading" },
|
||||
{ value: 60, label: "1m after reading" }
|
||||
], viewOnceMode ? viewOnceTtl : 0, (v) => {
|
||||
if (v === 0) {
|
||||
setViewOnceMode(false);
|
||||
} else {
|
||||
setViewOnceTtl(v);
|
||||
setViewOnceMode(true);
|
||||
}
|
||||
})
|
||||
]),
|
||||
// Timer — pick the disappearing duration.
|
||||
React.createElement("div", { key: "timer", className: "relative" }, [
|
||||
labelBtn("timer-btn", {
|
||||
active: disappearTtl > 0,
|
||||
icon: "fas fa-stopwatch",
|
||||
label: disappearTtl > 0 ? `Timer \xB7 ${fmt(disappearTtl)}` : "Timer",
|
||||
title: "Disappearing message \u2014 deletes on both sides",
|
||||
onClick: () => setOpenMenu(openMenu === "timer" ? null : "timer")
|
||||
}),
|
||||
openMenu === "timer" && picker([
|
||||
{ value: 0, label: "Off", icon: "fas fa-ban" },
|
||||
{ value: 30, label: "30 seconds" },
|
||||
{ value: 300, label: "5 minutes" },
|
||||
{ value: 3600, label: "1 hour" }
|
||||
], disappearTtl, (v) => setDisappearTtl(v))
|
||||
])
|
||||
]);
|
||||
};
|
||||
var EnhancedCopyButton = ({ text, className = "", children }) => {
|
||||
@@ -536,7 +689,7 @@ var VerificationStep = ({ verificationCode, onConfirm, onReject, localConfirmed,
|
||||
])
|
||||
]);
|
||||
};
|
||||
var EnhancedChatMessage = ({ message, type, timestamp, mid, viewOnce, expiresAt, nowTick, canUnsend, onUnsend, onExpire }) => {
|
||||
var EnhancedChatMessage = ({ message, type, timestamp, mid, viewOnce, viewOnceTtl, expiresAt, nowTick, canUnsend, onUnsend, onExpire }) => {
|
||||
const [revealed, setRevealed] = React.useState(false);
|
||||
const revealTimerRef = React.useRef(null);
|
||||
const formatTime = (ts) => {
|
||||
@@ -553,7 +706,7 @@ var EnhancedChatMessage = ({ message, type, timestamp, mid, viewOnce, expiresAt,
|
||||
switch (type) {
|
||||
case "sent":
|
||||
return {
|
||||
container: "ml-auto bg-orange-500/15 border-orange-500/20 text-primary",
|
||||
container: "ml-auto bg-orange-500/5 border-orange-500/20 text-primary",
|
||||
icon: "fas fa-lock accent-orange",
|
||||
label: "Encrypted"
|
||||
};
|
||||
@@ -583,9 +736,10 @@ var EnhancedChatMessage = ({ message, type, timestamp, mid, viewOnce, expiresAt,
|
||||
const handleReveal = () => {
|
||||
if (revealed) return;
|
||||
setRevealed(true);
|
||||
const ms = Math.max(1, typeof viewOnceTtl === "number" ? viewOnceTtl : 15) * 1e3;
|
||||
revealTimerRef.current = setTimeout(() => {
|
||||
onExpire && onExpire();
|
||||
}, 12e3);
|
||||
}, ms);
|
||||
};
|
||||
let body;
|
||||
if (isViewOnce && !revealed) {
|
||||
@@ -1531,12 +1685,13 @@ var EnhancedChatInterface = ({
|
||||
setCodeMode,
|
||||
viewOnceMode,
|
||||
setViewOnceMode,
|
||||
viewOnceTtl,
|
||||
setViewOnceTtl,
|
||||
disappearTtl,
|
||||
setDisappearTtl,
|
||||
nowTick,
|
||||
onUnsendMessage,
|
||||
onMessageExpire,
|
||||
onPanicWipe
|
||||
onMessageExpire
|
||||
}) => {
|
||||
const [showScrollButton, setShowScrollButton] = React.useState(false);
|
||||
const [showFileTransfer, setShowFileTransfer] = React.useState(false);
|
||||
@@ -1687,6 +1842,7 @@ var EnhancedChatInterface = ({
|
||||
timestamp: msg.timestamp,
|
||||
mid: msg.mid,
|
||||
viewOnce: msg.viewOnce,
|
||||
viewOnceTtl: msg.viewOnceTtl,
|
||||
expiresAt: msg.expiresAt,
|
||||
nowTick,
|
||||
canUnsend: typeof onUnsendMessage === "function",
|
||||
@@ -1726,36 +1882,53 @@ var EnhancedChatInterface = ({
|
||||
"div",
|
||||
{ className: "max-w-4xl mx-auto px-4" },
|
||||
[
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: () => setShowFileTransfer(!showFileTransfer),
|
||||
className: `flex items-center text-sm text-gray-400 hover:text-gray-300 transition-colors py-4 ${showFileTransfer ? "mb-4" : ""}`
|
||||
},
|
||||
[
|
||||
React.createElement(
|
||||
"svg",
|
||||
{
|
||||
className: `w-4 h-4 mr-2 transform transition-transform ${showFileTransfer ? "rotate-180" : ""}`,
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
viewBox: "0 0 24 24"
|
||||
},
|
||||
showFileTransfer ? React.createElement("path", {
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
strokeWidth: 2,
|
||||
d: "M5 15l7-7 7 7"
|
||||
}) : React.createElement("path", {
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
strokeWidth: 2,
|
||||
d: "M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"
|
||||
})
|
||||
),
|
||||
showFileTransfer ? "Hide file transfer" : "Send files"
|
||||
]
|
||||
),
|
||||
React.createElement("div", {
|
||||
key: "composer-bar",
|
||||
className: `flex items-center justify-between flex-wrap gap-2 ${showFileTransfer ? "mb-4" : ""}`
|
||||
}, [
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
key: "send-files-toggle",
|
||||
onClick: () => setShowFileTransfer(!showFileTransfer),
|
||||
className: "flex items-center text-sm text-gray-400 hover:text-gray-300 transition-colors py-4"
|
||||
},
|
||||
[
|
||||
React.createElement(
|
||||
"svg",
|
||||
{
|
||||
className: `w-4 h-4 mr-2 transform transition-transform ${showFileTransfer ? "rotate-180" : ""}`,
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
viewBox: "0 0 24 24"
|
||||
},
|
||||
showFileTransfer ? React.createElement("path", {
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
strokeWidth: 2,
|
||||
d: "M5 15l7-7 7 7"
|
||||
}) : React.createElement("path", {
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
strokeWidth: 2,
|
||||
d: "M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"
|
||||
})
|
||||
),
|
||||
showFileTransfer ? "Hide file transfer" : "Send files"
|
||||
]
|
||||
),
|
||||
React.createElement(ChatToolbar, {
|
||||
key: "toolbar",
|
||||
codeMode,
|
||||
setCodeMode,
|
||||
viewOnceMode,
|
||||
setViewOnceMode,
|
||||
viewOnceTtl,
|
||||
setViewOnceTtl,
|
||||
disappearTtl,
|
||||
setDisappearTtl
|
||||
})
|
||||
]),
|
||||
showFileTransfer && React.createElement(window.FileTransferComponent || (() => React.createElement("div", {
|
||||
className: "p-4 text-center text-red-400"
|
||||
}, "FileTransferComponent not loaded")), {
|
||||
@@ -1774,16 +1947,6 @@ var EnhancedChatInterface = ({
|
||||
"div",
|
||||
{ className: "max-w-4xl mx-auto p-4" },
|
||||
[
|
||||
React.createElement(ChatToolbar, {
|
||||
key: "toolbar",
|
||||
codeMode,
|
||||
setCodeMode,
|
||||
viewOnceMode,
|
||||
setViewOnceMode,
|
||||
disappearTtl,
|
||||
setDisappearTtl,
|
||||
onPanicWipe
|
||||
}),
|
||||
React.createElement(
|
||||
"div",
|
||||
{ key: "inputrow", className: "flex items-stretch space-x-3" },
|
||||
@@ -1796,11 +1959,11 @@ var EnhancedChatInterface = ({
|
||||
value: messageInput,
|
||||
onChange: (e) => setMessageInput(e.target.value),
|
||||
onKeyDown: handleKeyPress,
|
||||
placeholder: "Enter message to encrypt...",
|
||||
rows: 2,
|
||||
placeholder: codeMode ? "Paste or type code\u2026" : "Enter message to encrypt...",
|
||||
rows: codeMode ? 8 : 2,
|
||||
maxLength: 2e3,
|
||||
style: { backgroundColor: "#272827" },
|
||||
className: "w-full p-3 border border-gray-600 rounded-lg resize-none text-gray-300 placeholder-gray-500 focus:border-green-500/40 focus:outline-none transition-all custom-scrollbar text-sm"
|
||||
style: codeMode ? { backgroundColor: "#1b1c1b", fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" } : { backgroundColor: "#272827" },
|
||||
className: `w-full p-3 border rounded-lg resize-none text-gray-300 placeholder-gray-500 focus:outline-none transition-all custom-scrollbar text-sm ${codeMode ? "border-orange-500/40 focus:border-orange-500/60" : "border-gray-600 focus:border-green-500/40"}`
|
||||
}),
|
||||
React.createElement(
|
||||
"div",
|
||||
@@ -1842,6 +2005,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
const [messages, setMessages] = React.useState([]);
|
||||
const [codeMode, setCodeMode] = React.useState(false);
|
||||
const [viewOnceMode, setViewOnceMode] = React.useState(false);
|
||||
const [viewOnceTtl, setViewOnceTtl] = React.useState(15);
|
||||
const [disappearTtl, setDisappearTtl] = React.useState(0);
|
||||
const [nowTick, setNowTick] = React.useState(() => Date.now());
|
||||
const [connectionStatus, setConnectionStatus] = React.useState("disconnected");
|
||||
@@ -1970,6 +2134,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
timestamp: Date.now(),
|
||||
mid: opts.mid,
|
||||
viewOnce: opts.viewOnce === true,
|
||||
viewOnceTtl: typeof opts.viewOnceTtl === "number" ? opts.viewOnceTtl : 15,
|
||||
expiresAt: typeof opts.expiresAt === "number" ? opts.expiresAt : void 0
|
||||
};
|
||||
setMessages((prev) => {
|
||||
@@ -2106,7 +2271,10 @@ var EnhancedSecureP2PChat = () => {
|
||||
const opts = {};
|
||||
if (meta && typeof meta === "object") {
|
||||
if (typeof meta.mid === "string") opts.mid = meta.mid;
|
||||
if (meta.once === true) opts.viewOnce = true;
|
||||
if (meta.once === true) {
|
||||
opts.viewOnce = true;
|
||||
opts.viewOnceTtl = Number.isFinite(meta.onceTtl) ? meta.onceTtl : 15;
|
||||
}
|
||||
if (Number.isFinite(meta.ttl) && meta.ttl > 0) {
|
||||
opts.expiresAt = Date.now() + meta.ttl * 1e3;
|
||||
}
|
||||
@@ -2270,7 +2438,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
handleMessage(" SecureBit.chat Enhanced Security Edition v4.8.15 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.", "system");
|
||||
handleMessage(" SecureBit.chat Enhanced Security Edition v4.8.20 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.", "system");
|
||||
const handleBeforeUnload = (event) => {
|
||||
if (event.type === "beforeunload" && !isTabSwitching) {
|
||||
if (webrtcManagerRef.current && webrtcManagerRef.current.isConnected()) {
|
||||
@@ -3470,7 +3638,10 @@ var EnhancedSecureP2PChat = () => {
|
||||
const outText = codeMode ? "```\n" + baseText + "\n```" : baseText;
|
||||
const mid = `m_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
const meta = { mid };
|
||||
if (viewOnceMode) meta.once = true;
|
||||
if (viewOnceMode) {
|
||||
meta.once = true;
|
||||
meta.onceTtl = viewOnceTtl;
|
||||
}
|
||||
if (disappearTtl > 0) meta.ttl = disappearTtl;
|
||||
const localOpts = { mid };
|
||||
if (disappearTtl > 0) localOpts.expiresAt = Date.now() + disappearTtl * 1e3;
|
||||
@@ -3497,36 +3668,6 @@ var EnhancedSecureP2PChat = () => {
|
||||
const handleMessageExpire = React.useCallback((id) => {
|
||||
setMessages((prev) => prev.filter((m) => m.id !== id));
|
||||
}, []);
|
||||
const handlePanicWipe = React.useCallback(() => {
|
||||
setMessages([]);
|
||||
try {
|
||||
const mgr = webrtcManagerRef.current;
|
||||
if (mgr) {
|
||||
if (typeof mgr._secureWipeKeys === "function") {
|
||||
try {
|
||||
mgr._secureWipeKeys();
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
if (typeof mgr.disconnect === "function") {
|
||||
try {
|
||||
mgr.disconnect();
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
}
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent("disconnected"));
|
||||
} catch (_) {
|
||||
}
|
||||
setConnectionStatus("disconnected");
|
||||
setIsVerified(false);
|
||||
setKeyFingerprint("");
|
||||
setSecurityLevel(null);
|
||||
setMessageInput("");
|
||||
}, []);
|
||||
const handleClearData = () => {
|
||||
setOfferData("");
|
||||
setAnswerData("");
|
||||
@@ -3788,12 +3929,13 @@ var EnhancedSecureP2PChat = () => {
|
||||
setCodeMode,
|
||||
viewOnceMode,
|
||||
setViewOnceMode,
|
||||
viewOnceTtl,
|
||||
setViewOnceTtl,
|
||||
disappearTtl,
|
||||
setDisappearTtl,
|
||||
nowTick,
|
||||
onUnsendMessage: handleUnsendMessage,
|
||||
onMessageExpire: handleMessageExpire,
|
||||
onPanicWipe: handlePanicWipe
|
||||
onMessageExpire: handleMessageExpire
|
||||
});
|
||||
})() : React.createElement(EnhancedConnectionSetup, {
|
||||
onCreateOffer: handleCreateOffer,
|
||||
|
||||
Vendored
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../src/utils/debugWindowHooks.js", "../src/network/iceSettingsStore.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": ["offerData", "payload", "UpdateCheckerWrapper"]
|
||||
}
|
||||
+5
-5
@@ -113,7 +113,7 @@
|
||||
|
||||
|
||||
<!-- GitHub Pages SEO -->
|
||||
<meta name="description" content="SecureBit.chat v4.8.15 — P2P messenger with ECDH + DTLS + SAS security and 18-layer military-grade cryptography">
|
||||
<meta name="description" content="SecureBit.chat v4.8.20 — P2P messenger with ECDH + DTLS + SAS security and 18-layer military-grade cryptography">
|
||||
<meta name="keywords" content="P2P messenger, ECDH, DTLS, SAS, encryption, WebRTC, privacy, ASN.1 validation, military-grade security, 18-layer defense, MITM protection, PFS">
|
||||
<meta name="author" content="Volodymyr">
|
||||
<link rel="canonical" href="https://github.com/SecureBitChat/securebit-chat/">
|
||||
@@ -148,13 +148,13 @@
|
||||
<!-- Update Manager - система принудительного обновления -->
|
||||
<script src="src/utils/updateManager.js"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1781831678894"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1781831678894"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1781851206401"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1781851206401"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="dist/app-boot.js?v=1781831678894"></script>
|
||||
<script type="module" src="dist/app.js?v=1781831678894"></script>
|
||||
<script type="module" src="dist/app-boot.js?v=1781851206401"></script>
|
||||
<script type="module" src="dist/app.js?v=1781851206401"></script>
|
||||
|
||||
<script src="src/scripts/pwa-register.js"></script>
|
||||
<script src="./src/pwa/install-prompt.js" type="module"></script>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "SecureBit.chat v4.8.15 - ECDH + DTLS + SAS",
|
||||
"name": "SecureBit.chat v4.8.20 - ECDH + DTLS + SAS",
|
||||
"short_name": "SecureBit",
|
||||
"description": "P2P messenger with ECDH + DTLS + SAS security, military-grade cryptography and Lightning Network payments",
|
||||
"start_url": "./",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"version": "1781831678894",
|
||||
"buildVersion": "1781831678894",
|
||||
"appVersion": "4.8.15",
|
||||
"buildTime": "2026-06-19T01:14:38.934Z",
|
||||
"buildId": "1781831678894-15173a9",
|
||||
"gitHash": "15173a9",
|
||||
"version": "1781851206401",
|
||||
"buildVersion": "1781851206401",
|
||||
"appVersion": "4.8.20",
|
||||
"buildTime": "2026-06-19T06:40:06.446Z",
|
||||
"buildId": "1781851206401-cb72b9c",
|
||||
"gitHash": "cb72b9c",
|
||||
"generated": true,
|
||||
"generatedAt": "2026-06-19T01:14:38.935Z"
|
||||
"generatedAt": "2026-06-19T06:40:06.447Z"
|
||||
}
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "securebit-chat",
|
||||
"version": "4.8.15",
|
||||
"version": "4.8.20",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "securebit-chat",
|
||||
"version": "4.8.15",
|
||||
"version": "4.8.20",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "1.5.1",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "securebit-chat",
|
||||
"version": "4.8.15",
|
||||
"version": "4.8.20",
|
||||
"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/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/file-type-allowlist.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"
|
||||
"test": "node tests/sas-verification.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/file-type-allowlist.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"
|
||||
},
|
||||
"keywords": [
|
||||
"p2p",
|
||||
|
||||
+182
-99
@@ -55,6 +55,53 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
return segments.some(s => s.kind === 'code') ? segments : null;
|
||||
};
|
||||
|
||||
// Lightweight, dependency-free syntax highlighter. Returns React nodes
|
||||
// (no innerHTML / dangerouslySetInnerHTML) so it stays CSP-safe and adds
|
||||
// no XSS surface. Language-agnostic: highlights comments, strings,
|
||||
// numbers, common keywords and literals — good enough for snippets
|
||||
// without shipping a heavy library or allowing remote scripts.
|
||||
const HL_KEYWORDS = new Set([
|
||||
'const','let','var','function','return','if','else','for','while','do','switch',
|
||||
'case','break','continue','class','extends','new','this','super','import','export',
|
||||
'from','as','default','async','await','try','catch','finally','throw','typeof',
|
||||
'instanceof','delete','yield','in','of','def','elif','lambda','pass','with','global',
|
||||
'public','private','protected','static','final','void','int','long','float','double',
|
||||
'char','bool','boolean','string','struct','enum','interface','package','func','fn',
|
||||
'type','where','select','update','insert','delete','where','and','or','not','end',
|
||||
'then','fi','done','echo','use','mut','impl','trait','match','module','require'
|
||||
]);
|
||||
const HL_LITERALS = new Set(['true','false','null','undefined','None','True','False','nil','NaN','Infinity']);
|
||||
const highlightCode = (code) => {
|
||||
const re = /(\/\/[^\n]*|#[^\n]*|\/\*[\s\S]*?\*\/|--[^\n]*)|(`(?:\\.|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|(\b\d[\d_.]*(?:[eE][+-]?\d+)?\b|\b0[xX][0-9a-fA-F]+\b)|([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
||||
const nodes = [];
|
||||
let buffer = '';
|
||||
let last = 0;
|
||||
let key = 0;
|
||||
const flush = () => { if (buffer) { nodes.push(buffer); buffer = ''; } };
|
||||
let m;
|
||||
while ((m = re.exec(code)) !== null) {
|
||||
if (m.index > last) buffer += code.slice(last, m.index);
|
||||
last = re.lastIndex;
|
||||
let cls = null;
|
||||
if (m[1]) cls = 'text-gray-500 italic'; // comment
|
||||
else if (m[2]) cls = 'text-amber-300'; // string
|
||||
else if (m[3]) cls = 'text-sky-300'; // number
|
||||
else if (m[4]) { // identifier
|
||||
if (HL_KEYWORDS.has(m[4])) cls = 'text-purple-300';
|
||||
else if (HL_LITERALS.has(m[4])) cls = 'text-sky-300';
|
||||
}
|
||||
if (cls) {
|
||||
flush();
|
||||
nodes.push(React.createElement('span', { key: `h${key++}`, className: cls }, m[0]));
|
||||
} else {
|
||||
buffer += m[0];
|
||||
}
|
||||
}
|
||||
if (last < code.length) buffer += code.slice(last);
|
||||
flush();
|
||||
return nodes;
|
||||
};
|
||||
|
||||
// Monospace code window with a copy button (clipboard auto-clears in 30s).
|
||||
const CodeBlock = ({ code, lang }) => {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
@@ -66,13 +113,13 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
}
|
||||
};
|
||||
return React.createElement('div', {
|
||||
className: "my-1 rounded-lg overflow-hidden border border-gray-600/40",
|
||||
style: { backgroundColor: '#1b1c1b' }
|
||||
className: "my-1 rounded-lg overflow-hidden",
|
||||
style: { backgroundColor: '#1b1c1b', border: '0 solid #e5e7eb' }
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'hdr',
|
||||
className: "flex items-center justify-between px-3 py-1.5 border-b border-gray-600/30",
|
||||
style: { backgroundColor: '#222322' }
|
||||
className: "flex items-center justify-between px-3 py-1.5",
|
||||
style: { backgroundColor: '#222322', border: '0 solid #e5e7eb' }
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'lang',
|
||||
@@ -95,7 +142,7 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
key: 'pre',
|
||||
className: "px-3 py-2 overflow-x-auto text-xs leading-relaxed text-gray-200 custom-scrollbar",
|
||||
style: { whiteSpace: 'pre', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', margin: 0 }
|
||||
}, React.createElement('code', null, code))
|
||||
}, React.createElement('code', null, highlightCode(code)))
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -123,70 +170,103 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
);
|
||||
};
|
||||
|
||||
// Composer toolbar: code / view-once / disappearing / panic.
|
||||
const ChatToolbar = ({ codeMode, setCodeMode, viewOnceMode, setViewOnceMode, disappearTtl, setDisappearTtl, onPanicWipe }) => {
|
||||
const ttlCycle = [0, 30, 300, 3600];
|
||||
const ttlLabel = (s) => s === 0 ? 'Off' : (s >= 3600 ? `${Math.round(s / 3600)}h` : (s >= 60 ? `${Math.round(s / 60)}m` : `${s}s`));
|
||||
const cycleTtl = () => {
|
||||
const i = ttlCycle.indexOf(disappearTtl);
|
||||
setDisappearTtl(ttlCycle[(i + 1) % ttlCycle.length] || 0);
|
||||
};
|
||||
// Composer toolbar: code / view-once / disappearing.
|
||||
// Borderless icon+label buttons; active state uses the brand orange
|
||||
// (accent-orange). View-once and Timer open a small time picker.
|
||||
const ChatToolbar = ({ codeMode, setCodeMode, viewOnceMode, setViewOnceMode, viewOnceTtl, setViewOnceTtl, disappearTtl, setDisappearTtl }) => {
|
||||
const [openMenu, setOpenMenu] = React.useState(null); // 'once' | 'timer' | null
|
||||
const fmt = (s) => s >= 3600 ? `${Math.round(s / 3600)}h` : (s >= 60 ? `${Math.round(s / 60)}m` : `${s}s`);
|
||||
|
||||
const pill = (key, { active, activeClass, icon, label, title, onClick }) =>
|
||||
const btnClass = (active) =>
|
||||
`inline-flex items-center gap-2 h-9 px-3 rounded-lg text-xs font-medium transition-colors duration-150 ${active
|
||||
? 'accent-orange bg-orange-500/10'
|
||||
: 'text-gray-400 hover:text-gray-200 hover:bg-gray-700/40'}`;
|
||||
|
||||
const pickerItem = (opt, current, onPick) =>
|
||||
React.createElement('button', {
|
||||
key,
|
||||
key: String(opt.value),
|
||||
type: 'button',
|
||||
onClick,
|
||||
title,
|
||||
className: `inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs border transition-all duration-200 ${active
|
||||
? activeClass
|
||||
: 'text-gray-400 border-gray-600/50 hover:text-gray-200 hover:border-gray-500'}`
|
||||
onClick: () => { onPick(opt.value); setOpenMenu(null); },
|
||||
// Comfortable tap target + readable size on mobile.
|
||||
className: `w-full text-left px-4 py-3 sm:py-2.5 text-sm flex items-center gap-3 transition-colors ${current === opt.value
|
||||
? 'accent-orange bg-orange-500/10'
|
||||
: 'text-gray-200 hover:bg-gray-700/50 active:bg-gray-700/60'}`
|
||||
}, [
|
||||
React.createElement('i', { key: 'i', className: icon }),
|
||||
label ? React.createElement('span', { key: 'l' }, label) : null
|
||||
React.createElement('i', { key: 'i', className: `${opt.icon || 'far fa-clock'} w-4 text-center` }),
|
||||
React.createElement('span', { key: 'l' }, opt.label)
|
||||
]);
|
||||
|
||||
return React.createElement('div', {
|
||||
className: "flex items-center flex-wrap gap-2 pb-3"
|
||||
}, [
|
||||
pill('code', {
|
||||
// Opens UPWARD (bottom:100%) via inline style so it never depends on a
|
||||
// purgeable utility class and never pushes the composer layout down.
|
||||
const picker = (options, current, onPick) =>
|
||||
React.createElement('div', {
|
||||
className: "absolute right-0 z-50 min-w-[180px] max-w-[78vw] rounded-xl shadow-2xl overflow-hidden",
|
||||
style: { backgroundColor: '#1f201f', border: '0 solid #e5e7eb', bottom: '100%', marginBottom: '8px' }
|
||||
}, options.map(opt => pickerItem(opt, current, onPick)));
|
||||
|
||||
const labelBtn = (key, { active, icon, label, title, onClick }) =>
|
||||
React.createElement('button', {
|
||||
key, type: 'button', onClick, title, 'aria-pressed': !!active,
|
||||
className: btnClass(active)
|
||||
}, [
|
||||
React.createElement('i', { key: 'i', className: `${icon} text-[13px]` }),
|
||||
React.createElement('span', { key: 'l', className: 'leading-none' }, label)
|
||||
]);
|
||||
|
||||
return React.createElement('div', { className: "flex items-center gap-1" }, [
|
||||
// Invisible backdrop closes any open picker on outside click.
|
||||
openMenu && React.createElement('div', {
|
||||
key: 'backdrop',
|
||||
className: "fixed inset-0 z-40",
|
||||
onClick: () => setOpenMenu(null)
|
||||
}),
|
||||
|
||||
// Code — toggles code mode (expands the input box).
|
||||
labelBtn('code', {
|
||||
active: codeMode,
|
||||
activeClass: 'text-green-400 border-green-500/40 bg-green-500/10',
|
||||
icon: 'fas fa-code',
|
||||
label: 'Code',
|
||||
title: 'Send as a code block (with copy button)',
|
||||
title: 'Send as a code block (expands the input)',
|
||||
onClick: () => setCodeMode(v => !v)
|
||||
}),
|
||||
pill('once', {
|
||||
active: viewOnceMode,
|
||||
activeClass: 'text-orange-400 border-orange-500/40 bg-orange-500/10',
|
||||
icon: 'fas fa-eye-slash',
|
||||
label: 'View once',
|
||||
title: 'Recipient can read it once, then it is deleted (cooperative — not screenshot-proof)',
|
||||
onClick: () => setViewOnceMode(v => !v)
|
||||
}),
|
||||
pill('ttl', {
|
||||
active: disappearTtl > 0,
|
||||
activeClass: 'text-blue-400 border-blue-500/40 bg-blue-500/10',
|
||||
icon: 'fas fa-stopwatch',
|
||||
label: `Timer: ${ttlLabel(disappearTtl)}`,
|
||||
title: 'Disappearing messages — auto-delete on both sides',
|
||||
onClick: cycleTtl
|
||||
}),
|
||||
React.createElement('div', { key: 'spacer', className: 'flex-1' }),
|
||||
pill('panic', {
|
||||
active: true,
|
||||
activeClass: 'text-red-400 border-red-500/40 bg-red-500/10 hover:bg-red-500/20',
|
||||
icon: 'fas fa-fire-extinguisher',
|
||||
label: 'Panic',
|
||||
title: 'Wipe this conversation and keys, and disconnect',
|
||||
onClick: () => {
|
||||
const ok = (typeof window !== 'undefined' && window.confirm)
|
||||
? window.confirm('Panic wipe: delete all messages, wipe keys and disconnect now?')
|
||||
: true;
|
||||
if (ok && typeof onPanicWipe === 'function') onPanicWipe();
|
||||
}
|
||||
})
|
||||
|
||||
// View once — pick how long it stays after the peer opens it.
|
||||
React.createElement('div', { key: 'once', className: 'relative' }, [
|
||||
labelBtn('once-btn', {
|
||||
active: viewOnceMode,
|
||||
icon: 'fas fa-eye-slash',
|
||||
label: viewOnceMode ? `Once · ${fmt(viewOnceTtl)}` : 'View once',
|
||||
title: 'View once — vanishes after the peer reads it',
|
||||
onClick: () => setOpenMenu(openMenu === 'once' ? null : 'once')
|
||||
}),
|
||||
openMenu === 'once' && picker([
|
||||
{ value: 0, label: 'Off', icon: 'fas fa-ban' },
|
||||
{ value: 5, label: '5s after reading' },
|
||||
{ value: 15, label: '15s after reading' },
|
||||
{ value: 30, label: '30s after reading' },
|
||||
{ value: 60, label: '1m after reading' }
|
||||
], viewOnceMode ? viewOnceTtl : 0, (v) => {
|
||||
if (v === 0) { setViewOnceMode(false); }
|
||||
else { setViewOnceTtl(v); setViewOnceMode(true); }
|
||||
})
|
||||
]),
|
||||
|
||||
// Timer — pick the disappearing duration.
|
||||
React.createElement('div', { key: 'timer', className: 'relative' }, [
|
||||
labelBtn('timer-btn', {
|
||||
active: disappearTtl > 0,
|
||||
icon: 'fas fa-stopwatch',
|
||||
label: disappearTtl > 0 ? `Timer · ${fmt(disappearTtl)}` : 'Timer',
|
||||
title: 'Disappearing message — deletes on both sides',
|
||||
onClick: () => setOpenMenu(openMenu === 'timer' ? null : 'timer')
|
||||
}),
|
||||
openMenu === 'timer' && picker([
|
||||
{ value: 0, label: 'Off', icon: 'fas fa-ban' },
|
||||
{ value: 30, label: '30 seconds' },
|
||||
{ value: 300, label: '5 minutes' },
|
||||
{ value: 3600, label: '1 hour' }
|
||||
], disappearTtl, (v) => setDisappearTtl(v))
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -413,7 +493,7 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
};
|
||||
|
||||
// Enhanced Chat Message with better security indicators
|
||||
const EnhancedChatMessage = ({ message, type, timestamp, mid, viewOnce, expiresAt, nowTick, canUnsend, onUnsend, onExpire }) => {
|
||||
const EnhancedChatMessage = ({ message, type, timestamp, mid, viewOnce, viewOnceTtl, expiresAt, nowTick, canUnsend, onUnsend, onExpire }) => {
|
||||
const [revealed, setRevealed] = React.useState(false);
|
||||
const revealTimerRef = React.useRef(null);
|
||||
|
||||
@@ -433,7 +513,7 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
switch (type) {
|
||||
case 'sent':
|
||||
return {
|
||||
container: "ml-auto bg-orange-500/15 border-orange-500/20 text-primary",
|
||||
container: "ml-auto bg-orange-500/5 border-orange-500/20 text-primary",
|
||||
icon: "fas fa-lock accent-orange",
|
||||
label: "Encrypted"
|
||||
};
|
||||
@@ -470,10 +550,11 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
const handleReveal = () => {
|
||||
if (revealed) return;
|
||||
setRevealed(true);
|
||||
// One-time read: wipe shortly after the recipient opens it.
|
||||
// One-time read: wipe after the sender-chosen visible window.
|
||||
const ms = Math.max(1, (typeof viewOnceTtl === 'number' ? viewOnceTtl : 15)) * 1000;
|
||||
revealTimerRef.current = setTimeout(() => {
|
||||
onExpire && onExpire();
|
||||
}, 12000);
|
||||
}, ms);
|
||||
};
|
||||
|
||||
// Body: blurred placeholder for unopened view-once, else real content.
|
||||
@@ -1498,12 +1579,13 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
setCodeMode,
|
||||
viewOnceMode,
|
||||
setViewOnceMode,
|
||||
viewOnceTtl,
|
||||
setViewOnceTtl,
|
||||
disappearTtl,
|
||||
setDisappearTtl,
|
||||
nowTick,
|
||||
onUnsendMessage,
|
||||
onMessageExpire,
|
||||
onPanicWipe
|
||||
onMessageExpire
|
||||
}) => {
|
||||
const [showScrollButton, setShowScrollButton] = React.useState(false);
|
||||
const [showFileTransfer, setShowFileTransfer] = React.useState(false);
|
||||
@@ -1670,6 +1752,7 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
timestamp: msg.timestamp,
|
||||
mid: msg.mid,
|
||||
viewOnce: msg.viewOnce,
|
||||
viewOnceTtl: msg.viewOnceTtl,
|
||||
expiresAt: msg.expiresAt,
|
||||
nowTick: nowTick,
|
||||
canUnsend: typeof onUnsendMessage === 'function',
|
||||
@@ -1712,11 +1795,16 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
'div',
|
||||
{ className: "max-w-4xl mx-auto px-4" },
|
||||
[
|
||||
React.createElement('div', {
|
||||
key: 'composer-bar',
|
||||
className: `flex items-center justify-between flex-wrap gap-2 ${showFileTransfer ? 'mb-4' : ''}`
|
||||
}, [
|
||||
React.createElement(
|
||||
'button',
|
||||
{
|
||||
key: 'send-files-toggle',
|
||||
onClick: () => setShowFileTransfer(!showFileTransfer),
|
||||
className: `flex items-center text-sm text-gray-400 hover:text-gray-300 transition-colors py-4 ${showFileTransfer ? 'mb-4' : ''}`
|
||||
className: "flex items-center text-sm text-gray-400 hover:text-gray-300 transition-colors py-4"
|
||||
},
|
||||
[
|
||||
React.createElement(
|
||||
@@ -1744,8 +1832,16 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
showFileTransfer ? 'Hide file transfer' : 'Send files'
|
||||
]
|
||||
),
|
||||
React.createElement(ChatToolbar, {
|
||||
key: 'toolbar',
|
||||
codeMode, setCodeMode,
|
||||
viewOnceMode, setViewOnceMode,
|
||||
viewOnceTtl, setViewOnceTtl,
|
||||
disappearTtl, setDisappearTtl
|
||||
})
|
||||
]),
|
||||
showFileTransfer &&
|
||||
React.createElement(window.FileTransferComponent || (() =>
|
||||
React.createElement(window.FileTransferComponent || (() =>
|
||||
React.createElement('div', {
|
||||
className: "p-4 text-center text-red-400"
|
||||
}, 'FileTransferComponent not loaded')
|
||||
@@ -1766,13 +1862,6 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
'div',
|
||||
{ className: "max-w-4xl mx-auto p-4" },
|
||||
[
|
||||
React.createElement(ChatToolbar, {
|
||||
key: 'toolbar',
|
||||
codeMode, setCodeMode,
|
||||
viewOnceMode, setViewOnceMode,
|
||||
disappearTtl, setDisappearTtl,
|
||||
onPanicWipe
|
||||
}),
|
||||
React.createElement(
|
||||
'div',
|
||||
{ key: 'inputrow', className: "flex items-stretch space-x-3" },
|
||||
@@ -1785,11 +1874,13 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
value: messageInput,
|
||||
onChange: (e) => setMessageInput(e.target.value),
|
||||
onKeyDown: handleKeyPress,
|
||||
placeholder: "Enter message to encrypt...",
|
||||
rows: 2,
|
||||
placeholder: codeMode ? "Paste or type code…" : "Enter message to encrypt...",
|
||||
rows: codeMode ? 8 : 2,
|
||||
maxLength: 2000,
|
||||
style: { backgroundColor: '#272827' },
|
||||
className: "w-full p-3 border border-gray-600 rounded-lg resize-none text-gray-300 placeholder-gray-500 focus:border-green-500/40 focus:outline-none transition-all custom-scrollbar text-sm"
|
||||
style: codeMode
|
||||
? { backgroundColor: '#1b1c1b', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace' }
|
||||
: { backgroundColor: '#272827' },
|
||||
className: `w-full p-3 border rounded-lg resize-none text-gray-300 placeholder-gray-500 focus:outline-none transition-all custom-scrollbar text-sm ${codeMode ? 'border-orange-500/40 focus:border-orange-500/60' : 'border-gray-600 focus:border-green-500/40'}`
|
||||
}),
|
||||
React.createElement(
|
||||
'div',
|
||||
@@ -1836,6 +1927,7 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
// Secure chat extras: per-message send modes + 1s tick for countdowns.
|
||||
const [codeMode, setCodeMode] = React.useState(false);
|
||||
const [viewOnceMode, setViewOnceMode] = React.useState(false);
|
||||
const [viewOnceTtl, setViewOnceTtl] = React.useState(15); // seconds visible after the peer opens it
|
||||
const [disappearTtl, setDisappearTtl] = React.useState(0); // seconds; 0 = off (sticky)
|
||||
const [nowTick, setNowTick] = React.useState(() => Date.now());
|
||||
const [connectionStatus, setConnectionStatus] = React.useState('disconnected');
|
||||
@@ -2004,6 +2096,7 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
timestamp: Date.now(),
|
||||
mid: opts.mid,
|
||||
viewOnce: opts.viewOnce === true,
|
||||
viewOnceTtl: (typeof opts.viewOnceTtl === 'number') ? opts.viewOnceTtl : 15,
|
||||
expiresAt: (typeof opts.expiresAt === 'number') ? opts.expiresAt : undefined
|
||||
};
|
||||
|
||||
@@ -2171,7 +2264,10 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
const opts = {};
|
||||
if (meta && typeof meta === 'object') {
|
||||
if (typeof meta.mid === 'string') opts.mid = meta.mid;
|
||||
if (meta.once === true) opts.viewOnce = true;
|
||||
if (meta.once === true) {
|
||||
opts.viewOnce = true;
|
||||
opts.viewOnceTtl = Number.isFinite(meta.onceTtl) ? meta.onceTtl : 15;
|
||||
}
|
||||
if (Number.isFinite(meta.ttl) && meta.ttl > 0) {
|
||||
opts.expiresAt = Date.now() + meta.ttl * 1000;
|
||||
}
|
||||
@@ -2388,7 +2484,7 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(' SecureBit.chat Enhanced Security Edition v4.8.15 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
|
||||
handleMessage(' SecureBit.chat Enhanced Security Edition v4.8.20 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
|
||||
|
||||
const handleBeforeUnload = (event) => {
|
||||
if (event.type === 'beforeunload' && !isTabSwitching) {
|
||||
@@ -3799,7 +3895,10 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
// on both peers.
|
||||
const mid = `m_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
const meta = { mid };
|
||||
if (viewOnceMode) meta.once = true; // applies to the recipient
|
||||
if (viewOnceMode) { // applies to the recipient
|
||||
meta.once = true;
|
||||
meta.onceTtl = viewOnceTtl; // seconds visible after opening
|
||||
}
|
||||
if (disappearTtl > 0) meta.ttl = disappearTtl; // applies to both sides
|
||||
|
||||
// Local echo: sender sees their own text normally (view-once is a
|
||||
@@ -3834,23 +3933,6 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
}, []);
|
||||
|
||||
// Panic wipe: clear the conversation, tear down the session and wipe keys.
|
||||
const handlePanicWipe = React.useCallback(() => {
|
||||
setMessages([]);
|
||||
try {
|
||||
const mgr = webrtcManagerRef.current;
|
||||
if (mgr) {
|
||||
if (typeof mgr._secureWipeKeys === 'function') { try { mgr._secureWipeKeys(); } catch (_) {} }
|
||||
if (typeof mgr.disconnect === 'function') { try { mgr.disconnect(); } catch (_) {} }
|
||||
}
|
||||
} catch (_) {}
|
||||
try { document.dispatchEvent(new CustomEvent('disconnected')); } catch (_) {}
|
||||
setConnectionStatus('disconnected');
|
||||
setIsVerified(false);
|
||||
setKeyFingerprint('');
|
||||
setSecurityLevel(null);
|
||||
setMessageInput('');
|
||||
}, []);
|
||||
|
||||
const handleClearData = () => {
|
||||
setOfferData('');
|
||||
setAnswerData('');
|
||||
@@ -4177,12 +4259,13 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
|
||||
setCodeMode: setCodeMode,
|
||||
viewOnceMode: viewOnceMode,
|
||||
setViewOnceMode: setViewOnceMode,
|
||||
viewOnceTtl: viewOnceTtl,
|
||||
setViewOnceTtl: setViewOnceTtl,
|
||||
disappearTtl: disappearTtl,
|
||||
setDisappearTtl: setDisappearTtl,
|
||||
nowTick: nowTick,
|
||||
onUnsendMessage: handleUnsendMessage,
|
||||
onMessageExpire: handleMessageExpire,
|
||||
onPanicWipe: handlePanicWipe
|
||||
onMessageExpire: handleMessageExpire
|
||||
});
|
||||
})()
|
||||
: React.createElement(EnhancedConnectionSetup, {
|
||||
|
||||
@@ -539,7 +539,7 @@ const EnhancedMinimalHeader = ({
|
||||
React.createElement('p', {
|
||||
key: 'subtitle',
|
||||
className: 'text-xs sm:text-sm text-muted hidden sm:block'
|
||||
}, 'End-to-end freedom v4.8.15')
|
||||
}, 'End-to-end freedom v4.8.20')
|
||||
])
|
||||
]),
|
||||
|
||||
|
||||
@@ -6462,6 +6462,11 @@ async processOrderedPackets() {
|
||||
}
|
||||
if (meta.code === true) out.code = true;
|
||||
if (meta.once === true) out.once = true;
|
||||
if (Number.isFinite(meta.onceTtl)) {
|
||||
// View-once: seconds the message stays visible after the peer opens it.
|
||||
const onceTtl = Math.floor(meta.onceTtl);
|
||||
if (onceTtl >= 1 && onceTtl <= 3600) out.onceTtl = onceTtl;
|
||||
}
|
||||
if (Number.isFinite(meta.ttl)) {
|
||||
// Clamp disappearing timer to [5s, 24h]; ignore anything else.
|
||||
const ttl = Math.floor(meta.ttl);
|
||||
@@ -7847,10 +7852,19 @@ async processMessage(data) {
|
||||
return; // IMPORTANT: Do not process further
|
||||
}
|
||||
|
||||
// Per-message delete (unsend) from the peer.
|
||||
if (parsed.type === EnhancedSecureWebRTCManager.MESSAGE_TYPES.MESSAGE_DELETE) {
|
||||
const messageId = parsed?.data?.messageId ?? parsed?.messageId;
|
||||
if (typeof messageId === 'string' && messageId) {
|
||||
try { this.onMessageDelete?.(messageId.slice(0, 64)); } catch (_) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SYSTEM MESSAGES (WITHOUT MUTEX)
|
||||
// ============================================
|
||||
|
||||
|
||||
if (parsed.type && ['heartbeat', 'verification', 'verification_response', 'verification_confirmed', 'verification_both_confirmed', 'sas_code', 'peer_disconnect', 'security_upgrade'].includes(parsed.type)) {
|
||||
this.handleSystemMessage(parsed);
|
||||
return;
|
||||
@@ -7865,7 +7879,7 @@ async processMessage(data) {
|
||||
return;
|
||||
}
|
||||
if (this.onMessage) {
|
||||
this.deliverMessageToUI(parsed.data, 'received');
|
||||
this.deliverMessageToUI(parsed.data, 'received', parsed.meta);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -7994,7 +8008,7 @@ async processMessage(data) {
|
||||
}
|
||||
if (decryptedContent && decryptedContent.type === 'message' && typeof decryptedContent.data === 'string') {
|
||||
if (this.onMessage) {
|
||||
this.deliverMessageToUI(decryptedContent.data, 'received');
|
||||
this.deliverMessageToUI(decryptedContent.data, 'received', decryptedContent.meta);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,13 +42,15 @@ class NotificationIntegration {
|
||||
this.originalOnStatusChange = this.webrtcManager.onStatusChange;
|
||||
|
||||
|
||||
// Wrap the original onMessage callback
|
||||
this.webrtcManager.onMessage = (message, type) => {
|
||||
// Wrap the original onMessage callback.
|
||||
// IMPORTANT: forward ALL arguments (incl. per-message `meta`) so the app
|
||||
// still receives view-once / disappearing / unsend metadata.
|
||||
this.webrtcManager.onMessage = (message, type, ...rest) => {
|
||||
this.handleIncomingMessage(message, type);
|
||||
|
||||
|
||||
// Call original callback if it exists
|
||||
if (this.originalOnMessage) {
|
||||
this.originalOnMessage(message, type);
|
||||
this.originalOnMessage(message, type, ...rest);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,12 +64,14 @@ class NotificationIntegration {
|
||||
}
|
||||
};
|
||||
|
||||
// Also hook into the deliverMessageToUI method if it exists
|
||||
// Also hook into the deliverMessageToUI method if it exists.
|
||||
// IMPORTANT: forward ALL arguments (incl. per-message `meta`) to the
|
||||
// original, otherwise view-once / disappearing / unsend metadata is lost.
|
||||
if (this.webrtcManager.deliverMessageToUI) {
|
||||
this.originalDeliverMessageToUI = this.webrtcManager.deliverMessageToUI.bind(this.webrtcManager);
|
||||
this.webrtcManager.deliverMessageToUI = (message, type) => {
|
||||
this.webrtcManager.deliverMessageToUI = (message, type, ...rest) => {
|
||||
this.handleIncomingMessage(message, type);
|
||||
this.originalDeliverMessageToUI(message, type);
|
||||
this.originalDeliverMessageToUI(message, type, ...rest);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
// NotificationIntegration wraps webrtcManager.onMessage and .deliverMessageToUI.
|
||||
// Regression: those wrappers must forward the 3rd argument (per-message `meta`)
|
||||
// to the originals, otherwise view-once / disappearing / unsend break ONLY when
|
||||
// notifications are enabled (which is exactly how it shipped broken).
|
||||
|
||||
const dom = new JSDOM('<!doctype html><html><body></body></html>', { url: 'https://localhost/' });
|
||||
globalThis.window = dom.window;
|
||||
globalThis.document = dom.window.document;
|
||||
// Minimal Notification stub so init() does not throw.
|
||||
globalThis.Notification = dom.window.Notification = class { static permission = 'granted'; static requestPermission() { return Promise.resolve('granted'); } close() {} };
|
||||
|
||||
await import('../src/notifications/NotificationIntegration.js');
|
||||
const NotificationIntegration = window.NotificationIntegration;
|
||||
|
||||
const received = [];
|
||||
const delivered = [];
|
||||
const manager = {
|
||||
onMessage: (message, type, meta) => received.push({ message, type, meta }),
|
||||
onStatusChange: () => {},
|
||||
deliverMessageToUI: (message, type, meta) => delivered.push({ message, type, meta })
|
||||
};
|
||||
|
||||
const integration = new NotificationIntegration(manager);
|
||||
await integration.init();
|
||||
|
||||
// After init, the manager's callbacks are the wrappers. Calling them with meta
|
||||
// must forward meta to the originals.
|
||||
manager.onMessage('hi', 'received', { mid: 'm1', once: true });
|
||||
manager.deliverMessageToUI('yo', 'received', { mid: 'm2', ttl: 30 });
|
||||
|
||||
assert.equal(received.length, 1, 'original onMessage called once');
|
||||
assert.deepEqual(received[0].meta, { mid: 'm1', once: true }, 'meta forwarded through onMessage wrapper');
|
||||
|
||||
assert.equal(delivered.length, 1, 'original deliverMessageToUI called once');
|
||||
assert.deepEqual(delivered[0].meta, { mid: 'm2', ttl: 30 }, 'meta forwarded through deliverMessageToUI wrapper');
|
||||
|
||||
console.log('Notification meta-forwarding tests passed');
|
||||
@@ -10,8 +10,12 @@ const T = EnhancedSecureWebRTCManager.MESSAGE_TYPES;
|
||||
|
||||
// ── _sanitizeMessageMeta: whitelist + bounds ────────────────────────────────
|
||||
{
|
||||
const ok = P._sanitizeMessageMeta.call({}, { mid: 'm_1-a', once: true, ttl: 300, code: true });
|
||||
assert.deepEqual(ok, { mid: 'm_1-a', code: true, once: true, ttl: 300 });
|
||||
const ok = P._sanitizeMessageMeta.call({}, { mid: 'm_1-a', once: true, onceTtl: 15, ttl: 300, code: true });
|
||||
assert.deepEqual(ok, { mid: 'm_1-a', code: true, once: true, onceTtl: 15, ttl: 300 });
|
||||
|
||||
// onceTtl is clamped to [1, 3600]; out-of-range is dropped.
|
||||
assert.equal(P._sanitizeMessageMeta.call({}, { once: true, onceTtl: 99999 }).onceTtl, undefined);
|
||||
assert.equal(P._sanitizeMessageMeta.call({}, { once: true, onceTtl: 30 }).onceTtl, 30);
|
||||
|
||||
// Junk and out-of-range values are stripped; with no valid keys -> null.
|
||||
assert.equal(P._sanitizeMessageMeta.call({}, { foo: 1 }), null);
|
||||
@@ -58,6 +62,29 @@ const T = EnhancedSecureWebRTCManager.MESSAGE_TYPES;
|
||||
assert.deepEqual(deleted, ['m_42']);
|
||||
}
|
||||
|
||||
// ── live enhanced-message path delivers metadata to the UI ───────────────────
|
||||
// This is the path real chat uses (dataChannel.onmessage -> _processEnhancedMessageWithoutMutex).
|
||||
{
|
||||
const envelope = JSON.stringify({ type: 'message', data: 'hi there', meta: { mid: 'm7', once: true, ttl: 30 } });
|
||||
globalThis.window.EnhancedSecureCryptoUtils = {
|
||||
decryptMessage: async () => ({ message: envelope })
|
||||
};
|
||||
const calls = [];
|
||||
const manager = {
|
||||
encryptionKey: {}, macKey: {}, metadataKey: {},
|
||||
_secureLog() {},
|
||||
_checkInboundRateLimit: () => true,
|
||||
_sanitizeIncomingChatMessage: (m) => m,
|
||||
_sanitizeMessageMeta: P._sanitizeMessageMeta,
|
||||
onMessage: (message, type, meta) => calls.push({ message, type, meta }),
|
||||
deliverMessageToUI: P.deliverMessageToUI
|
||||
};
|
||||
await P._processEnhancedMessageWithoutMutex.call(manager, { type: 'enhanced_message', data: 'enc' });
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].message, 'hi there');
|
||||
assert.deepEqual(calls[0].meta, { mid: 'm7', once: true, ttl: 30 });
|
||||
}
|
||||
|
||||
// ── sendMessageDelete emits a well-formed control message ────────────────────
|
||||
{
|
||||
const sent = [];
|
||||
|
||||
Reference in new issue
Block a user