342 lines
12 KiB
TypeScript
342 lines
12 KiB
TypeScript
/* eslint-disable */
|
|
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
|
import CryptoJS from 'crypto-js';
|
|
|
|
export interface EncryptedMessage {
|
|
id: string;
|
|
sender: string;
|
|
ciphertext: string;
|
|
timestamp: number;
|
|
}
|
|
|
|
export interface DecryptedMessage {
|
|
id: string;
|
|
sender: string;
|
|
content: string;
|
|
timestamp: number;
|
|
status: string;
|
|
}
|
|
|
|
/**
|
|
* XCUTelepathyMatrix FASE 2:
|
|
* Telah berevolusi meninggalkan NodeJS, Yjs, dan WebSockets usang.
|
|
* Menggunakan arsitektur WebTransport Datagrams yang menembak langsung ke Rust Engine (Port 8443).
|
|
*/
|
|
export class XCUTelepathyMatrix {
|
|
private roomName: string;
|
|
private secretKey: string;
|
|
private transport: any = null; // WebTransport instance
|
|
private isActive: boolean = false;
|
|
private participantId: number;
|
|
private username: string = "";
|
|
|
|
public onMessagesUpdate: ((messages: DecryptedMessage[]) => void) | null = null;
|
|
public onTypingUpdate: ((typingUsers: Record<string, number>) => void) | null = null;
|
|
public onQuantumResonance: ((senderId: number, type: string) => void) | null = null;
|
|
public onSovereignSignal: ((command: string, targetId?: number) => void) | null = null;
|
|
|
|
// Local state untuk dirender
|
|
private messages: DecryptedMessage[] = [];
|
|
private typingState: Record<string, number> = {};
|
|
|
|
constructor(roomName: string, secretKey: string = 'QUANTUM-X-SECRET-256') {
|
|
this.roomName = roomName;
|
|
this.secretKey = secretKey;
|
|
this.participantId = Math.floor(Math.random() * 65534) + 1;
|
|
}
|
|
|
|
public async ignite(serverUrl: string, username: string) {
|
|
this.username = username;
|
|
this.isActive = true;
|
|
|
|
try {
|
|
// 1. Ekstrak host dari serverUrl
|
|
let host = window.location.hostname;
|
|
if (serverUrl && serverUrl !== "/") {
|
|
const urlObj = new URL(serverUrl);
|
|
host = urlObj.hostname;
|
|
}
|
|
|
|
const secureProto = window.location.protocol;
|
|
const wtUrl = `${secureProto}//${host}:8443/neural-link/${this.roomName}`;
|
|
console.log("[XTM] Menginisialisasi WebTransport ke:", wtUrl);
|
|
|
|
// 2. Setup WebTransport
|
|
// Gunakan any cast karena WebTransport mungkin belum diakui di semua tsconfig
|
|
const WT = (window as any).WebTransport;
|
|
if (!WT) {
|
|
throw new Error("WebTransport tidak didukung di browser ini!");
|
|
}
|
|
|
|
this.transport = new WT(wtUrl);
|
|
await this.transport.ready;
|
|
console.log("[XTM] Pipa WebTransport Kuantum TERHUBUNG!");
|
|
|
|
// 3. Mulai Membaca Datagrams
|
|
this.readDatagrams();
|
|
|
|
} catch (e) {
|
|
console.error("[XTM] Ignition Failed:", e);
|
|
// Fallback: Jika WebTransport diblokir firewall, XTM akan mengaktifkan SSE Relay (TBD)
|
|
}
|
|
}
|
|
|
|
private async readDatagrams() {
|
|
if (!this.transport || !this.transport.datagrams) return;
|
|
|
|
try {
|
|
const reader = this.transport.datagrams.readable.getReader();
|
|
while (this.isActive) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
|
|
if (value && value.length >= 8) {
|
|
const type = value[0];
|
|
// byte 2-3 = participantId pengirim
|
|
const senderId = new DataView(value.buffer).getUint16(2, true);
|
|
|
|
if (senderId === this.participantId) continue;
|
|
|
|
const payload = value.slice(8);
|
|
|
|
if (type === 7) { // 7 = Chat Text
|
|
this.decryptAndPush(payload);
|
|
} else if (type === 8) { // 8 = Telepathic Resonance (Typing)
|
|
// Mendekripsi siapa yang mengetik
|
|
this.handleTypingResonance(payload);
|
|
} else if (type === 9) { // 9 = PKEPX Resonance (Emoji)
|
|
this.handleQuantumResonance(payload, senderId);
|
|
} else if (type === 10) { // 10 = PKEPX Sovereign Command
|
|
this.handleSovereignCommand(payload);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (this.isActive) console.error("[XTM] Datagram Reader terputus:", e);
|
|
}
|
|
}
|
|
|
|
private async decryptAndPush(payload: Uint8Array) {
|
|
try {
|
|
// Convert Uint8Array to WordArray
|
|
const wordArr = CryptoJS.lib.WordArray.create(payload as any);
|
|
const ciphertext = CryptoJS.enc.Base64.stringify(wordArr);
|
|
|
|
const bytes = CryptoJS.AES.decrypt(ciphertext, this.secretKey);
|
|
const originalText = bytes.toString(CryptoJS.enc.Utf8);
|
|
|
|
if (originalText) {
|
|
const parsed = JSON.parse(originalText);
|
|
this.messages.push({
|
|
id: Math.random().toString(),
|
|
sender: parsed.sender,
|
|
content: parsed.text,
|
|
timestamp: parsed.timestamp,
|
|
status: 'delivered'
|
|
});
|
|
|
|
if (this.onMessagesUpdate) {
|
|
this.onMessagesUpdate([...this.messages]);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("Gagal dekripsi pesan XTM", e);
|
|
}
|
|
}
|
|
|
|
private async handleTypingResonance(payload: Uint8Array) {
|
|
try {
|
|
const wordArr = CryptoJS.lib.WordArray.create(payload as any);
|
|
const ciphertext = CryptoJS.enc.Base64.stringify(wordArr);
|
|
const bytes = CryptoJS.AES.decrypt(ciphertext, this.secretKey);
|
|
const name = bytes.toString(CryptoJS.enc.Utf8);
|
|
|
|
if (name) {
|
|
this.typingState[name] = Date.now();
|
|
if (this.onTypingUpdate) this.onTypingUpdate({...this.typingState});
|
|
|
|
setTimeout(() => {
|
|
delete this.typingState[name];
|
|
if (this.onTypingUpdate) this.onTypingUpdate({...this.typingState});
|
|
}, 3000);
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
// =====================================
|
|
// PKEPX ZOOM-KILLER: JUMPA CHAT PORT
|
|
// =====================================
|
|
|
|
private async handleQuantumResonance(payload: Uint8Array, senderId: number) {
|
|
try {
|
|
const wordArr = CryptoJS.lib.WordArray.create(payload as any);
|
|
const ciphertext = CryptoJS.enc.Base64.stringify(wordArr);
|
|
const bytes = CryptoJS.AES.decrypt(ciphertext, this.secretKey);
|
|
const reactionType = bytes.toString(CryptoJS.enc.Utf8);
|
|
if (reactionType && this.onQuantumResonance) {
|
|
this.onQuantumResonance(senderId, reactionType);
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
private async handleSovereignCommand(payload: Uint8Array) {
|
|
try {
|
|
const wordArr = CryptoJS.lib.WordArray.create(payload as any);
|
|
const ciphertext = CryptoJS.enc.Base64.stringify(wordArr);
|
|
const bytes = CryptoJS.AES.decrypt(ciphertext, this.secretKey);
|
|
const cmdStr = bytes.toString(CryptoJS.enc.Utf8);
|
|
if (cmdStr && this.onSovereignSignal) {
|
|
const cmd = JSON.parse(cmdStr);
|
|
this.onSovereignSignal(cmd.command, cmd.targetId);
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
public async emitResonance(reactionType: string) {
|
|
if (!this.transport || !this.transport.datagrams) return;
|
|
|
|
const ciphertext = CryptoJS.AES.encrypt(reactionType, this.secretKey).toString();
|
|
const encWordArr = CryptoJS.enc.Base64.parse(ciphertext);
|
|
const encPayload = new Uint8Array(encWordArr.sigBytes);
|
|
for (let i = 0; i < encWordArr.sigBytes; i++) {
|
|
encPayload[i] = (encWordArr.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
|
}
|
|
|
|
const header = new Uint8Array(8);
|
|
header[0] = 9; // Tipe 9 = Resonance
|
|
new DataView(header.buffer).setUint16(2, this.participantId, true);
|
|
|
|
const fullPacket = new Uint8Array(8 + encPayload.length);
|
|
fullPacket.set(header, 0);
|
|
fullPacket.set(encPayload, 8);
|
|
|
|
let writer: any = null;
|
|
try {
|
|
writer = this.transport.datagrams.writable.getWriter();
|
|
await writer.write(fullPacket);
|
|
if (this.onQuantumResonance) this.onQuantumResonance(this.participantId, reactionType);
|
|
} catch (e) {} finally {
|
|
if (writer) writer.releaseLock();
|
|
}
|
|
}
|
|
|
|
public async broadcastSovereignSignal(command: string, targetId?: number) {
|
|
if (!this.transport || !this.transport.datagrams) return;
|
|
|
|
const payloadStr = JSON.stringify({ command, targetId });
|
|
const ciphertext = CryptoJS.AES.encrypt(payloadStr, this.secretKey).toString();
|
|
const encWordArr = CryptoJS.enc.Base64.parse(ciphertext);
|
|
const encPayload = new Uint8Array(encWordArr.sigBytes);
|
|
for (let i = 0; i < encWordArr.sigBytes; i++) {
|
|
encPayload[i] = (encWordArr.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
|
}
|
|
|
|
const header = new Uint8Array(8);
|
|
header[0] = 10; // Tipe 10 = Sovereign
|
|
new DataView(header.buffer).setUint16(2, this.participantId, true);
|
|
|
|
const fullPacket = new Uint8Array(8 + encPayload.length);
|
|
fullPacket.set(header, 0);
|
|
fullPacket.set(encPayload, 8);
|
|
|
|
let writer: any = null;
|
|
try {
|
|
writer = this.transport.datagrams.writable.getWriter();
|
|
await writer.write(fullPacket);
|
|
} catch (e) {} finally {
|
|
if (writer) writer.releaseLock();
|
|
}
|
|
}
|
|
|
|
// Encrypt and send
|
|
public async sendMessage(sender: string, content: string) {
|
|
if (!this.transport || !this.transport.datagrams) {
|
|
// Fallback simpan ke local buffer jika belum konek
|
|
this.messages.push({
|
|
id: Math.random().toString(),
|
|
sender,
|
|
content,
|
|
timestamp: Date.now(),
|
|
status: 'failed_offline'
|
|
});
|
|
if (this.onMessagesUpdate) this.onMessagesUpdate([...this.messages]);
|
|
return;
|
|
}
|
|
|
|
const payloadStr = JSON.stringify({ sender, text: content, timestamp: Date.now() });
|
|
|
|
// E2EE: AES Encryption
|
|
const ciphertext = CryptoJS.AES.encrypt(payloadStr, this.secretKey).toString();
|
|
const encWordArr = CryptoJS.enc.Base64.parse(ciphertext);
|
|
// Convert WordArray to Uint8Array
|
|
const encPayload = new Uint8Array(encWordArr.sigBytes);
|
|
for (let i = 0; i < encWordArr.sigBytes; i++) {
|
|
encPayload[i] = (encWordArr.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
|
}
|
|
|
|
// Header
|
|
const header = new Uint8Array(8);
|
|
header[0] = 7; // Tipe 7 = Text Message
|
|
header[1] = 0;
|
|
new DataView(header.buffer).setUint16(2, this.participantId, true);
|
|
|
|
const fullPacket = new Uint8Array(8 + encPayload.length);
|
|
fullPacket.set(header, 0);
|
|
fullPacket.set(encPayload, 8);
|
|
|
|
let writer: any = null;
|
|
try {
|
|
writer = this.transport.datagrams.writable.getWriter();
|
|
await writer.write(fullPacket);
|
|
|
|
// Optimistic UI Update
|
|
this.messages.push({
|
|
id: Math.random().toString(),
|
|
sender,
|
|
content,
|
|
timestamp: Date.now(),
|
|
status: 'delivered'
|
|
});
|
|
if (this.onMessagesUpdate) this.onMessagesUpdate([...this.messages]);
|
|
} catch (e) {
|
|
console.error("Gagal mengirim pesan Kuantum:", e);
|
|
} finally {
|
|
if (writer) writer.releaseLock();
|
|
}
|
|
}
|
|
|
|
public async setTyping(username: string, charCount: number) {
|
|
if (!this.transport || !this.transport.datagrams) return;
|
|
|
|
const ciphertext = CryptoJS.AES.encrypt(username, this.secretKey).toString();
|
|
const encWordArr = CryptoJS.enc.Base64.parse(ciphertext);
|
|
const encPayload = new Uint8Array(encWordArr.sigBytes);
|
|
for (let i = 0; i < encWordArr.sigBytes; i++) {
|
|
encPayload[i] = (encWordArr.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
|
}
|
|
|
|
const header = new Uint8Array(8);
|
|
header[0] = 8; // Tipe 8 = Typing
|
|
new DataView(header.buffer).setUint16(2, this.participantId, true);
|
|
|
|
const fullPacket = new Uint8Array(8 + encPayload.length);
|
|
fullPacket.set(header, 0);
|
|
fullPacket.set(encPayload, 8);
|
|
|
|
let writer: any = null;
|
|
try {
|
|
writer = this.transport.datagrams.writable.getWriter();
|
|
await writer.write(fullPacket);
|
|
} catch (e) {} finally {
|
|
if (writer) writer.releaseLock();
|
|
}
|
|
}
|
|
|
|
public shutdown() {
|
|
this.isActive = false;
|
|
if (this.transport) {
|
|
try { this.transport.close(); } catch(e){}
|
|
}
|
|
}
|
|
}
|