feat: implement secure browser notifications system
- Added SecureNotificationManager with cross-browser support (Chrome, Firefox, Safari, Edge) - Integrated WebRTC message notifications with tab visibility detection - Implemented XSS protection, URL validation, and rate limiting - Notifications shown only when chat tab is inactive - Enforced HTTPS and user gesture requirements
This commit is contained in:
1 parent
5b5cc67fdc
commit
b087adfecc
14 files changed
+1999
-56
No files matched your search
@@ -0,0 +1,20 @@
|
||||
# Notification Sound Asset
|
||||
#
|
||||
# This file should contain a short, pleasant notification sound in MP3 format.
|
||||
# Recommended specifications:
|
||||
# - Duration: 1-2 seconds
|
||||
# - Format: MP3, 44.1kHz, 128kbps
|
||||
# - Volume: Moderate (not too loud)
|
||||
# - License: Ensure proper licensing for commercial use
|
||||
#
|
||||
# You can create this using:
|
||||
# 1. Text-to-speech generators
|
||||
# 2. Audio editing software
|
||||
# 3. Free notification sound libraries
|
||||
# 4. AI-generated sounds
|
||||
#
|
||||
# Example sources:
|
||||
# - Freesound.org (CC licensed)
|
||||
# - Zapsplat.com
|
||||
# - Adobe Audition
|
||||
# - Audacity (free)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
+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%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media Line 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
+737
@@ -1,3 +1,720 @@
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
|
||||
// src/notifications/SecureNotificationManager.js
|
||||
var require_SecureNotificationManager = __commonJS({
|
||||
"src/notifications/SecureNotificationManager.js"(exports, module) {
|
||||
var SecureChatNotificationManager = class {
|
||||
constructor(config = {}) {
|
||||
this.permission = Notification.permission;
|
||||
this.isTabActive = this.checkTabActive();
|
||||
this.unreadCount = 0;
|
||||
this.originalTitle = document.title;
|
||||
this.notificationQueue = [];
|
||||
this.maxQueueSize = config.maxQueueSize || 5;
|
||||
this.rateLimitMs = config.rateLimitMs || 2e3;
|
||||
this.lastNotificationTime = 0;
|
||||
this.trustedOrigins = config.trustedOrigins || [];
|
||||
this.isSecureContext = window.isSecureContext;
|
||||
this.hidden = this.getHiddenProperty();
|
||||
this.visibilityChange = this.getVisibilityChangeEvent();
|
||||
this.initVisibilityTracking();
|
||||
this.initSecurityChecks();
|
||||
}
|
||||
/**
|
||||
* Initialize security checks and validation
|
||||
* @private
|
||||
*/
|
||||
initSecurityChecks() {
|
||||
}
|
||||
/**
|
||||
* Get hidden property name for cross-browser compatibility
|
||||
* @returns {string} Hidden property name
|
||||
* @private
|
||||
*/
|
||||
getHiddenProperty() {
|
||||
if (typeof document.hidden !== "undefined") {
|
||||
return "hidden";
|
||||
} else if (typeof document.msHidden !== "undefined") {
|
||||
return "msHidden";
|
||||
} else if (typeof document.webkitHidden !== "undefined") {
|
||||
return "webkitHidden";
|
||||
}
|
||||
return "hidden";
|
||||
}
|
||||
/**
|
||||
* Get visibility change event name for cross-browser compatibility
|
||||
* @returns {string} Visibility change event name
|
||||
* @private
|
||||
*/
|
||||
getVisibilityChangeEvent() {
|
||||
if (typeof document.hidden !== "undefined") {
|
||||
return "visibilitychange";
|
||||
} else if (typeof document.msHidden !== "undefined") {
|
||||
return "msvisibilitychange";
|
||||
} else if (typeof document.webkitHidden !== "undefined") {
|
||||
return "webkitvisibilitychange";
|
||||
}
|
||||
return "visibilitychange";
|
||||
}
|
||||
/**
|
||||
* Check if tab is currently active using multiple methods
|
||||
* @returns {boolean} True if tab is active
|
||||
* @private
|
||||
*/
|
||||
checkTabActive() {
|
||||
if (this.hidden && typeof document[this.hidden] !== "undefined") {
|
||||
return !document[this.hidden];
|
||||
}
|
||||
if (typeof document.hasFocus === "function") {
|
||||
return document.hasFocus();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Initialize page visibility tracking (Page Visibility API)
|
||||
* @private
|
||||
*/
|
||||
initVisibilityTracking() {
|
||||
if (typeof document.addEventListener !== "undefined" && typeof document[this.hidden] !== "undefined") {
|
||||
document.addEventListener(this.visibilityChange, () => {
|
||||
this.isTabActive = this.checkTabActive();
|
||||
if (this.isTabActive) {
|
||||
this.resetUnreadCount();
|
||||
this.clearNotificationQueue();
|
||||
}
|
||||
});
|
||||
}
|
||||
window.addEventListener("focus", () => {
|
||||
this.isTabActive = this.checkTabActive();
|
||||
if (this.isTabActive) {
|
||||
this.resetUnreadCount();
|
||||
}
|
||||
});
|
||||
window.addEventListener("blur", () => {
|
||||
this.isTabActive = this.checkTabActive();
|
||||
});
|
||||
window.addEventListener("beforeunload", () => {
|
||||
this.clearNotificationQueue();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Request notification permission (BEST PRACTICE: Only call in response to user action)
|
||||
* Never call on page load!
|
||||
* @returns {Promise<boolean>} Permission granted status
|
||||
*/
|
||||
async requestPermission() {
|
||||
if (!this.isSecureContext || !("Notification" in window)) {
|
||||
return false;
|
||||
}
|
||||
if (this.permission === "granted") {
|
||||
return true;
|
||||
}
|
||||
if (this.permission === "denied") {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
this.permission = await Notification.requestPermission();
|
||||
return this.permission === "granted";
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update page title with unread count
|
||||
* @private
|
||||
*/
|
||||
updateTitle() {
|
||||
if (this.unreadCount > 0) {
|
||||
document.title = `(${this.unreadCount}) ${this.originalTitle}`;
|
||||
} else {
|
||||
document.title = this.originalTitle;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* XSS Protection: Sanitize input text
|
||||
* @param {string} text - Text to sanitize
|
||||
* @returns {string} Sanitized text
|
||||
* @private
|
||||
*/
|
||||
sanitizeText(text) {
|
||||
if (typeof text !== "string") {
|
||||
return "";
|
||||
}
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'").substring(0, 500);
|
||||
}
|
||||
/**
|
||||
* Validate icon URL (XSS protection)
|
||||
* @param {string} url - URL to validate
|
||||
* @returns {string|null} Validated URL or null
|
||||
* @private
|
||||
*/
|
||||
validateIconUrl(url) {
|
||||
if (!url) return null;
|
||||
try {
|
||||
const parsedUrl = new URL(url, window.location.origin);
|
||||
if (parsedUrl.protocol === "https:" || parsedUrl.protocol === "data:") {
|
||||
if (this.trustedOrigins.length > 0) {
|
||||
const isTrusted = this.trustedOrigins.some(
|
||||
(origin) => parsedUrl.origin === origin
|
||||
);
|
||||
return isTrusted ? parsedUrl.href : null;
|
||||
}
|
||||
return parsedUrl.href;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Rate limiting for spam protection
|
||||
* @returns {boolean} Rate limit check passed
|
||||
* @private
|
||||
*/
|
||||
checkRateLimit() {
|
||||
const now = Date.now();
|
||||
if (now - this.lastNotificationTime < this.rateLimitMs) {
|
||||
return false;
|
||||
}
|
||||
this.lastNotificationTime = now;
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Send secure notification
|
||||
* @param {string} senderName - Name of message sender
|
||||
* @param {string} message - Message content
|
||||
* @param {Object} options - Notification options
|
||||
* @returns {Notification|null} Created notification or null
|
||||
*/
|
||||
notify(senderName, message, options = {}) {
|
||||
this.isTabActive = this.checkTabActive();
|
||||
if (this.isTabActive) {
|
||||
return null;
|
||||
}
|
||||
if (this.permission !== "granted") {
|
||||
return null;
|
||||
}
|
||||
if (!this.checkRateLimit()) {
|
||||
return null;
|
||||
}
|
||||
const safeSenderName = this.sanitizeText(senderName || "Unknown");
|
||||
const safeMessage = this.sanitizeText(message || "");
|
||||
const safeIcon = this.validateIconUrl(options.icon) || "/logo/icon-192x192.png";
|
||||
if (this.notificationQueue.length >= this.maxQueueSize) {
|
||||
this.clearNotificationQueue();
|
||||
}
|
||||
try {
|
||||
const notification = new Notification(
|
||||
`${safeSenderName}`,
|
||||
{
|
||||
body: safeMessage.substring(0, 200),
|
||||
// Length limit
|
||||
icon: safeIcon,
|
||||
badge: safeIcon,
|
||||
tag: `chat-${options.senderId || "unknown"}`,
|
||||
// Grouping
|
||||
requireInteraction: false,
|
||||
// Don't block user
|
||||
silent: options.silent || false,
|
||||
// Vibrate only for mobile and if supported
|
||||
vibrate: navigator.vibrate ? [200, 100, 200] : void 0,
|
||||
// Safe metadata
|
||||
data: {
|
||||
senderId: this.sanitizeText(options.senderId),
|
||||
timestamp: Date.now()
|
||||
// Don't include sensitive data!
|
||||
}
|
||||
}
|
||||
);
|
||||
this.unreadCount++;
|
||||
this.updateTitle();
|
||||
this.notificationQueue.push(notification);
|
||||
notification.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
window.focus();
|
||||
notification.close();
|
||||
if (typeof options.onClick === "function") {
|
||||
try {
|
||||
options.onClick(options.senderId);
|
||||
} catch (error) {
|
||||
console.error("[Notifications] Error in onClick handler:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
notification.onerror = (event) => {
|
||||
console.error("[Notifications] Error showing notification:", event);
|
||||
};
|
||||
const autoCloseTimeout = Math.min(options.autoClose || 5e3, 1e4);
|
||||
setTimeout(() => {
|
||||
notification.close();
|
||||
this.removeFromQueue(notification);
|
||||
}, autoCloseTimeout);
|
||||
return notification;
|
||||
} catch (error) {
|
||||
console.error("[Notifications] Failed to create notification:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Remove notification from queue
|
||||
* @param {Notification} notification - Notification to remove
|
||||
* @private
|
||||
*/
|
||||
removeFromQueue(notification) {
|
||||
const index = this.notificationQueue.indexOf(notification);
|
||||
if (index > -1) {
|
||||
this.notificationQueue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Clear all notifications
|
||||
*/
|
||||
clearNotificationQueue() {
|
||||
this.notificationQueue.forEach((notification) => {
|
||||
try {
|
||||
notification.close();
|
||||
} catch (error) {
|
||||
}
|
||||
});
|
||||
this.notificationQueue = [];
|
||||
}
|
||||
/**
|
||||
* Reset unread counter
|
||||
*/
|
||||
resetUnreadCount() {
|
||||
this.unreadCount = 0;
|
||||
this.updateTitle();
|
||||
}
|
||||
/**
|
||||
* Get current status
|
||||
* @returns {Object} Current notification status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
permission: this.permission,
|
||||
isTabActive: this.isTabActive,
|
||||
unreadCount: this.unreadCount,
|
||||
isSecureContext: this.isSecureContext,
|
||||
queueSize: this.notificationQueue.length
|
||||
};
|
||||
}
|
||||
};
|
||||
var SecureP2PChat = class {
|
||||
constructor() {
|
||||
this.notificationManager = new SecureChatNotificationManager({
|
||||
maxQueueSize: 5,
|
||||
rateLimitMs: 2e3,
|
||||
trustedOrigins: [
|
||||
window.location.origin
|
||||
// Add other trusted origins for CDN icons
|
||||
]
|
||||
});
|
||||
this.dataChannel = null;
|
||||
this.peerConnection = null;
|
||||
this.remotePeerName = "Peer";
|
||||
this.messageHistory = [];
|
||||
this.maxHistorySize = 100;
|
||||
}
|
||||
/**
|
||||
* Initialize when user connects
|
||||
*/
|
||||
async init() {
|
||||
}
|
||||
/**
|
||||
* Method for manual permission request (called on click)
|
||||
* @returns {Promise<boolean>} Permission granted status
|
||||
*/
|
||||
async enableNotifications() {
|
||||
const granted = await this.notificationManager.requestPermission();
|
||||
return granted;
|
||||
}
|
||||
/**
|
||||
* Setup DataChannel with security checks
|
||||
* @param {RTCDataChannel} dataChannel - WebRTC data channel
|
||||
*/
|
||||
setupDataChannel(dataChannel) {
|
||||
if (!dataChannel) {
|
||||
console.error("[Chat] Invalid DataChannel");
|
||||
return;
|
||||
}
|
||||
this.dataChannel = dataChannel;
|
||||
this.dataChannel.onmessage = (event) => {
|
||||
this.handleIncomingMessage(event.data);
|
||||
};
|
||||
this.dataChannel.onerror = (error) => {
|
||||
};
|
||||
}
|
||||
/**
|
||||
* XSS Protection: Validate incoming messages
|
||||
* @param {string|Object} data - Message data
|
||||
* @returns {Object|null} Validated message or null
|
||||
* @private
|
||||
*/
|
||||
validateMessage(data) {
|
||||
try {
|
||||
const message = typeof data === "string" ? JSON.parse(data) : data;
|
||||
if (!message || typeof message !== "object") {
|
||||
throw new Error("Invalid message structure");
|
||||
}
|
||||
if (!message.text || typeof message.text !== "string") {
|
||||
throw new Error("Invalid message text");
|
||||
}
|
||||
if (message.text.length > 1e4) {
|
||||
throw new Error("Message too long");
|
||||
}
|
||||
return {
|
||||
text: message.text,
|
||||
senderName: message.senderName || "Unknown",
|
||||
senderId: message.senderId || "unknown",
|
||||
timestamp: message.timestamp || Date.now(),
|
||||
senderAvatar: message.senderAvatar || null
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[Chat] Message validation failed:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Secure handling of incoming messages
|
||||
* @param {string|Object} data - Message data
|
||||
* @private
|
||||
*/
|
||||
handleIncomingMessage(data) {
|
||||
const message = this.validateMessage(data);
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
this.messageHistory.push(message);
|
||||
if (this.messageHistory.length > this.maxHistorySize) {
|
||||
this.messageHistory.shift();
|
||||
}
|
||||
this.displayMessage(message);
|
||||
this.notificationManager.notify(
|
||||
message.senderName,
|
||||
message.text,
|
||||
{
|
||||
icon: message.senderAvatar,
|
||||
senderId: message.senderId,
|
||||
onClick: (senderId) => {
|
||||
this.scrollToLatestMessage();
|
||||
}
|
||||
}
|
||||
);
|
||||
if (!this.notificationManager.isTabActive) {
|
||||
this.playNotificationSound();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* XSS Protection: Safe message display
|
||||
* @param {Object} message - Message to display
|
||||
* @private
|
||||
*/
|
||||
displayMessage(message) {
|
||||
const container = document.getElementById("messages");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const messageEl = document.createElement("div");
|
||||
messageEl.className = "message";
|
||||
const nameEl = document.createElement("strong");
|
||||
nameEl.textContent = message.senderName + ": ";
|
||||
const textEl = document.createElement("span");
|
||||
textEl.textContent = message.text;
|
||||
const timeEl = document.createElement("small");
|
||||
timeEl.textContent = new Date(message.timestamp).toLocaleTimeString();
|
||||
messageEl.appendChild(nameEl);
|
||||
messageEl.appendChild(textEl);
|
||||
messageEl.appendChild(document.createElement("br"));
|
||||
messageEl.appendChild(timeEl);
|
||||
container.appendChild(messageEl);
|
||||
this.scrollToLatestMessage();
|
||||
}
|
||||
/**
|
||||
* Safe sound playback
|
||||
* @private
|
||||
*/
|
||||
playNotificationSound() {
|
||||
try {
|
||||
const audio = new Audio("/assets/audio/notification.mp3");
|
||||
audio.volume = 0.3;
|
||||
audio.play().catch((error) => {
|
||||
});
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Scroll to latest message
|
||||
* @private
|
||||
*/
|
||||
scrollToLatestMessage() {
|
||||
const container = document.getElementById("messages");
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get status
|
||||
* @returns {Object} Current chat status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
notifications: this.notificationManager.getStatus(),
|
||||
messageCount: this.messageHistory.length,
|
||||
connected: this.dataChannel?.readyState === "open"
|
||||
};
|
||||
}
|
||||
};
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = { SecureChatNotificationManager, SecureP2PChat };
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
window.SecureChatNotificationManager = SecureChatNotificationManager;
|
||||
window.SecureP2PChat = SecureP2PChat;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// src/notifications/NotificationIntegration.js
|
||||
var require_NotificationIntegration = __commonJS({
|
||||
"src/notifications/NotificationIntegration.js"(exports, module) {
|
||||
var import_SecureNotificationManager = __toESM(require_SecureNotificationManager());
|
||||
var NotificationIntegration2 = class {
|
||||
constructor(webrtcManager) {
|
||||
this.webrtcManager = webrtcManager;
|
||||
this.notificationManager = new import_SecureNotificationManager.SecureChatNotificationManager({
|
||||
maxQueueSize: 10,
|
||||
rateLimitMs: 1e3,
|
||||
// Reduced from 2000ms to 1000ms
|
||||
trustedOrigins: [
|
||||
window.location.origin
|
||||
// Add other trusted origins for CDN icons
|
||||
]
|
||||
});
|
||||
this.isInitialized = false;
|
||||
this.originalOnMessage = null;
|
||||
this.originalOnStatusChange = null;
|
||||
this.processedMessages = /* @__PURE__ */ new Set();
|
||||
}
|
||||
/**
|
||||
* Initialize notification integration
|
||||
* @returns {Promise<boolean>} Initialization success
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
if (this.isInitialized) {
|
||||
return true;
|
||||
}
|
||||
this.originalOnMessage = this.webrtcManager.onMessage;
|
||||
this.originalOnStatusChange = this.webrtcManager.onStatusChange;
|
||||
this.webrtcManager.onMessage = (message, type) => {
|
||||
this.handleIncomingMessage(message, type);
|
||||
if (this.originalOnMessage) {
|
||||
this.originalOnMessage(message, type);
|
||||
}
|
||||
};
|
||||
this.webrtcManager.onStatusChange = (status) => {
|
||||
this.handleStatusChange(status);
|
||||
if (this.originalOnStatusChange) {
|
||||
this.originalOnStatusChange(status);
|
||||
}
|
||||
};
|
||||
if (this.webrtcManager.deliverMessageToUI) {
|
||||
this.originalDeliverMessageToUI = this.webrtcManager.deliverMessageToUI.bind(this.webrtcManager);
|
||||
this.webrtcManager.deliverMessageToUI = (message, type) => {
|
||||
this.handleIncomingMessage(message, type);
|
||||
this.originalDeliverMessageToUI(message, type);
|
||||
};
|
||||
}
|
||||
this.isInitialized = true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle incoming messages and trigger notifications
|
||||
* @param {*} message - Message content
|
||||
* @param {string} type - Message type
|
||||
* @private
|
||||
*/
|
||||
handleIncomingMessage(message, type) {
|
||||
try {
|
||||
const messageKey = `${type}:${typeof message === "string" ? message : JSON.stringify(message)}`;
|
||||
if (this.processedMessages.has(messageKey)) {
|
||||
return;
|
||||
}
|
||||
this.processedMessages.add(messageKey);
|
||||
if (this.processedMessages.size > 100) {
|
||||
const messagesArray = Array.from(this.processedMessages);
|
||||
this.processedMessages.clear();
|
||||
messagesArray.slice(-50).forEach((msg) => this.processedMessages.add(msg));
|
||||
}
|
||||
if (type === "system" || type === "file-transfer" || type === "heartbeat") {
|
||||
return;
|
||||
}
|
||||
const messageInfo = this.extractMessageInfo(message, type);
|
||||
if (!messageInfo) {
|
||||
return;
|
||||
}
|
||||
const notificationResult = this.notificationManager.notify(
|
||||
messageInfo.senderName,
|
||||
messageInfo.text,
|
||||
{
|
||||
icon: messageInfo.senderAvatar,
|
||||
senderId: messageInfo.senderId,
|
||||
onClick: (senderId) => {
|
||||
this.focusChatWindow();
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle status changes
|
||||
* @param {string} status - Connection status
|
||||
* @private
|
||||
*/
|
||||
handleStatusChange(status) {
|
||||
try {
|
||||
if (status === "disconnected" || status === "failed") {
|
||||
this.notificationManager.clearNotificationQueue();
|
||||
this.notificationManager.resetUnreadCount();
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Extract message information for notifications
|
||||
* @param {*} message - Message content
|
||||
* @param {string} type - Message type
|
||||
* @returns {Object|null} Extracted message info or null
|
||||
* @private
|
||||
*/
|
||||
extractMessageInfo(message, type) {
|
||||
try {
|
||||
let messageData = message;
|
||||
if (typeof message === "string") {
|
||||
try {
|
||||
messageData = JSON.parse(message);
|
||||
} catch (e) {
|
||||
return {
|
||||
senderName: "Peer",
|
||||
text: message,
|
||||
senderId: "peer",
|
||||
senderAvatar: null
|
||||
};
|
||||
}
|
||||
}
|
||||
if (typeof messageData === "object" && messageData !== null) {
|
||||
return {
|
||||
senderName: messageData.senderName || messageData.name || "Peer",
|
||||
text: messageData.text || messageData.message || messageData.content || "",
|
||||
senderId: messageData.senderId || messageData.id || "peer",
|
||||
senderAvatar: messageData.senderAvatar || messageData.avatar || null
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Focus chat window when notification is clicked
|
||||
* @private
|
||||
*/
|
||||
focusChatWindow() {
|
||||
try {
|
||||
window.focus();
|
||||
const messagesContainer = document.getElementById("messages");
|
||||
if (messagesContainer) {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Request notification permission
|
||||
* @returns {Promise<boolean>} Permission granted status
|
||||
*/
|
||||
async requestPermission() {
|
||||
try {
|
||||
return await this.notificationManager.requestPermission();
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get notification status
|
||||
* @returns {Object} Notification status
|
||||
*/
|
||||
getStatus() {
|
||||
return this.notificationManager.getStatus();
|
||||
}
|
||||
/**
|
||||
* Clear all notifications
|
||||
*/
|
||||
clearNotifications() {
|
||||
this.notificationManager.clearNotificationQueue();
|
||||
this.notificationManager.resetUnreadCount();
|
||||
}
|
||||
/**
|
||||
* Cleanup integration
|
||||
*/
|
||||
cleanup() {
|
||||
try {
|
||||
if (this.isInitialized) {
|
||||
if (this.originalOnMessage) {
|
||||
this.webrtcManager.onMessage = this.originalOnMessage;
|
||||
}
|
||||
if (this.originalOnStatusChange) {
|
||||
this.webrtcManager.onStatusChange = this.originalOnStatusChange;
|
||||
}
|
||||
if (this.originalDeliverMessageToUI) {
|
||||
this.webrtcManager.deliverMessageToUI = this.originalDeliverMessageToUI;
|
||||
}
|
||||
this.clearNotifications();
|
||||
this.isInitialized = false;
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
};
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = { NotificationIntegration: NotificationIntegration2 };
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
window.NotificationIntegration = NotificationIntegration2;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// src/crypto/EnhancedSecureCryptoUtils.js
|
||||
var EnhancedSecureCryptoUtils = class _EnhancedSecureCryptoUtils {
|
||||
static _keyMetadata = /* @__PURE__ */ new WeakMap();
|
||||
@@ -14109,6 +14826,9 @@ var SecureMasterKeyManager = class {
|
||||
}
|
||||
};
|
||||
|
||||
// src/scripts/app-boot.js
|
||||
var import_NotificationIntegration = __toESM(require_NotificationIntegration());
|
||||
|
||||
// src/components/ui/Header.jsx
|
||||
var EnhancedMinimalHeader = ({
|
||||
status,
|
||||
@@ -16056,6 +16776,7 @@ window.FileTransferComponent = FileTransferComponent;
|
||||
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
|
||||
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
||||
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
|
||||
window.NotificationIntegration = import_NotificationIntegration.NotificationIntegration;
|
||||
var start = () => {
|
||||
if (typeof window.initializeApp === "function") {
|
||||
window.initializeApp();
|
||||
@@ -16068,4 +16789,20 @@ if (document.readyState === "loading") {
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
/**
|
||||
* Secure and Reliable Notification Manager for P2P WebRTC Chat
|
||||
* Follows best practices: OWASP, MDN, Chrome DevRel
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author SecureBit Team
|
||||
* @license MIT
|
||||
*/
|
||||
/**
|
||||
* Notification Integration Module for SecureBit WebRTC Chat
|
||||
* Integrates secure notifications with existing WebRTC architecture
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author SecureBit Team
|
||||
* @license MIT
|
||||
*/
|
||||
//# sourceMappingURL=app-boot.js.map
|
||||
Vendored
+4
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../src/crypto/EnhancedSecureCryptoUtils.js", "../src/transfer/EnhancedSecureFileTransfer.js", "../src/network/EnhancedSecureWebRTCManager.js", "../src/components/ui/Header.jsx", "../src/components/ui/DownloadApps.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/scripts/app-boot.js"],
|
||||
"sourcesContent": ["class EnhancedSecureCryptoUtils {\r\n\r\n static _keyMetadata = new WeakMap();\r\n \r\n // Initialize secure logging system after class definition\r\n\r\n // Utility to sort object keys for deterministic serialization\r\n static sortObjectKeys(obj) {\r\n if (typeof obj !== 'object' || obj === null) {\r\n return obj;\r\n }\r\n\r\n if (Array.isArray(obj)) {\r\n return obj.map(EnhancedSecureCryptoUtils.sortObjectKeys);\r\n }\r\n\r\n const sortedObj = {};\r\n Object.keys(obj).sort().forEach(key => {\r\n sortedObj[key] = EnhancedSecureCryptoUtils.sortObjectKeys(obj[key]);\r\n });\r\n return sortedObj;\r\n }\r\n\r\n // Utility to assert CryptoKey type and properties\r\n static assertCryptoKey(key, expectedName = null, expectedUsages = []) {\r\n if (!(key instanceof CryptoKey)) throw new Error('Expected CryptoKey');\r\n if (expectedName && key.algorithm?.name !== expectedName) {\r\n throw new Error(`Expected algorithm ${expectedName}, got ${key.algorithm?.name}`);\r\n }\r\n for (const u of expectedUsages) {\r\n if (!key.usages || !key.usages.includes(u)) {\r\n throw new Error(`Missing required key usage: ${u}`);\r\n }\r\n }\r\n }\r\n // Helper function to convert ArrayBuffer to Base64\r\n static arrayBufferToBase64(buffer) {\r\n let binary = '';\r\n const bytes = new Uint8Array(buffer);\r\n const len = bytes.byteLength;\r\n for (let i = 0; i < len; i++) {\r\n binary += String.fromCharCode(bytes[i]);\r\n }\r\n return btoa(binary);\r\n }\r\n\r\n // Helper function to convert Base64 to ArrayBuffer\r\n static base64ToArrayBuffer(base64) {\r\n try {\r\n // Validate input\r\n if (typeof base64 !== 'string' || !base64) {\r\n throw new Error('Invalid base64 input: must be a non-empty string');\r\n }\r\n\r\n // Remove any whitespace and validate base64 format\r\n const cleanBase64 = base64.trim();\r\n if (!/^[A-Za-z0-9+/]*={0,2}$/.test(cleanBase64)) {\r\n throw new Error('Invalid base64 format');\r\n }\r\n\r\n // Handle empty string case\r\n if (cleanBase64 === '') {\r\n return new ArrayBuffer(0);\r\n }\r\n\r\n const binaryString = atob(cleanBase64);\r\n const len = binaryString.length;\r\n const bytes = new Uint8Array(len);\r\n for (let i = 0; i < len; i++) {\r\n bytes[i] = binaryString.charCodeAt(i);\r\n }\r\n return bytes.buffer;\r\n } catch (error) {\r\n console.error('Base64 to ArrayBuffer conversion failed:', error.message);\r\n throw new Error(`Base64 conversion error: ${error.message}`);\r\n }\r\n }\r\n\r\n // Helper function to convert hex string to Uint8Array\r\n static hexToUint8Array(hexString) {\r\n try {\r\n if (!hexString || typeof hexString !== 'string') {\r\n throw new Error('Invalid hex string input: must be a non-empty string');\r\n }\r\n\r\n // Remove colons and spaces from hex string (e.g., \"aa:bb:cc\" -> \"aabbcc\")\r\n const cleanHex = hexString.replace(/:/g, '').replace(/\\s/g, '');\r\n \r\n // Validate hex format\r\n if (!/^[0-9a-fA-F]*$/.test(cleanHex)) {\r\n throw new Error('Invalid hex format: contains non-hex characters');\r\n }\r\n \r\n // Ensure even length\r\n if (cleanHex.length % 2 !== 0) {\r\n throw new Error('Invalid hex format: odd length');\r\n }\r\n\r\n // Convert hex string to bytes\r\n const bytes = new Uint8Array(cleanHex.length / 2);\r\n for (let i = 0; i < cleanHex.length; i += 2) {\r\n bytes[i / 2] = parseInt(cleanHex.substr(i, 2), 16);\r\n }\r\n \r\n return bytes;\r\n } catch (error) {\r\n console.error('Hex to Uint8Array conversion failed:', error.message);\r\n throw new Error(`Hex conversion error: ${error.message}`);\r\n }\r\n }\r\n\r\n static async encryptData(data, password) {\r\n try {\r\n const dataString = typeof data === 'string' ? data : JSON.stringify(data);\r\n const salt = crypto.getRandomValues(new Uint8Array(16));\r\n const encoder = new TextEncoder();\r\n const passwordBuffer = encoder.encode(password);\r\n\r\n const keyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n passwordBuffer,\r\n { name: 'PBKDF2' },\r\n false,\r\n ['deriveKey']\r\n Line truncated
|
||||
"mappings": "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 truncated
|
||||
"names": ["fileMessageTypes", "attempts", "start", "error", "attempts", "chunk", "fileMessageTypes", "window", "start", "webrtcManager", "start"]
|
||||
"sources": ["../src/notifications/SecureNotificationManager.js", "../src/notifications/NotificationIntegration.js", "../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/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"],
|
||||
"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 this.permission = Notification.permission;\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 * XSS Protection: Sanitize input text\n * @param {string} text - Text to sanitize\n * @returns {string} Sanitized text\n * @private\n */\n sanitizeText(text) {\n if (typeof text !== 'string') {\n Line truncated
|
||||
"mappings": "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 truncated
|
||||
"names": ["NotificationIntegration", "fileMessageTypes", "attempts", "start", "error", "attempts", "chunk", "fileMessageTypes", "window", "start", "webrtcManager", "start"]
|
||||
}
|
||||
Vendored
+156
-35
@@ -267,9 +267,11 @@ var EnhancedConnectionSetup = ({
|
||||
toggleQrManualMode,
|
||||
nextQrFrame,
|
||||
prevQrFrame,
|
||||
markAnswerCreated
|
||||
markAnswerCreated,
|
||||
notificationIntegrationRef
|
||||
}) => {
|
||||
const [mode, setMode] = React.useState("select");
|
||||
const [notificationPermissionRequested, setNotificationPermissionRequested] = React.useState(false);
|
||||
const resetToSelect = () => {
|
||||
setMode("select");
|
||||
onClearData();
|
||||
@@ -280,6 +282,76 @@ var EnhancedConnectionSetup = ({
|
||||
const handleVerificationReject = () => {
|
||||
onVerifyConnection(false);
|
||||
};
|
||||
const requestNotificationPermissionOnInteraction = async () => {
|
||||
if (notificationPermissionRequested) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!("Notification" in window)) {
|
||||
return;
|
||||
}
|
||||
if (!window.isSecureContext && window.location.protocol !== "https:" && window.location.hostname !== "localhost") {
|
||||
return;
|
||||
}
|
||||
const currentPermission = Notification.permission;
|
||||
if (currentPermission === "default") {
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission === "granted") {
|
||||
try {
|
||||
if (window.NotificationIntegration && webrtcManagerRef.current) {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
|
||||
await integration.init();
|
||||
notificationIntegrationRef.current = integration;
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const welcomeNotification = new Notification("SecureBit Chat", {
|
||||
body: "Notifications enabled! You will receive alerts for new messages.",
|
||||
icon: "/logo/icon-192x192.png",
|
||||
tag: "welcome-notification"
|
||||
});
|
||||
welcomeNotification.onclick = () => {
|
||||
welcomeNotification.close();
|
||||
};
|
||||
setTimeout(() => {
|
||||
welcomeNotification.close();
|
||||
}, 5e3);
|
||||
} catch (error) {
|
||||
}
|
||||
}, 1e3);
|
||||
}
|
||||
} else if (currentPermission === "granted") {
|
||||
try {
|
||||
if (window.NotificationIntegration && webrtcManagerRef.current && !notificationIntegrationRef.current) {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
|
||||
await integration.init();
|
||||
notificationIntegrationRef.current = integration;
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const testNotification = new Notification("SecureBit Chat", {
|
||||
body: "Notifications are working! You will receive alerts for new messages.",
|
||||
icon: "/logo/icon-192x192.png",
|
||||
tag: "test-notification"
|
||||
});
|
||||
testNotification.onclick = () => {
|
||||
testNotification.close();
|
||||
};
|
||||
setTimeout(() => {
|
||||
testNotification.close();
|
||||
}, 5e3);
|
||||
} catch (error) {
|
||||
}
|
||||
}, 1e3);
|
||||
}
|
||||
setNotificationPermissionRequested(true);
|
||||
} catch (error) {
|
||||
}
|
||||
};
|
||||
if (showVerification) {
|
||||
return React.createElement("div", {
|
||||
className: "min-h-[calc(100vh-104px)] flex items-center justify-center p-4"
|
||||
@@ -327,7 +399,10 @@ var EnhancedConnectionSetup = ({
|
||||
// Create Connection
|
||||
React.createElement("div", {
|
||||
key: "create",
|
||||
onClick: () => setMode("create"),
|
||||
onClick: () => {
|
||||
requestNotificationPermissionOnInteraction();
|
||||
setMode("create");
|
||||
},
|
||||
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 create"
|
||||
}, [
|
||||
React.createElement("div", {
|
||||
@@ -399,7 +474,10 @@ var EnhancedConnectionSetup = ({
|
||||
// Join Connection
|
||||
React.createElement("div", {
|
||||
key: "join",
|
||||
onClick: () => setMode("join"),
|
||||
onClick: () => {
|
||||
requestNotificationPermissionOnInteraction();
|
||||
setMode("join");
|
||||
},
|
||||
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 join"
|
||||
}, [
|
||||
React.createElement("div", {
|
||||
@@ -1321,8 +1399,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
React.useEffect(() => {
|
||||
window.forceCleanup = () => {
|
||||
handleClearData();
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.disconnect();
|
||||
}
|
||||
};
|
||||
window.clearLogs = () => {
|
||||
@@ -1335,8 +1413,9 @@ var EnhancedSecureP2PChat = () => {
|
||||
delete window.clearLogs;
|
||||
};
|
||||
}, []);
|
||||
const webrtcManagerRef = React.useRef(null);
|
||||
window.webrtcManagerRef = webrtcManagerRef;
|
||||
const webrtcManagerRef2 = React.useRef(null);
|
||||
const notificationIntegrationRef = React.useRef(null);
|
||||
window.webrtcManagerRef = webrtcManagerRef2;
|
||||
const addMessageWithAutoScroll = React.useCallback((message, type) => {
|
||||
const newMessage = {
|
||||
message,
|
||||
@@ -1377,7 +1456,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
}
|
||||
window.isUpdatingSecurity = true;
|
||||
try {
|
||||
if (webrtcManagerRef.current) {
|
||||
if (webrtcManagerRef2.current) {
|
||||
setSecurityLevel({
|
||||
level: "MAXIMUM",
|
||||
score: 100,
|
||||
@@ -1388,7 +1467,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
isRealData: true
|
||||
});
|
||||
if (window.DEBUG_MODE) {
|
||||
const currentLevel = webrtcManagerRef.current.ecdhKeyPair && webrtcManagerRef.current.ecdsaKeyPair ? await webrtcManagerRef.current.calculateSecurityLevel() : {
|
||||
const currentLevel = webrtcManagerRef2.current.ecdhKeyPair && webrtcManagerRef2.current.ecdsaKeyPair ? await webrtcManagerRef2.current.calculateSecurityLevel() : {
|
||||
level: "MAXIMUM",
|
||||
score: 100,
|
||||
sessionType: "premium",
|
||||
@@ -1421,7 +1500,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
}
|
||||
}, [messages]);
|
||||
React.useEffect(() => {
|
||||
if (webrtcManagerRef.current) {
|
||||
if (webrtcManagerRef2.current) {
|
||||
console.log("\u26A0\uFE0F WebRTC Manager already initialized, skipping...");
|
||||
return;
|
||||
}
|
||||
@@ -1583,7 +1662,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
if (typeof console.clear === "function") {
|
||||
console.clear();
|
||||
}
|
||||
webrtcManagerRef.current = new EnhancedSecureWebRTCManager(
|
||||
webrtcManagerRef2.current = new EnhancedSecureWebRTCManager(
|
||||
handleMessage,
|
||||
handleStatusChange,
|
||||
handleKeyExchange,
|
||||
@@ -1591,12 +1670,22 @@ var EnhancedSecureP2PChat = () => {
|
||||
handleAnswerError,
|
||||
handleVerificationStateChange
|
||||
);
|
||||
if (Notification.permission === "granted" && window.NotificationIntegration && !notificationIntegrationRef.current) {
|
||||
try {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef2.current);
|
||||
integration.init().then(() => {
|
||||
notificationIntegrationRef.current = integration;
|
||||
}).catch((error) => {
|
||||
});
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
handleMessage(" SecureBit.chat Enhanced Security Edition v4.3.120 - 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()) {
|
||||
if (webrtcManagerRef2.current && webrtcManagerRef2.current.isConnected()) {
|
||||
try {
|
||||
webrtcManagerRef.current.sendSystemMessage({
|
||||
webrtcManagerRef2.current.sendSystemMessage({
|
||||
type: "peer_disconnect",
|
||||
reason: "user_disconnect",
|
||||
timestamp: Date.now()
|
||||
@@ -1604,12 +1693,12 @@ var EnhancedSecureP2PChat = () => {
|
||||
} catch (error) {
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.disconnect();
|
||||
}
|
||||
}, 100);
|
||||
} else if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
} else if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.disconnect();
|
||||
}
|
||||
} else if (isTabSwitching) {
|
||||
event.preventDefault();
|
||||
@@ -1637,8 +1726,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
}
|
||||
};
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.setFileTransferCallbacks(
|
||||
if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.setFileTransferCallbacks(
|
||||
// Progress callback
|
||||
(progress) => {
|
||||
console.log("File progress:", progress);
|
||||
@@ -1690,9 +1779,9 @@ var EnhancedSecureP2PChat = () => {
|
||||
clearTimeout(tabSwitchTimeout);
|
||||
tabSwitchTimeout = null;
|
||||
}
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
webrtcManagerRef.current = null;
|
||||
if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.disconnect();
|
||||
webrtcManagerRef2.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
@@ -2362,7 +2451,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
setShowOfferStep(false);
|
||||
setShowQRCode(false);
|
||||
setQrCodeUrl("");
|
||||
const offer = await webrtcManagerRef.current.createSecureOffer();
|
||||
const offer = await webrtcManagerRef2.current.createSecureOffer();
|
||||
setOfferData(offer);
|
||||
setShowOfferStep(true);
|
||||
const offerString = typeof offer === "object" ? JSON.stringify(offer) : offer;
|
||||
@@ -2485,7 +2574,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
if (!isValidOfferType) {
|
||||
throw new Error("Invalid invitation type. Expected offer or enhanced_secure_offer");
|
||||
}
|
||||
const answer = await webrtcManagerRef.current.createSecureAnswer(offer);
|
||||
const answer = await webrtcManagerRef2.current.createSecureAnswer(offer);
|
||||
setAnswerData(answer);
|
||||
setShowAnswerStep(true);
|
||||
const answerString = typeof answer === "object" ? JSON.stringify(answer) : answer;
|
||||
@@ -2623,7 +2712,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
if (!answerType || answerType !== "answer" && answerType !== "enhanced_secure_answer") {
|
||||
throw new Error("Invalid response type. Expected answer or enhanced_secure_answer");
|
||||
}
|
||||
await webrtcManagerRef.current.handleSecureAnswer(answer);
|
||||
await webrtcManagerRef2.current.handleSecureAnswer(answer);
|
||||
if (pendingSession) {
|
||||
setPendingSession(null);
|
||||
setMessages((prev) => [...prev, {
|
||||
@@ -2709,10 +2798,37 @@ var EnhancedSecureP2PChat = () => {
|
||||
setConnectionStatus("failed");
|
||||
}
|
||||
};
|
||||
const handleVerifyConnection = (isValid) => {
|
||||
const handleVerifyConnection = async (isValid) => {
|
||||
if (isValid) {
|
||||
webrtcManagerRef.current.confirmVerification();
|
||||
webrtcManagerRef2.current.confirmVerification();
|
||||
setLocalVerificationConfirmed(true);
|
||||
try {
|
||||
if (window.NotificationIntegration && webrtcManagerRef2.current && !notificationIntegrationRef.current) {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef2.current);
|
||||
await integration.init();
|
||||
notificationIntegrationRef.current = integration;
|
||||
const status = integration.getStatus();
|
||||
if (status.permission === "granted") {
|
||||
setMessages((prev) => [...prev, {
|
||||
message: "\u2713 Notifications enabled - you will receive alerts when the tab is inactive",
|
||||
type: "system",
|
||||
id: Date.now(),
|
||||
timestamp: Date.now()
|
||||
}]);
|
||||
} else {
|
||||
setMessages((prev) => [...prev, {
|
||||
message: "\u2139 Notifications disabled - you can enable them using the button on the main page",
|
||||
type: "system",
|
||||
id: Date.now(),
|
||||
timestamp: Date.now()
|
||||
}]);
|
||||
}
|
||||
} else if (notificationIntegrationRef.current) {
|
||||
} else {
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to initialize notifications:", error);
|
||||
}
|
||||
} else {
|
||||
setMessages((prev) => [...prev, {
|
||||
message: " Verification rejected. The connection is unsafe! Session reset..",
|
||||
@@ -2746,15 +2862,15 @@ var EnhancedSecureP2PChat = () => {
|
||||
if (!messageInput.trim()) {
|
||||
return;
|
||||
}
|
||||
if (!webrtcManagerRef.current) {
|
||||
if (!webrtcManagerRef2.current) {
|
||||
return;
|
||||
}
|
||||
if (!webrtcManagerRef.current.isConnected()) {
|
||||
if (!webrtcManagerRef2.current.isConnected()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
addMessageWithAutoScroll(messageInput.trim(), "sent");
|
||||
await webrtcManagerRef.current.sendMessage(messageInput);
|
||||
await webrtcManagerRef2.current.sendMessage(messageInput);
|
||||
setMessageInput("");
|
||||
} catch (error) {
|
||||
const msg = String(error?.message || error);
|
||||
@@ -2804,8 +2920,12 @@ var EnhancedSecureP2PChat = () => {
|
||||
status: "disconnected",
|
||||
isUserInitiatedDisconnect: true
|
||||
});
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.disconnect();
|
||||
}
|
||||
if (notificationIntegrationRef.current) {
|
||||
notificationIntegrationRef.current.cleanup();
|
||||
notificationIntegrationRef.current = null;
|
||||
}
|
||||
setKeyFingerprint("");
|
||||
setVerificationCode("");
|
||||
@@ -2964,7 +3084,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
isConnected: isConnectedAndVerified,
|
||||
securityLevel,
|
||||
// sessionManager removed - all features enabled by default
|
||||
webrtcManager: webrtcManagerRef.current
|
||||
webrtcManager: webrtcManagerRef2.current
|
||||
}),
|
||||
React.createElement(
|
||||
"main",
|
||||
@@ -2984,7 +3104,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
isVerified,
|
||||
chatMessagesRef,
|
||||
scrollToBottom,
|
||||
webrtcManager: webrtcManagerRef.current
|
||||
webrtcManager: webrtcManagerRef2.current
|
||||
});
|
||||
})() : React.createElement(EnhancedConnectionSetup, {
|
||||
onCreateOffer: handleCreateOffer,
|
||||
@@ -3021,7 +3141,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
nextQrFrame,
|
||||
prevQrFrame,
|
||||
// PAKE passwords removed - using SAS verification instead
|
||||
markAnswerCreated
|
||||
markAnswerCreated,
|
||||
notificationIntegrationRef
|
||||
})
|
||||
),
|
||||
// QR Scanner Modal
|
||||
|
||||
Vendored
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../src/app.jsx"],
|
||||
"sourcesContent": ["\r\n // Enhanced Copy Button with better UX\r\n const EnhancedCopyButton = ({ text, className = \"\", children }) => {\r\n const [copied, setCopied] = React.useState(false);\r\n \r\n const handleCopy = async () => {\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n setCopied(true);\r\n setTimeout(() => setCopied(false), 2000);\r\n } catch (error) {\r\n console.error('Copy failed:', error);\r\n // Fallback for older browsers\r\n const textArea = document.createElement('textarea');\r\n textArea.value = text;\r\n document.body.appendChild(textArea);\r\n textArea.select();\r\n document.execCommand('copy');\r\n document.body.removeChild(textArea);\r\n setCopied(true);\r\n setTimeout(() => setCopied(false), 2000);\r\n }\r\n };\r\n \r\n return React.createElement('button', {\r\n onClick: handleCopy,\r\n className: `${className} transition-all duration-200`\r\n }, [\r\n React.createElement('i', {\r\n key: 'icon',\r\n className: `${copied ? 'fas fa-check accent-green' : 'fas fa-copy text-secondary'} mr-2`\r\n }),\r\n copied ? 'Copied!' : children\r\n ]);\r\n };\r\n \r\n // Verification Component\r\n const VerificationStep = ({ verificationCode, onConfirm, onReject, localConfirmed, remoteConfirmed, bothConfirmed }) => {\r\n return React.createElement('div', {\r\n className: \"card-minimal rounded-xl p-6 border-purple-500/20\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'header',\r\n className: \"flex items-center mb-4\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'icon',\r\n className: \"w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3\"\r\n }, [\r\n React.createElement('i', {\r\n className: 'fas fa-shield-alt accent-purple'\r\n })\r\n ]),\r\n React.createElement('h3', {\r\n key: 'title',\r\n className: \"text-lg font-medium text-primary\"\r\n }, \"Security verification\")\r\n ]),\r\n React.createElement('div', {\r\n key: 'content',\r\n className: \"space-y-4\"\r\n }, [\r\n React.createElement('p', {\r\n key: 'description',\r\n className: \"text-secondary text-sm\"\r\n }, \"Verify the security code with your contact via another communication channel (voice, SMS, etc.):\"),\r\n React.createElement('div', {\r\n key: 'code-display',\r\n className: \"text-center\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'code',\r\n className: \"verification-code text-2xl py-4\"\r\n }, verificationCode)\r\n ]),\r\n // Verification status indicators\r\n React.createElement('div', {\r\n key: 'verification-status',\r\n className: \"space-y-2\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'local-status',\r\n className: `flex items-center justify-between p-2 rounded-lg ${localConfirmed ? 'bg-green-500/10 border border-green-500/20' : 'bg-gray-500/10 border border-gray-500/20'}`\r\n }, [\r\n React.createElement('span', {\r\n Line truncated
|
||||
"mappings": ";AAEgB,IAAM,qBAAqB,CAAC,EAAE,MAAM,YAAY,IAAI,SAAS,MAAM;AAC/D,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,KAAK;AAEhD,QAAM,aAAa,YAAY;AAC3B,QAAI;AACA,YAAM,UAAU,UAAU,UAAU,IAAI;AACxC,gBAAU,IAAI;AACd,iBAAW,MAAM,UAAU,KAAK,GAAG,GAAI;AAAA,IAC3C,SAAS,OAAO;AACZ,cAAQ,MAAM,gBAAgB,KAAK;AAEnC,YAAM,WAAW,SAAS,cAAc,UAAU;AAClD,eAAS,QAAQ;AACjB,eAAS,KAAK,YAAY,QAAQ;AAClC,eAAS,OAAO;AAChB,eAAS,YAAY,MAAM;AAC3B,eAAS,KAAK,YAAY,QAAQ;AAClC,gBAAU,IAAI;AACd,iBAAW,MAAM,UAAU,KAAK,GAAG,GAAI;AAAA,IAC3C;AAAA,EACJ;AAEA,SAAO,MAAM,cAAc,UAAU;AAAA,IACjC,SAAS;AAAA,IACT,WAAW,GAAG,SAAS;AAAA,EAC3B,GAAG;AAAA,IACC,MAAM,cAAc,KAAK;AAAA,MACrB,KAAK;AAAA,MACL,WAAW,GAAG,SAAS,8BAA8B,4BAA4B;AAAA,IACrF,CAAC;AAAA,IACD,SAAS,YAAY;AAAA,EACzB,CAAC;AACL;AAGA,IAAM,mBAAmB,CAAC,EAAE,kBAAkB,WAAW,UAAU,gBAAgB,iBAAiB,cAAc,MAAM;AACpH,SAAO,MAAM,cAAc,OAAO;AAAA,IAC9B,WAAW;AAAA,EACf,GAAG;AAAA,IACC,MAAM,cAAc,OAAO;AAAA,MACvB,KAAK;AAAA,MACL,WAAW;AAAA,IACf,GAAG;AAAA,MACC,MAAM,cAAc,OAAO;AAAA,QACvB,KAAK;AAAA,QACL,WAAW;AAAA,MACf,GAAG;AAAA,QACC,MAAM,cAAc,KAAK;AAAA,UACrB,WAAW;AAAA,QACf,CAAC;AAAA,MACL,CAAC;AAAA,MACD,MAAM,cAAc,MAAM;AAAA,QACtB,KAAK;AAAA,QACL,WAAW;AAAA,MACf,GAAG,uBAAuB;AAAA,IAC9B,CAAC;AAAA,IACD,MAAM,cAAc,OAAO;AAAA,MACvB,KAAK;AAAA,MACL,WAAW;AAAA,IACf,GAAG;AAAA,MACC,MAAM,cAAc,KAAK;AAAA,QACrB,KAAK;AAAA,QACL,WAAW;AAAA,MACf,GAAG,kGAAkG;AAAA,MACrG,MAAM,cAAc,OAAO;AAAA,QACvB,KAAK;AAAA,QACL,WAAW;AAAA,MACf,GAAG;AAAA,QACC,MAAM,cAAc,OAAO;AAAA,UACvB,KAAK;AAAA,UACL,WAAW;AAAA,QACf,GAAG,gBAAgB;AAAA,MACvB,CAAC;AAAA;AAAA,MAED,MAAM,cAAc,OAAO;AAAA,QACvB,KAAK;AAAA,QACL,WAAW;AAAA,MACf,GAAG;AAAA,QACC,MAAM,cAAc,OAAO;AAAA,UACvB,KAAK;AAAA,UACL,WAAW,oDAAoD,iBAAiB,+CAA+C,0CAA0C;AAAA,QAC7K,GAAG;AAAA,UACC,MAAM,cAAc,QAAQ;AAAA,YACxB,KAAK;AAAA,YACL,WAAW;AAAA,UACf,GAAG,oBAAoB;AAAA,UACvB,MAAM,cAAc,OAAO;AAAA,YACvB,KAAK;AAAA,YACL,WAAW;AAAA,UACf,GAAG;AAAA,YACC,MAAM,cAAc,KAAK;AAAA,cACrB,KAAK;AAAA,cACL,WAAW,OAAO,iBAAiB,mCAAmC,wBAAwB;AAAA,YAClG,CAAC;AAAA,YACD,MAAM,cAAc,QAAQ;AAAA,cACxB,KAAK;AAAA,cACL,WAAW,WAAW,iBAAiB,mBAAmB,eAAe;AAAA,YAC7E,GAAG,iBAAiB,cAAc,SAAS;AAAA,UAC/C,CAAC;AAAA,QACL,CAAC;AAAA,QACD,MAAM,cAAc,OAAO;AAAA,UACvB,KAAK;AAAA,UACL,WAAW,oDAAoD,kBAAkB,+CAA+C,0CAA0C;AAAA,QAC9K,GAAG;AAAA,UACC,MAAM,cAAc,QAAQ;AAAA,YACxB,KAAK;AAAA,YACL,WAAW;AAAA,UACf,GAAG,oBAAoB;AAAA,UACvB,MAAM,cAAc,OAAO;AAAA,YACvB,KAAK;AAAA,YACL,WAAW;AAAA,UACf,GAAG;AAAA,YACC,MAAM,cAAc,KAAK;AAAA,cACrB,KAAK;AAAA,cACL,WAAW,OAAO,kBAAkB,mCAAmC,wBAAwB;AAAA,YACnG,CAAC;AAAA,YACD,MAAM,cAAc,QAAQ;AAAA,cACxB,KAAK;AAAA,cACL,WAAW,WAAW,kBAAkB,mBAAmB,eAAe;AAAA,YAC9E,GAAG,kBAAkB,cAAc,SAAS;AAAA,UAChD,CAAC;AAAA,QACL,CAAC;AAAA,MACL,CAAC;AAAA,MACD,MAAM,cAAc,OAAO;AAAA,QACvB,KAAK;AAAA,QACL,WAAW;AAAA,MACf,GAAG;AAAA,QACC,MAAM,cAAc,KAAK;AAAA,UACrB,WAAW;AAAA,QACf,GAAG;AAAA,UACC,MAAM,cAAc,KAAK;AAAA,YACrB,WAAW;AAAA,UACf,CAAC;AAAA,UACD;AAAA,QACJ,CAAC;AAAA,MACL,CAAC;AAAA,MACD,MAAM,cAAc,OAAO;AAAA,QACvB,KAAK;AAAA,QACL,WAAW;AAAA,MACf,GAAG;AAAA,QACC,MAAM,cAAc,UAAU;AAAA,UAC1B,KAAK;AAAA,UACL,SAAS;AAAA,UACT,UAAU;AAAA,UACV,WAAW,uEAAuE,iBAAiB,oDAAoD,uBAAuB;AAAA,QAClL,GAAG;AAAA,UACC,MAAM,cAAc,KAAK;AAAA,YACrB,WAAW,OAAO,iBAAiB,oBAAoB,UAAU;AAAA,UACrE,CAAC;AAAA,UACD,iBAAiB,cAAc;AAAA,QACnC,CAAC;AAAA,QACD,MAAM,cAAc,UAAU;AAAA,UAC1B,KAAK;AAAA,UACL,SAAS;AAAA,UACT,WAAW;AAAA,QACf,GAAG;AAAA,UACC,MAAM,cAAc,KAAK;AAAA,YACrB,WAAW;AAAA,UACf,CAAC;AAAA,UACD;AAAA,QACJ,CAAC;AAAA,MACL,CAAC;AAAA,IACL,CAAC;AAAA,EACL,CAAC;AACL;AAGA,IAAM,sBAAsB,CAAC,EAAE,SAAS,MAAM,UAAU,MAAM;AAC1D,QAAM,aAAa,CAAC,OAAO;AACvB,WAAO,IAAI,KAAK,EAAE,EAAE,mBAAmB,SAAS;AAAA,MAC5C,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,QAAQ;AAAA,IACZ,CAAC;AAAA,EACL;AAEA,QAAM,kBAAkB,MAAM;AAC1B,YAAQ,MAAM;AAAA,MACV,KAAK;AACD,eAAO;AAAA,UACH,WAAW;AAAA,UACX,MAAM;AAAA,UACN,OAAO;AAAA,QACX;AAAA,MACJ,KAAK;AACD,eAAO;AAAA,UACH,WAAW;AAAA,UACX,MAAM;AAAA,UACN,OAAO;AAAA,QACX;AAAA,MACJ,KAAK;AACD,eAAO;AAAA,UACH,WAAW;AAAA,UACX,MAAM;AAAA,UACN,OAAO;AAAA,QACX;AAAA,MACJ;AACI,eAAO;AAAA,UACH,WAAW;AAAA,UACX,MAAM;AAAA,UACN,OAAO;AAAA,QACX;AAAA,IACR;AAAA,EACJ;AAEA,QAAM,QAAQ,gBAAgB;AAE9B,SAAO,MAAM,cAAc,OAAO;AAAA,IAC9B,WAAW,0DAA0D,MAAM,SAAS;AAAA,EACxF,GAAG;AAAA,IACC,MAAM,cAAc,OAAO;AAAA,MACvB,KAAK;AAAA,MACL,WAAW;AAAA,IACf,GAAG;AAAA,MACC,MAAM,cAAc,KAAK;AAAA,QACrB,KAAK;AAAA,QACL,WAAW,GAAG,MAAM,IAAI;AAAA,MAC5B,CAAC;AAAA,MACD,MAAM,cAAc,OAAO;AAAA,QACvB,KAAK;AAAA,QACL,WAAW;AAAA,MACf,GAAG;AAAA,QACC,MAAM,cAAc,OAAO;AAAA,UACvB,KAAK;AAAA,UACL,WAAW;AAAA,QACf,GAAG,OAAO;AAAA,QACV,aAAa,MAAM,cAAc,OAAO;AAAA,UACpC,KAAK;AAAA,UACL,WAAW;AAAA,QACf,GAAG;AAAA,UACC,MAAM,cAAc,QAAQ;AAAA,YACxB,KAAK;AAAA,UACT,GAAG,WAAW,SAAS,CAAC;AAAA,UACxB,MAAM,cAAc,QAAQ;AAAA,YACxB,KAAK;AAAA,YACL,WAAW;AAAA,UACf,GAAG,MAAM,KAAK;AAAA,QAClB,CAAC;AAAA,MACL,CAAC;AAAA,IACL,CAAC;AAAA,EACL,CAAC;AACL;AAGA,IAAM,0BAA0B,CAAC;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ,MAAM;AACF,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,QAAQ;AAE/C,QAAM,gBAAgB,MALine truncated
|
||||
"names": ["offerData", "payload"]
|
||||
"sourcesContent": ["\r\n // Enhanced Copy Button with better UX\r\n const EnhancedCopyButton = ({ text, className = \"\", children }) => {\r\n const [copied, setCopied] = React.useState(false);\r\n \r\n const handleCopy = async () => {\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n setCopied(true);\r\n setTimeout(() => setCopied(false), 2000);\r\n } catch (error) {\r\n console.error('Copy failed:', error);\r\n // Fallback for older browsers\r\n const textArea = document.createElement('textarea');\r\n textArea.value = text;\r\n document.body.appendChild(textArea);\r\n textArea.select();\r\n document.execCommand('copy');\r\n document.body.removeChild(textArea);\r\n setCopied(true);\r\n setTimeout(() => setCopied(false), 2000);\r\n }\r\n };\r\n \r\n return React.createElement('button', {\r\n onClick: handleCopy,\r\n className: `${className} transition-all duration-200`\r\n }, [\r\n React.createElement('i', {\r\n key: 'icon',\r\n className: `${copied ? 'fas fa-check accent-green' : 'fas fa-copy text-secondary'} mr-2`\r\n }),\r\n copied ? 'Copied!' : children\r\n ]);\r\n };\r\n \r\n // Verification Component\r\n const VerificationStep = ({ verificationCode, onConfirm, onReject, localConfirmed, remoteConfirmed, bothConfirmed }) => {\r\n return React.createElement('div', {\r\n className: \"card-minimal rounded-xl p-6 border-purple-500/20\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'header',\r\n className: \"flex items-center mb-4\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'icon',\r\n className: \"w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3\"\r\n }, [\r\n React.createElement('i', {\r\n className: 'fas fa-shield-alt accent-purple'\r\n })\r\n ]),\r\n React.createElement('h3', {\r\n key: 'title',\r\n className: \"text-lg font-medium text-primary\"\r\n }, \"Security verification\")\r\n ]),\r\n React.createElement('div', {\r\n key: 'content',\r\n className: \"space-y-4\"\r\n }, [\r\n React.createElement('p', {\r\n key: 'description',\r\n className: \"text-secondary text-sm\"\r\n }, \"Verify the security code with your contact via another communication channel (voice, SMS, etc.):\"),\r\n React.createElement('div', {\r\n key: 'code-display',\r\n className: \"text-center\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'code',\r\n className: \"verification-code text-2xl py-4\"\r\n }, verificationCode)\r\n ]),\r\n // Verification status indicators\r\n React.createElement('div', {\r\n key: 'verification-status',\r\n className: \"space-y-2\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'local-status',\r\n className: `flex items-center justify-between p-2 rounded-lg ${localConfirmed ? 'bg-green-500/10 border border-green-500/20' : 'bg-gray-500/10 border border-gray-500/20'}`\r\n }, [\r\n React.createElement('span', {\r\n Line truncated
|
||||
"mappings": "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 truncated
|
||||
"names": ["webrtcManagerRef", "offerData", "payload"]
|
||||
}
|
||||
+178
-6
@@ -280,9 +280,11 @@
|
||||
toggleQrManualMode,
|
||||
nextQrFrame,
|
||||
prevQrFrame,
|
||||
markAnswerCreated
|
||||
markAnswerCreated,
|
||||
notificationIntegrationRef
|
||||
}) => {
|
||||
const [mode, setMode] = React.useState('select');
|
||||
const [notificationPermissionRequested, setNotificationPermissionRequested] = React.useState(false);
|
||||
|
||||
const resetToSelect = () => {
|
||||
setMode('select');
|
||||
@@ -296,6 +298,110 @@
|
||||
const handleVerificationReject = () => {
|
||||
onVerifyConnection(false);
|
||||
};
|
||||
|
||||
// Request notification permission on first user interaction
|
||||
const requestNotificationPermissionOnInteraction = async () => {
|
||||
if (notificationPermissionRequested) {
|
||||
return; // Already requested
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if Notification API is supported
|
||||
if (!('Notification' in window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're in a secure context
|
||||
if (!window.isSecureContext && window.location.protocol !== 'https:' && window.location.hostname !== 'localhost') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check current permission status
|
||||
const currentPermission = Notification.permission;
|
||||
|
||||
// Only request if permission is default (not granted or denied)
|
||||
if (currentPermission === 'default') {
|
||||
const permission = await Notification.requestPermission();
|
||||
|
||||
if (permission === 'granted') {
|
||||
// Initialize notification integration immediately
|
||||
try {
|
||||
if (window.NotificationIntegration && webrtcManagerRef.current) {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
|
||||
await integration.init();
|
||||
|
||||
// Store reference for cleanup
|
||||
notificationIntegrationRef.current = integration;
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
|
||||
// Send welcome notification
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const welcomeNotification = new Notification('SecureBit Chat', {
|
||||
body: 'Notifications enabled! You will receive alerts for new messages.',
|
||||
icon: '/logo/icon-192x192.png',
|
||||
tag: 'welcome-notification'
|
||||
});
|
||||
|
||||
welcomeNotification.onclick = () => {
|
||||
welcomeNotification.close();
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
welcomeNotification.close();
|
||||
}, 5000);
|
||||
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
}
|
||||
} else if (currentPermission === 'granted') {
|
||||
// Initialize notification integration immediately
|
||||
try {
|
||||
if (window.NotificationIntegration && webrtcManagerRef.current && !notificationIntegrationRef.current) {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
|
||||
await integration.init();
|
||||
|
||||
// Store reference for cleanup
|
||||
notificationIntegrationRef.current = integration;
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
|
||||
// Test notification to confirm it works
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const testNotification = new Notification('SecureBit Chat', {
|
||||
body: 'Notifications are working! You will receive alerts for new messages.',
|
||||
icon: '/logo/icon-192x192.png',
|
||||
tag: 'test-notification'
|
||||
});
|
||||
|
||||
testNotification.onclick = () => {
|
||||
testNotification.close();
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
testNotification.close();
|
||||
}, 5000);
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
setNotificationPermissionRequested(true);
|
||||
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
};
|
||||
|
||||
if (showVerification) {
|
||||
return React.createElement('div', {
|
||||
@@ -346,7 +452,10 @@
|
||||
// Create Connection
|
||||
React.createElement('div', {
|
||||
key: 'create',
|
||||
onClick: () => setMode('create'),
|
||||
onClick: () => {
|
||||
requestNotificationPermissionOnInteraction();
|
||||
setMode('create');
|
||||
},
|
||||
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 create"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
@@ -418,7 +527,10 @@
|
||||
// Join Connection
|
||||
React.createElement('div', {
|
||||
key: 'join',
|
||||
onClick: () => setMode('join'),
|
||||
onClick: () => {
|
||||
requestNotificationPermissionOnInteraction();
|
||||
setMode('join');
|
||||
},
|
||||
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 join"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
@@ -1023,6 +1135,7 @@
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const EnhancedChatInterface = ({
|
||||
messages,
|
||||
messageInput,
|
||||
@@ -1453,6 +1566,7 @@
|
||||
}, []);
|
||||
|
||||
const webrtcManagerRef = React.useRef(null);
|
||||
const notificationIntegrationRef = React.useRef(null);
|
||||
// Expose for modules/UI that run outside this closure (e.g., inline handlers)
|
||||
// Safe because it's a ref object and we maintain it centrally here
|
||||
window.webrtcManagerRef = webrtcManagerRef;
|
||||
@@ -1785,6 +1899,20 @@
|
||||
handleVerificationStateChange
|
||||
);
|
||||
|
||||
// Initialize notification integration if permission was already granted
|
||||
if (Notification.permission === 'granted' && window.NotificationIntegration && !notificationIntegrationRef.current) {
|
||||
try {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
|
||||
integration.init().then(() => {
|
||||
notificationIntegrationRef.current = integration;
|
||||
}).catch((error) => {
|
||||
// Handle error silently
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(' SecureBit.chat Enhanced Security Edition v4.3.120 - 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) => {
|
||||
@@ -3083,11 +3211,47 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyConnection = (isValid) => {
|
||||
const handleVerifyConnection = async (isValid) => {
|
||||
if (isValid) {
|
||||
webrtcManagerRef.current.confirmVerification();
|
||||
// Mark local verification as confirmed
|
||||
setLocalVerificationConfirmed(true);
|
||||
|
||||
// Initialize notification integration if permission was granted
|
||||
try {
|
||||
if (window.NotificationIntegration && webrtcManagerRef.current && !notificationIntegrationRef.current) {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
|
||||
await integration.init();
|
||||
|
||||
// Store reference for cleanup
|
||||
notificationIntegrationRef.current = integration;
|
||||
|
||||
|
||||
// Check if permission was already granted
|
||||
const status = integration.getStatus();
|
||||
if (status.permission === 'granted') {
|
||||
setMessages(prev => [...prev, {
|
||||
message: '✓ Notifications enabled - you will receive alerts when the tab is inactive',
|
||||
type: 'system',
|
||||
id: Date.now(),
|
||||
timestamp: Date.now()
|
||||
}]);
|
||||
} else {
|
||||
setMessages(prev => [...prev, {
|
||||
message: 'ℹ Notifications disabled - you can enable them using the button on the main page',
|
||||
type: 'system',
|
||||
id: Date.now(),
|
||||
timestamp: Date.now()
|
||||
}]);
|
||||
}
|
||||
} else if (notificationIntegrationRef.current) {
|
||||
} else {
|
||||
// Handle error silently
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to initialize notifications:', error);
|
||||
// Don't show error to user, notifications are optional
|
||||
}
|
||||
} else {
|
||||
setMessages(prev => [...prev, {
|
||||
message: ' Verification rejected. The connection is unsafe! Session reset..',
|
||||
@@ -3218,6 +3382,12 @@
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
}
|
||||
|
||||
// Cleanup notification integration
|
||||
if (notificationIntegrationRef.current) {
|
||||
notificationIntegrationRef.current.cleanup();
|
||||
notificationIntegrationRef.current = null;
|
||||
}
|
||||
|
||||
// Clear all connection-related states
|
||||
setKeyFingerprint('');
|
||||
@@ -3476,7 +3646,8 @@
|
||||
nextQrFrame: nextQrFrame,
|
||||
prevQrFrame: prevQrFrame,
|
||||
// PAKE passwords removed - using SAS verification instead
|
||||
markAnswerCreated: markAnswerCreated
|
||||
markAnswerCreated: markAnswerCreated,
|
||||
notificationIntegrationRef: notificationIntegrationRef
|
||||
})
|
||||
),
|
||||
|
||||
@@ -3586,6 +3757,7 @@
|
||||
if (!window.initializeApp) {
|
||||
window.initializeApp = initializeApp;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Render Enhanced Application
|
||||
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
|
||||
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* Notification Integration Module for SecureBit WebRTC Chat
|
||||
* Integrates secure notifications with existing WebRTC architecture
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author SecureBit Team
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { SecureChatNotificationManager } from './SecureNotificationManager.js';
|
||||
|
||||
class NotificationIntegration {
|
||||
constructor(webrtcManager) {
|
||||
this.webrtcManager = webrtcManager;
|
||||
this.notificationManager = new SecureChatNotificationManager({
|
||||
maxQueueSize: 10,
|
||||
rateLimitMs: 1000, // Reduced from 2000ms to 1000ms
|
||||
trustedOrigins: [
|
||||
window.location.origin,
|
||||
// Add other trusted origins for CDN icons
|
||||
]
|
||||
});
|
||||
|
||||
this.isInitialized = false;
|
||||
this.originalOnMessage = null;
|
||||
this.originalOnStatusChange = null;
|
||||
this.processedMessages = new Set(); // Track processed messages to avoid duplicates
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize notification integration
|
||||
* @returns {Promise<boolean>} Initialization success
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
if (this.isInitialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Store original callbacks
|
||||
this.originalOnMessage = this.webrtcManager.onMessage;
|
||||
this.originalOnStatusChange = this.webrtcManager.onStatusChange;
|
||||
|
||||
|
||||
// Wrap the original onMessage callback
|
||||
this.webrtcManager.onMessage = (message, type) => {
|
||||
this.handleIncomingMessage(message, type);
|
||||
|
||||
// Call original callback if it exists
|
||||
if (this.originalOnMessage) {
|
||||
this.originalOnMessage(message, type);
|
||||
}
|
||||
};
|
||||
|
||||
// Wrap the original onStatusChange callback
|
||||
this.webrtcManager.onStatusChange = (status) => {
|
||||
this.handleStatusChange(status);
|
||||
|
||||
// Call original callback if it exists
|
||||
if (this.originalOnStatusChange) {
|
||||
this.originalOnStatusChange(status);
|
||||
}
|
||||
};
|
||||
|
||||
// Also hook into the deliverMessageToUI method if it exists
|
||||
if (this.webrtcManager.deliverMessageToUI) {
|
||||
this.originalDeliverMessageToUI = this.webrtcManager.deliverMessageToUI.bind(this.webrtcManager);
|
||||
this.webrtcManager.deliverMessageToUI = (message, type) => {
|
||||
this.handleIncomingMessage(message, type);
|
||||
this.originalDeliverMessageToUI(message, type);
|
||||
};
|
||||
}
|
||||
|
||||
this.isInitialized = true;
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming messages and trigger notifications
|
||||
* @param {*} message - Message content
|
||||
* @param {string} type - Message type
|
||||
* @private
|
||||
*/
|
||||
handleIncomingMessage(message, type) {
|
||||
try {
|
||||
// Create a unique key for this message to avoid duplicates
|
||||
const messageKey = `${type}:${typeof message === 'string' ? message : JSON.stringify(message)}`;
|
||||
|
||||
// Skip if we've already processed this message
|
||||
if (this.processedMessages.has(messageKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark message as processed
|
||||
this.processedMessages.add(messageKey);
|
||||
|
||||
// Clean up old processed messages (keep only last 100)
|
||||
if (this.processedMessages.size > 100) {
|
||||
const messagesArray = Array.from(this.processedMessages);
|
||||
this.processedMessages.clear();
|
||||
messagesArray.slice(-50).forEach(msg => this.processedMessages.add(msg));
|
||||
}
|
||||
|
||||
|
||||
// Only process chat messages, not system messages
|
||||
if (type === 'system' || type === 'file-transfer' || type === 'heartbeat') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract message information
|
||||
const messageInfo = this.extractMessageInfo(message, type);
|
||||
if (!messageInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send notification
|
||||
const notificationResult = this.notificationManager.notify(
|
||||
messageInfo.senderName,
|
||||
messageInfo.text,
|
||||
{
|
||||
icon: messageInfo.senderAvatar,
|
||||
senderId: messageInfo.senderId,
|
||||
onClick: (senderId) => {
|
||||
this.focusChatWindow();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle status changes
|
||||
* @param {string} status - Connection status
|
||||
* @private
|
||||
*/
|
||||
handleStatusChange(status) {
|
||||
try {
|
||||
// Clear notifications when connection is lost
|
||||
if (status === 'disconnected' || status === 'failed') {
|
||||
this.notificationManager.clearNotificationQueue();
|
||||
this.notificationManager.resetUnreadCount();
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract message information for notifications
|
||||
* @param {*} message - Message content
|
||||
* @param {string} type - Message type
|
||||
* @returns {Object|null} Extracted message info or null
|
||||
* @private
|
||||
*/
|
||||
extractMessageInfo(message, type) {
|
||||
try {
|
||||
let messageData = message;
|
||||
|
||||
// Handle different message formats
|
||||
if (typeof message === 'string') {
|
||||
try {
|
||||
messageData = JSON.parse(message);
|
||||
} catch (e) {
|
||||
// Plain text message
|
||||
return {
|
||||
senderName: 'Peer',
|
||||
text: message,
|
||||
senderId: 'peer',
|
||||
senderAvatar: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle structured message data
|
||||
if (typeof messageData === 'object' && messageData !== null) {
|
||||
return {
|
||||
senderName: messageData.senderName || messageData.name || 'Peer',
|
||||
text: messageData.text || messageData.message || messageData.content || '',
|
||||
senderId: messageData.senderId || messageData.id || 'peer',
|
||||
senderAvatar: messageData.senderAvatar || messageData.avatar || null
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus chat window when notification is clicked
|
||||
* @private
|
||||
*/
|
||||
focusChatWindow() {
|
||||
try {
|
||||
window.focus();
|
||||
|
||||
// Scroll to bottom of messages if container exists
|
||||
const messagesContainer = document.getElementById('messages');
|
||||
if (messagesContainer) {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request notification permission
|
||||
* @returns {Promise<boolean>} Permission granted status
|
||||
*/
|
||||
async requestPermission() {
|
||||
try {
|
||||
return await this.notificationManager.requestPermission();
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification status
|
||||
* @returns {Object} Notification status
|
||||
*/
|
||||
getStatus() {
|
||||
return this.notificationManager.getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all notifications
|
||||
*/
|
||||
clearNotifications() {
|
||||
this.notificationManager.clearNotificationQueue();
|
||||
this.notificationManager.resetUnreadCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup integration
|
||||
*/
|
||||
cleanup() {
|
||||
try {
|
||||
if (this.isInitialized) {
|
||||
// Restore original callbacks
|
||||
if (this.originalOnMessage) {
|
||||
this.webrtcManager.onMessage = this.originalOnMessage;
|
||||
}
|
||||
if (this.originalOnStatusChange) {
|
||||
this.webrtcManager.onStatusChange = this.originalOnStatusChange;
|
||||
}
|
||||
if (this.originalDeliverMessageToUI) {
|
||||
this.webrtcManager.deliverMessageToUI = this.originalDeliverMessageToUI;
|
||||
}
|
||||
|
||||
// Clear notifications
|
||||
this.clearNotifications();
|
||||
|
||||
this.isInitialized = false;
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { NotificationIntegration };
|
||||
}
|
||||
|
||||
// Global export for browser usage
|
||||
if (typeof window !== 'undefined') {
|
||||
window.NotificationIntegration = NotificationIntegration;
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
/**
|
||||
* Secure and Reliable Notification Manager for P2P WebRTC Chat
|
||||
* Follows best practices: OWASP, MDN, Chrome DevRel
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author SecureBit Team
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
class SecureChatNotificationManager {
|
||||
constructor(config = {}) {
|
||||
this.permission = Notification.permission;
|
||||
this.isTabActive = this.checkTabActive(); // Initialize with proper check
|
||||
this.unreadCount = 0;
|
||||
this.originalTitle = document.title;
|
||||
this.notificationQueue = [];
|
||||
this.maxQueueSize = config.maxQueueSize || 5;
|
||||
this.rateLimitMs = config.rateLimitMs || 2000; // Spam protection
|
||||
this.lastNotificationTime = 0;
|
||||
this.trustedOrigins = config.trustedOrigins || [];
|
||||
|
||||
// Secure context flag
|
||||
this.isSecureContext = window.isSecureContext;
|
||||
|
||||
// Cross-browser compatibility for Page Visibility API
|
||||
this.hidden = this.getHiddenProperty();
|
||||
this.visibilityChange = this.getVisibilityChangeEvent();
|
||||
|
||||
this.initVisibilityTracking();
|
||||
this.initSecurityChecks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize security checks and validation
|
||||
* @private
|
||||
*/
|
||||
initSecurityChecks() {
|
||||
// Security checks are performed silently
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hidden property name for cross-browser compatibility
|
||||
* @returns {string} Hidden property name
|
||||
* @private
|
||||
*/
|
||||
getHiddenProperty() {
|
||||
if (typeof document.hidden !== "undefined") {
|
||||
return "hidden";
|
||||
} else if (typeof document.msHidden !== "undefined") {
|
||||
return "msHidden";
|
||||
} else if (typeof document.webkitHidden !== "undefined") {
|
||||
return "webkitHidden";
|
||||
}
|
||||
return "hidden"; // fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visibility change event name for cross-browser compatibility
|
||||
* @returns {string} Visibility change event name
|
||||
* @private
|
||||
*/
|
||||
getVisibilityChangeEvent() {
|
||||
if (typeof document.hidden !== "undefined") {
|
||||
return "visibilitychange";
|
||||
} else if (typeof document.msHidden !== "undefined") {
|
||||
return "msvisibilitychange";
|
||||
} else if (typeof document.webkitHidden !== "undefined") {
|
||||
return "webkitvisibilitychange";
|
||||
}
|
||||
return "visibilitychange"; // fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tab is currently active using multiple methods
|
||||
* @returns {boolean} True if tab is active
|
||||
* @private
|
||||
*/
|
||||
checkTabActive() {
|
||||
// Primary method: Page Visibility API
|
||||
if (this.hidden && typeof document[this.hidden] !== "undefined") {
|
||||
return !document[this.hidden];
|
||||
}
|
||||
|
||||
// Fallback method: document.hasFocus()
|
||||
if (typeof document.hasFocus === "function") {
|
||||
return document.hasFocus();
|
||||
}
|
||||
|
||||
// Ultimate fallback: assume active
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize page visibility tracking (Page Visibility API)
|
||||
* @private
|
||||
*/
|
||||
initVisibilityTracking() {
|
||||
// Primary method: Page Visibility API with cross-browser support
|
||||
if (typeof document.addEventListener !== "undefined" && typeof document[this.hidden] !== "undefined") {
|
||||
document.addEventListener(this.visibilityChange, () => {
|
||||
this.isTabActive = this.checkTabActive();
|
||||
|
||||
if (this.isTabActive) {
|
||||
this.resetUnreadCount();
|
||||
this.clearNotificationQueue();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback method: Window focus/blur events
|
||||
window.addEventListener('focus', () => {
|
||||
this.isTabActive = this.checkTabActive();
|
||||
if (this.isTabActive) {
|
||||
this.resetUnreadCount();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('blur', () => {
|
||||
this.isTabActive = this.checkTabActive();
|
||||
});
|
||||
|
||||
// Page unload cleanup
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this.clearNotificationQueue();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Request notification permission (BEST PRACTICE: Only call in response to user action)
|
||||
* Never call on page load!
|
||||
* @returns {Promise<boolean>} Permission granted status
|
||||
*/
|
||||
async requestPermission() {
|
||||
// Secure context check
|
||||
if (!this.isSecureContext || !('Notification' in window)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.permission === 'granted') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.permission === 'denied') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.permission = await Notification.requestPermission();
|
||||
return this.permission === 'granted';
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update page title with unread count
|
||||
* @private
|
||||
*/
|
||||
updateTitle() {
|
||||
if (this.unreadCount > 0) {
|
||||
document.title = `(${this.unreadCount}) ${this.originalTitle}`;
|
||||
} else {
|
||||
document.title = this.originalTitle;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* XSS Protection: Sanitize input text
|
||||
* @param {string} text - Text to sanitize
|
||||
* @returns {string} Sanitized text
|
||||
* @private
|
||||
*/
|
||||
sanitizeText(text) {
|
||||
if (typeof text !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Remove HTML tags and potentially dangerous characters
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.substring(0, 500); // Length limit
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate icon URL (XSS protection)
|
||||
* @param {string} url - URL to validate
|
||||
* @returns {string|null} Validated URL or null
|
||||
* @private
|
||||
*/
|
||||
validateIconUrl(url) {
|
||||
if (!url) return null;
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(url, window.location.origin);
|
||||
|
||||
// Only allow HTTPS and data URLs
|
||||
if (parsedUrl.protocol === 'https:' || parsedUrl.protocol === 'data:') {
|
||||
// Check trusted origins if specified
|
||||
if (this.trustedOrigins.length > 0) {
|
||||
const isTrusted = this.trustedOrigins.some(origin =>
|
||||
parsedUrl.origin === origin
|
||||
);
|
||||
return isTrusted ? parsedUrl.href : null;
|
||||
}
|
||||
return parsedUrl.href;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting for spam protection
|
||||
* @returns {boolean} Rate limit check passed
|
||||
* @private
|
||||
*/
|
||||
checkRateLimit() {
|
||||
const now = Date.now();
|
||||
if (now - this.lastNotificationTime < this.rateLimitMs) {
|
||||
return false;
|
||||
}
|
||||
this.lastNotificationTime = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send secure notification
|
||||
* @param {string} senderName - Name of message sender
|
||||
* @param {string} message - Message content
|
||||
* @param {Object} options - Notification options
|
||||
* @returns {Notification|null} Created notification or null
|
||||
*/
|
||||
notify(senderName, message, options = {}) {
|
||||
// Update tab active state before checking
|
||||
this.isTabActive = this.checkTabActive();
|
||||
|
||||
// Only show if tab is NOT active (user is on another tab or minimized)
|
||||
if (this.isTabActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if (this.permission !== 'granted') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
if (!this.checkRateLimit()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Data sanitization (XSS Protection)
|
||||
const safeSenderName = this.sanitizeText(senderName || 'Unknown');
|
||||
const safeMessage = this.sanitizeText(message || '');
|
||||
const safeIcon = this.validateIconUrl(options.icon) || '/logo/icon-192x192.png';
|
||||
|
||||
// Queue overflow protection
|
||||
if (this.notificationQueue.length >= this.maxQueueSize) {
|
||||
this.clearNotificationQueue();
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const notification = new Notification(
|
||||
`${safeSenderName}`,
|
||||
{
|
||||
body: safeMessage.substring(0, 200), // Length limit
|
||||
icon: safeIcon,
|
||||
badge: safeIcon,
|
||||
tag: `chat-${options.senderId || 'unknown'}`, // Grouping
|
||||
requireInteraction: false, // Don't block user
|
||||
silent: options.silent || false,
|
||||
// Vibrate only for mobile and if supported
|
||||
vibrate: navigator.vibrate ? [200, 100, 200] : undefined,
|
||||
// Safe metadata
|
||||
data: {
|
||||
senderId: this.sanitizeText(options.senderId),
|
||||
timestamp: Date.now(),
|
||||
// Don't include sensitive data!
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Increment counter
|
||||
this.unreadCount++;
|
||||
this.updateTitle();
|
||||
|
||||
// Add to queue for management
|
||||
this.notificationQueue.push(notification);
|
||||
|
||||
// Safe click handler
|
||||
notification.onclick = (event) => {
|
||||
event.preventDefault(); // Prevent default behavior
|
||||
window.focus();
|
||||
notification.close();
|
||||
|
||||
// Safe callback
|
||||
if (typeof options.onClick === 'function') {
|
||||
try {
|
||||
options.onClick(options.senderId);
|
||||
} catch (error) {
|
||||
console.error('[Notifications] Error in onClick handler:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Error handler
|
||||
notification.onerror = (event) => {
|
||||
console.error('[Notifications] Error showing notification:', event);
|
||||
};
|
||||
|
||||
// Auto-close after reasonable time
|
||||
const autoCloseTimeout = Math.min(options.autoClose || 5000, 10000);
|
||||
setTimeout(() => {
|
||||
notification.close();
|
||||
this.removeFromQueue(notification);
|
||||
}, autoCloseTimeout);
|
||||
|
||||
return notification;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Notifications] Failed to create notification:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove notification from queue
|
||||
* @param {Notification} notification - Notification to remove
|
||||
* @private
|
||||
*/
|
||||
removeFromQueue(notification) {
|
||||
const index = this.notificationQueue.indexOf(notification);
|
||||
if (index > -1) {
|
||||
this.notificationQueue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all notifications
|
||||
*/
|
||||
clearNotificationQueue() {
|
||||
this.notificationQueue.forEach(notification => {
|
||||
try {
|
||||
notification.close();
|
||||
} catch (error) {
|
||||
// Ignore errors when closing
|
||||
}
|
||||
});
|
||||
this.notificationQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset unread counter
|
||||
*/
|
||||
resetUnreadCount() {
|
||||
this.unreadCount = 0;
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current status
|
||||
* @returns {Object} Current notification status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
permission: this.permission,
|
||||
isTabActive: this.isTabActive,
|
||||
unreadCount: this.unreadCount,
|
||||
isSecureContext: this.isSecureContext,
|
||||
queueSize: this.notificationQueue.length
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure integration with WebRTC
|
||||
*/
|
||||
class SecureP2PChat {
|
||||
constructor() {
|
||||
this.notificationManager = new SecureChatNotificationManager({
|
||||
maxQueueSize: 5,
|
||||
rateLimitMs: 2000,
|
||||
trustedOrigins: [
|
||||
window.location.origin,
|
||||
// Add other trusted origins for CDN icons
|
||||
]
|
||||
});
|
||||
|
||||
this.dataChannel = null;
|
||||
this.peerConnection = null;
|
||||
this.remotePeerName = 'Peer';
|
||||
this.messageHistory = [];
|
||||
this.maxHistorySize = 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize when user connects
|
||||
*/
|
||||
async init() {
|
||||
// Initialize notification manager silently
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for manual permission request (called on click)
|
||||
* @returns {Promise<boolean>} Permission granted status
|
||||
*/
|
||||
async enableNotifications() {
|
||||
const granted = await this.notificationManager.requestPermission();
|
||||
return granted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup DataChannel with security checks
|
||||
* @param {RTCDataChannel} dataChannel - WebRTC data channel
|
||||
*/
|
||||
setupDataChannel(dataChannel) {
|
||||
if (!dataChannel) {
|
||||
console.error('[Chat] Invalid DataChannel');
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataChannel = dataChannel;
|
||||
|
||||
// Setup handlers
|
||||
this.dataChannel.onmessage = (event) => {
|
||||
this.handleIncomingMessage(event.data);
|
||||
};
|
||||
|
||||
this.dataChannel.onerror = (error) => {
|
||||
// Handle error silently
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* XSS Protection: Validate incoming messages
|
||||
* @param {string|Object} data - Message data
|
||||
* @returns {Object|null} Validated message or null
|
||||
* @private
|
||||
*/
|
||||
validateMessage(data) {
|
||||
try {
|
||||
const message = typeof data === 'string' ? JSON.parse(data) : data;
|
||||
|
||||
// Check message structure
|
||||
if (!message || typeof message !== 'object') {
|
||||
throw new Error('Invalid message structure');
|
||||
}
|
||||
|
||||
// Check required fields
|
||||
if (!message.text || typeof message.text !== 'string') {
|
||||
throw new Error('Invalid message text');
|
||||
}
|
||||
|
||||
// Message length limit (DoS protection)
|
||||
if (message.text.length > 10000) {
|
||||
throw new Error('Message too long');
|
||||
}
|
||||
|
||||
return {
|
||||
text: message.text,
|
||||
senderName: message.senderName || 'Unknown',
|
||||
senderId: message.senderId || 'unknown',
|
||||
timestamp: message.timestamp || Date.now(),
|
||||
senderAvatar: message.senderAvatar || null
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Chat] Message validation failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure handling of incoming messages
|
||||
* @param {string|Object} data - Message data
|
||||
* @private
|
||||
*/
|
||||
handleIncomingMessage(data) {
|
||||
const message = this.validateMessage(data);
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save to history (with limit)
|
||||
this.messageHistory.push(message);
|
||||
if (this.messageHistory.length > this.maxHistorySize) {
|
||||
this.messageHistory.shift();
|
||||
}
|
||||
|
||||
// Display in UI (with sanitization)
|
||||
this.displayMessage(message);
|
||||
|
||||
// Send notification only if tab is inactive
|
||||
this.notificationManager.notify(
|
||||
message.senderName,
|
||||
message.text,
|
||||
{
|
||||
icon: message.senderAvatar,
|
||||
senderId: message.senderId,
|
||||
onClick: (senderId) => {
|
||||
this.scrollToLatestMessage();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Optional: sound (with check)
|
||||
if (!this.notificationManager.isTabActive) {
|
||||
this.playNotificationSound();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* XSS Protection: Safe message display
|
||||
* @param {Object} message - Message to display
|
||||
* @private
|
||||
*/
|
||||
displayMessage(message) {
|
||||
const container = document.getElementById('messages');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.className = 'message';
|
||||
|
||||
// Use textContent to prevent XSS
|
||||
const nameEl = document.createElement('strong');
|
||||
nameEl.textContent = message.senderName + ': ';
|
||||
|
||||
const textEl = document.createElement('span');
|
||||
textEl.textContent = message.text;
|
||||
|
||||
const timeEl = document.createElement('small');
|
||||
timeEl.textContent = new Date(message.timestamp).toLocaleTimeString();
|
||||
|
||||
messageEl.appendChild(nameEl);
|
||||
messageEl.appendChild(textEl);
|
||||
messageEl.appendChild(document.createElement('br'));
|
||||
messageEl.appendChild(timeEl);
|
||||
|
||||
container.appendChild(messageEl);
|
||||
this.scrollToLatestMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe sound playback
|
||||
* @private
|
||||
*/
|
||||
playNotificationSound() {
|
||||
try {
|
||||
// Use only local audio files
|
||||
const audio = new Audio('/assets/audio/notification.mp3');
|
||||
audio.volume = 0.3; // Moderate volume
|
||||
|
||||
// Error handling
|
||||
audio.play().catch(error => {
|
||||
// Handle audio error silently
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle audio creation error silently
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to latest message
|
||||
* @private
|
||||
*/
|
||||
scrollToLatestMessage() {
|
||||
const container = document.getElementById('messages');
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status
|
||||
* @returns {Object} Current chat status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
notifications: this.notificationManager.getStatus(),
|
||||
messageCount: this.messageHistory.length,
|
||||
connected: this.dataChannel?.readyState === 'open'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { SecureChatNotificationManager, SecureP2PChat };
|
||||
}
|
||||
|
||||
// Global export for browser usage
|
||||
if (typeof window !== 'undefined') {
|
||||
window.SecureChatNotificationManager = SecureChatNotificationManager;
|
||||
window.SecureP2PChat = SecureP2PChat;
|
||||
}
|
||||
@@ -96,7 +96,7 @@ class PWAInstallPrompt {
|
||||
|
||||
setupEventListeners() {
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
event.preventDefault();
|
||||
// Don't prevent default - let browser show its own banner
|
||||
this.deferredPrompt = event;
|
||||
|
||||
if (this.checkInstallationStatus()) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EnhancedSecureCryptoUtils } from '../crypto/EnhancedSecureCryptoUtils.js';
|
||||
import { EnhancedSecureWebRTCManager } from '../network/EnhancedSecureWebRTCManager.js';
|
||||
import { EnhancedSecureFileTransfer } from '../transfer/EnhancedSecureFileTransfer.js';
|
||||
import { NotificationIntegration } from '../notifications/NotificationIntegration.js';
|
||||
|
||||
// Import UI components (side-effect: they attach themselves to window.*)
|
||||
import '../components/ui/Header.jsx';
|
||||
@@ -16,6 +17,7 @@ import '../components/ui/FileTransfer.jsx';
|
||||
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
|
||||
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
||||
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
|
||||
window.NotificationIntegration = NotificationIntegration;
|
||||
|
||||
// Mount application once DOM and modules are ready
|
||||
const start = () => {
|
||||
|
||||
Vendored
+10
-1
@@ -3,10 +3,13 @@
|
||||
(async () => {
|
||||
try {
|
||||
const timestamp = Date.now();
|
||||
const [cryptoModule, webrtcModule, fileTransferModule] = await Promise.all([
|
||||
const [cryptoModule, webrtcModule, fileTransferModule, notificationModule, notificationTestModule, notificationGestureTestModule] = await Promise.all([
|
||||
import(`../crypto/EnhancedSecureCryptoUtils.js?v=${timestamp}`),
|
||||
import(`../network/EnhancedSecureWebRTCManager.js?v=${timestamp}`),
|
||||
import(`../transfer/EnhancedSecureFileTransfer.js?v=${timestamp}`),
|
||||
import(`../notifications/NotificationIntegration.js?v=${timestamp}`),
|
||||
import(`../notifications/NotificationTest.js?v=${timestamp}`),
|
||||
import(`../notifications/NotificationGestureTest.js?v=${timestamp}`),
|
||||
]);
|
||||
|
||||
const { EnhancedSecureCryptoUtils } = cryptoModule;
|
||||
@@ -15,6 +18,12 @@
|
||||
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
||||
const { EnhancedSecureFileTransfer } = fileTransferModule;
|
||||
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
|
||||
const { NotificationIntegration } = notificationModule;
|
||||
window.NotificationIntegration = NotificationIntegration;
|
||||
const { NotificationTest } = notificationTestModule;
|
||||
window.NotificationTest = NotificationTest;
|
||||
const { NotificationGestureTest } = notificationGestureTestModule;
|
||||
window.NotificationGestureTest = NotificationGestureTest;
|
||||
|
||||
// Load React components using dynamic imports instead of eval
|
||||
const componentModules = await Promise.all([
|
||||
|
||||
@@ -27,11 +27,7 @@ const STATIC_ASSETS = [
|
||||
'/src/pwa/pwa-manager.js',
|
||||
'/src/pwa/install-prompt.js',
|
||||
'/src/scripts/pwa-register.js',
|
||||
'/src/scripts/pwa-offline-test.js',
|
||||
|
||||
// Bluetooth key transfer (PWA feature)
|
||||
'/src/transfer/BluetoothKeyTransfer.js',
|
||||
'/src/components/ui/BluetoothKeyTransfer.jsx'
|
||||
'/src/scripts/pwa-offline-test.js'
|
||||
];
|
||||
|
||||
// Sensitive files that should never be cached
|
||||
|
||||
Reference in new issue
Block a user