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:
20
assets/audio/notification.mp3
Normal file
20
assets/audio/notification.mp3
Normal file
@@ -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)
|
||||||
1
assets/notification.mp3
Normal file
1
assets/notification.mp3
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
File diff suppressed because one or more lines are too long
737
dist/app-boot.js
vendored
737
dist/app-boot.js
vendored
@@ -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
|
// src/crypto/EnhancedSecureCryptoUtils.js
|
||||||
var EnhancedSecureCryptoUtils = class _EnhancedSecureCryptoUtils {
|
var EnhancedSecureCryptoUtils = class _EnhancedSecureCryptoUtils {
|
||||||
static _keyMetadata = /* @__PURE__ */ new WeakMap();
|
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
|
// src/components/ui/Header.jsx
|
||||||
var EnhancedMinimalHeader = ({
|
var EnhancedMinimalHeader = ({
|
||||||
status,
|
status,
|
||||||
@@ -16056,6 +16776,7 @@ window.FileTransferComponent = FileTransferComponent;
|
|||||||
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
|
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
|
||||||
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
||||||
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
|
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
|
||||||
|
window.NotificationIntegration = import_NotificationIntegration.NotificationIntegration;
|
||||||
var start = () => {
|
var start = () => {
|
||||||
if (typeof window.initializeApp === "function") {
|
if (typeof window.initializeApp === "function") {
|
||||||
window.initializeApp();
|
window.initializeApp();
|
||||||
@@ -16068,4 +16789,20 @@ if (document.readyState === "loading") {
|
|||||||
} else {
|
} else {
|
||||||
start();
|
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
|
//# sourceMappingURL=app-boot.js.map
|
||||||
|
|||||||
8
dist/app-boot.js.map
vendored
8
dist/app-boot.js.map
vendored
File diff suppressed because one or more lines are too long
191
dist/app.js
vendored
191
dist/app.js
vendored
@@ -267,9 +267,11 @@ var EnhancedConnectionSetup = ({
|
|||||||
toggleQrManualMode,
|
toggleQrManualMode,
|
||||||
nextQrFrame,
|
nextQrFrame,
|
||||||
prevQrFrame,
|
prevQrFrame,
|
||||||
markAnswerCreated
|
markAnswerCreated,
|
||||||
|
notificationIntegrationRef
|
||||||
}) => {
|
}) => {
|
||||||
const [mode, setMode] = React.useState("select");
|
const [mode, setMode] = React.useState("select");
|
||||||
|
const [notificationPermissionRequested, setNotificationPermissionRequested] = React.useState(false);
|
||||||
const resetToSelect = () => {
|
const resetToSelect = () => {
|
||||||
setMode("select");
|
setMode("select");
|
||||||
onClearData();
|
onClearData();
|
||||||
@@ -280,6 +282,76 @@ var EnhancedConnectionSetup = ({
|
|||||||
const handleVerificationReject = () => {
|
const handleVerificationReject = () => {
|
||||||
onVerifyConnection(false);
|
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) {
|
if (showVerification) {
|
||||||
return React.createElement("div", {
|
return React.createElement("div", {
|
||||||
className: "min-h-[calc(100vh-104px)] flex items-center justify-center p-4"
|
className: "min-h-[calc(100vh-104px)] flex items-center justify-center p-4"
|
||||||
@@ -327,7 +399,10 @@ var EnhancedConnectionSetup = ({
|
|||||||
// Create Connection
|
// Create Connection
|
||||||
React.createElement("div", {
|
React.createElement("div", {
|
||||||
key: "create",
|
key: "create",
|
||||||
onClick: () => setMode("create"),
|
onClick: () => {
|
||||||
|
requestNotificationPermissionOnInteraction();
|
||||||
|
setMode("create");
|
||||||
|
},
|
||||||
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 create"
|
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 create"
|
||||||
}, [
|
}, [
|
||||||
React.createElement("div", {
|
React.createElement("div", {
|
||||||
@@ -399,7 +474,10 @@ var EnhancedConnectionSetup = ({
|
|||||||
// Join Connection
|
// Join Connection
|
||||||
React.createElement("div", {
|
React.createElement("div", {
|
||||||
key: "join",
|
key: "join",
|
||||||
onClick: () => setMode("join"),
|
onClick: () => {
|
||||||
|
requestNotificationPermissionOnInteraction();
|
||||||
|
setMode("join");
|
||||||
|
},
|
||||||
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 join"
|
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 join"
|
||||||
}, [
|
}, [
|
||||||
React.createElement("div", {
|
React.createElement("div", {
|
||||||
@@ -1321,8 +1399,8 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
window.forceCleanup = () => {
|
window.forceCleanup = () => {
|
||||||
handleClearData();
|
handleClearData();
|
||||||
if (webrtcManagerRef.current) {
|
if (webrtcManagerRef2.current) {
|
||||||
webrtcManagerRef.current.disconnect();
|
webrtcManagerRef2.current.disconnect();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.clearLogs = () => {
|
window.clearLogs = () => {
|
||||||
@@ -1335,8 +1413,9 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
delete window.clearLogs;
|
delete window.clearLogs;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
const webrtcManagerRef = React.useRef(null);
|
const webrtcManagerRef2 = React.useRef(null);
|
||||||
window.webrtcManagerRef = webrtcManagerRef;
|
const notificationIntegrationRef = React.useRef(null);
|
||||||
|
window.webrtcManagerRef = webrtcManagerRef2;
|
||||||
const addMessageWithAutoScroll = React.useCallback((message, type) => {
|
const addMessageWithAutoScroll = React.useCallback((message, type) => {
|
||||||
const newMessage = {
|
const newMessage = {
|
||||||
message,
|
message,
|
||||||
@@ -1377,7 +1456,7 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
}
|
}
|
||||||
window.isUpdatingSecurity = true;
|
window.isUpdatingSecurity = true;
|
||||||
try {
|
try {
|
||||||
if (webrtcManagerRef.current) {
|
if (webrtcManagerRef2.current) {
|
||||||
setSecurityLevel({
|
setSecurityLevel({
|
||||||
level: "MAXIMUM",
|
level: "MAXIMUM",
|
||||||
score: 100,
|
score: 100,
|
||||||
@@ -1388,7 +1467,7 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
isRealData: true
|
isRealData: true
|
||||||
});
|
});
|
||||||
if (window.DEBUG_MODE) {
|
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",
|
level: "MAXIMUM",
|
||||||
score: 100,
|
score: 100,
|
||||||
sessionType: "premium",
|
sessionType: "premium",
|
||||||
@@ -1421,7 +1500,7 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
}
|
}
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (webrtcManagerRef.current) {
|
if (webrtcManagerRef2.current) {
|
||||||
console.log("\u26A0\uFE0F WebRTC Manager already initialized, skipping...");
|
console.log("\u26A0\uFE0F WebRTC Manager already initialized, skipping...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1583,7 +1662,7 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
if (typeof console.clear === "function") {
|
if (typeof console.clear === "function") {
|
||||||
console.clear();
|
console.clear();
|
||||||
}
|
}
|
||||||
webrtcManagerRef.current = new EnhancedSecureWebRTCManager(
|
webrtcManagerRef2.current = new EnhancedSecureWebRTCManager(
|
||||||
handleMessage,
|
handleMessage,
|
||||||
handleStatusChange,
|
handleStatusChange,
|
||||||
handleKeyExchange,
|
handleKeyExchange,
|
||||||
@@ -1591,12 +1670,22 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
handleAnswerError,
|
handleAnswerError,
|
||||||
handleVerificationStateChange
|
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");
|
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) => {
|
const handleBeforeUnload = (event) => {
|
||||||
if (event.type === "beforeunload" && !isTabSwitching) {
|
if (event.type === "beforeunload" && !isTabSwitching) {
|
||||||
if (webrtcManagerRef.current && webrtcManagerRef.current.isConnected()) {
|
if (webrtcManagerRef2.current && webrtcManagerRef2.current.isConnected()) {
|
||||||
try {
|
try {
|
||||||
webrtcManagerRef.current.sendSystemMessage({
|
webrtcManagerRef2.current.sendSystemMessage({
|
||||||
type: "peer_disconnect",
|
type: "peer_disconnect",
|
||||||
reason: "user_disconnect",
|
reason: "user_disconnect",
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
@@ -1604,12 +1693,12 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (webrtcManagerRef.current) {
|
if (webrtcManagerRef2.current) {
|
||||||
webrtcManagerRef.current.disconnect();
|
webrtcManagerRef2.current.disconnect();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
} else if (webrtcManagerRef.current) {
|
} else if (webrtcManagerRef2.current) {
|
||||||
webrtcManagerRef.current.disconnect();
|
webrtcManagerRef2.current.disconnect();
|
||||||
}
|
}
|
||||||
} else if (isTabSwitching) {
|
} else if (isTabSwitching) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -1637,8 +1726,8 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||||
if (webrtcManagerRef.current) {
|
if (webrtcManagerRef2.current) {
|
||||||
webrtcManagerRef.current.setFileTransferCallbacks(
|
webrtcManagerRef2.current.setFileTransferCallbacks(
|
||||||
// Progress callback
|
// Progress callback
|
||||||
(progress) => {
|
(progress) => {
|
||||||
console.log("File progress:", progress);
|
console.log("File progress:", progress);
|
||||||
@@ -1690,9 +1779,9 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
clearTimeout(tabSwitchTimeout);
|
clearTimeout(tabSwitchTimeout);
|
||||||
tabSwitchTimeout = null;
|
tabSwitchTimeout = null;
|
||||||
}
|
}
|
||||||
if (webrtcManagerRef.current) {
|
if (webrtcManagerRef2.current) {
|
||||||
webrtcManagerRef.current.disconnect();
|
webrtcManagerRef2.current.disconnect();
|
||||||
webrtcManagerRef.current = null;
|
webrtcManagerRef2.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -2362,7 +2451,7 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
setShowOfferStep(false);
|
setShowOfferStep(false);
|
||||||
setShowQRCode(false);
|
setShowQRCode(false);
|
||||||
setQrCodeUrl("");
|
setQrCodeUrl("");
|
||||||
const offer = await webrtcManagerRef.current.createSecureOffer();
|
const offer = await webrtcManagerRef2.current.createSecureOffer();
|
||||||
setOfferData(offer);
|
setOfferData(offer);
|
||||||
setShowOfferStep(true);
|
setShowOfferStep(true);
|
||||||
const offerString = typeof offer === "object" ? JSON.stringify(offer) : offer;
|
const offerString = typeof offer === "object" ? JSON.stringify(offer) : offer;
|
||||||
@@ -2485,7 +2574,7 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
if (!isValidOfferType) {
|
if (!isValidOfferType) {
|
||||||
throw new Error("Invalid invitation type. Expected offer or enhanced_secure_offer");
|
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);
|
setAnswerData(answer);
|
||||||
setShowAnswerStep(true);
|
setShowAnswerStep(true);
|
||||||
const answerString = typeof answer === "object" ? JSON.stringify(answer) : answer;
|
const answerString = typeof answer === "object" ? JSON.stringify(answer) : answer;
|
||||||
@@ -2623,7 +2712,7 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
if (!answerType || answerType !== "answer" && answerType !== "enhanced_secure_answer") {
|
if (!answerType || answerType !== "answer" && answerType !== "enhanced_secure_answer") {
|
||||||
throw new Error("Invalid response type. Expected answer or 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) {
|
if (pendingSession) {
|
||||||
setPendingSession(null);
|
setPendingSession(null);
|
||||||
setMessages((prev) => [...prev, {
|
setMessages((prev) => [...prev, {
|
||||||
@@ -2709,10 +2798,37 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
setConnectionStatus("failed");
|
setConnectionStatus("failed");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleVerifyConnection = (isValid) => {
|
const handleVerifyConnection = async (isValid) => {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
webrtcManagerRef.current.confirmVerification();
|
webrtcManagerRef2.current.confirmVerification();
|
||||||
setLocalVerificationConfirmed(true);
|
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 {
|
} else {
|
||||||
setMessages((prev) => [...prev, {
|
setMessages((prev) => [...prev, {
|
||||||
message: " Verification rejected. The connection is unsafe! Session reset..",
|
message: " Verification rejected. The connection is unsafe! Session reset..",
|
||||||
@@ -2746,15 +2862,15 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
if (!messageInput.trim()) {
|
if (!messageInput.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!webrtcManagerRef.current) {
|
if (!webrtcManagerRef2.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!webrtcManagerRef.current.isConnected()) {
|
if (!webrtcManagerRef2.current.isConnected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
addMessageWithAutoScroll(messageInput.trim(), "sent");
|
addMessageWithAutoScroll(messageInput.trim(), "sent");
|
||||||
await webrtcManagerRef.current.sendMessage(messageInput);
|
await webrtcManagerRef2.current.sendMessage(messageInput);
|
||||||
setMessageInput("");
|
setMessageInput("");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = String(error?.message || error);
|
const msg = String(error?.message || error);
|
||||||
@@ -2804,8 +2920,12 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
status: "disconnected",
|
status: "disconnected",
|
||||||
isUserInitiatedDisconnect: true
|
isUserInitiatedDisconnect: true
|
||||||
});
|
});
|
||||||
if (webrtcManagerRef.current) {
|
if (webrtcManagerRef2.current) {
|
||||||
webrtcManagerRef.current.disconnect();
|
webrtcManagerRef2.current.disconnect();
|
||||||
|
}
|
||||||
|
if (notificationIntegrationRef.current) {
|
||||||
|
notificationIntegrationRef.current.cleanup();
|
||||||
|
notificationIntegrationRef.current = null;
|
||||||
}
|
}
|
||||||
setKeyFingerprint("");
|
setKeyFingerprint("");
|
||||||
setVerificationCode("");
|
setVerificationCode("");
|
||||||
@@ -2964,7 +3084,7 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
isConnected: isConnectedAndVerified,
|
isConnected: isConnectedAndVerified,
|
||||||
securityLevel,
|
securityLevel,
|
||||||
// sessionManager removed - all features enabled by default
|
// sessionManager removed - all features enabled by default
|
||||||
webrtcManager: webrtcManagerRef.current
|
webrtcManager: webrtcManagerRef2.current
|
||||||
}),
|
}),
|
||||||
React.createElement(
|
React.createElement(
|
||||||
"main",
|
"main",
|
||||||
@@ -2984,7 +3104,7 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
isVerified,
|
isVerified,
|
||||||
chatMessagesRef,
|
chatMessagesRef,
|
||||||
scrollToBottom,
|
scrollToBottom,
|
||||||
webrtcManager: webrtcManagerRef.current
|
webrtcManager: webrtcManagerRef2.current
|
||||||
});
|
});
|
||||||
})() : React.createElement(EnhancedConnectionSetup, {
|
})() : React.createElement(EnhancedConnectionSetup, {
|
||||||
onCreateOffer: handleCreateOffer,
|
onCreateOffer: handleCreateOffer,
|
||||||
@@ -3021,7 +3141,8 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
nextQrFrame,
|
nextQrFrame,
|
||||||
prevQrFrame,
|
prevQrFrame,
|
||||||
// PAKE passwords removed - using SAS verification instead
|
// PAKE passwords removed - using SAS verification instead
|
||||||
markAnswerCreated
|
markAnswerCreated,
|
||||||
|
notificationIntegrationRef
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
// QR Scanner Modal
|
// QR Scanner Modal
|
||||||
|
|||||||
6
dist/app.js.map
vendored
6
dist/app.js.map
vendored
File diff suppressed because one or more lines are too long
184
src/app.jsx
184
src/app.jsx
@@ -280,9 +280,11 @@
|
|||||||
toggleQrManualMode,
|
toggleQrManualMode,
|
||||||
nextQrFrame,
|
nextQrFrame,
|
||||||
prevQrFrame,
|
prevQrFrame,
|
||||||
markAnswerCreated
|
markAnswerCreated,
|
||||||
|
notificationIntegrationRef
|
||||||
}) => {
|
}) => {
|
||||||
const [mode, setMode] = React.useState('select');
|
const [mode, setMode] = React.useState('select');
|
||||||
|
const [notificationPermissionRequested, setNotificationPermissionRequested] = React.useState(false);
|
||||||
|
|
||||||
const resetToSelect = () => {
|
const resetToSelect = () => {
|
||||||
setMode('select');
|
setMode('select');
|
||||||
@@ -296,6 +298,110 @@
|
|||||||
const handleVerificationReject = () => {
|
const handleVerificationReject = () => {
|
||||||
onVerifyConnection(false);
|
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) {
|
if (showVerification) {
|
||||||
return React.createElement('div', {
|
return React.createElement('div', {
|
||||||
@@ -346,7 +452,10 @@
|
|||||||
// Create Connection
|
// Create Connection
|
||||||
React.createElement('div', {
|
React.createElement('div', {
|
||||||
key: 'create',
|
key: 'create',
|
||||||
onClick: () => setMode('create'),
|
onClick: () => {
|
||||||
|
requestNotificationPermissionOnInteraction();
|
||||||
|
setMode('create');
|
||||||
|
},
|
||||||
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 create"
|
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 create"
|
||||||
}, [
|
}, [
|
||||||
React.createElement('div', {
|
React.createElement('div', {
|
||||||
@@ -418,7 +527,10 @@
|
|||||||
// Join Connection
|
// Join Connection
|
||||||
React.createElement('div', {
|
React.createElement('div', {
|
||||||
key: 'join',
|
key: 'join',
|
||||||
onClick: () => setMode('join'),
|
onClick: () => {
|
||||||
|
requestNotificationPermissionOnInteraction();
|
||||||
|
setMode('join');
|
||||||
|
},
|
||||||
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 join"
|
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 join"
|
||||||
}, [
|
}, [
|
||||||
React.createElement('div', {
|
React.createElement('div', {
|
||||||
@@ -1023,6 +1135,7 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const EnhancedChatInterface = ({
|
const EnhancedChatInterface = ({
|
||||||
messages,
|
messages,
|
||||||
messageInput,
|
messageInput,
|
||||||
@@ -1453,6 +1566,7 @@
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const webrtcManagerRef = React.useRef(null);
|
const webrtcManagerRef = React.useRef(null);
|
||||||
|
const notificationIntegrationRef = React.useRef(null);
|
||||||
// Expose for modules/UI that run outside this closure (e.g., inline handlers)
|
// 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
|
// Safe because it's a ref object and we maintain it centrally here
|
||||||
window.webrtcManagerRef = webrtcManagerRef;
|
window.webrtcManagerRef = webrtcManagerRef;
|
||||||
@@ -1785,6 +1899,20 @@
|
|||||||
handleVerificationStateChange
|
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');
|
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) => {
|
const handleBeforeUnload = (event) => {
|
||||||
@@ -3083,11 +3211,47 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVerifyConnection = (isValid) => {
|
const handleVerifyConnection = async (isValid) => {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
webrtcManagerRef.current.confirmVerification();
|
webrtcManagerRef.current.confirmVerification();
|
||||||
// Mark local verification as confirmed
|
// Mark local verification as confirmed
|
||||||
setLocalVerificationConfirmed(true);
|
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 {
|
} else {
|
||||||
setMessages(prev => [...prev, {
|
setMessages(prev => [...prev, {
|
||||||
message: ' Verification rejected. The connection is unsafe! Session reset..',
|
message: ' Verification rejected. The connection is unsafe! Session reset..',
|
||||||
@@ -3218,6 +3382,12 @@
|
|||||||
if (webrtcManagerRef.current) {
|
if (webrtcManagerRef.current) {
|
||||||
webrtcManagerRef.current.disconnect();
|
webrtcManagerRef.current.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup notification integration
|
||||||
|
if (notificationIntegrationRef.current) {
|
||||||
|
notificationIntegrationRef.current.cleanup();
|
||||||
|
notificationIntegrationRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Clear all connection-related states
|
// Clear all connection-related states
|
||||||
setKeyFingerprint('');
|
setKeyFingerprint('');
|
||||||
@@ -3476,7 +3646,8 @@
|
|||||||
nextQrFrame: nextQrFrame,
|
nextQrFrame: nextQrFrame,
|
||||||
prevQrFrame: prevQrFrame,
|
prevQrFrame: prevQrFrame,
|
||||||
// PAKE passwords removed - using SAS verification instead
|
// PAKE passwords removed - using SAS verification instead
|
||||||
markAnswerCreated: markAnswerCreated
|
markAnswerCreated: markAnswerCreated,
|
||||||
|
notificationIntegrationRef: notificationIntegrationRef
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -3586,6 +3757,7 @@
|
|||||||
if (!window.initializeApp) {
|
if (!window.initializeApp) {
|
||||||
window.initializeApp = initializeApp;
|
window.initializeApp = initializeApp;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Render Enhanced Application
|
// Render Enhanced Application
|
||||||
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
|
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
|
||||||
279
src/notifications/NotificationIntegration.js
Normal file
279
src/notifications/NotificationIntegration.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
606
src/notifications/SecureNotificationManager.js
Normal file
606
src/notifications/SecureNotificationManager.js
Normal file
@@ -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() {
|
setupEventListeners() {
|
||||||
window.addEventListener('beforeinstallprompt', (event) => {
|
window.addEventListener('beforeinstallprompt', (event) => {
|
||||||
event.preventDefault();
|
// Don't prevent default - let browser show its own banner
|
||||||
this.deferredPrompt = event;
|
this.deferredPrompt = event;
|
||||||
|
|
||||||
if (this.checkInstallationStatus()) {
|
if (this.checkInstallationStatus()) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { EnhancedSecureCryptoUtils } from '../crypto/EnhancedSecureCryptoUtils.js';
|
import { EnhancedSecureCryptoUtils } from '../crypto/EnhancedSecureCryptoUtils.js';
|
||||||
import { EnhancedSecureWebRTCManager } from '../network/EnhancedSecureWebRTCManager.js';
|
import { EnhancedSecureWebRTCManager } from '../network/EnhancedSecureWebRTCManager.js';
|
||||||
import { EnhancedSecureFileTransfer } from '../transfer/EnhancedSecureFileTransfer.js';
|
import { EnhancedSecureFileTransfer } from '../transfer/EnhancedSecureFileTransfer.js';
|
||||||
|
import { NotificationIntegration } from '../notifications/NotificationIntegration.js';
|
||||||
|
|
||||||
// Import UI components (side-effect: they attach themselves to window.*)
|
// Import UI components (side-effect: they attach themselves to window.*)
|
||||||
import '../components/ui/Header.jsx';
|
import '../components/ui/Header.jsx';
|
||||||
@@ -16,6 +17,7 @@ import '../components/ui/FileTransfer.jsx';
|
|||||||
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
|
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
|
||||||
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
||||||
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
|
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
|
||||||
|
window.NotificationIntegration = NotificationIntegration;
|
||||||
|
|
||||||
// Mount application once DOM and modules are ready
|
// Mount application once DOM and modules are ready
|
||||||
const start = () => {
|
const start = () => {
|
||||||
|
|||||||
11
src/scripts/bootstrap-modules.js
vendored
11
src/scripts/bootstrap-modules.js
vendored
@@ -3,10 +3,13 @@
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const timestamp = Date.now();
|
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(`../crypto/EnhancedSecureCryptoUtils.js?v=${timestamp}`),
|
||||||
import(`../network/EnhancedSecureWebRTCManager.js?v=${timestamp}`),
|
import(`../network/EnhancedSecureWebRTCManager.js?v=${timestamp}`),
|
||||||
import(`../transfer/EnhancedSecureFileTransfer.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;
|
const { EnhancedSecureCryptoUtils } = cryptoModule;
|
||||||
@@ -15,6 +18,12 @@
|
|||||||
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
||||||
const { EnhancedSecureFileTransfer } = fileTransferModule;
|
const { EnhancedSecureFileTransfer } = fileTransferModule;
|
||||||
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
|
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
|
// Load React components using dynamic imports instead of eval
|
||||||
const componentModules = await Promise.all([
|
const componentModules = await Promise.all([
|
||||||
|
|||||||
6
sw.js
6
sw.js
@@ -27,11 +27,7 @@ const STATIC_ASSETS = [
|
|||||||
'/src/pwa/pwa-manager.js',
|
'/src/pwa/pwa-manager.js',
|
||||||
'/src/pwa/install-prompt.js',
|
'/src/pwa/install-prompt.js',
|
||||||
'/src/scripts/pwa-register.js',
|
'/src/scripts/pwa-register.js',
|
||||||
'/src/scripts/pwa-offline-test.js',
|
'/src/scripts/pwa-offline-test.js'
|
||||||
|
|
||||||
// Bluetooth key transfer (PWA feature)
|
|
||||||
'/src/transfer/BluetoothKeyTransfer.js',
|
|
||||||
'/src/components/ui/BluetoothKeyTransfer.jsx'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Sensitive files that should never be cached
|
// Sensitive files that should never be cached
|
||||||
|
|||||||
Reference in New Issue
Block a user