web-agent-bridge 1.0.0
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 -0
- package/README.ar.md +446 -0
- package/README.md +544 -0
- package/bin/cli.js +80 -0
- package/bin/wab.js +80 -0
- package/examples/bidi-agent.js +119 -0
- package/examples/puppeteer-agent.js +108 -0
- package/examples/vision-agent.js +159 -0
- package/package.json +67 -0
- package/public/css/styles.css +1235 -0
- package/public/dashboard.html +566 -0
- package/public/docs.html +582 -0
- package/public/index.html +306 -0
- package/public/login.html +81 -0
- package/public/register.html +94 -0
- package/script/ai-agent-bridge.js +1282 -0
- package/sdk/README.md +55 -0
- package/sdk/index.js +167 -0
- package/sdk/package.json +14 -0
- package/server/index.js +105 -0
- package/server/middleware/auth.js +44 -0
- package/server/models/adapters/index.js +33 -0
- package/server/models/adapters/mysql.js +183 -0
- package/server/models/adapters/postgresql.js +172 -0
- package/server/models/adapters/sqlite.js +7 -0
- package/server/models/db.js +205 -0
- package/server/routes/api.js +121 -0
- package/server/routes/auth.js +51 -0
- package/server/routes/license.js +51 -0
- package/server/ws.js +81 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Adapter for WAB
|
|
3
|
+
*
|
|
4
|
+
* Prerequisites: npm install pg
|
|
5
|
+
* Set DATABASE_URL=postgres://user:pass@host:5432/wab
|
|
6
|
+
*
|
|
7
|
+
* This adapter implements the same interface as the SQLite adapter
|
|
8
|
+
* so it can be used as a drop-in replacement.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { Pool } = require('pg');
|
|
12
|
+
const bcrypt = require('bcryptjs');
|
|
13
|
+
const { v4: uuidv4 } = require('uuid');
|
|
14
|
+
|
|
15
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
16
|
+
|
|
17
|
+
// Initialize tables
|
|
18
|
+
async function initDB() {
|
|
19
|
+
await pool.query(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
email TEXT UNIQUE NOT NULL,
|
|
23
|
+
password TEXT NOT NULL,
|
|
24
|
+
name TEXT NOT NULL,
|
|
25
|
+
company TEXT,
|
|
26
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
27
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS sites (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
33
|
+
domain TEXT NOT NULL,
|
|
34
|
+
name TEXT NOT NULL,
|
|
35
|
+
description TEXT,
|
|
36
|
+
tier TEXT DEFAULT 'free' CHECK(tier IN ('free','starter','pro','enterprise')),
|
|
37
|
+
license_key TEXT UNIQUE NOT NULL,
|
|
38
|
+
api_key TEXT UNIQUE,
|
|
39
|
+
config JSONB DEFAULT '{}',
|
|
40
|
+
active BOOLEAN DEFAULT TRUE,
|
|
41
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
42
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE IF NOT EXISTS analytics (
|
|
46
|
+
id SERIAL PRIMARY KEY,
|
|
47
|
+
site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
|
48
|
+
action_name TEXT NOT NULL,
|
|
49
|
+
agent_id TEXT,
|
|
50
|
+
trigger_type TEXT,
|
|
51
|
+
success BOOLEAN,
|
|
52
|
+
metadata JSONB DEFAULT '{}',
|
|
53
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
59
|
+
site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
|
60
|
+
tier TEXT NOT NULL CHECK(tier IN ('free','starter','pro','enterprise')),
|
|
61
|
+
status TEXT DEFAULT 'active' CHECK(status IN ('active','cancelled','expired','trial')),
|
|
62
|
+
started_at TIMESTAMPTZ DEFAULT NOW(),
|
|
63
|
+
expires_at TIMESTAMPTZ
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_sites_domain ON sites(domain);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_sites_license ON sites(license_key);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_analytics_site ON analytics(site_id);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_analytics_created ON analytics(created_at);
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
initDB().catch(console.error);
|
|
74
|
+
|
|
75
|
+
function generateLicenseKey() {
|
|
76
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
77
|
+
const segments = [];
|
|
78
|
+
for (let s = 0; s < 4; s++) {
|
|
79
|
+
let seg = '';
|
|
80
|
+
for (let i = 0; i < 5; i++) seg += chars[Math.floor(Math.random() * chars.length)];
|
|
81
|
+
segments.push(seg);
|
|
82
|
+
}
|
|
83
|
+
return `WAB-${segments.join('-')}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function generateApiKey() {
|
|
87
|
+
return `wab_${uuidv4().replace(/-/g, '')}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── User Operations ──────────────────────────────────────────────────
|
|
91
|
+
async function registerUser({ email, password, name, company }) {
|
|
92
|
+
const id = uuidv4();
|
|
93
|
+
const hashed = bcrypt.hashSync(password, 12);
|
|
94
|
+
await pool.query(
|
|
95
|
+
'INSERT INTO users (id, email, password, name, company) VALUES ($1, $2, $3, $4, $5)',
|
|
96
|
+
[id, email, hashed, name, company || null]
|
|
97
|
+
);
|
|
98
|
+
return { id, email, name, company };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function loginUser({ email, password }) {
|
|
102
|
+
const { rows } = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
|
|
103
|
+
const user = rows[0];
|
|
104
|
+
if (!user) return null;
|
|
105
|
+
if (!bcrypt.compareSync(password, user.password)) return null;
|
|
106
|
+
return { id: user.id, email: user.email, name: user.name, company: user.company };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Site Operations ──────────────────────────────────────────────────
|
|
110
|
+
async function addSite({ userId, domain, name, description, tier }) {
|
|
111
|
+
const id = uuidv4();
|
|
112
|
+
const licenseKey = generateLicenseKey();
|
|
113
|
+
const apiKey = generateApiKey();
|
|
114
|
+
const config = {
|
|
115
|
+
agentPermissions: { readContent: true, click: true, fillForms: false, scroll: true, navigate: false, apiAccess: false, automatedLogin: false, extractData: false },
|
|
116
|
+
features: { advancedAnalytics: false, realTimeUpdates: false },
|
|
117
|
+
restrictions: { allowedSelectors: [], blockedSelectors: ['.private', '[data-private]'], rateLimit: { maxCallsPerMinute: 60 } },
|
|
118
|
+
logging: { enabled: false, level: 'basic' }
|
|
119
|
+
};
|
|
120
|
+
await pool.query(
|
|
121
|
+
'INSERT INTO sites (id, user_id, domain, name, description, tier, license_key, api_key, config) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)',
|
|
122
|
+
[id, userId, domain, name, description || '', tier || 'free', licenseKey, apiKey, JSON.stringify(config)]
|
|
123
|
+
);
|
|
124
|
+
return { id, domain, name, licenseKey, apiKey, tier: tier || 'free' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Analytics ────────────────────────────────────────────────────────
|
|
128
|
+
async function recordAnalytic({ siteId, actionName, agentId, triggerType, success, metadata }) {
|
|
129
|
+
await pool.query(
|
|
130
|
+
'INSERT INTO analytics (site_id, action_name, agent_id, trigger_type, success, metadata) VALUES ($1,$2,$3,$4,$5,$6)',
|
|
131
|
+
[siteId, actionName, agentId || null, triggerType || null, success, JSON.stringify(metadata || {})]
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── License Verification ─────────────────────────────────────────────
|
|
136
|
+
async function verifyLicense(domain, licenseKey) {
|
|
137
|
+
const { rows } = await pool.query(
|
|
138
|
+
'SELECT * FROM sites WHERE domain = $1 AND license_key = $2 AND active = TRUE', [domain, licenseKey]
|
|
139
|
+
);
|
|
140
|
+
const site = rows[0];
|
|
141
|
+
if (!site) {
|
|
142
|
+
const { rows: byKey } = await pool.query('SELECT * FROM sites WHERE license_key = $1 AND active = TRUE', [licenseKey]);
|
|
143
|
+
if (byKey[0]) return { valid: false, error: 'Domain mismatch', tier: 'free' };
|
|
144
|
+
return { valid: false, error: 'Invalid license key', tier: 'free' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const tierPermissions = {
|
|
148
|
+
free: { apiAccess: false, automatedLogin: false, extractData: false, advancedAnalytics: false },
|
|
149
|
+
starter: { apiAccess: false, automatedLogin: true, extractData: false, advancedAnalytics: true },
|
|
150
|
+
pro: { apiAccess: true, automatedLogin: true, extractData: true, advancedAnalytics: true },
|
|
151
|
+
enterprise: { apiAccess: true, automatedLogin: true, extractData: true, advancedAnalytics: true }
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const config = typeof site.config === 'string' ? JSON.parse(site.config) : site.config;
|
|
155
|
+
return {
|
|
156
|
+
valid: true,
|
|
157
|
+
tier: site.tier,
|
|
158
|
+
permissions: { ...config.agentPermissions, ...tierPermissions[site.tier] },
|
|
159
|
+
restrictions: config.restrictions,
|
|
160
|
+
features: config.features,
|
|
161
|
+
siteId: site.id
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
registerUser,
|
|
167
|
+
loginUser,
|
|
168
|
+
addSite,
|
|
169
|
+
recordAnalytic,
|
|
170
|
+
verifyLicense,
|
|
171
|
+
pool
|
|
172
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
const Database = require('better-sqlite3');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const bcrypt = require('bcryptjs');
|
|
5
|
+
const { v4: uuidv4 } = require('uuid');
|
|
6
|
+
|
|
7
|
+
const isTest = process.env.NODE_ENV === 'test';
|
|
8
|
+
const DATA_DIR = isTest
|
|
9
|
+
? path.join(__dirname, '..', '..', 'data-test')
|
|
10
|
+
: path.join(__dirname, '..', '..', 'data');
|
|
11
|
+
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
12
|
+
|
|
13
|
+
const dbFile = isTest ? 'wab-test.db' : 'wab.db';
|
|
14
|
+
const db = new Database(path.join(DATA_DIR, dbFile));
|
|
15
|
+
|
|
16
|
+
db.pragma('journal_mode = WAL');
|
|
17
|
+
db.pragma('foreign_keys = ON');
|
|
18
|
+
|
|
19
|
+
db.exec(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
email TEXT UNIQUE NOT NULL,
|
|
23
|
+
password TEXT NOT NULL,
|
|
24
|
+
name TEXT NOT NULL,
|
|
25
|
+
company TEXT,
|
|
26
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
27
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS sites (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
user_id TEXT NOT NULL,
|
|
33
|
+
domain TEXT NOT NULL,
|
|
34
|
+
name TEXT NOT NULL,
|
|
35
|
+
description TEXT,
|
|
36
|
+
tier TEXT DEFAULT 'free' CHECK(tier IN ('free','starter','pro','enterprise')),
|
|
37
|
+
license_key TEXT UNIQUE NOT NULL,
|
|
38
|
+
api_key TEXT UNIQUE,
|
|
39
|
+
config TEXT DEFAULT '{}',
|
|
40
|
+
active INTEGER DEFAULT 1,
|
|
41
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
42
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
43
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS analytics (
|
|
47
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
48
|
+
site_id TEXT NOT NULL,
|
|
49
|
+
action_name TEXT NOT NULL,
|
|
50
|
+
agent_id TEXT,
|
|
51
|
+
trigger_type TEXT,
|
|
52
|
+
success INTEGER,
|
|
53
|
+
metadata TEXT DEFAULT '{}',
|
|
54
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
55
|
+
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
user_id TEXT NOT NULL,
|
|
61
|
+
site_id TEXT NOT NULL,
|
|
62
|
+
tier TEXT NOT NULL CHECK(tier IN ('free','starter','pro','enterprise')),
|
|
63
|
+
status TEXT DEFAULT 'active' CHECK(status IN ('active','cancelled','expired','trial')),
|
|
64
|
+
started_at TEXT DEFAULT (datetime('now')),
|
|
65
|
+
expires_at TEXT,
|
|
66
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
67
|
+
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_sites_domain ON sites(domain);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_sites_license ON sites(license_key);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_analytics_site ON analytics(site_id);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_analytics_created ON analytics(created_at);
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
function generateLicenseKey() {
|
|
77
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
78
|
+
const segments = [];
|
|
79
|
+
for (let s = 0; s < 4; s++) {
|
|
80
|
+
let seg = '';
|
|
81
|
+
for (let i = 0; i < 5; i++) seg += chars[Math.floor(Math.random() * chars.length)];
|
|
82
|
+
segments.push(seg);
|
|
83
|
+
}
|
|
84
|
+
return `WAB-${segments.join('-')}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function generateApiKey() {
|
|
88
|
+
return `wab_${uuidv4().replace(/-/g, '')}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── User Operations ──────────────────────────────────────────────────
|
|
92
|
+
const createUser = db.prepare(`
|
|
93
|
+
INSERT INTO users (id, email, password, name, company) VALUES (?, ?, ?, ?, ?)
|
|
94
|
+
`);
|
|
95
|
+
|
|
96
|
+
const findUserByEmail = db.prepare(`SELECT * FROM users WHERE email = ?`);
|
|
97
|
+
const findUserById = db.prepare(`SELECT id, email, name, company, created_at FROM users WHERE id = ?`);
|
|
98
|
+
|
|
99
|
+
function registerUser({ email, password, name, company }) {
|
|
100
|
+
const id = uuidv4();
|
|
101
|
+
const hashed = bcrypt.hashSync(password, 12);
|
|
102
|
+
createUser.run(id, email, hashed, name, company || null);
|
|
103
|
+
return { id, email, name, company };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function loginUser({ email, password }) {
|
|
107
|
+
const user = findUserByEmail.get(email);
|
|
108
|
+
if (!user) return null;
|
|
109
|
+
if (!bcrypt.compareSync(password, user.password)) return null;
|
|
110
|
+
return { id: user.id, email: user.email, name: user.name, company: user.company };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Site Operations ──────────────────────────────────────────────────
|
|
114
|
+
const createSite = db.prepare(`
|
|
115
|
+
INSERT INTO sites (id, user_id, domain, name, description, tier, license_key, api_key, config)
|
|
116
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
117
|
+
`);
|
|
118
|
+
|
|
119
|
+
const findSitesByUser = db.prepare(`SELECT * FROM sites WHERE user_id = ? ORDER BY created_at DESC`);
|
|
120
|
+
const findSiteById = db.prepare(`SELECT * FROM sites WHERE id = ?`);
|
|
121
|
+
const findSiteByLicense = db.prepare(`SELECT * FROM sites WHERE license_key = ? AND active = 1`);
|
|
122
|
+
const findSiteByDomainAndLicense = db.prepare(`SELECT * FROM sites WHERE domain = ? AND license_key = ? AND active = 1`);
|
|
123
|
+
const updateSiteConfig = db.prepare(`UPDATE sites SET config = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`);
|
|
124
|
+
const updateSiteTier = db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`);
|
|
125
|
+
const deleteSite = db.prepare(`UPDATE sites SET active = 0, updated_at = datetime('now') WHERE id = ? AND user_id = ?`);
|
|
126
|
+
|
|
127
|
+
function addSite({ userId, domain, name, description, tier }) {
|
|
128
|
+
const id = uuidv4();
|
|
129
|
+
const licenseKey = generateLicenseKey();
|
|
130
|
+
const apiKey = generateApiKey();
|
|
131
|
+
const config = JSON.stringify({
|
|
132
|
+
agentPermissions: { readContent: true, click: true, fillForms: false, scroll: true, navigate: false, apiAccess: false, automatedLogin: false, extractData: false },
|
|
133
|
+
features: { advancedAnalytics: false, realTimeUpdates: false },
|
|
134
|
+
restrictions: { allowedSelectors: [], blockedSelectors: ['.private', '[data-private]'], rateLimit: { maxCallsPerMinute: 60 } },
|
|
135
|
+
logging: { enabled: false, level: 'basic' }
|
|
136
|
+
});
|
|
137
|
+
createSite.run(id, userId, domain, name, description || '', tier || 'free', licenseKey, apiKey, config);
|
|
138
|
+
return { id, domain, name, licenseKey, apiKey, tier: tier || 'free' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Analytics ────────────────────────────────────────────────────────
|
|
142
|
+
const insertAnalytic = db.prepare(`
|
|
143
|
+
INSERT INTO analytics (site_id, action_name, agent_id, trigger_type, success, metadata)
|
|
144
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
145
|
+
`);
|
|
146
|
+
|
|
147
|
+
const getAnalyticsBySite = db.prepare(`
|
|
148
|
+
SELECT action_name, trigger_type, COUNT(*) as count, SUM(success) as successes
|
|
149
|
+
FROM analytics WHERE site_id = ? AND created_at >= ? GROUP BY action_name, trigger_type
|
|
150
|
+
ORDER BY count DESC
|
|
151
|
+
`);
|
|
152
|
+
|
|
153
|
+
const getAnalyticsTimeline = db.prepare(`
|
|
154
|
+
SELECT date(created_at) as day, COUNT(*) as count
|
|
155
|
+
FROM analytics WHERE site_id = ? AND created_at >= ?
|
|
156
|
+
GROUP BY day ORDER BY day
|
|
157
|
+
`);
|
|
158
|
+
|
|
159
|
+
function recordAnalytic({ siteId, actionName, agentId, triggerType, success, metadata }) {
|
|
160
|
+
insertAnalytic.run(siteId, actionName, agentId || null, triggerType || null, success ? 1 : 0, JSON.stringify(metadata || {}));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─── License Verification ─────────────────────────────────────────────
|
|
164
|
+
function verifyLicense(domain, licenseKey) {
|
|
165
|
+
const site = findSiteByDomainAndLicense.get(domain, licenseKey);
|
|
166
|
+
if (!site) {
|
|
167
|
+
const siteByKey = findSiteByLicense.get(licenseKey);
|
|
168
|
+
if (siteByKey) return { valid: false, error: 'Domain mismatch', tier: 'free' };
|
|
169
|
+
return { valid: false, error: 'Invalid license key', tier: 'free' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const tierPermissions = {
|
|
173
|
+
free: { apiAccess: false, automatedLogin: false, extractData: false, advancedAnalytics: false },
|
|
174
|
+
starter: { apiAccess: false, automatedLogin: true, extractData: false, advancedAnalytics: true },
|
|
175
|
+
pro: { apiAccess: true, automatedLogin: true, extractData: true, advancedAnalytics: true },
|
|
176
|
+
enterprise: { apiAccess: true, automatedLogin: true, extractData: true, advancedAnalytics: true }
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
valid: true,
|
|
181
|
+
tier: site.tier,
|
|
182
|
+
domain: site.domain,
|
|
183
|
+
allowedPermissions: tierPermissions[site.tier] || tierPermissions.free
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
db,
|
|
189
|
+
registerUser,
|
|
190
|
+
loginUser,
|
|
191
|
+
findUserById,
|
|
192
|
+
addSite,
|
|
193
|
+
findSitesByUser,
|
|
194
|
+
findSiteById,
|
|
195
|
+
findSiteByLicense,
|
|
196
|
+
updateSiteConfig,
|
|
197
|
+
updateSiteTier,
|
|
198
|
+
deleteSite,
|
|
199
|
+
recordAnalytic,
|
|
200
|
+
getAnalyticsBySite,
|
|
201
|
+
getAnalyticsTimeline,
|
|
202
|
+
verifyLicense,
|
|
203
|
+
generateLicenseKey,
|
|
204
|
+
generateApiKey
|
|
205
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const { authenticateToken } = require('../middleware/auth');
|
|
4
|
+
const {
|
|
5
|
+
addSite, findSitesByUser, findSiteById,
|
|
6
|
+
updateSiteConfig, updateSiteTier, deleteSite,
|
|
7
|
+
getAnalyticsBySite, getAnalyticsTimeline
|
|
8
|
+
} = require('../models/db');
|
|
9
|
+
|
|
10
|
+
// ─── Sites ──────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
router.get('/sites', authenticateToken, (req, res) => {
|
|
13
|
+
const sites = findSitesByUser.all(req.user.id);
|
|
14
|
+
res.json({
|
|
15
|
+
sites: sites.filter(s => s.active).map(s => ({
|
|
16
|
+
...s,
|
|
17
|
+
config: JSON.parse(s.config || '{}')
|
|
18
|
+
}))
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
router.post('/sites', authenticateToken, (req, res) => {
|
|
23
|
+
const { domain, name, description, tier } = req.body;
|
|
24
|
+
|
|
25
|
+
if (!domain || !name) {
|
|
26
|
+
return res.status(400).json({ error: 'Domain and name are required' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const site = addSite({ userId: req.user.id, domain, name, description, tier });
|
|
31
|
+
res.status(201).json({ site });
|
|
32
|
+
} catch (err) {
|
|
33
|
+
res.status(500).json({ error: 'Failed to create site' });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
router.get('/sites/:id', authenticateToken, (req, res) => {
|
|
38
|
+
const site = findSiteById.get(req.params.id);
|
|
39
|
+
if (!site || site.user_id !== req.user.id) {
|
|
40
|
+
return res.status(404).json({ error: 'Site not found' });
|
|
41
|
+
}
|
|
42
|
+
res.json({ site: { ...site, config: JSON.parse(site.config || '{}') } });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
router.put('/sites/:id/config', authenticateToken, (req, res) => {
|
|
46
|
+
const { config } = req.body;
|
|
47
|
+
if (!config) return res.status(400).json({ error: 'Config is required' });
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
updateSiteConfig.run(JSON.stringify(config), req.params.id, req.user.id);
|
|
51
|
+
res.json({ success: true });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
res.status(500).json({ error: 'Failed to update config' });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
router.put('/sites/:id/tier', authenticateToken, (req, res) => {
|
|
58
|
+
const { tier } = req.body;
|
|
59
|
+
if (!['free', 'starter', 'pro', 'enterprise'].includes(tier)) {
|
|
60
|
+
return res.status(400).json({ error: 'Invalid tier' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
updateSiteTier.run(tier, req.params.id, req.user.id);
|
|
65
|
+
res.json({ success: true, tier });
|
|
66
|
+
} catch (err) {
|
|
67
|
+
res.status(500).json({ error: 'Failed to update tier' });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
router.delete('/sites/:id', authenticateToken, (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
deleteSite.run(req.params.id, req.user.id);
|
|
74
|
+
res.json({ success: true });
|
|
75
|
+
} catch (err) {
|
|
76
|
+
res.status(500).json({ error: 'Failed to delete site' });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ─── Analytics ──────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
router.get('/sites/:id/analytics', authenticateToken, (req, res) => {
|
|
83
|
+
const site = findSiteById.get(req.params.id);
|
|
84
|
+
if (!site || site.user_id !== req.user.id) {
|
|
85
|
+
return res.status(404).json({ error: 'Site not found' });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const days = parseInt(req.query.days) || 30;
|
|
89
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
90
|
+
|
|
91
|
+
const summary = getAnalyticsBySite.all(site.id, since);
|
|
92
|
+
const timeline = getAnalyticsTimeline.all(site.id, since);
|
|
93
|
+
|
|
94
|
+
res.json({ summary, timeline, period: `${days} days` });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ─── Script Generation ──────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
router.get('/sites/:id/snippet', authenticateToken, (req, res) => {
|
|
100
|
+
const site = findSiteById.get(req.params.id);
|
|
101
|
+
if (!site || site.user_id !== req.user.id) {
|
|
102
|
+
return res.status(404).json({ error: 'Site not found' });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const config = JSON.parse(site.config || '{}');
|
|
106
|
+
const snippet = `<!-- Web Agent Bridge -->
|
|
107
|
+
<script>
|
|
108
|
+
window.AIBridgeConfig = {
|
|
109
|
+
licenseKey: "${site.license_key}",
|
|
110
|
+
subscriptionTier: "${site.tier}",
|
|
111
|
+
agentPermissions: ${JSON.stringify(config.agentPermissions || {}, null, 4)},
|
|
112
|
+
restrictions: ${JSON.stringify(config.restrictions || {}, null, 4)},
|
|
113
|
+
logging: ${JSON.stringify(config.logging || {}, null, 4)}
|
|
114
|
+
};
|
|
115
|
+
</script>
|
|
116
|
+
<script src="/script/ai-agent-bridge.js"></script>`;
|
|
117
|
+
|
|
118
|
+
res.json({ snippet, licenseKey: site.license_key });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
module.exports = router;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const { registerUser, loginUser, findUserById } = require('../models/db');
|
|
4
|
+
const { generateToken, authenticateToken } = require('../middleware/auth');
|
|
5
|
+
|
|
6
|
+
router.post('/register', (req, res) => {
|
|
7
|
+
const { email, password, name, company } = req.body;
|
|
8
|
+
|
|
9
|
+
if (!email || !password || !name) {
|
|
10
|
+
return res.status(400).json({ error: 'Email, password, and name are required' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (password.length < 8) {
|
|
14
|
+
return res.status(400).json({ error: 'Password must be at least 8 characters' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const user = registerUser({ email, password, name, company });
|
|
19
|
+
const token = generateToken(user);
|
|
20
|
+
res.status(201).json({ user, token });
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err.message.includes('UNIQUE constraint')) {
|
|
23
|
+
return res.status(409).json({ error: 'Email already registered' });
|
|
24
|
+
}
|
|
25
|
+
res.status(500).json({ error: 'Registration failed' });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
router.post('/login', (req, res) => {
|
|
30
|
+
const { email, password } = req.body;
|
|
31
|
+
|
|
32
|
+
if (!email || !password) {
|
|
33
|
+
return res.status(400).json({ error: 'Email and password are required' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const user = loginUser({ email, password });
|
|
37
|
+
if (!user) {
|
|
38
|
+
return res.status(401).json({ error: 'Invalid email or password' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const token = generateToken(user);
|
|
42
|
+
res.json({ user, token });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
router.get('/me', authenticateToken, (req, res) => {
|
|
46
|
+
const user = findUserById.get(req.user.id);
|
|
47
|
+
if (!user) return res.status(404).json({ error: 'User not found' });
|
|
48
|
+
res.json({ user });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
module.exports = router;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const { verifyLicense, recordAnalytic, findSiteByLicense } = require('../models/db');
|
|
4
|
+
const { broadcastAnalytic } = require('../ws');
|
|
5
|
+
|
|
6
|
+
router.post('/verify', (req, res) => {
|
|
7
|
+
const { domain, licenseKey } = req.body;
|
|
8
|
+
|
|
9
|
+
if (!domain || !licenseKey) {
|
|
10
|
+
return res.status(400).json({ valid: false, error: 'Domain and licenseKey are required', tier: 'free' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const result = verifyLicense(domain, licenseKey);
|
|
14
|
+
res.json(result);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
router.post('/track', (req, res) => {
|
|
18
|
+
const { licenseKey, actionName, agentId, triggerType, success, metadata } = req.body;
|
|
19
|
+
|
|
20
|
+
if (!licenseKey || !actionName) {
|
|
21
|
+
return res.status(400).json({ error: 'licenseKey and actionName are required' });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const site = findSiteByLicense.get(licenseKey);
|
|
26
|
+
if (!site) return res.status(404).json({ error: 'Site not found' });
|
|
27
|
+
|
|
28
|
+
recordAnalytic({
|
|
29
|
+
siteId: site.id,
|
|
30
|
+
actionName,
|
|
31
|
+
agentId,
|
|
32
|
+
triggerType,
|
|
33
|
+
success: success !== false,
|
|
34
|
+
metadata
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Broadcast real-time analytics via WebSocket
|
|
38
|
+
broadcastAnalytic(site.id, {
|
|
39
|
+
actionName,
|
|
40
|
+
agentId,
|
|
41
|
+
triggerType,
|
|
42
|
+
success: success !== false
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
res.json({ recorded: true });
|
|
46
|
+
} catch (err) {
|
|
47
|
+
res.status(500).json({ error: 'Failed to record analytics' });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
module.exports = router;
|
package/server/ws.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const WebSocket = require('ws');
|
|
2
|
+
const jwt = require('jsonwebtoken');
|
|
3
|
+
|
|
4
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production';
|
|
5
|
+
|
|
6
|
+
// Map of siteId → Set of WebSocket clients
|
|
7
|
+
const siteClients = new Map();
|
|
8
|
+
|
|
9
|
+
function setupWebSocket(server) {
|
|
10
|
+
const wss = new WebSocket.Server({ server, path: '/ws/analytics' });
|
|
11
|
+
|
|
12
|
+
wss.on('connection', (ws, req) => {
|
|
13
|
+
let authenticatedSiteId = null;
|
|
14
|
+
|
|
15
|
+
ws.isAlive = true;
|
|
16
|
+
ws.on('pong', () => { ws.isAlive = true; });
|
|
17
|
+
|
|
18
|
+
ws.on('message', (data) => {
|
|
19
|
+
try {
|
|
20
|
+
const msg = JSON.parse(data);
|
|
21
|
+
|
|
22
|
+
if (msg.type === 'auth') {
|
|
23
|
+
// Authenticate via JWT token and subscribe to a site
|
|
24
|
+
const decoded = jwt.verify(msg.token, JWT_SECRET);
|
|
25
|
+
if (decoded && msg.siteId) {
|
|
26
|
+
authenticatedSiteId = msg.siteId;
|
|
27
|
+
if (!siteClients.has(msg.siteId)) {
|
|
28
|
+
siteClients.set(msg.siteId, new Set());
|
|
29
|
+
}
|
|
30
|
+
siteClients.get(msg.siteId).add(ws);
|
|
31
|
+
ws.send(JSON.stringify({ type: 'auth:success', siteId: msg.siteId }));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Invalid message or auth failed' }));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
ws.on('close', () => {
|
|
40
|
+
if (authenticatedSiteId && siteClients.has(authenticatedSiteId)) {
|
|
41
|
+
siteClients.get(authenticatedSiteId).delete(ws);
|
|
42
|
+
if (siteClients.get(authenticatedSiteId).size === 0) {
|
|
43
|
+
siteClients.delete(authenticatedSiteId);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Heartbeat to clean up dead connections
|
|
50
|
+
const interval = setInterval(() => {
|
|
51
|
+
wss.clients.forEach((ws) => {
|
|
52
|
+
if (!ws.isAlive) return ws.terminate();
|
|
53
|
+
ws.isAlive = false;
|
|
54
|
+
ws.ping();
|
|
55
|
+
});
|
|
56
|
+
}, 30000);
|
|
57
|
+
|
|
58
|
+
wss.on('close', () => clearInterval(interval));
|
|
59
|
+
|
|
60
|
+
return wss;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Broadcast an analytics event to all clients watching a specific site
|
|
64
|
+
function broadcastAnalytic(siteId, eventData) {
|
|
65
|
+
const clients = siteClients.get(siteId);
|
|
66
|
+
if (!clients || clients.size === 0) return;
|
|
67
|
+
|
|
68
|
+
const message = JSON.stringify({
|
|
69
|
+
type: 'analytic',
|
|
70
|
+
timestamp: new Date().toISOString(),
|
|
71
|
+
...eventData
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
clients.forEach((ws) => {
|
|
75
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
76
|
+
ws.send(message);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { setupWebSocket, broadcastAnalytic };
|