web-agent-bridge 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.ar.md +446 -446
- package/README.md +844 -844
- package/bin/cli.js +80 -80
- package/bin/wab.js +80 -80
- package/docs/DEPLOY.md +118 -118
- package/docs/SPEC.md +1540 -1540
- package/examples/bidi-agent.js +119 -119
- package/examples/mcp-agent.js +94 -94
- package/examples/puppeteer-agent.js +108 -108
- package/examples/vision-agent.js +171 -171
- package/package.json +78 -78
- package/public/admin/dashboard.html +848 -848
- package/public/admin/login.html +84 -84
- package/public/cookies.html +208 -208
- package/public/css/styles.css +1235 -1235
- package/public/dashboard.html +704 -704
- package/public/docs.html +585 -585
- package/public/index.html +332 -332
- package/public/js/auth-nav.js +31 -31
- package/public/js/auth-redirect.js +12 -12
- package/public/js/cookie-consent.js +56 -56
- package/public/js/ws-client.js +74 -74
- package/public/login.html +83 -83
- package/public/privacy.html +295 -295
- package/public/register.html +103 -103
- package/public/terms.html +254 -254
- package/script/ai-agent-bridge.js +1513 -1513
- package/sdk/README.md +55 -55
- package/sdk/index.js +203 -203
- package/sdk/package.json +14 -14
- package/server/config/secrets.js +92 -92
- package/server/index.js +181 -181
- package/server/middleware/adminAuth.js +30 -30
- package/server/middleware/auth.js +41 -41
- package/server/middleware/rateLimits.js +24 -24
- package/server/migrations/001_add_analytics_indexes.sql +7 -7
- package/server/models/adapters/index.js +33 -33
- package/server/models/adapters/mysql.js +183 -183
- package/server/models/adapters/postgresql.js +172 -172
- package/server/models/adapters/sqlite.js +7 -7
- package/server/models/db.js +561 -561
- package/server/routes/admin.js +247 -247
- package/server/routes/api.js +138 -138
- package/server/routes/auth.js +51 -51
- package/server/routes/billing.js +45 -45
- package/server/routes/discovery.js +329 -329
- package/server/routes/license.js +240 -240
- package/server/routes/noscript.js +543 -543
- package/server/routes/wab-api.js +476 -476
- package/server/services/email.js +204 -204
- package/server/services/fairness.js +420 -420
- package/server/services/stripe.js +192 -192
- package/server/utils/cache.js +125 -125
- package/server/utils/migrate.js +81 -81
- package/server/utils/secureFields.js +50 -50
- package/server/ws.js +101 -101
- package/wab-mcp-adapter/README.md +136 -136
- package/wab-mcp-adapter/index.js +555 -555
- package/wab-mcp-adapter/package.json +17 -17
- package/public/css/premium.css +0 -317
- package/public/premium-dashboard.html +0 -2075
- package/public/premium.html +0 -791
- package/server/migrations/002_premium_features.sql +0 -418
- package/server/routes/premium.js +0 -724
- package/server/services/premium.js +0 -1680
package/server/utils/migrate.js
CHANGED
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Database Migration Runner
|
|
3
|
-
* Tracks and applies SQL migrations from server/migrations/ in order.
|
|
4
|
-
* Uses a `_migrations` table to record applied migrations.
|
|
5
|
-
*/
|
|
6
|
-
const Database = require('better-sqlite3');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
|
|
10
|
-
const DATA_DIR = process.env.NODE_ENV === 'test'
|
|
11
|
-
? path.join(__dirname, '..', '..', 'data-test')
|
|
12
|
-
: path.join(__dirname, '..', '..', 'data');
|
|
13
|
-
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
14
|
-
|
|
15
|
-
const dbFile = process.env.NODE_ENV === 'test' ? 'wab-test.db' : 'wab.db';
|
|
16
|
-
const db = new Database(path.join(DATA_DIR, dbFile));
|
|
17
|
-
|
|
18
|
-
// Ensure migrations tracking table exists
|
|
19
|
-
db.exec(`
|
|
20
|
-
CREATE TABLE IF NOT EXISTS _migrations (
|
|
21
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
22
|
-
name TEXT UNIQUE NOT NULL,
|
|
23
|
-
applied_at TEXT DEFAULT (datetime('now'))
|
|
24
|
-
);
|
|
25
|
-
`);
|
|
26
|
-
|
|
27
|
-
const MIGRATIONS_DIR = path.join(__dirname, '..', 'migrations');
|
|
28
|
-
|
|
29
|
-
function getAppliedMigrations() {
|
|
30
|
-
return new Set(
|
|
31
|
-
db.prepare('SELECT name FROM _migrations ORDER BY id').all().map(r => r.name)
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function runMigrations() {
|
|
36
|
-
if (!fs.existsSync(MIGRATIONS_DIR)) {
|
|
37
|
-
console.log('No migrations directory found.');
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const files = fs.readdirSync(MIGRATIONS_DIR)
|
|
42
|
-
.filter(f => f.endsWith('.sql'))
|
|
43
|
-
.sort();
|
|
44
|
-
|
|
45
|
-
const applied = getAppliedMigrations();
|
|
46
|
-
let count = 0;
|
|
47
|
-
|
|
48
|
-
const applyMigration = db.transaction((name, sql) => {
|
|
49
|
-
db.exec(sql);
|
|
50
|
-
db.prepare('INSERT INTO _migrations (name) VALUES (?)').run(name);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
for (const file of files) {
|
|
54
|
-
if (applied.has(file)) continue;
|
|
55
|
-
|
|
56
|
-
const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, file), 'utf8');
|
|
57
|
-
try {
|
|
58
|
-
applyMigration(file, sql);
|
|
59
|
-
console.log(` ✅ Migration applied: ${file}`);
|
|
60
|
-
count++;
|
|
61
|
-
} catch (err) {
|
|
62
|
-
console.error(` ❌ Migration failed: ${file} — ${err.message}`);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (count === 0) {
|
|
68
|
-
console.log(' All migrations up to date.');
|
|
69
|
-
} else {
|
|
70
|
-
console.log(` ${count} migration(s) applied.`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Run when called directly: node server/utils/migrate.js
|
|
75
|
-
if (require.main === module) {
|
|
76
|
-
console.log('Running database migrations...');
|
|
77
|
-
runMigrations();
|
|
78
|
-
db.close();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
module.exports = { runMigrations };
|
|
1
|
+
/**
|
|
2
|
+
* Database Migration Runner
|
|
3
|
+
* Tracks and applies SQL migrations from server/migrations/ in order.
|
|
4
|
+
* Uses a `_migrations` table to record applied migrations.
|
|
5
|
+
*/
|
|
6
|
+
const Database = require('better-sqlite3');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
const DATA_DIR = process.env.NODE_ENV === 'test'
|
|
11
|
+
? path.join(__dirname, '..', '..', 'data-test')
|
|
12
|
+
: path.join(__dirname, '..', '..', 'data');
|
|
13
|
+
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
14
|
+
|
|
15
|
+
const dbFile = process.env.NODE_ENV === 'test' ? 'wab-test.db' : 'wab.db';
|
|
16
|
+
const db = new Database(path.join(DATA_DIR, dbFile));
|
|
17
|
+
|
|
18
|
+
// Ensure migrations tracking table exists
|
|
19
|
+
db.exec(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
21
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
22
|
+
name TEXT UNIQUE NOT NULL,
|
|
23
|
+
applied_at TEXT DEFAULT (datetime('now'))
|
|
24
|
+
);
|
|
25
|
+
`);
|
|
26
|
+
|
|
27
|
+
const MIGRATIONS_DIR = path.join(__dirname, '..', 'migrations');
|
|
28
|
+
|
|
29
|
+
function getAppliedMigrations() {
|
|
30
|
+
return new Set(
|
|
31
|
+
db.prepare('SELECT name FROM _migrations ORDER BY id').all().map(r => r.name)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function runMigrations() {
|
|
36
|
+
if (!fs.existsSync(MIGRATIONS_DIR)) {
|
|
37
|
+
console.log('No migrations directory found.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const files = fs.readdirSync(MIGRATIONS_DIR)
|
|
42
|
+
.filter(f => f.endsWith('.sql'))
|
|
43
|
+
.sort();
|
|
44
|
+
|
|
45
|
+
const applied = getAppliedMigrations();
|
|
46
|
+
let count = 0;
|
|
47
|
+
|
|
48
|
+
const applyMigration = db.transaction((name, sql) => {
|
|
49
|
+
db.exec(sql);
|
|
50
|
+
db.prepare('INSERT INTO _migrations (name) VALUES (?)').run(name);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
if (applied.has(file)) continue;
|
|
55
|
+
|
|
56
|
+
const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, file), 'utf8');
|
|
57
|
+
try {
|
|
58
|
+
applyMigration(file, sql);
|
|
59
|
+
console.log(` ✅ Migration applied: ${file}`);
|
|
60
|
+
count++;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error(` ❌ Migration failed: ${file} — ${err.message}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (count === 0) {
|
|
68
|
+
console.log(' All migrations up to date.');
|
|
69
|
+
} else {
|
|
70
|
+
console.log(` ${count} migration(s) applied.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Run when called directly: node server/utils/migrate.js
|
|
75
|
+
if (require.main === module) {
|
|
76
|
+
console.log('Running database migrations...');
|
|
77
|
+
runMigrations();
|
|
78
|
+
db.close();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { runMigrations };
|
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional AES-256-GCM encryption for sensitive DB fields (e.g. SMTP password).
|
|
3
|
-
* Set CREDENTIALS_ENCRYPTION_KEY (any long random string) to enable at-rest encryption.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const crypto = require('crypto');
|
|
7
|
-
|
|
8
|
-
const PREFIX = 'enc:v1:';
|
|
9
|
-
|
|
10
|
-
function getKey() {
|
|
11
|
-
const raw = process.env.CREDENTIALS_ENCRYPTION_KEY;
|
|
12
|
-
if (!raw || String(raw).length < 8) return null;
|
|
13
|
-
return crypto.createHash('sha256').update(String(raw)).digest();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function encryptOptional(plain) {
|
|
17
|
-
if (plain == null || plain === '') return plain;
|
|
18
|
-
const key = getKey();
|
|
19
|
-
if (!key) return plain;
|
|
20
|
-
const iv = crypto.randomBytes(12);
|
|
21
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
22
|
-
const enc = Buffer.concat([cipher.update(String(plain), 'utf8'), cipher.final()]);
|
|
23
|
-
const tag = cipher.getAuthTag();
|
|
24
|
-
return `${PREFIX}${iv.toString('hex')}:${tag.toString('hex')}:${enc.toString('hex')}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function decryptOptional(stored) {
|
|
28
|
-
if (stored == null || stored === '') return stored;
|
|
29
|
-
if (typeof stored !== 'string' || !stored.startsWith(PREFIX)) return stored;
|
|
30
|
-
const key = getKey();
|
|
31
|
-
if (!key) {
|
|
32
|
-
console.warn('[WAB] CREDENTIALS_ENCRYPTION_KEY missing; cannot decrypt stored credential');
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
try {
|
|
36
|
-
const rest = stored.slice(PREFIX.length);
|
|
37
|
-
const [ivHex, tagHex, dataHex] = rest.split(':');
|
|
38
|
-
const iv = Buffer.from(ivHex, 'hex');
|
|
39
|
-
const tag = Buffer.from(tagHex, 'hex');
|
|
40
|
-
const data = Buffer.from(dataHex, 'hex');
|
|
41
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
42
|
-
decipher.setAuthTag(tag);
|
|
43
|
-
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
|
|
44
|
-
} catch (e) {
|
|
45
|
-
console.error('[WAB] Decrypt failed:', e.message);
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
module.exports = { encryptOptional, decryptOptional };
|
|
1
|
+
/**
|
|
2
|
+
* Optional AES-256-GCM encryption for sensitive DB fields (e.g. SMTP password).
|
|
3
|
+
* Set CREDENTIALS_ENCRYPTION_KEY (any long random string) to enable at-rest encryption.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const PREFIX = 'enc:v1:';
|
|
9
|
+
|
|
10
|
+
function getKey() {
|
|
11
|
+
const raw = process.env.CREDENTIALS_ENCRYPTION_KEY;
|
|
12
|
+
if (!raw || String(raw).length < 8) return null;
|
|
13
|
+
return crypto.createHash('sha256').update(String(raw)).digest();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function encryptOptional(plain) {
|
|
17
|
+
if (plain == null || plain === '') return plain;
|
|
18
|
+
const key = getKey();
|
|
19
|
+
if (!key) return plain;
|
|
20
|
+
const iv = crypto.randomBytes(12);
|
|
21
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
22
|
+
const enc = Buffer.concat([cipher.update(String(plain), 'utf8'), cipher.final()]);
|
|
23
|
+
const tag = cipher.getAuthTag();
|
|
24
|
+
return `${PREFIX}${iv.toString('hex')}:${tag.toString('hex')}:${enc.toString('hex')}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function decryptOptional(stored) {
|
|
28
|
+
if (stored == null || stored === '') return stored;
|
|
29
|
+
if (typeof stored !== 'string' || !stored.startsWith(PREFIX)) return stored;
|
|
30
|
+
const key = getKey();
|
|
31
|
+
if (!key) {
|
|
32
|
+
console.warn('[WAB] CREDENTIALS_ENCRYPTION_KEY missing; cannot decrypt stored credential');
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const rest = stored.slice(PREFIX.length);
|
|
37
|
+
const [ivHex, tagHex, dataHex] = rest.split(':');
|
|
38
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
39
|
+
const tag = Buffer.from(tagHex, 'hex');
|
|
40
|
+
const data = Buffer.from(dataHex, 'hex');
|
|
41
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
42
|
+
decipher.setAuthTag(tag);
|
|
43
|
+
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.error('[WAB] Decrypt failed:', e.message);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { encryptOptional, decryptOptional };
|
package/server/ws.js
CHANGED
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
const WebSocket = require('ws');
|
|
2
|
-
const { verifyUserToken, verifyAdminToken } = require('./config/secrets');
|
|
3
|
-
const { findSiteById } = require('./models/db');
|
|
4
|
-
|
|
5
|
-
// Map of siteId → Set of WebSocket clients
|
|
6
|
-
const siteClients = new Map();
|
|
7
|
-
|
|
8
|
-
function setupWebSocket(server) {
|
|
9
|
-
const wss = new WebSocket.Server({ server, path: '/ws/analytics' });
|
|
10
|
-
|
|
11
|
-
wss.on('connection', (ws, req) => {
|
|
12
|
-
let authenticatedSiteId = null;
|
|
13
|
-
|
|
14
|
-
ws.isAlive = true;
|
|
15
|
-
ws.on('pong', () => { ws.isAlive = true; });
|
|
16
|
-
|
|
17
|
-
ws.on('message', (data) => {
|
|
18
|
-
try {
|
|
19
|
-
const msg = JSON.parse(data);
|
|
20
|
-
|
|
21
|
-
if (msg.type === 'auth') {
|
|
22
|
-
if (!msg.token || !msg.siteId) {
|
|
23
|
-
ws.send(JSON.stringify({ type: 'error', message: 'token and siteId required' }));
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let decoded;
|
|
28
|
-
let isAdmin = false;
|
|
29
|
-
try {
|
|
30
|
-
decoded = verifyUserToken(msg.token);
|
|
31
|
-
} catch {
|
|
32
|
-
try {
|
|
33
|
-
decoded = verifyAdminToken(msg.token);
|
|
34
|
-
isAdmin = decoded.isAdmin === true;
|
|
35
|
-
} catch {
|
|
36
|
-
ws.send(JSON.stringify({ type: 'error', message: 'Invalid message or auth failed' }));
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (!isAdmin) {
|
|
42
|
-
const site = findSiteById.get(msg.siteId);
|
|
43
|
-
if (!site || site.user_id !== decoded.id) {
|
|
44
|
-
ws.send(JSON.stringify({ type: 'error', message: 'Forbidden: not your site' }));
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
authenticatedSiteId = msg.siteId;
|
|
50
|
-
if (!siteClients.has(msg.siteId)) {
|
|
51
|
-
siteClients.set(msg.siteId, new Set());
|
|
52
|
-
}
|
|
53
|
-
siteClients.get(msg.siteId).add(ws);
|
|
54
|
-
ws.send(JSON.stringify({ type: 'auth:success', siteId: msg.siteId }));
|
|
55
|
-
}
|
|
56
|
-
} catch (e) {
|
|
57
|
-
ws.send(JSON.stringify({ type: 'error', message: 'Invalid message or auth failed' }));
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
ws.on('close', () => {
|
|
62
|
-
if (authenticatedSiteId && siteClients.has(authenticatedSiteId)) {
|
|
63
|
-
siteClients.get(authenticatedSiteId).delete(ws);
|
|
64
|
-
if (siteClients.get(authenticatedSiteId).size === 0) {
|
|
65
|
-
siteClients.delete(authenticatedSiteId);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const interval = setInterval(() => {
|
|
72
|
-
wss.clients.forEach((ws) => {
|
|
73
|
-
if (!ws.isAlive) return ws.terminate();
|
|
74
|
-
ws.isAlive = false;
|
|
75
|
-
ws.ping();
|
|
76
|
-
});
|
|
77
|
-
}, 30000);
|
|
78
|
-
|
|
79
|
-
wss.on('close', () => clearInterval(interval));
|
|
80
|
-
|
|
81
|
-
return wss;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function broadcastAnalytic(siteId, eventData) {
|
|
85
|
-
const clients = siteClients.get(siteId);
|
|
86
|
-
if (!clients || clients.size === 0) return;
|
|
87
|
-
|
|
88
|
-
const message = JSON.stringify({
|
|
89
|
-
type: 'analytic',
|
|
90
|
-
timestamp: new Date().toISOString(),
|
|
91
|
-
...eventData
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
clients.forEach((ws) => {
|
|
95
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
96
|
-
ws.send(message);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
module.exports = { setupWebSocket, broadcastAnalytic };
|
|
1
|
+
const WebSocket = require('ws');
|
|
2
|
+
const { verifyUserToken, verifyAdminToken } = require('./config/secrets');
|
|
3
|
+
const { findSiteById } = require('./models/db');
|
|
4
|
+
|
|
5
|
+
// Map of siteId → Set of WebSocket clients
|
|
6
|
+
const siteClients = new Map();
|
|
7
|
+
|
|
8
|
+
function setupWebSocket(server) {
|
|
9
|
+
const wss = new WebSocket.Server({ server, path: '/ws/analytics' });
|
|
10
|
+
|
|
11
|
+
wss.on('connection', (ws, req) => {
|
|
12
|
+
let authenticatedSiteId = null;
|
|
13
|
+
|
|
14
|
+
ws.isAlive = true;
|
|
15
|
+
ws.on('pong', () => { ws.isAlive = true; });
|
|
16
|
+
|
|
17
|
+
ws.on('message', (data) => {
|
|
18
|
+
try {
|
|
19
|
+
const msg = JSON.parse(data);
|
|
20
|
+
|
|
21
|
+
if (msg.type === 'auth') {
|
|
22
|
+
if (!msg.token || !msg.siteId) {
|
|
23
|
+
ws.send(JSON.stringify({ type: 'error', message: 'token and siteId required' }));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let decoded;
|
|
28
|
+
let isAdmin = false;
|
|
29
|
+
try {
|
|
30
|
+
decoded = verifyUserToken(msg.token);
|
|
31
|
+
} catch {
|
|
32
|
+
try {
|
|
33
|
+
decoded = verifyAdminToken(msg.token);
|
|
34
|
+
isAdmin = decoded.isAdmin === true;
|
|
35
|
+
} catch {
|
|
36
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Invalid message or auth failed' }));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!isAdmin) {
|
|
42
|
+
const site = findSiteById.get(msg.siteId);
|
|
43
|
+
if (!site || site.user_id !== decoded.id) {
|
|
44
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Forbidden: not your site' }));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
authenticatedSiteId = msg.siteId;
|
|
50
|
+
if (!siteClients.has(msg.siteId)) {
|
|
51
|
+
siteClients.set(msg.siteId, new Set());
|
|
52
|
+
}
|
|
53
|
+
siteClients.get(msg.siteId).add(ws);
|
|
54
|
+
ws.send(JSON.stringify({ type: 'auth:success', siteId: msg.siteId }));
|
|
55
|
+
}
|
|
56
|
+
} catch (e) {
|
|
57
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Invalid message or auth failed' }));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
ws.on('close', () => {
|
|
62
|
+
if (authenticatedSiteId && siteClients.has(authenticatedSiteId)) {
|
|
63
|
+
siteClients.get(authenticatedSiteId).delete(ws);
|
|
64
|
+
if (siteClients.get(authenticatedSiteId).size === 0) {
|
|
65
|
+
siteClients.delete(authenticatedSiteId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const interval = setInterval(() => {
|
|
72
|
+
wss.clients.forEach((ws) => {
|
|
73
|
+
if (!ws.isAlive) return ws.terminate();
|
|
74
|
+
ws.isAlive = false;
|
|
75
|
+
ws.ping();
|
|
76
|
+
});
|
|
77
|
+
}, 30000);
|
|
78
|
+
|
|
79
|
+
wss.on('close', () => clearInterval(interval));
|
|
80
|
+
|
|
81
|
+
return wss;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function broadcastAnalytic(siteId, eventData) {
|
|
85
|
+
const clients = siteClients.get(siteId);
|
|
86
|
+
if (!clients || clients.size === 0) return;
|
|
87
|
+
|
|
88
|
+
const message = JSON.stringify({
|
|
89
|
+
type: 'analytic',
|
|
90
|
+
timestamp: new Date().toISOString(),
|
|
91
|
+
...eventData
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
clients.forEach((ws) => {
|
|
95
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
96
|
+
ws.send(message);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { setupWebSocket, broadcastAnalytic };
|