web-agent-bridge 1.2.0 → 2.1.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 -21
- package/README.ar.md +572 -446
- package/README.md +968 -933
- package/bin/agent-runner.js +465 -0
- package/bin/cli.js +138 -80
- package/bin/wab.js +80 -80
- package/examples/bidi-agent.js +119 -119
- package/examples/mcp-agent.js +94 -94
- package/examples/next-app-router/README.md +44 -0
- package/examples/puppeteer-agent.js +108 -108
- package/examples/saas-dashboard/README.md +55 -0
- package/examples/shopify-hydrogen/README.md +74 -0
- package/examples/vision-agent.js +171 -171
- package/examples/wordpress-elementor/README.md +77 -0
- package/package.json +71 -78
- package/public/.well-known/ai-assets.json +59 -0
- package/public/admin/login.html +84 -84
- package/public/ai.html +196 -0
- package/public/cookies.html +208 -208
- package/public/css/premium.css +317 -0
- package/public/css/styles.css +1235 -1235
- package/public/dashboard.html +704 -704
- package/public/demo.html +259 -0
- package/public/docs.html +585 -585
- package/public/feed.xml +89 -0
- package/public/index.html +581 -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/wab-demo-page.js +721 -0
- package/public/js/ws-client.js +74 -74
- package/public/llms-full.txt +309 -0
- package/public/llms.txt +85 -0
- package/public/login.html +83 -83
- package/public/openapi.json +580 -0
- package/public/premium-dashboard.html +2487 -0
- package/public/premium.html +791 -0
- package/public/privacy.html +295 -295
- package/public/register.html +103 -103
- package/public/robots.txt +87 -0
- package/public/script/wab-consent.d.ts +36 -0
- package/public/script/wab-consent.js +104 -0
- package/public/script/wab-schema.js +131 -0
- package/public/script/wab.d.ts +108 -0
- package/public/script/wab.min.js +405 -0
- package/public/sitemap.xml +93 -0
- package/public/sovereign.html +660 -0
- package/public/terms.html +254 -254
- package/public/video/tutorial.mp4 +0 -0
- package/script/ai-agent-bridge.js +1558 -1513
- package/sdk/README.md +55 -55
- package/sdk/index.d.ts +118 -0
- package/sdk/index.js +257 -203
- package/sdk/package.json +14 -14
- package/sdk/schema-discovery.js +83 -0
- package/server/config/secrets.js +94 -92
- package/server/index.js +2 -9
- 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/migrations/002_premium_features.sql +418 -0
- 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-premium.js +671 -0
- package/server/routes/admin.js +247 -247
- package/server/routes/api.js +131 -138
- package/server/routes/auth.js +51 -51
- package/server/routes/billing.js +45 -45
- package/server/routes/discovery.js +406 -329
- package/server/routes/license.js +240 -240
- package/server/routes/noscript.js +543 -543
- package/server/routes/premium-v2.js +686 -0
- package/server/routes/premium.js +724 -0
- package/server/routes/sovereign.js +307 -0
- package/server/routes/wab-api.js +476 -476
- package/server/services/agent-memory.js +625 -0
- package/server/services/email.js +204 -204
- package/server/services/fairness.js +420 -420
- package/server/services/negotiation.js +439 -0
- package/server/services/plugins.js +747 -0
- package/server/services/premium.js +1883 -0
- package/server/services/reputation.js +465 -0
- package/server/services/self-healing.js +843 -0
- package/server/services/stripe.js +192 -192
- package/server/services/swarm.js +788 -0
- package/server/services/verification.js +481 -0
- package/server/services/vision.js +871 -0
- 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/templates/artisan-marketplace.yaml +104 -0
- package/templates/book-price-scout.yaml +98 -0
- package/templates/electronics-price-tracker.yaml +108 -0
- package/templates/flight-deal-hunter.yaml +113 -0
- package/templates/freelancer-direct.yaml +116 -0
- package/templates/grocery-price-compare.yaml +93 -0
- package/templates/hotel-direct-booking.yaml +113 -0
- package/templates/local-services.yaml +98 -0
- package/templates/olive-oil-tunisia.yaml +88 -0
- package/templates/organic-farm-fresh.yaml +101 -0
- package/templates/restaurant-direct.yaml +97 -0
- package/docs/DEPLOY.md +0 -118
- package/docs/SPEC.md +0 -1540
- package/wab-mcp-adapter/README.md +0 -136
- package/wab-mcp-adapter/index.js +0 -555
- package/wab-mcp-adapter/package.json +0 -17
package/server/utils/cache.js
CHANGED
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WAB Caching Layer — In-memory cache with TTL for hot data
|
|
3
|
-
* Reduces DB reads for license verification, config, and stats
|
|
4
|
-
*/
|
|
5
|
-
class Cache {
|
|
6
|
-
constructor(defaultTTL = 60000) {
|
|
7
|
-
this.store = new Map();
|
|
8
|
-
this.defaultTTL = defaultTTL;
|
|
9
|
-
this._interval = setInterval(() => this._cleanup(), 120000);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
get(key) {
|
|
13
|
-
const entry = this.store.get(key);
|
|
14
|
-
if (!entry) return undefined;
|
|
15
|
-
if (Date.now() > entry.expiresAt) {
|
|
16
|
-
this.store.delete(key);
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
entry.hits++;
|
|
20
|
-
return entry.value;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
set(key, value, ttl) {
|
|
24
|
-
this.store.set(key, {
|
|
25
|
-
value,
|
|
26
|
-
expiresAt: Date.now() + (ttl || this.defaultTTL),
|
|
27
|
-
hits: 0
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
del(key) {
|
|
32
|
-
this.store.delete(key);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
invalidatePattern(pattern) {
|
|
36
|
-
for (const key of this.store.keys()) {
|
|
37
|
-
if (key.includes(pattern)) this.store.delete(key);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
stats() {
|
|
42
|
-
return {
|
|
43
|
-
size: this.store.size,
|
|
44
|
-
keys: Array.from(this.store.keys())
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
_cleanup() {
|
|
49
|
-
const now = Date.now();
|
|
50
|
-
for (const [key, entry] of this.store) {
|
|
51
|
-
if (now > entry.expiresAt) this.store.delete(key);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
destroy() {
|
|
56
|
-
clearInterval(this._interval);
|
|
57
|
-
this.store.clear();
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Analytics Queue — Batches analytics inserts for better write performance
|
|
63
|
-
* Flushes every N seconds or when buffer reaches max size.
|
|
64
|
-
* maxBufferTotal caps memory if DB writes fail repeatedly (DoS mitigation).
|
|
65
|
-
*/
|
|
66
|
-
class AnalyticsQueue {
|
|
67
|
-
constructor(db, options = {}) {
|
|
68
|
-
this.db = db;
|
|
69
|
-
this.buffer = [];
|
|
70
|
-
this.maxSize = options.maxSize || 50;
|
|
71
|
-
this.maxBufferTotal = options.maxBufferTotal || 5000;
|
|
72
|
-
this.flushInterval = options.flushInterval || 5000;
|
|
73
|
-
this._timer = setInterval(() => this.flush(), this.flushInterval);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
push(analytic) {
|
|
77
|
-
while (this.buffer.length >= this.maxBufferTotal) {
|
|
78
|
-
this.buffer.shift();
|
|
79
|
-
console.warn('[WAB] Analytics buffer at cap; dropped oldest event');
|
|
80
|
-
}
|
|
81
|
-
this.buffer.push(analytic);
|
|
82
|
-
if (this.buffer.length >= this.maxSize) {
|
|
83
|
-
this.flush();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
flush() {
|
|
88
|
-
if (this.buffer.length === 0) return;
|
|
89
|
-
const batch = this.buffer.splice(0);
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
const insert = this.db.prepare(
|
|
93
|
-
`INSERT INTO analytics (site_id, action_name, agent_id, trigger_type, success, metadata) VALUES (?, ?, ?, ?, ?, ?)`
|
|
94
|
-
);
|
|
95
|
-
const insertMany = this.db.transaction((items) => {
|
|
96
|
-
for (const item of items) {
|
|
97
|
-
insert.run(
|
|
98
|
-
item.siteId,
|
|
99
|
-
item.actionName,
|
|
100
|
-
item.agentId || null,
|
|
101
|
-
item.triggerType || null,
|
|
102
|
-
item.success ? 1 : 0,
|
|
103
|
-
JSON.stringify(item.metadata || {})
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
insertMany(batch);
|
|
108
|
-
} catch (err) {
|
|
109
|
-
console.error('[WAB Cache] Analytics batch insert failed:', err.message);
|
|
110
|
-
while (this.buffer.length + batch.length > this.maxBufferTotal) {
|
|
111
|
-
batch.shift();
|
|
112
|
-
}
|
|
113
|
-
this.buffer.unshift(...batch);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
destroy() {
|
|
118
|
-
clearInterval(this._timer);
|
|
119
|
-
this.flush();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const cache = new Cache(60000);
|
|
124
|
-
|
|
125
|
-
module.exports = { Cache, AnalyticsQueue, cache };
|
|
1
|
+
/**
|
|
2
|
+
* WAB Caching Layer — In-memory cache with TTL for hot data
|
|
3
|
+
* Reduces DB reads for license verification, config, and stats
|
|
4
|
+
*/
|
|
5
|
+
class Cache {
|
|
6
|
+
constructor(defaultTTL = 60000) {
|
|
7
|
+
this.store = new Map();
|
|
8
|
+
this.defaultTTL = defaultTTL;
|
|
9
|
+
this._interval = setInterval(() => this._cleanup(), 120000);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get(key) {
|
|
13
|
+
const entry = this.store.get(key);
|
|
14
|
+
if (!entry) return undefined;
|
|
15
|
+
if (Date.now() > entry.expiresAt) {
|
|
16
|
+
this.store.delete(key);
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
entry.hits++;
|
|
20
|
+
return entry.value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
set(key, value, ttl) {
|
|
24
|
+
this.store.set(key, {
|
|
25
|
+
value,
|
|
26
|
+
expiresAt: Date.now() + (ttl || this.defaultTTL),
|
|
27
|
+
hits: 0
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
del(key) {
|
|
32
|
+
this.store.delete(key);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
invalidatePattern(pattern) {
|
|
36
|
+
for (const key of this.store.keys()) {
|
|
37
|
+
if (key.includes(pattern)) this.store.delete(key);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
stats() {
|
|
42
|
+
return {
|
|
43
|
+
size: this.store.size,
|
|
44
|
+
keys: Array.from(this.store.keys())
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_cleanup() {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
for (const [key, entry] of this.store) {
|
|
51
|
+
if (now > entry.expiresAt) this.store.delete(key);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
destroy() {
|
|
56
|
+
clearInterval(this._interval);
|
|
57
|
+
this.store.clear();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Analytics Queue — Batches analytics inserts for better write performance
|
|
63
|
+
* Flushes every N seconds or when buffer reaches max size.
|
|
64
|
+
* maxBufferTotal caps memory if DB writes fail repeatedly (DoS mitigation).
|
|
65
|
+
*/
|
|
66
|
+
class AnalyticsQueue {
|
|
67
|
+
constructor(db, options = {}) {
|
|
68
|
+
this.db = db;
|
|
69
|
+
this.buffer = [];
|
|
70
|
+
this.maxSize = options.maxSize || 50;
|
|
71
|
+
this.maxBufferTotal = options.maxBufferTotal || 5000;
|
|
72
|
+
this.flushInterval = options.flushInterval || 5000;
|
|
73
|
+
this._timer = setInterval(() => this.flush(), this.flushInterval);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
push(analytic) {
|
|
77
|
+
while (this.buffer.length >= this.maxBufferTotal) {
|
|
78
|
+
this.buffer.shift();
|
|
79
|
+
console.warn('[WAB] Analytics buffer at cap; dropped oldest event');
|
|
80
|
+
}
|
|
81
|
+
this.buffer.push(analytic);
|
|
82
|
+
if (this.buffer.length >= this.maxSize) {
|
|
83
|
+
this.flush();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
flush() {
|
|
88
|
+
if (this.buffer.length === 0) return;
|
|
89
|
+
const batch = this.buffer.splice(0);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const insert = this.db.prepare(
|
|
93
|
+
`INSERT INTO analytics (site_id, action_name, agent_id, trigger_type, success, metadata) VALUES (?, ?, ?, ?, ?, ?)`
|
|
94
|
+
);
|
|
95
|
+
const insertMany = this.db.transaction((items) => {
|
|
96
|
+
for (const item of items) {
|
|
97
|
+
insert.run(
|
|
98
|
+
item.siteId,
|
|
99
|
+
item.actionName,
|
|
100
|
+
item.agentId || null,
|
|
101
|
+
item.triggerType || null,
|
|
102
|
+
item.success ? 1 : 0,
|
|
103
|
+
JSON.stringify(item.metadata || {})
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
insertMany(batch);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error('[WAB Cache] Analytics batch insert failed:', err.message);
|
|
110
|
+
while (this.buffer.length + batch.length > this.maxBufferTotal) {
|
|
111
|
+
batch.shift();
|
|
112
|
+
}
|
|
113
|
+
this.buffer.unshift(...batch);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
destroy() {
|
|
118
|
+
clearInterval(this._timer);
|
|
119
|
+
this.flush();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const cache = new Cache(60000);
|
|
124
|
+
|
|
125
|
+
module.exports = { Cache, AnalyticsQueue, cache };
|
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 };
|