web-agent-bridge 3.4.0 → 3.9.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.
Files changed (315) hide show
  1. package/LICENSE +84 -84
  2. package/README.ar.md +1565 -1304
  3. package/README.md +171 -298
  4. package/bin/agent-runner.js +474 -474
  5. package/bin/cli.js +237 -237
  6. package/bin/wab-init.js +244 -223
  7. package/bin/wab.js +80 -80
  8. package/examples/azure-dns-wab.js +83 -83
  9. package/examples/bidi-agent.js +119 -119
  10. package/examples/cloudflare-wab-dns.js +121 -121
  11. package/examples/cpanel-wab-dns.js +114 -114
  12. package/examples/cross-site-agent.js +91 -91
  13. package/examples/dns-discovery-agent.js +166 -166
  14. package/examples/gcp-dns-wab.js +76 -76
  15. package/examples/governance-agent.js +169 -169
  16. package/examples/mcp-agent.js +94 -94
  17. package/examples/next-app-router/README.md +44 -44
  18. package/examples/plesk-wab-dns.js +103 -103
  19. package/examples/puppeteer-agent.js +108 -108
  20. package/examples/route53-wab-dns.js +144 -144
  21. package/examples/saas-dashboard/README.md +55 -55
  22. package/examples/safe-mode-agent.js +96 -96
  23. package/examples/self-discovery.js +106 -0
  24. package/examples/shopify-hydrogen/README.md +74 -74
  25. package/examples/vision-agent.js +171 -171
  26. package/examples/wab-sign.js +74 -74
  27. package/examples/wab-verify.js +60 -60
  28. package/examples/wordpress-elementor/README.md +77 -77
  29. package/package.json +93 -93
  30. package/public/.well-known/agent-tools.json +180 -180
  31. package/public/.well-known/ai-assets.json +59 -59
  32. package/public/.well-known/security.txt +8 -8
  33. package/public/.well-known/wab.json +28 -28
  34. package/public/activate.html +448 -368
  35. package/public/adopt.html +236 -0
  36. package/public/adoption-metrics.html +188 -188
  37. package/public/agent-workspace.html +359 -349
  38. package/public/ai.html +198 -198
  39. package/public/api.html +397 -413
  40. package/public/atp.html +171 -0
  41. package/public/azure-dns-integration.html +289 -289
  42. package/public/browser.html +486 -486
  43. package/public/cloudflare-integration.html +380 -380
  44. package/public/commander-dashboard.html +243 -243
  45. package/public/cookies.html +210 -210
  46. package/public/cpanel-integration.html +398 -398
  47. package/public/css/agent-workspace.css +1713 -1713
  48. package/public/css/premium.css +317 -317
  49. package/public/css/styles.css +1401 -1263
  50. package/public/dashboard-shieldlink.html +295 -0
  51. package/public/dashboard.html +711 -707
  52. package/public/dns.html +436 -436
  53. package/public/docs.html +588 -588
  54. package/public/enterprise-mesh.ar.html +80 -0
  55. package/public/enterprise-mesh.html +81 -0
  56. package/public/feed.xml +89 -89
  57. package/public/gcp-dns-integration.html +318 -318
  58. package/public/governance.ar.html +70 -0
  59. package/public/governance.html +69 -0
  60. package/public/growth.html +465 -465
  61. package/public/index.html +1372 -1266
  62. package/public/integrations.html +556 -556
  63. package/public/js/activate.js +449 -145
  64. package/public/js/agent-workspace.js +1740 -1740
  65. package/public/js/auth-nav.js +117 -65
  66. package/public/js/auth-redirect.js +12 -12
  67. package/public/js/cookie-consent.js +56 -56
  68. package/public/js/dns.js +438 -438
  69. package/public/js/wab-demo-page.js +721 -721
  70. package/public/js/ws-client.js +74 -74
  71. package/public/l-preview.html +242 -0
  72. package/public/llms-full.txt +360 -360
  73. package/public/llms.txt +125 -125
  74. package/public/login.html +85 -85
  75. package/public/mesh-dashboard.html +328 -328
  76. package/public/milestones.html +346 -0
  77. package/public/one-click.html +779 -0
  78. package/public/openapi.json +669 -669
  79. package/public/partners.ar.html +145 -0
  80. package/public/partners.html +143 -0
  81. package/public/phone-shield.html +281 -281
  82. package/public/plesk-integration.html +375 -375
  83. package/public/premium-dashboard.html +2489 -2489
  84. package/public/premium.html +793 -793
  85. package/public/privacy.html +297 -297
  86. package/public/provider-onboarding.html +172 -172
  87. package/public/provider-sandbox.html +134 -134
  88. package/public/providers.html +359 -359
  89. package/public/refusals.html +172 -0
  90. package/public/register.html +105 -105
  91. package/public/registrar-integrations.html +141 -141
  92. package/public/ring4.html +292 -0
  93. package/public/robots.txt +99 -99
  94. package/public/route53-integration.html +531 -531
  95. package/public/score.html +263 -0
  96. package/public/script/wab-consent.d.ts +36 -36
  97. package/public/script/wab-consent.js +104 -104
  98. package/public/script/wab-schema.js +131 -131
  99. package/public/script/wab.d.ts +108 -108
  100. package/public/script/wab.min.js +580 -580
  101. package/public/security.txt +8 -8
  102. package/public/shieldlink.html +244 -0
  103. package/public/shieldqr.html +231 -231
  104. package/public/sitemap.xml +13 -1
  105. package/public/terms.html +256 -256
  106. package/public/trust-graph-api.ar.html +92 -0
  107. package/public/trust-graph-api.html +91 -0
  108. package/public/wab-features.html +560 -0
  109. package/public/wab-trust.html +200 -200
  110. package/public/wab-truth.html +375 -0
  111. package/public/wab-vs-protocols.html +210 -210
  112. package/public/whitepaper.html +449 -449
  113. package/script/ai-agent-bridge.js +1754 -1754
  114. package/sdk/README.md +99 -99
  115. package/sdk/agent-mesh.js +449 -449
  116. package/sdk/atp.js +103 -0
  117. package/sdk/auto-discovery.js +301 -288
  118. package/sdk/commander.js +262 -262
  119. package/sdk/governance.js +262 -262
  120. package/sdk/index.d.ts +464 -464
  121. package/sdk/index.js +653 -649
  122. package/sdk/multi-agent.js +318 -318
  123. package/sdk/safe-mode.js +221 -221
  124. package/sdk/safety-shield.js +219 -219
  125. package/sdk/schema-discovery.js +83 -83
  126. package/server/adapters/index.js +520 -520
  127. package/server/config/plans.js +412 -367
  128. package/server/config/secrets.js +102 -102
  129. package/server/control-plane/index.js +301 -301
  130. package/server/data-plane/index.js +354 -354
  131. package/server/index.js +793 -670
  132. package/server/llm/index.js +404 -404
  133. package/server/middleware/adminAuth.js +35 -35
  134. package/server/middleware/api-tier.js +170 -0
  135. package/server/middleware/auth.js +50 -50
  136. package/server/middleware/featureGate.js +88 -88
  137. package/server/middleware/rateLimits.js +100 -100
  138. package/server/middleware/sensitiveAction.js +157 -157
  139. package/server/middleware/wab-trust.js +141 -0
  140. package/server/migrations/001_add_analytics_indexes.sql +7 -7
  141. package/server/migrations/002_premium_features.sql +418 -418
  142. package/server/migrations/003_ads_integer_cents.sql +33 -33
  143. package/server/migrations/004_agent_os.sql +158 -158
  144. package/server/migrations/005_marketplace_metering.sql +126 -126
  145. package/server/migrations/006_growth_suite.sql +138 -0
  146. package/server/migrations/007_governance.sql +106 -106
  147. package/server/migrations/008_plans.sql +144 -144
  148. package/server/migrations/009_shieldqr.sql +30 -30
  149. package/server/migrations/010_extended_trust.sql +33 -33
  150. package/server/migrations/011_outreach.sql +47 -0
  151. package/server/migrations/012_shieldlink.sql +116 -0
  152. package/server/migrations/013_ct_monitor.sql +13 -0
  153. package/server/migrations/014_wab_advanced_features.sql +128 -0
  154. package/server/migrations/015_wab_truth_layer.sql +101 -0
  155. package/server/migrations/016_ring4_external_trust.sql +84 -0
  156. package/server/migrations/017_ring4_extensions.sql +69 -0
  157. package/server/migrations/018_commercial_foundations.sql +167 -0
  158. package/server/migrations/019_unify_tier_constraints.sql +133 -0
  159. package/server/migrations/020_agent_transaction_primitive.sql +119 -0
  160. package/server/models/adapters/index.js +33 -33
  161. package/server/models/adapters/mysql.js +183 -183
  162. package/server/models/adapters/postgresql.js +172 -172
  163. package/server/models/adapters/sqlite.js +7 -7
  164. package/server/models/db.js +740 -740
  165. package/server/observability/failure-analysis.js +337 -337
  166. package/server/observability/index.js +394 -394
  167. package/server/protocol/capabilities.js +223 -223
  168. package/server/protocol/index.js +243 -243
  169. package/server/protocol/schema.js +584 -584
  170. package/server/registry/certification.js +271 -271
  171. package/server/registry/index.js +326 -326
  172. package/server/routes/activate.js +478 -0
  173. package/server/routes/admin-outreach.js +239 -0
  174. package/server/routes/admin-plans.js +76 -76
  175. package/server/routes/admin-premium.js +674 -673
  176. package/server/routes/admin-shieldlink.js +137 -0
  177. package/server/routes/admin-shieldqr.js +90 -90
  178. package/server/routes/admin-trust-monitor.js +139 -83
  179. package/server/routes/admin.js +550 -549
  180. package/server/routes/adopt.js +61 -0
  181. package/server/routes/ads.js +130 -130
  182. package/server/routes/agent-workspace.js +540 -540
  183. package/server/routes/api-keys.js +127 -0
  184. package/server/routes/api.js +150 -150
  185. package/server/routes/auth.js +71 -71
  186. package/server/routes/billing.js +57 -57
  187. package/server/routes/commander.js +316 -316
  188. package/server/routes/customer-shieldlink.js +133 -0
  189. package/server/routes/demo-showcase.js +332 -332
  190. package/server/routes/demo-store.js +154 -154
  191. package/server/routes/diagnose.js +373 -0
  192. package/server/routes/discovery.js +2348 -2348
  193. package/server/routes/enterprise-mesh.js +170 -0
  194. package/server/routes/gateway.js +173 -173
  195. package/server/routes/governance-saas.js +203 -0
  196. package/server/routes/governance.js +208 -208
  197. package/server/routes/growth.js +1048 -0
  198. package/server/routes/intent.js +328 -0
  199. package/server/routes/license.js +251 -251
  200. package/server/routes/mesh.js +469 -469
  201. package/server/routes/noscript.js +543 -543
  202. package/server/routes/partners.js +201 -0
  203. package/server/routes/plans.js +33 -33
  204. package/server/routes/premium-v2.js +686 -686
  205. package/server/routes/premium.js +724 -724
  206. package/server/routes/providers.js +650 -650
  207. package/server/routes/reputation.js +411 -0
  208. package/server/routes/ring4.js +885 -0
  209. package/server/routes/runtime.js +2148 -2148
  210. package/server/routes/shieldlink.js +70 -0
  211. package/server/routes/shieldqr.js +88 -88
  212. package/server/routes/sovereign.js +465 -465
  213. package/server/routes/transactions.js +233 -0
  214. package/server/routes/truth-layer.js +670 -0
  215. package/server/routes/universal.js +200 -200
  216. package/server/routes/unsubscribe.js +51 -0
  217. package/server/routes/wab-api.js +850 -850
  218. package/server/routes/wab-cache.js +282 -0
  219. package/server/runtime/container-worker.js +111 -111
  220. package/server/runtime/container.js +448 -448
  221. package/server/runtime/distributed-worker.js +362 -362
  222. package/server/runtime/event-bus.js +210 -210
  223. package/server/runtime/index.js +253 -253
  224. package/server/runtime/queue.js +599 -599
  225. package/server/runtime/replay.js +666 -666
  226. package/server/runtime/sandbox.js +266 -266
  227. package/server/runtime/scheduler.js +534 -534
  228. package/server/runtime/session-engine.js +293 -293
  229. package/server/runtime/state-manager.js +188 -188
  230. package/server/secrets/wab-signing-key.pem +3 -0
  231. package/server/secrets/wab-signing-pub.pem +3 -0
  232. package/server/security/cross-site-redactor.js +196 -196
  233. package/server/security/dry-run.js +180 -180
  234. package/server/security/human-gate-rate-limit.js +147 -147
  235. package/server/security/human-gate-transports.js +178 -178
  236. package/server/security/human-gate.js +281 -281
  237. package/server/security/index.js +368 -368
  238. package/server/security/intent-engine.js +245 -245
  239. package/server/security/reward-guard.js +171 -171
  240. package/server/security/rollback-store.js +239 -239
  241. package/server/security/token-scope.js +404 -404
  242. package/server/security/url-policy.js +139 -139
  243. package/server/services/adoption-agent.js +182 -0
  244. package/server/services/agent-chat.js +506 -506
  245. package/server/services/agent-learning.js +601 -601
  246. package/server/services/agent-memory.js +625 -625
  247. package/server/services/agent-mesh.js +555 -555
  248. package/server/services/agent-symphony.js +717 -717
  249. package/server/services/agent-tasks.js +1807 -1807
  250. package/server/services/api-key-engine.js +292 -292
  251. package/server/services/cluster.js +894 -894
  252. package/server/services/commander.js +738 -738
  253. package/server/services/edge-compute.js +440 -440
  254. package/server/services/email.js +233 -233
  255. package/server/services/fairness-engine.js +409 -0
  256. package/server/services/fairness.js +420 -0
  257. package/server/services/governance.js +466 -466
  258. package/server/services/hosted-runtime.js +205 -205
  259. package/server/services/lfd.js +635 -635
  260. package/server/services/local-ai.js +389 -389
  261. package/server/services/marketplace.js +270 -270
  262. package/server/services/metering.js +182 -182
  263. package/server/services/modules/affiliate-intelligence.js +93 -93
  264. package/server/services/modules/agent-firewall.js +90 -90
  265. package/server/services/modules/bounty.js +89 -89
  266. package/server/services/modules/collective-bargaining.js +92 -92
  267. package/server/services/modules/dark-pattern.js +66 -66
  268. package/server/services/modules/gov-intelligence.js +45 -45
  269. package/server/services/modules/neural.js +55 -55
  270. package/server/services/modules/notary.js +49 -49
  271. package/server/services/modules/price-time-machine.js +86 -86
  272. package/server/services/modules/protocol.js +104 -104
  273. package/server/services/negotiation.js +439 -439
  274. package/server/services/outreach-agent.js +312 -0
  275. package/server/services/plans.js +214 -214
  276. package/server/services/plugins.js +771 -771
  277. package/server/services/price-intelligence.js +566 -566
  278. package/server/services/price-shield.js +1137 -1137
  279. package/server/services/provider-clients.js +740 -740
  280. package/server/services/reputation.js +465 -465
  281. package/server/services/search-engine.js +357 -357
  282. package/server/services/security.js +513 -513
  283. package/server/services/self-healing.js +843 -843
  284. package/server/services/shieldlink.js +492 -0
  285. package/server/services/shieldqr.js +322 -322
  286. package/server/services/sovereign-shield.js +542 -542
  287. package/server/services/ssl-ct-monitor.js +224 -0
  288. package/server/services/ssl-inspector.js +42 -42
  289. package/server/services/ssl-monitor.js +167 -167
  290. package/server/services/stripe.js +206 -205
  291. package/server/services/swarm.js +788 -788
  292. package/server/services/transactions.js +525 -0
  293. package/server/services/universal-scraper.js +662 -662
  294. package/server/services/verification.js +481 -481
  295. package/server/services/vision.js +1163 -1163
  296. package/server/services/wab-crypto.js +178 -178
  297. package/server/utils/cache.js +125 -125
  298. package/server/utils/migrate.js +81 -81
  299. package/server/utils/safe-fetch.js +228 -228
  300. package/server/utils/secureFields.js +50 -50
  301. package/server/ws.js +161 -161
  302. package/templates/artisan-marketplace.yaml +104 -104
  303. package/templates/book-price-scout.yaml +98 -98
  304. package/templates/electronics-price-tracker.yaml +108 -108
  305. package/templates/flight-deal-hunter.yaml +113 -113
  306. package/templates/freelancer-direct.yaml +116 -116
  307. package/templates/grocery-price-compare.yaml +93 -93
  308. package/templates/hotel-direct-booking.yaml +113 -113
  309. package/templates/local-services.yaml +98 -98
  310. package/templates/olive-oil-tunisia.yaml +88 -88
  311. package/templates/organic-farm-fresh.yaml +101 -101
  312. package/templates/restaurant-direct.yaml +97 -97
  313. package/templates/ring4/banking-sovereign.yaml +55 -0
  314. package/templates/ring4/ecommerce-sovereign.yaml +58 -0
  315. package/templates/ring4/healthcare-sovereign.yaml +60 -0
@@ -1,740 +1,740 @@
1
- const Database = require('better-sqlite3');
2
- const path = require('path');
3
- const fs = require('fs');
4
- const crypto = require('crypto');
5
- const bcrypt = require('bcryptjs');
6
- const { randomUUID: uuidv4 } = require('crypto');
7
- const { encryptOptional, decryptOptional } = require('../utils/secureFields');
8
-
9
- const isTest = process.env.NODE_ENV === 'test';
10
- const DATA_DIR = isTest
11
- ? path.join(__dirname, '..', '..', 'data-test')
12
- : (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
13
- if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
14
-
15
- const dbFile = isTest ? 'wab-test.db' : 'wab.db';
16
- const db = new Database(path.join(DATA_DIR, dbFile));
17
-
18
- db.pragma('journal_mode = WAL');
19
- db.pragma('foreign_keys = ON');
20
-
21
- db.exec(`
22
- CREATE TABLE IF NOT EXISTS users (
23
- id TEXT PRIMARY KEY,
24
- email TEXT UNIQUE NOT NULL,
25
- password TEXT NOT NULL,
26
- name TEXT NOT NULL,
27
- company TEXT,
28
- created_at TEXT DEFAULT (datetime('now')),
29
- updated_at TEXT DEFAULT (datetime('now'))
30
- );
31
-
32
- CREATE TABLE IF NOT EXISTS sites (
33
- id TEXT PRIMARY KEY,
34
- user_id TEXT NOT NULL,
35
- domain TEXT NOT NULL,
36
- name TEXT NOT NULL,
37
- description TEXT,
38
- tier TEXT DEFAULT 'free' CHECK(tier IN ('free','starter','pro','enterprise')),
39
- license_key TEXT UNIQUE NOT NULL,
40
- api_key TEXT UNIQUE,
41
- config TEXT DEFAULT '{}',
42
- active INTEGER DEFAULT 1,
43
- created_at TEXT DEFAULT (datetime('now')),
44
- updated_at TEXT DEFAULT (datetime('now')),
45
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
46
- );
47
-
48
- CREATE TABLE IF NOT EXISTS analytics (
49
- id INTEGER PRIMARY KEY AUTOINCREMENT,
50
- site_id TEXT NOT NULL,
51
- action_name TEXT NOT NULL,
52
- agent_id TEXT,
53
- trigger_type TEXT,
54
- success INTEGER,
55
- metadata TEXT DEFAULT '{}',
56
- created_at TEXT DEFAULT (datetime('now')),
57
- FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
58
- );
59
-
60
- CREATE TABLE IF NOT EXISTS subscriptions (
61
- id TEXT PRIMARY KEY,
62
- user_id TEXT NOT NULL,
63
- site_id TEXT NOT NULL,
64
- tier TEXT NOT NULL CHECK(tier IN ('free','starter','pro','enterprise')),
65
- status TEXT DEFAULT 'active' CHECK(status IN ('active','cancelled','expired','trial')),
66
- started_at TEXT DEFAULT (datetime('now')),
67
- expires_at TEXT,
68
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
69
- FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
70
- );
71
-
72
- CREATE INDEX IF NOT EXISTS idx_sites_domain ON sites(domain);
73
- CREATE INDEX IF NOT EXISTS idx_sites_license ON sites(license_key);
74
- CREATE INDEX IF NOT EXISTS idx_analytics_site ON analytics(site_id);
75
- CREATE INDEX IF NOT EXISTS idx_analytics_created ON analytics(created_at);
76
-
77
- CREATE TABLE IF NOT EXISTS admins (
78
- id TEXT PRIMARY KEY,
79
- email TEXT UNIQUE NOT NULL,
80
- password TEXT NOT NULL,
81
- name TEXT NOT NULL,
82
- role TEXT DEFAULT 'admin' CHECK(role IN ('admin','superadmin')),
83
- created_at TEXT DEFAULT (datetime('now'))
84
- );
85
-
86
- CREATE TABLE IF NOT EXISTS free_grants (
87
- id TEXT PRIMARY KEY,
88
- user_id TEXT NOT NULL,
89
- site_id TEXT,
90
- granted_tier TEXT NOT NULL CHECK(granted_tier IN ('starter','pro','enterprise')),
91
- reason TEXT,
92
- granted_by TEXT NOT NULL,
93
- granted_at TEXT DEFAULT (datetime('now')),
94
- expires_at TEXT,
95
- active INTEGER DEFAULT 1,
96
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
97
- FOREIGN KEY (granted_by) REFERENCES admins(id)
98
- );
99
-
100
- CREATE TABLE IF NOT EXISTS stripe_customers (
101
- id TEXT PRIMARY KEY,
102
- user_id TEXT UNIQUE NOT NULL,
103
- stripe_customer_id TEXT UNIQUE,
104
- created_at TEXT DEFAULT (datetime('now')),
105
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
106
- );
107
-
108
- CREATE TABLE IF NOT EXISTS stripe_subscriptions (
109
- id TEXT PRIMARY KEY,
110
- user_id TEXT NOT NULL,
111
- site_id TEXT NOT NULL,
112
- stripe_subscription_id TEXT UNIQUE,
113
- stripe_price_id TEXT,
114
- tier TEXT NOT NULL CHECK(tier IN ('starter','pro','enterprise')),
115
- status TEXT DEFAULT 'active' CHECK(status IN ('active','cancelled','past_due','trialing','incomplete')),
116
- current_period_start TEXT,
117
- current_period_end TEXT,
118
- created_at TEXT DEFAULT (datetime('now')),
119
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
120
- FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
121
- );
122
-
123
- CREATE TABLE IF NOT EXISTS payments (
124
- id TEXT PRIMARY KEY,
125
- user_id TEXT NOT NULL,
126
- stripe_payment_id TEXT UNIQUE,
127
- amount INTEGER NOT NULL,
128
- currency TEXT DEFAULT 'usd',
129
- status TEXT DEFAULT 'succeeded',
130
- description TEXT,
131
- created_at TEXT DEFAULT (datetime('now')),
132
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
133
- );
134
-
135
- CREATE TABLE IF NOT EXISTS notifications_log (
136
- id INTEGER PRIMARY KEY AUTOINCREMENT,
137
- user_id TEXT,
138
- email_to TEXT NOT NULL,
139
- template TEXT NOT NULL,
140
- subject TEXT NOT NULL,
141
- status TEXT DEFAULT 'sent' CHECK(status IN ('sent','failed','queued')),
142
- error_message TEXT,
143
- created_at TEXT DEFAULT (datetime('now'))
144
- );
145
-
146
- CREATE TABLE IF NOT EXISTS smtp_settings (
147
- id INTEGER PRIMARY KEY CHECK(id = 1),
148
- host TEXT,
149
- port INTEGER DEFAULT 587,
150
- secure INTEGER DEFAULT 0,
151
- username TEXT,
152
- password TEXT,
153
- from_name TEXT DEFAULT 'Web Agent Bridge',
154
- from_email TEXT,
155
- enabled INTEGER DEFAULT 0,
156
- updated_at TEXT DEFAULT (datetime('now'))
157
- );
158
-
159
- INSERT OR IGNORE INTO smtp_settings (id) VALUES (1);
160
-
161
- CREATE TABLE IF NOT EXISTS platform_settings (
162
- key TEXT PRIMARY KEY,
163
- value TEXT,
164
- updated_at TEXT DEFAULT (datetime('now'))
165
- );
166
-
167
- CREATE INDEX IF NOT EXISTS idx_free_grants_user ON free_grants(user_id);
168
- CREATE INDEX IF NOT EXISTS idx_stripe_subs_user ON stripe_subscriptions(user_id);
169
- CREATE INDEX IF NOT EXISTS idx_payments_user ON payments(user_id);
170
- CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications_log(user_id);
171
-
172
- CREATE TABLE IF NOT EXISTS wab_ads (
173
- id TEXT PRIMARY KEY,
174
- title TEXT NOT NULL,
175
- description TEXT,
176
- image_url TEXT,
177
- target_url TEXT NOT NULL,
178
- advertiser_name TEXT NOT NULL,
179
- advertiser_email TEXT NOT NULL,
180
- status TEXT DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected','paused','expired')),
181
- position TEXT DEFAULT 'new-tab' CHECK(position IN ('new-tab','sidebar','search')),
182
- budget_cents INTEGER DEFAULT 0,
183
- spent_cents INTEGER DEFAULT 0,
184
- cpc_cents INTEGER DEFAULT 5,
185
- cpi_cents INTEGER DEFAULT 1,
186
- impressions INTEGER DEFAULT 0,
187
- clicks INTEGER DEFAULT 0,
188
- created_at TEXT DEFAULT (datetime('now')),
189
- approved_by TEXT,
190
- approved_at TEXT,
191
- expires_at TEXT,
192
- FOREIGN KEY (approved_by) REFERENCES admins(id)
193
- );
194
-
195
- CREATE TABLE IF NOT EXISTS ad_events (
196
- id INTEGER PRIMARY KEY AUTOINCREMENT,
197
- ad_id TEXT NOT NULL,
198
- event_type TEXT NOT NULL CHECK(event_type IN ('impression','click')),
199
- platform TEXT DEFAULT 'browser',
200
- ip_hash TEXT,
201
- created_at TEXT DEFAULT (datetime('now')),
202
- FOREIGN KEY (ad_id) REFERENCES wab_ads(id) ON DELETE CASCADE
203
- );
204
-
205
- CREATE INDEX IF NOT EXISTS idx_wab_ads_status ON wab_ads(status);
206
- CREATE INDEX IF NOT EXISTS idx_ad_events_ad ON ad_events(ad_id);
207
- CREATE INDEX IF NOT EXISTS idx_ad_events_created ON ad_events(created_at);
208
-
209
- -- ─── DNS Provider Account System (v1.3) ────────────────────────────
210
- -- Stores user-supplied credentials (encrypted at rest) for managed
211
- -- one-click WAB DNS Discovery enable/disable across DNS providers
212
- -- and registrars. Credentials never leave the server unencrypted.
213
- CREATE TABLE IF NOT EXISTS provider_accounts (
214
- id TEXT PRIMARY KEY,
215
- user_id TEXT NOT NULL,
216
- provider_type TEXT NOT NULL CHECK(provider_type IN (
217
- 'cloudflare','route53','azure','gcp','cpanel','plesk','godaddy','namecheap'
218
- )),
219
- label TEXT NOT NULL,
220
- credentials TEXT NOT NULL,
221
- config TEXT DEFAULT '{}',
222
- status TEXT DEFAULT 'pending' CHECK(status IN ('pending','active','error','disabled')),
223
- last_test_at TEXT,
224
- last_test_ok INTEGER DEFAULT 0,
225
- last_test_error TEXT,
226
- last_sync_at TEXT,
227
- domains_count INTEGER DEFAULT 0,
228
- created_at TEXT DEFAULT (datetime('now')),
229
- updated_at TEXT,
230
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
231
- );
232
- CREATE INDEX IF NOT EXISTS idx_provider_accounts_user ON provider_accounts(user_id);
233
- CREATE INDEX IF NOT EXISTS idx_provider_accounts_type ON provider_accounts(provider_type);
234
-
235
- CREATE TABLE IF NOT EXISTS provider_domains (
236
- id INTEGER PRIMARY KEY AUTOINCREMENT,
237
- account_id TEXT NOT NULL,
238
- domain TEXT NOT NULL,
239
- zone_id TEXT,
240
- wab_enabled INTEGER DEFAULT 0,
241
- wab_record_value TEXT,
242
- last_action TEXT,
243
- last_action_at TEXT,
244
- last_action_status TEXT,
245
- last_action_error TEXT,
246
- created_at TEXT DEFAULT (datetime('now')),
247
- updated_at TEXT,
248
- FOREIGN KEY (account_id) REFERENCES provider_accounts(id) ON DELETE CASCADE,
249
- UNIQUE(account_id, domain)
250
- );
251
- CREATE INDEX IF NOT EXISTS idx_provider_domains_account ON provider_domains(account_id);
252
- CREATE INDEX IF NOT EXISTS idx_provider_domains_domain ON provider_domains(domain);
253
-
254
- CREATE TABLE IF NOT EXISTS provider_action_log (
255
- id INTEGER PRIMARY KEY AUTOINCREMENT,
256
- account_id TEXT NOT NULL,
257
- domain TEXT,
258
- action TEXT NOT NULL,
259
- status TEXT NOT NULL,
260
- duration_ms INTEGER,
261
- detail TEXT,
262
- created_at TEXT DEFAULT (datetime('now')),
263
- FOREIGN KEY (account_id) REFERENCES provider_accounts(id) ON DELETE CASCADE
264
- );
265
- CREATE INDEX IF NOT EXISTS idx_provider_action_log_account_time
266
- ON provider_action_log(account_id, created_at DESC);
267
- `);
268
-
269
- function generateLicenseKey() {
270
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
271
- const segments = [];
272
- for (let s = 0; s < 4; s++) {
273
- let seg = '';
274
- const bytes = crypto.randomBytes(5);
275
- for (let i = 0; i < 5; i++) seg += chars[bytes[i] % chars.length];
276
- segments.push(seg);
277
- }
278
- return `WAB-${segments.join('-')}`;
279
- }
280
-
281
- function generateApiKey() {
282
- return `wab_${uuidv4().replace(/-/g, '')}`;
283
- }
284
-
285
- // ─── User Operations ──────────────────────────────────────────────────
286
- const createUser = db.prepare(`
287
- INSERT INTO users (id, email, password, name, company) VALUES (?, ?, ?, ?, ?)
288
- `);
289
-
290
- const findUserByEmail = db.prepare(`SELECT * FROM users WHERE email = ?`);
291
- const findUserById = db.prepare(`SELECT id, email, name, company, created_at FROM users WHERE id = ?`);
292
-
293
- function registerUser({ email, password, name, company }) {
294
- const id = uuidv4();
295
- const hashed = bcrypt.hashSync(password, 12);
296
- createUser.run(id, email, hashed, name, company || null);
297
- return { id, email, name, company };
298
- }
299
-
300
- function loginUser({ email, password }) {
301
- const user = findUserByEmail.get(email);
302
- if (!user) return null;
303
- if (!bcrypt.compareSync(password, user.password)) return null;
304
- return { id: user.id, email: user.email, name: user.name, company: user.company };
305
- }
306
-
307
- // ─── Site Operations ──────────────────────────────────────────────────
308
- const createSite = db.prepare(`
309
- INSERT INTO sites (id, user_id, domain, name, description, tier, license_key, api_key, config)
310
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
311
- `);
312
-
313
- const findSitesByUser = db.prepare(`SELECT * FROM sites WHERE user_id = ? ORDER BY created_at DESC`);
314
- const findSiteById = db.prepare(`SELECT * FROM sites WHERE id = ?`);
315
- const findSiteByLicense = db.prepare(`SELECT * FROM sites WHERE license_key = ? AND active = 1`);
316
- const findSiteByDomainAndLicense = db.prepare(`SELECT * FROM sites WHERE domain = ? AND license_key = ? AND active = 1`);
317
- const findSiteByDomain = db.prepare(`SELECT * FROM sites WHERE domain = ? AND active = 1 LIMIT 1`);
318
- const updateSiteConfig = db.prepare(`UPDATE sites SET config = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`);
319
- const updateSiteTier = db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`);
320
- const deleteSite = db.prepare(`UPDATE sites SET active = 0, updated_at = datetime('now') WHERE id = ? AND user_id = ?`);
321
-
322
- function addSite({ userId, domain, name, description, tier }) {
323
- const id = uuidv4();
324
- const licenseKey = generateLicenseKey();
325
- const apiKey = generateApiKey();
326
- const config = JSON.stringify({
327
- agentPermissions: { readContent: true, click: true, fillForms: false, scroll: true, navigate: false, apiAccess: false, automatedLogin: false, extractData: false },
328
- features: { advancedAnalytics: false, realTimeUpdates: false },
329
- restrictions: { allowedSelectors: [], blockedSelectors: ['.private', '[data-private]'], rateLimit: { maxCallsPerMinute: 60 } },
330
- logging: { enabled: false, level: 'basic' }
331
- });
332
- createSite.run(id, userId, domain, name, description || '', tier || 'free', licenseKey, apiKey, config);
333
- return { id, domain, name, licenseKey, apiKey, tier: tier || 'free' };
334
- }
335
-
336
- // ─── Analytics ────────────────────────────────────────────────────────
337
- const insertAnalytic = db.prepare(`
338
- INSERT INTO analytics (site_id, action_name, agent_id, trigger_type, success, metadata)
339
- VALUES (?, ?, ?, ?, ?, ?)
340
- `);
341
-
342
- const getAnalyticsBySite = db.prepare(`
343
- SELECT action_name, trigger_type, COUNT(*) as count, SUM(success) as successes
344
- FROM analytics WHERE site_id = ? AND created_at >= ? GROUP BY action_name, trigger_type
345
- ORDER BY count DESC
346
- `);
347
-
348
- const getAnalyticsTimeline = db.prepare(`
349
- SELECT date(created_at) as day, COUNT(*) as count
350
- FROM analytics WHERE site_id = ? AND created_at >= ?
351
- GROUP BY day ORDER BY day
352
- `);
353
-
354
- function recordAnalytic({ siteId, actionName, agentId, triggerType, success, metadata }) {
355
- insertAnalytic.run(siteId, actionName, agentId || null, triggerType || null, success ? 1 : 0, JSON.stringify(metadata || {}));
356
- }
357
-
358
- // ─── License Verification ─────────────────────────────────────────────
359
- function verifyLicense(domain, licenseKey) {
360
- const site = findSiteByDomainAndLicense.get(domain, licenseKey);
361
- if (!site) {
362
- const siteByKey = findSiteByLicense.get(licenseKey);
363
- if (siteByKey) return { valid: false, error: 'Domain mismatch', tier: 'free' };
364
- return { valid: false, error: 'Invalid license key', tier: 'free' };
365
- }
366
-
367
- // Check for free grant override
368
- const grant = db.prepare(`SELECT * FROM free_grants WHERE user_id = ? AND (site_id = ? OR site_id IS NULL) AND active = 1 AND (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY granted_at DESC LIMIT 1`).get(site.user_id, site.id);
369
- const effectiveTier = grant ? grant.granted_tier : site.tier;
370
-
371
- const tierPermissions = {
372
- free: { apiAccess: false, automatedLogin: false, extractData: false, advancedAnalytics: false },
373
- starter: { apiAccess: false, automatedLogin: true, extractData: false, advancedAnalytics: true },
374
- pro: { apiAccess: true, automatedLogin: true, extractData: true, advancedAnalytics: true },
375
- enterprise: { apiAccess: true, automatedLogin: true, extractData: true, advancedAnalytics: true }
376
- };
377
-
378
- return {
379
- valid: true,
380
- tier: effectiveTier,
381
- domain: site.domain,
382
- allowedPermissions: tierPermissions[effectiveTier] || tierPermissions.free
383
- };
384
- }
385
-
386
- // ─── Admin Operations ─────────────────────────────────────────────────
387
- function createAdmin({ email, password, name, role }) {
388
- const id = uuidv4();
389
- const hashed = bcrypt.hashSync(password, 12);
390
- db.prepare(`INSERT INTO admins (id, email, password, name, role) VALUES (?, ?, ?, ?, ?)`).run(id, email, hashed, name, role || 'admin');
391
- return { id, email, name, role: role || 'admin' };
392
- }
393
-
394
- function loginAdmin({ email, password }) {
395
- const admin = db.prepare(`SELECT * FROM admins WHERE email = ?`).get(email);
396
- if (!admin) return null;
397
- if (!bcrypt.compareSync(password, admin.password)) return null;
398
- return { id: admin.id, email: admin.email, name: admin.name, role: admin.role };
399
- }
400
-
401
- function findAdminById(id) {
402
- return db.prepare(`SELECT id, email, name, role, created_at FROM admins WHERE id = ?`).get(id);
403
- }
404
-
405
- /**
406
- * First-run admin creation from env only (no hardcoded password).
407
- * Alternatively use: node scripts/create-admin.js <email> <password>
408
- */
409
- function maybeBootstrapAdmin() {
410
- if (isTest) return;
411
- const count = db.prepare(`SELECT COUNT(*) as c FROM admins`).get().c;
412
- if (count > 0) return;
413
- const email = process.env.BOOTSTRAP_ADMIN_EMAIL;
414
- const password = process.env.BOOTSTRAP_ADMIN_PASSWORD;
415
- if (!email || !password) {
416
- console.warn('[WAB] No admin accounts. Set BOOTSTRAP_ADMIN_EMAIL and BOOTSTRAP_ADMIN_PASSWORD for first boot, or run: node scripts/create-admin.js <email> <password>');
417
- return;
418
- }
419
- createAdmin({ email, password, name: 'Bootstrap Admin', role: 'superadmin' });
420
- console.log('[WAB] Bootstrap admin created from BOOTSTRAP_ADMIN_* environment variables.');
421
- }
422
-
423
- // ─── Admin Queries ────────────────────────────────────────────────────
424
- function getAllUsers() {
425
- return db.prepare(`SELECT id, email, name, company, created_at FROM users ORDER BY created_at DESC`).all();
426
- }
427
-
428
- function getAllSites() {
429
- return db.prepare(`SELECT s.*, u.email as user_email, u.name as user_name FROM sites s LEFT JOIN users u ON s.user_id = u.id ORDER BY s.created_at DESC`).all();
430
- }
431
-
432
- function getAdminStats() {
433
- const totalUsers = db.prepare(`SELECT COUNT(*) as c FROM users`).get().c;
434
- const totalSites = db.prepare(`SELECT COUNT(*) as c FROM sites WHERE active = 1`).get().c;
435
- const totalAnalytics = db.prepare(`SELECT COUNT(*) as c FROM analytics`).get().c;
436
- const todayAnalytics = db.prepare(`SELECT COUNT(*) as c FROM analytics WHERE created_at >= date('now')`).get().c;
437
- const tierBreakdown = db.prepare(`SELECT tier, COUNT(*) as count FROM sites WHERE active = 1 GROUP BY tier`).all();
438
- const recentUsers = db.prepare(`SELECT id, email, name, company, created_at FROM users ORDER BY created_at DESC LIMIT 10`).all();
439
- const totalRevenue = db.prepare(`SELECT COALESCE(SUM(amount), 0) as total FROM payments WHERE status = 'succeeded'`).get().total;
440
- const activeGrants = db.prepare(`SELECT COUNT(*) as c FROM free_grants WHERE active = 1 AND (expires_at IS NULL OR expires_at > datetime('now'))`).get().c;
441
- const monthlySignups = db.prepare(`SELECT COUNT(*) as c FROM users WHERE created_at >= date('now', '-30 days')`).get().c;
442
- return { totalUsers, totalSites, totalAnalytics, todayAnalytics, tierBreakdown, recentUsers, totalRevenue, activeGrants, monthlySignups };
443
- }
444
-
445
- function getPlatformAnalytics(days) {
446
- const since = new Date(Date.now() - days * 86400000).toISOString();
447
- const timeline = db.prepare(`SELECT date(created_at) as day, COUNT(*) as count FROM analytics WHERE created_at >= ? GROUP BY day ORDER BY day`).all(since);
448
- const topActions = db.prepare(`SELECT action_name, COUNT(*) as count FROM analytics WHERE created_at >= ? GROUP BY action_name ORDER BY count DESC LIMIT 20`).all(since);
449
- const signups = db.prepare(`SELECT date(created_at) as day, COUNT(*) as count FROM users WHERE created_at >= ? GROUP BY day ORDER BY day`).all(since);
450
- return { timeline, topActions, signups };
451
- }
452
-
453
- // ─── Free Grant Operations ────────────────────────────────────────────
454
- function grantFreeTier({ userId, siteId, tier, reason, grantedBy, expiresAt }) {
455
- const id = uuidv4();
456
- db.prepare(`INSERT INTO free_grants (id, user_id, site_id, granted_tier, reason, granted_by, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, userId, siteId || null, tier, reason || null, grantedBy, expiresAt || null);
457
- if (siteId) {
458
- db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE id = ?`).run(tier, siteId);
459
- } else {
460
- db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE user_id = ? AND active = 1`).run(tier, userId);
461
- }
462
- return { id, userId, siteId, tier, reason };
463
- }
464
-
465
- function revokeGrant(grantId) {
466
- const grant = db.prepare(`SELECT * FROM free_grants WHERE id = ?`).get(grantId);
467
- if (!grant) return false;
468
- db.prepare(`UPDATE free_grants SET active = 0 WHERE id = ?`).run(grantId);
469
- if (grant.site_id) {
470
- db.prepare(`UPDATE sites SET tier = 'free', updated_at = datetime('now') WHERE id = ?`).run(grant.site_id);
471
- } else {
472
- db.prepare(`UPDATE sites SET tier = 'free', updated_at = datetime('now') WHERE user_id = ? AND active = 1`).run(grant.user_id);
473
- }
474
- return true;
475
- }
476
-
477
- function getActiveGrants() {
478
- return db.prepare(`SELECT g.*, u.email as user_email, u.name as user_name, a.name as admin_name FROM free_grants g LEFT JOIN users u ON g.user_id = u.id LEFT JOIN admins a ON g.granted_by = a.id WHERE g.active = 1 ORDER BY g.granted_at DESC`).all();
479
- }
480
-
481
- // ─── Stripe DB Operations ─────────────────────────────────────────────
482
- function saveStripeCustomer(userId, stripeCustomerId) {
483
- const id = uuidv4();
484
- db.prepare(`INSERT OR REPLACE INTO stripe_customers (id, user_id, stripe_customer_id) VALUES (?, ?, ?)`).run(id, userId, stripeCustomerId);
485
- }
486
-
487
- function getStripeCustomer(userId) {
488
- return db.prepare(`SELECT * FROM stripe_customers WHERE user_id = ?`).get(userId);
489
- }
490
-
491
- function saveStripeSubscription({ userId, siteId, stripeSubId, stripePriceId, tier, status, periodStart, periodEnd }) {
492
- const id = uuidv4();
493
- db.prepare(`INSERT INTO stripe_subscriptions (id, user_id, site_id, stripe_subscription_id, stripe_price_id, tier, status, current_period_start, current_period_end) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, userId, siteId, stripeSubId, stripePriceId, tier, status || 'active', periodStart, periodEnd);
494
- }
495
-
496
- function updateStripeSubscription(stripeSubId, { status, periodStart, periodEnd, tier }) {
497
- const updates = [];
498
- const params = [];
499
- if (status) { updates.push('status = ?'); params.push(status); }
500
- if (periodStart) { updates.push('current_period_start = ?'); params.push(periodStart); }
501
- if (periodEnd) { updates.push('current_period_end = ?'); params.push(periodEnd); }
502
- if (tier) { updates.push('tier = ?'); params.push(tier); }
503
- if (updates.length === 0) return;
504
- params.push(stripeSubId);
505
- db.prepare(`UPDATE stripe_subscriptions SET ${updates.join(', ')} WHERE stripe_subscription_id = ?`).run(...params);
506
- }
507
-
508
- function getStripeSubscriptionBySubId(stripeSubId) {
509
- return db.prepare(`SELECT * FROM stripe_subscriptions WHERE stripe_subscription_id = ?`).get(stripeSubId);
510
- }
511
-
512
- function savePayment({ userId, stripePaymentId, amount, currency, status, description }) {
513
- const id = uuidv4();
514
- db.prepare(`INSERT INTO payments (id, user_id, stripe_payment_id, amount, currency, status, description) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, userId, stripePaymentId, amount, currency || 'usd', status || 'succeeded', description || null);
515
- }
516
-
517
- function getPayments(limit) {
518
- return db.prepare(`SELECT p.*, u.email as user_email, u.name as user_name FROM payments p LEFT JOIN users u ON p.user_id = u.id ORDER BY p.created_at DESC LIMIT ?`).all(limit || 50);
519
- }
520
-
521
- // ─── SMTP Settings ────────────────────────────────────────────────────
522
- function getSmtpSettings() {
523
- const row = db.prepare(`SELECT * FROM smtp_settings WHERE id = 1`).get();
524
- if (!row) return null;
525
- if (row.password) {
526
- const dec = decryptOptional(row.password);
527
- return { ...row, password: dec != null ? dec : row.password };
528
- }
529
- return row;
530
- }
531
-
532
- function updateSmtpSettings({ host, port, secure, username, password, fromName, fromEmail, enabled }) {
533
- const current = db.prepare(`SELECT password FROM smtp_settings WHERE id = 1`).get();
534
- let nextPassword = current && current.password;
535
- if (password !== undefined) {
536
- nextPassword = encryptOptional(password);
537
- }
538
- db.prepare(`UPDATE smtp_settings SET host = ?, port = ?, secure = ?, username = ?, password = ?, from_name = ?, from_email = ?, enabled = ?, updated_at = datetime('now') WHERE id = 1`).run(host, port || 587, secure ? 1 : 0, username, nextPassword, fromName || 'Web Agent Bridge', fromEmail, enabled ? 1 : 0);
539
- }
540
-
541
- function logNotification({ userId, emailTo, template, subject, status, errorMessage }) {
542
- db.prepare(`INSERT INTO notifications_log (user_id, email_to, template, subject, status, error_message) VALUES (?, ?, ?, ?, ?, ?)`).run(userId || null, emailTo, template, subject, status || 'sent', errorMessage || null);
543
- }
544
-
545
- function getNotificationLogs(limit) {
546
- return db.prepare(`SELECT * FROM notifications_log ORDER BY created_at DESC LIMIT ?`).all(limit || 100);
547
- }
548
-
549
- // ─── Admin User Management ───────────────────────────────────────────
550
- function adminUpdateUserTier(userId, siteId, tier) {
551
- if (siteId) {
552
- db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`).run(tier, siteId, userId);
553
- } else {
554
- db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE user_id = ? AND active = 1`).run(tier, userId);
555
- }
556
- }
557
-
558
- /**
559
- * Admin: update any site by id (tier and/or active).
560
- *
561
- * @param {string} siteId Site UUID.
562
- * @param {{ tier?: string, active?: boolean }} updates Partial updates.
563
- * @returns {boolean}
564
- */
565
- function adminUpdateSite(siteId, updates) {
566
- const site = findSiteById.get(siteId);
567
- if (!site) return false;
568
- let tier = site.tier;
569
- let active = site.active;
570
- if (updates.tier !== undefined) {
571
- if (!['free', 'starter', 'pro', 'enterprise'].includes(updates.tier)) return false;
572
- tier = updates.tier;
573
- }
574
- if (updates.active !== undefined) {
575
- active = updates.active ? 1 : 0;
576
- }
577
- db.prepare(`UPDATE sites SET tier = ?, active = ?, updated_at = datetime('now') WHERE id = ?`).run(tier, active, siteId);
578
- return true;
579
- }
580
-
581
- function adminDeleteUser(userId) {
582
- db.prepare(`UPDATE sites SET active = 0 WHERE user_id = ?`).run(userId);
583
- db.prepare(`DELETE FROM users WHERE id = ?`).run(userId);
584
- }
585
-
586
- function getUserFullDetails(userId) {
587
- const user = db.prepare(`SELECT id, email, name, company, created_at FROM users WHERE id = ?`).get(userId);
588
- if (!user) return null;
589
- const sites = db.prepare(`SELECT * FROM sites WHERE user_id = ? ORDER BY created_at DESC`).all(userId);
590
- const grants = db.prepare(`SELECT * FROM free_grants WHERE user_id = ? AND active = 1`).all(userId);
591
- const payments = db.prepare(`SELECT * FROM payments WHERE user_id = ? ORDER BY created_at DESC`).all(userId);
592
- const stripeCustomer = db.prepare(`SELECT * FROM stripe_customers WHERE user_id = ?`).get(userId);
593
- return { ...user, sites, grants, payments, stripeCustomer };
594
- }
595
-
596
- // ─── Platform Settings ───────────────────────────────────────────────
597
- function getPlatformSetting(key) {
598
- const row = db.prepare(`SELECT value FROM platform_settings WHERE key = ?`).get(key);
599
- return row ? row.value : null;
600
- }
601
-
602
- function setPlatformSetting(key, value) {
603
- db.prepare(`INSERT OR REPLACE INTO platform_settings (key, value, updated_at) VALUES (?, ?, datetime('now'))`).run(key, value);
604
- }
605
-
606
- // ─── Ads Operations ──────────────────────────────────────────────────
607
- function submitAd({ title, description, imageUrl, targetUrl, advertiserName, advertiserEmail, position, budgetCents, cpcCents, cpiCents, expiresAt }) {
608
- const id = uuidv4();
609
- db.prepare(`INSERT INTO wab_ads (id, title, description, image_url, target_url, advertiser_name, advertiser_email, position, budget_cents, cpc_cents, cpi_cents, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, title, description || '', imageUrl || '', targetUrl, advertiserName, advertiserEmail, position || 'new-tab', budgetCents || 0, cpcCents || 5, cpiCents || 1, expiresAt || null);
610
- return { id, title, advertiserName, status: 'pending' };
611
- }
612
-
613
- function getActiveAds(position) {
614
- let q = `SELECT id, title, description, image_url, target_url, advertiser_name, position FROM wab_ads WHERE status = 'approved' AND (expires_at IS NULL OR expires_at > datetime('now')) AND (budget_cents <= 0 OR spent_cents < budget_cents)`;
615
- const params = [];
616
- if (position) { q += ` AND position = ?`; params.push(position); }
617
- q += ` ORDER BY created_at DESC LIMIT 10`;
618
- return db.prepare(q).all(...params);
619
- }
620
-
621
- function getAllAds() {
622
- return db.prepare(`SELECT * FROM wab_ads ORDER BY created_at DESC`).all();
623
- }
624
-
625
- function getPendingAds() {
626
- return db.prepare(`SELECT * FROM wab_ads WHERE status = 'pending' ORDER BY created_at ASC`).all();
627
- }
628
-
629
- function getAdById(id) {
630
- return db.prepare(`SELECT * FROM wab_ads WHERE id = ?`).get(id);
631
- }
632
-
633
- function updateAdStatus(id, status, adminId) {
634
- const sets = ['status = ?'];
635
- const params = [status];
636
- if (status === 'approved') {
637
- sets.push('approved_by = ?', 'approved_at = datetime(\'now\')');
638
- params.push(adminId);
639
- }
640
- params.push(id);
641
- db.prepare(`UPDATE wab_ads SET ${sets.join(', ')} WHERE id = ?`).run(...params);
642
- }
643
-
644
- function deleteAd(id) {
645
- db.prepare(`DELETE FROM ad_events WHERE ad_id = ?`).run(id);
646
- db.prepare(`DELETE FROM wab_ads WHERE id = ?`).run(id);
647
- }
648
-
649
- function recordAdEvent(adId, eventType, ipHash) {
650
- // Deduplicate: skip if same ip+ad+event in last 60s
651
- const recent = db.prepare(`SELECT 1 FROM ad_events WHERE ad_id = ? AND event_type = ? AND ip_hash = ? AND created_at > datetime('now', '-60 seconds') LIMIT 1`).get(adId, eventType, ipHash || '');
652
- if (recent) return;
653
- db.prepare(`INSERT INTO ad_events (ad_id, event_type, ip_hash) VALUES (?, ?, ?)`).run(adId, eventType, ipHash || null);
654
- if (eventType === 'click') {
655
- const ad = db.prepare(`SELECT cpc_cents FROM wab_ads WHERE id = ?`).get(adId);
656
- if (ad) {
657
- db.prepare(`UPDATE wab_ads SET clicks = clicks + 1, spent_cents = spent_cents + ? WHERE id = ?`).run(ad.cpc_cents, adId);
658
- }
659
- } else {
660
- const ad = db.prepare(`SELECT cpi_cents FROM wab_ads WHERE id = ?`).get(adId);
661
- if (ad) {
662
- db.prepare(`UPDATE wab_ads SET impressions = impressions + 1, spent_cents = spent_cents + ? WHERE id = ?`).run(ad.cpi_cents, adId);
663
- }
664
- }
665
- }
666
-
667
- function getAdStats() {
668
- const total = db.prepare(`SELECT COUNT(*) as c FROM wab_ads`).get().c;
669
- const pending = db.prepare(`SELECT COUNT(*) as c FROM wab_ads WHERE status = 'pending'`).get().c;
670
- const approved = db.prepare(`SELECT COUNT(*) as c FROM wab_ads WHERE status = 'approved'`).get().c;
671
- const totalImpressions = db.prepare(`SELECT COALESCE(SUM(impressions), 0) as c FROM wab_ads`).get().c;
672
- const totalClicks = db.prepare(`SELECT COALESCE(SUM(clicks), 0) as c FROM wab_ads`).get().c;
673
- const totalRevenueCents = db.prepare(`SELECT COALESCE(SUM(spent_cents), 0) as c FROM wab_ads`).get().c;
674
- return { total, pending, approved, totalImpressions, totalClicks, totalRevenueCents };
675
- }
676
-
677
- module.exports = {
678
- db,
679
- registerUser,
680
- loginUser,
681
- findUserById,
682
- findUserByEmail,
683
- addSite,
684
- findSitesByUser,
685
- findSiteById,
686
- findSiteByLicense,
687
- findSiteByDomain,
688
- updateSiteConfig,
689
- updateSiteTier,
690
- deleteSite,
691
- recordAnalytic,
692
- getAnalyticsBySite,
693
- getAnalyticsTimeline,
694
- verifyLicense,
695
- generateLicenseKey,
696
- generateApiKey,
697
- // Admin
698
- createAdmin,
699
- loginAdmin,
700
- findAdminById,
701
- maybeBootstrapAdmin,
702
- getAllUsers,
703
- getAllSites,
704
- getAdminStats,
705
- getPlatformAnalytics,
706
- adminUpdateUserTier,
707
- adminUpdateSite,
708
- adminDeleteUser,
709
- getUserFullDetails,
710
- // Free Grants
711
- grantFreeTier,
712
- revokeGrant,
713
- getActiveGrants,
714
- // Stripe
715
- saveStripeCustomer,
716
- getStripeCustomer,
717
- saveStripeSubscription,
718
- updateStripeSubscription,
719
- getStripeSubscriptionBySubId,
720
- savePayment,
721
- getPayments,
722
- // SMTP
723
- getSmtpSettings,
724
- updateSmtpSettings,
725
- logNotification,
726
- getNotificationLogs,
727
- // Platform
728
- getPlatformSetting,
729
- setPlatformSetting,
730
- // Ads
731
- submitAd,
732
- getActiveAds,
733
- getAllAds,
734
- getPendingAds,
735
- getAdById,
736
- updateAdStatus,
737
- deleteAd,
738
- recordAdEvent,
739
- getAdStats
740
- };
1
+ const Database = require('better-sqlite3');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const crypto = require('crypto');
5
+ const bcrypt = require('bcryptjs');
6
+ const { randomUUID: uuidv4 } = require('crypto');
7
+ const { encryptOptional, decryptOptional } = require('../utils/secureFields');
8
+
9
+ const isTest = process.env.NODE_ENV === 'test';
10
+ const DATA_DIR = isTest
11
+ ? path.join(__dirname, '..', '..', 'data-test')
12
+ : (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
13
+ if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
14
+
15
+ const dbFile = isTest ? 'wab-test.db' : 'wab.db';
16
+ const db = new Database(path.join(DATA_DIR, dbFile));
17
+
18
+ db.pragma('journal_mode = WAL');
19
+ db.pragma('foreign_keys = ON');
20
+
21
+ db.exec(`
22
+ CREATE TABLE IF NOT EXISTS users (
23
+ id TEXT PRIMARY KEY,
24
+ email TEXT UNIQUE NOT NULL,
25
+ password TEXT NOT NULL,
26
+ name TEXT NOT NULL,
27
+ company TEXT,
28
+ created_at TEXT DEFAULT (datetime('now')),
29
+ updated_at TEXT DEFAULT (datetime('now'))
30
+ );
31
+
32
+ CREATE TABLE IF NOT EXISTS sites (
33
+ id TEXT PRIMARY KEY,
34
+ user_id TEXT NOT NULL,
35
+ domain TEXT NOT NULL,
36
+ name TEXT NOT NULL,
37
+ description TEXT,
38
+ tier TEXT DEFAULT 'free' CHECK(tier IN ('free','starter','pro','business','enterprise')),
39
+ license_key TEXT UNIQUE NOT NULL,
40
+ api_key TEXT UNIQUE,
41
+ config TEXT DEFAULT '{}',
42
+ active INTEGER DEFAULT 1,
43
+ created_at TEXT DEFAULT (datetime('now')),
44
+ updated_at TEXT DEFAULT (datetime('now')),
45
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS analytics (
49
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ site_id TEXT NOT NULL,
51
+ action_name TEXT NOT NULL,
52
+ agent_id TEXT,
53
+ trigger_type TEXT,
54
+ success INTEGER,
55
+ metadata TEXT DEFAULT '{}',
56
+ created_at TEXT DEFAULT (datetime('now')),
57
+ FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
58
+ );
59
+
60
+ CREATE TABLE IF NOT EXISTS subscriptions (
61
+ id TEXT PRIMARY KEY,
62
+ user_id TEXT NOT NULL,
63
+ site_id TEXT NOT NULL,
64
+ tier TEXT NOT NULL CHECK(tier IN ('free','starter','pro','business','enterprise')),
65
+ status TEXT DEFAULT 'active' CHECK(status IN ('active','cancelled','expired','trial')),
66
+ started_at TEXT DEFAULT (datetime('now')),
67
+ expires_at TEXT,
68
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
69
+ FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
70
+ );
71
+
72
+ CREATE INDEX IF NOT EXISTS idx_sites_domain ON sites(domain);
73
+ CREATE INDEX IF NOT EXISTS idx_sites_license ON sites(license_key);
74
+ CREATE INDEX IF NOT EXISTS idx_analytics_site ON analytics(site_id);
75
+ CREATE INDEX IF NOT EXISTS idx_analytics_created ON analytics(created_at);
76
+
77
+ CREATE TABLE IF NOT EXISTS admins (
78
+ id TEXT PRIMARY KEY,
79
+ email TEXT UNIQUE NOT NULL,
80
+ password TEXT NOT NULL,
81
+ name TEXT NOT NULL,
82
+ role TEXT DEFAULT 'admin' CHECK(role IN ('admin','superadmin')),
83
+ created_at TEXT DEFAULT (datetime('now'))
84
+ );
85
+
86
+ CREATE TABLE IF NOT EXISTS free_grants (
87
+ id TEXT PRIMARY KEY,
88
+ user_id TEXT NOT NULL,
89
+ site_id TEXT,
90
+ granted_tier TEXT NOT NULL CHECK(granted_tier IN ('starter','pro','business','enterprise')),
91
+ reason TEXT,
92
+ granted_by TEXT NOT NULL,
93
+ granted_at TEXT DEFAULT (datetime('now')),
94
+ expires_at TEXT,
95
+ active INTEGER DEFAULT 1,
96
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
97
+ FOREIGN KEY (granted_by) REFERENCES admins(id)
98
+ );
99
+
100
+ CREATE TABLE IF NOT EXISTS stripe_customers (
101
+ id TEXT PRIMARY KEY,
102
+ user_id TEXT UNIQUE NOT NULL,
103
+ stripe_customer_id TEXT UNIQUE,
104
+ created_at TEXT DEFAULT (datetime('now')),
105
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
106
+ );
107
+
108
+ CREATE TABLE IF NOT EXISTS stripe_subscriptions (
109
+ id TEXT PRIMARY KEY,
110
+ user_id TEXT NOT NULL,
111
+ site_id TEXT NOT NULL,
112
+ stripe_subscription_id TEXT UNIQUE,
113
+ stripe_price_id TEXT,
114
+ tier TEXT NOT NULL CHECK(tier IN ('starter','pro','business','enterprise')),
115
+ status TEXT DEFAULT 'active' CHECK(status IN ('active','cancelled','past_due','trialing','incomplete')),
116
+ current_period_start TEXT,
117
+ current_period_end TEXT,
118
+ created_at TEXT DEFAULT (datetime('now')),
119
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
120
+ FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
121
+ );
122
+
123
+ CREATE TABLE IF NOT EXISTS payments (
124
+ id TEXT PRIMARY KEY,
125
+ user_id TEXT NOT NULL,
126
+ stripe_payment_id TEXT UNIQUE,
127
+ amount INTEGER NOT NULL,
128
+ currency TEXT DEFAULT 'usd',
129
+ status TEXT DEFAULT 'succeeded',
130
+ description TEXT,
131
+ created_at TEXT DEFAULT (datetime('now')),
132
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
133
+ );
134
+
135
+ CREATE TABLE IF NOT EXISTS notifications_log (
136
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
137
+ user_id TEXT,
138
+ email_to TEXT NOT NULL,
139
+ template TEXT NOT NULL,
140
+ subject TEXT NOT NULL,
141
+ status TEXT DEFAULT 'sent' CHECK(status IN ('sent','failed','queued')),
142
+ error_message TEXT,
143
+ created_at TEXT DEFAULT (datetime('now'))
144
+ );
145
+
146
+ CREATE TABLE IF NOT EXISTS smtp_settings (
147
+ id INTEGER PRIMARY KEY CHECK(id = 1),
148
+ host TEXT,
149
+ port INTEGER DEFAULT 587,
150
+ secure INTEGER DEFAULT 0,
151
+ username TEXT,
152
+ password TEXT,
153
+ from_name TEXT DEFAULT 'Web Agent Bridge',
154
+ from_email TEXT,
155
+ enabled INTEGER DEFAULT 0,
156
+ updated_at TEXT DEFAULT (datetime('now'))
157
+ );
158
+
159
+ INSERT OR IGNORE INTO smtp_settings (id) VALUES (1);
160
+
161
+ CREATE TABLE IF NOT EXISTS platform_settings (
162
+ key TEXT PRIMARY KEY,
163
+ value TEXT,
164
+ updated_at TEXT DEFAULT (datetime('now'))
165
+ );
166
+
167
+ CREATE INDEX IF NOT EXISTS idx_free_grants_user ON free_grants(user_id);
168
+ CREATE INDEX IF NOT EXISTS idx_stripe_subs_user ON stripe_subscriptions(user_id);
169
+ CREATE INDEX IF NOT EXISTS idx_payments_user ON payments(user_id);
170
+ CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications_log(user_id);
171
+
172
+ CREATE TABLE IF NOT EXISTS wab_ads (
173
+ id TEXT PRIMARY KEY,
174
+ title TEXT NOT NULL,
175
+ description TEXT,
176
+ image_url TEXT,
177
+ target_url TEXT NOT NULL,
178
+ advertiser_name TEXT NOT NULL,
179
+ advertiser_email TEXT NOT NULL,
180
+ status TEXT DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected','paused','expired')),
181
+ position TEXT DEFAULT 'new-tab' CHECK(position IN ('new-tab','sidebar','search')),
182
+ budget_cents INTEGER DEFAULT 0,
183
+ spent_cents INTEGER DEFAULT 0,
184
+ cpc_cents INTEGER DEFAULT 5,
185
+ cpi_cents INTEGER DEFAULT 1,
186
+ impressions INTEGER DEFAULT 0,
187
+ clicks INTEGER DEFAULT 0,
188
+ created_at TEXT DEFAULT (datetime('now')),
189
+ approved_by TEXT,
190
+ approved_at TEXT,
191
+ expires_at TEXT,
192
+ FOREIGN KEY (approved_by) REFERENCES admins(id)
193
+ );
194
+
195
+ CREATE TABLE IF NOT EXISTS ad_events (
196
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
197
+ ad_id TEXT NOT NULL,
198
+ event_type TEXT NOT NULL CHECK(event_type IN ('impression','click')),
199
+ platform TEXT DEFAULT 'browser',
200
+ ip_hash TEXT,
201
+ created_at TEXT DEFAULT (datetime('now')),
202
+ FOREIGN KEY (ad_id) REFERENCES wab_ads(id) ON DELETE CASCADE
203
+ );
204
+
205
+ CREATE INDEX IF NOT EXISTS idx_wab_ads_status ON wab_ads(status);
206
+ CREATE INDEX IF NOT EXISTS idx_ad_events_ad ON ad_events(ad_id);
207
+ CREATE INDEX IF NOT EXISTS idx_ad_events_created ON ad_events(created_at);
208
+
209
+ -- ─── DNS Provider Account System (v1.3) ────────────────────────────
210
+ -- Stores user-supplied credentials (encrypted at rest) for managed
211
+ -- one-click WAB DNS Discovery enable/disable across DNS providers
212
+ -- and registrars. Credentials never leave the server unencrypted.
213
+ CREATE TABLE IF NOT EXISTS provider_accounts (
214
+ id TEXT PRIMARY KEY,
215
+ user_id TEXT NOT NULL,
216
+ provider_type TEXT NOT NULL CHECK(provider_type IN (
217
+ 'cloudflare','route53','azure','gcp','cpanel','plesk','godaddy','namecheap'
218
+ )),
219
+ label TEXT NOT NULL,
220
+ credentials TEXT NOT NULL,
221
+ config TEXT DEFAULT '{}',
222
+ status TEXT DEFAULT 'pending' CHECK(status IN ('pending','active','error','disabled')),
223
+ last_test_at TEXT,
224
+ last_test_ok INTEGER DEFAULT 0,
225
+ last_test_error TEXT,
226
+ last_sync_at TEXT,
227
+ domains_count INTEGER DEFAULT 0,
228
+ created_at TEXT DEFAULT (datetime('now')),
229
+ updated_at TEXT,
230
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
231
+ );
232
+ CREATE INDEX IF NOT EXISTS idx_provider_accounts_user ON provider_accounts(user_id);
233
+ CREATE INDEX IF NOT EXISTS idx_provider_accounts_type ON provider_accounts(provider_type);
234
+
235
+ CREATE TABLE IF NOT EXISTS provider_domains (
236
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
237
+ account_id TEXT NOT NULL,
238
+ domain TEXT NOT NULL,
239
+ zone_id TEXT,
240
+ wab_enabled INTEGER DEFAULT 0,
241
+ wab_record_value TEXT,
242
+ last_action TEXT,
243
+ last_action_at TEXT,
244
+ last_action_status TEXT,
245
+ last_action_error TEXT,
246
+ created_at TEXT DEFAULT (datetime('now')),
247
+ updated_at TEXT,
248
+ FOREIGN KEY (account_id) REFERENCES provider_accounts(id) ON DELETE CASCADE,
249
+ UNIQUE(account_id, domain)
250
+ );
251
+ CREATE INDEX IF NOT EXISTS idx_provider_domains_account ON provider_domains(account_id);
252
+ CREATE INDEX IF NOT EXISTS idx_provider_domains_domain ON provider_domains(domain);
253
+
254
+ CREATE TABLE IF NOT EXISTS provider_action_log (
255
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
256
+ account_id TEXT NOT NULL,
257
+ domain TEXT,
258
+ action TEXT NOT NULL,
259
+ status TEXT NOT NULL,
260
+ duration_ms INTEGER,
261
+ detail TEXT,
262
+ created_at TEXT DEFAULT (datetime('now')),
263
+ FOREIGN KEY (account_id) REFERENCES provider_accounts(id) ON DELETE CASCADE
264
+ );
265
+ CREATE INDEX IF NOT EXISTS idx_provider_action_log_account_time
266
+ ON provider_action_log(account_id, created_at DESC);
267
+ `);
268
+
269
+ function generateLicenseKey() {
270
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
271
+ const segments = [];
272
+ for (let s = 0; s < 4; s++) {
273
+ let seg = '';
274
+ const bytes = crypto.randomBytes(5);
275
+ for (let i = 0; i < 5; i++) seg += chars[bytes[i] % chars.length];
276
+ segments.push(seg);
277
+ }
278
+ return `WAB-${segments.join('-')}`;
279
+ }
280
+
281
+ function generateApiKey() {
282
+ return `wab_${uuidv4().replace(/-/g, '')}`;
283
+ }
284
+
285
+ // ─── User Operations ──────────────────────────────────────────────────
286
+ const createUser = db.prepare(`
287
+ INSERT INTO users (id, email, password, name, company) VALUES (?, ?, ?, ?, ?)
288
+ `);
289
+
290
+ const findUserByEmail = db.prepare(`SELECT * FROM users WHERE email = ?`);
291
+ const findUserById = db.prepare(`SELECT id, email, name, company, created_at FROM users WHERE id = ?`);
292
+
293
+ function registerUser({ email, password, name, company }) {
294
+ const id = uuidv4();
295
+ const hashed = bcrypt.hashSync(password, 12);
296
+ createUser.run(id, email, hashed, name, company || null);
297
+ return { id, email, name, company };
298
+ }
299
+
300
+ function loginUser({ email, password }) {
301
+ const user = findUserByEmail.get(email);
302
+ if (!user) return null;
303
+ if (!bcrypt.compareSync(password, user.password)) return null;
304
+ return { id: user.id, email: user.email, name: user.name, company: user.company };
305
+ }
306
+
307
+ // ─── Site Operations ──────────────────────────────────────────────────
308
+ const createSite = db.prepare(`
309
+ INSERT INTO sites (id, user_id, domain, name, description, tier, license_key, api_key, config)
310
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
311
+ `);
312
+
313
+ const findSitesByUser = db.prepare(`SELECT * FROM sites WHERE user_id = ? ORDER BY created_at DESC`);
314
+ const findSiteById = db.prepare(`SELECT * FROM sites WHERE id = ?`);
315
+ const findSiteByLicense = db.prepare(`SELECT * FROM sites WHERE license_key = ? AND active = 1`);
316
+ const findSiteByDomainAndLicense = db.prepare(`SELECT * FROM sites WHERE domain = ? AND license_key = ? AND active = 1`);
317
+ const findSiteByDomain = db.prepare(`SELECT * FROM sites WHERE domain = ? AND active = 1 LIMIT 1`);
318
+ const updateSiteConfig = db.prepare(`UPDATE sites SET config = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`);
319
+ const updateSiteTier = db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`);
320
+ const deleteSite = db.prepare(`UPDATE sites SET active = 0, updated_at = datetime('now') WHERE id = ? AND user_id = ?`);
321
+
322
+ function addSite({ userId, domain, name, description, tier }) {
323
+ const id = uuidv4();
324
+ const licenseKey = generateLicenseKey();
325
+ const apiKey = generateApiKey();
326
+ const config = JSON.stringify({
327
+ agentPermissions: { readContent: true, click: true, fillForms: false, scroll: true, navigate: false, apiAccess: false, automatedLogin: false, extractData: false },
328
+ features: { advancedAnalytics: false, realTimeUpdates: false },
329
+ restrictions: { allowedSelectors: [], blockedSelectors: ['.private', '[data-private]'], rateLimit: { maxCallsPerMinute: 60 } },
330
+ logging: { enabled: false, level: 'basic' }
331
+ });
332
+ createSite.run(id, userId, domain, name, description || '', tier || 'free', licenseKey, apiKey, config);
333
+ return { id, domain, name, licenseKey, apiKey, tier: tier || 'free' };
334
+ }
335
+
336
+ // ─── Analytics ────────────────────────────────────────────────────────
337
+ const insertAnalytic = db.prepare(`
338
+ INSERT INTO analytics (site_id, action_name, agent_id, trigger_type, success, metadata)
339
+ VALUES (?, ?, ?, ?, ?, ?)
340
+ `);
341
+
342
+ const getAnalyticsBySite = db.prepare(`
343
+ SELECT action_name, trigger_type, COUNT(*) as count, SUM(success) as successes
344
+ FROM analytics WHERE site_id = ? AND created_at >= ? GROUP BY action_name, trigger_type
345
+ ORDER BY count DESC
346
+ `);
347
+
348
+ const getAnalyticsTimeline = db.prepare(`
349
+ SELECT date(created_at) as day, COUNT(*) as count
350
+ FROM analytics WHERE site_id = ? AND created_at >= ?
351
+ GROUP BY day ORDER BY day
352
+ `);
353
+
354
+ function recordAnalytic({ siteId, actionName, agentId, triggerType, success, metadata }) {
355
+ insertAnalytic.run(siteId, actionName, agentId || null, triggerType || null, success ? 1 : 0, JSON.stringify(metadata || {}));
356
+ }
357
+
358
+ // ─── License Verification ─────────────────────────────────────────────
359
+ function verifyLicense(domain, licenseKey) {
360
+ const site = findSiteByDomainAndLicense.get(domain, licenseKey);
361
+ if (!site) {
362
+ const siteByKey = findSiteByLicense.get(licenseKey);
363
+ if (siteByKey) return { valid: false, error: 'Domain mismatch', tier: 'free' };
364
+ return { valid: false, error: 'Invalid license key', tier: 'free' };
365
+ }
366
+
367
+ // Check for free grant override
368
+ const grant = db.prepare(`SELECT * FROM free_grants WHERE user_id = ? AND (site_id = ? OR site_id IS NULL) AND active = 1 AND (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY granted_at DESC LIMIT 1`).get(site.user_id, site.id);
369
+ const effectiveTier = grant ? grant.granted_tier : site.tier;
370
+
371
+ const tierPermissions = {
372
+ free: { apiAccess: false, automatedLogin: false, extractData: false, advancedAnalytics: false },
373
+ starter: { apiAccess: false, automatedLogin: true, extractData: false, advancedAnalytics: true },
374
+ pro: { apiAccess: true, automatedLogin: true, extractData: true, advancedAnalytics: true },
375
+ enterprise: { apiAccess: true, automatedLogin: true, extractData: true, advancedAnalytics: true }
376
+ };
377
+
378
+ return {
379
+ valid: true,
380
+ tier: effectiveTier,
381
+ domain: site.domain,
382
+ allowedPermissions: tierPermissions[effectiveTier] || tierPermissions.free
383
+ };
384
+ }
385
+
386
+ // ─── Admin Operations ─────────────────────────────────────────────────
387
+ function createAdmin({ email, password, name, role }) {
388
+ const id = uuidv4();
389
+ const hashed = bcrypt.hashSync(password, 12);
390
+ db.prepare(`INSERT INTO admins (id, email, password, name, role) VALUES (?, ?, ?, ?, ?)`).run(id, email, hashed, name, role || 'admin');
391
+ return { id, email, name, role: role || 'admin' };
392
+ }
393
+
394
+ function loginAdmin({ email, password }) {
395
+ const admin = db.prepare(`SELECT * FROM admins WHERE email = ?`).get(email);
396
+ if (!admin) return null;
397
+ if (!bcrypt.compareSync(password, admin.password)) return null;
398
+ return { id: admin.id, email: admin.email, name: admin.name, role: admin.role };
399
+ }
400
+
401
+ function findAdminById(id) {
402
+ return db.prepare(`SELECT id, email, name, role, created_at FROM admins WHERE id = ?`).get(id);
403
+ }
404
+
405
+ /**
406
+ * First-run admin creation from env only (no hardcoded password).
407
+ * Alternatively use: node scripts/create-admin.js <email> <password>
408
+ */
409
+ function maybeBootstrapAdmin() {
410
+ if (isTest) return;
411
+ const count = db.prepare(`SELECT COUNT(*) as c FROM admins`).get().c;
412
+ if (count > 0) return;
413
+ const email = process.env.BOOTSTRAP_ADMIN_EMAIL;
414
+ const password = process.env.BOOTSTRAP_ADMIN_PASSWORD;
415
+ if (!email || !password) {
416
+ console.warn('[WAB] No admin accounts. Set BOOTSTRAP_ADMIN_EMAIL and BOOTSTRAP_ADMIN_PASSWORD for first boot, or run: node scripts/create-admin.js <email> <password>');
417
+ return;
418
+ }
419
+ createAdmin({ email, password, name: 'Bootstrap Admin', role: 'superadmin' });
420
+ console.log('[WAB] Bootstrap admin created from BOOTSTRAP_ADMIN_* environment variables.');
421
+ }
422
+
423
+ // ─── Admin Queries ────────────────────────────────────────────────────
424
+ function getAllUsers() {
425
+ return db.prepare(`SELECT id, email, name, company, created_at FROM users ORDER BY created_at DESC`).all();
426
+ }
427
+
428
+ function getAllSites() {
429
+ return db.prepare(`SELECT s.*, u.email as user_email, u.name as user_name FROM sites s LEFT JOIN users u ON s.user_id = u.id ORDER BY s.created_at DESC`).all();
430
+ }
431
+
432
+ function getAdminStats() {
433
+ const totalUsers = db.prepare(`SELECT COUNT(*) as c FROM users`).get().c;
434
+ const totalSites = db.prepare(`SELECT COUNT(*) as c FROM sites WHERE active = 1`).get().c;
435
+ const totalAnalytics = db.prepare(`SELECT COUNT(*) as c FROM analytics`).get().c;
436
+ const todayAnalytics = db.prepare(`SELECT COUNT(*) as c FROM analytics WHERE created_at >= date('now')`).get().c;
437
+ const tierBreakdown = db.prepare(`SELECT tier, COUNT(*) as count FROM sites WHERE active = 1 GROUP BY tier`).all();
438
+ const recentUsers = db.prepare(`SELECT id, email, name, company, created_at FROM users ORDER BY created_at DESC LIMIT 10`).all();
439
+ const totalRevenue = db.prepare(`SELECT COALESCE(SUM(amount), 0) as total FROM payments WHERE status = 'succeeded'`).get().total;
440
+ const activeGrants = db.prepare(`SELECT COUNT(*) as c FROM free_grants WHERE active = 1 AND (expires_at IS NULL OR expires_at > datetime('now'))`).get().c;
441
+ const monthlySignups = db.prepare(`SELECT COUNT(*) as c FROM users WHERE created_at >= date('now', '-30 days')`).get().c;
442
+ return { totalUsers, totalSites, totalAnalytics, todayAnalytics, tierBreakdown, recentUsers, totalRevenue, activeGrants, monthlySignups };
443
+ }
444
+
445
+ function getPlatformAnalytics(days) {
446
+ const since = new Date(Date.now() - days * 86400000).toISOString();
447
+ const timeline = db.prepare(`SELECT date(created_at) as day, COUNT(*) as count FROM analytics WHERE created_at >= ? GROUP BY day ORDER BY day`).all(since);
448
+ const topActions = db.prepare(`SELECT action_name, COUNT(*) as count FROM analytics WHERE created_at >= ? GROUP BY action_name ORDER BY count DESC LIMIT 20`).all(since);
449
+ const signups = db.prepare(`SELECT date(created_at) as day, COUNT(*) as count FROM users WHERE created_at >= ? GROUP BY day ORDER BY day`).all(since);
450
+ return { timeline, topActions, signups };
451
+ }
452
+
453
+ // ─── Free Grant Operations ────────────────────────────────────────────
454
+ function grantFreeTier({ userId, siteId, tier, reason, grantedBy, expiresAt }) {
455
+ const id = uuidv4();
456
+ db.prepare(`INSERT INTO free_grants (id, user_id, site_id, granted_tier, reason, granted_by, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, userId, siteId || null, tier, reason || null, grantedBy, expiresAt || null);
457
+ if (siteId) {
458
+ db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE id = ?`).run(tier, siteId);
459
+ } else {
460
+ db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE user_id = ? AND active = 1`).run(tier, userId);
461
+ }
462
+ return { id, userId, siteId, tier, reason };
463
+ }
464
+
465
+ function revokeGrant(grantId) {
466
+ const grant = db.prepare(`SELECT * FROM free_grants WHERE id = ?`).get(grantId);
467
+ if (!grant) return false;
468
+ db.prepare(`UPDATE free_grants SET active = 0 WHERE id = ?`).run(grantId);
469
+ if (grant.site_id) {
470
+ db.prepare(`UPDATE sites SET tier = 'free', updated_at = datetime('now') WHERE id = ?`).run(grant.site_id);
471
+ } else {
472
+ db.prepare(`UPDATE sites SET tier = 'free', updated_at = datetime('now') WHERE user_id = ? AND active = 1`).run(grant.user_id);
473
+ }
474
+ return true;
475
+ }
476
+
477
+ function getActiveGrants() {
478
+ return db.prepare(`SELECT g.*, u.email as user_email, u.name as user_name, a.name as admin_name FROM free_grants g LEFT JOIN users u ON g.user_id = u.id LEFT JOIN admins a ON g.granted_by = a.id WHERE g.active = 1 ORDER BY g.granted_at DESC`).all();
479
+ }
480
+
481
+ // ─── Stripe DB Operations ─────────────────────────────────────────────
482
+ function saveStripeCustomer(userId, stripeCustomerId) {
483
+ const id = uuidv4();
484
+ db.prepare(`INSERT OR REPLACE INTO stripe_customers (id, user_id, stripe_customer_id) VALUES (?, ?, ?)`).run(id, userId, stripeCustomerId);
485
+ }
486
+
487
+ function getStripeCustomer(userId) {
488
+ return db.prepare(`SELECT * FROM stripe_customers WHERE user_id = ?`).get(userId);
489
+ }
490
+
491
+ function saveStripeSubscription({ userId, siteId, stripeSubId, stripePriceId, tier, status, periodStart, periodEnd }) {
492
+ const id = uuidv4();
493
+ db.prepare(`INSERT INTO stripe_subscriptions (id, user_id, site_id, stripe_subscription_id, stripe_price_id, tier, status, current_period_start, current_period_end) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, userId, siteId, stripeSubId, stripePriceId, tier, status || 'active', periodStart, periodEnd);
494
+ }
495
+
496
+ function updateStripeSubscription(stripeSubId, { status, periodStart, periodEnd, tier }) {
497
+ const updates = [];
498
+ const params = [];
499
+ if (status) { updates.push('status = ?'); params.push(status); }
500
+ if (periodStart) { updates.push('current_period_start = ?'); params.push(periodStart); }
501
+ if (periodEnd) { updates.push('current_period_end = ?'); params.push(periodEnd); }
502
+ if (tier) { updates.push('tier = ?'); params.push(tier); }
503
+ if (updates.length === 0) return;
504
+ params.push(stripeSubId);
505
+ db.prepare(`UPDATE stripe_subscriptions SET ${updates.join(', ')} WHERE stripe_subscription_id = ?`).run(...params);
506
+ }
507
+
508
+ function getStripeSubscriptionBySubId(stripeSubId) {
509
+ return db.prepare(`SELECT * FROM stripe_subscriptions WHERE stripe_subscription_id = ?`).get(stripeSubId);
510
+ }
511
+
512
+ function savePayment({ userId, stripePaymentId, amount, currency, status, description }) {
513
+ const id = uuidv4();
514
+ db.prepare(`INSERT INTO payments (id, user_id, stripe_payment_id, amount, currency, status, description) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, userId, stripePaymentId, amount, currency || 'usd', status || 'succeeded', description || null);
515
+ }
516
+
517
+ function getPayments(limit) {
518
+ return db.prepare(`SELECT p.*, u.email as user_email, u.name as user_name FROM payments p LEFT JOIN users u ON p.user_id = u.id ORDER BY p.created_at DESC LIMIT ?`).all(limit || 50);
519
+ }
520
+
521
+ // ─── SMTP Settings ────────────────────────────────────────────────────
522
+ function getSmtpSettings() {
523
+ const row = db.prepare(`SELECT * FROM smtp_settings WHERE id = 1`).get();
524
+ if (!row) return null;
525
+ if (row.password) {
526
+ const dec = decryptOptional(row.password);
527
+ return { ...row, password: dec != null ? dec : row.password };
528
+ }
529
+ return row;
530
+ }
531
+
532
+ function updateSmtpSettings({ host, port, secure, username, password, fromName, fromEmail, enabled }) {
533
+ const current = db.prepare(`SELECT password FROM smtp_settings WHERE id = 1`).get();
534
+ let nextPassword = current && current.password;
535
+ if (password !== undefined) {
536
+ nextPassword = encryptOptional(password);
537
+ }
538
+ db.prepare(`UPDATE smtp_settings SET host = ?, port = ?, secure = ?, username = ?, password = ?, from_name = ?, from_email = ?, enabled = ?, updated_at = datetime('now') WHERE id = 1`).run(host, port || 587, secure ? 1 : 0, username, nextPassword, fromName || 'Web Agent Bridge', fromEmail, enabled ? 1 : 0);
539
+ }
540
+
541
+ function logNotification({ userId, emailTo, template, subject, status, errorMessage }) {
542
+ db.prepare(`INSERT INTO notifications_log (user_id, email_to, template, subject, status, error_message) VALUES (?, ?, ?, ?, ?, ?)`).run(userId || null, emailTo, template, subject, status || 'sent', errorMessage || null);
543
+ }
544
+
545
+ function getNotificationLogs(limit) {
546
+ return db.prepare(`SELECT * FROM notifications_log ORDER BY created_at DESC LIMIT ?`).all(limit || 100);
547
+ }
548
+
549
+ // ─── Admin User Management ───────────────────────────────────────────
550
+ function adminUpdateUserTier(userId, siteId, tier) {
551
+ if (siteId) {
552
+ db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`).run(tier, siteId, userId);
553
+ } else {
554
+ db.prepare(`UPDATE sites SET tier = ?, updated_at = datetime('now') WHERE user_id = ? AND active = 1`).run(tier, userId);
555
+ }
556
+ }
557
+
558
+ /**
559
+ * Admin: update any site by id (tier and/or active).
560
+ *
561
+ * @param {string} siteId Site UUID.
562
+ * @param {{ tier?: string, active?: boolean }} updates Partial updates.
563
+ * @returns {boolean}
564
+ */
565
+ function adminUpdateSite(siteId, updates) {
566
+ const site = findSiteById.get(siteId);
567
+ if (!site) return false;
568
+ let tier = site.tier;
569
+ let active = site.active;
570
+ if (updates.tier !== undefined) {
571
+ if (!['free', 'starter', 'pro', 'business', 'enterprise'].includes(updates.tier)) return false;
572
+ tier = updates.tier;
573
+ }
574
+ if (updates.active !== undefined) {
575
+ active = updates.active ? 1 : 0;
576
+ }
577
+ db.prepare(`UPDATE sites SET tier = ?, active = ?, updated_at = datetime('now') WHERE id = ?`).run(tier, active, siteId);
578
+ return true;
579
+ }
580
+
581
+ function adminDeleteUser(userId) {
582
+ db.prepare(`UPDATE sites SET active = 0 WHERE user_id = ?`).run(userId);
583
+ db.prepare(`DELETE FROM users WHERE id = ?`).run(userId);
584
+ }
585
+
586
+ function getUserFullDetails(userId) {
587
+ const user = db.prepare(`SELECT id, email, name, company, created_at FROM users WHERE id = ?`).get(userId);
588
+ if (!user) return null;
589
+ const sites = db.prepare(`SELECT * FROM sites WHERE user_id = ? ORDER BY created_at DESC`).all(userId);
590
+ const grants = db.prepare(`SELECT * FROM free_grants WHERE user_id = ? AND active = 1`).all(userId);
591
+ const payments = db.prepare(`SELECT * FROM payments WHERE user_id = ? ORDER BY created_at DESC`).all(userId);
592
+ const stripeCustomer = db.prepare(`SELECT * FROM stripe_customers WHERE user_id = ?`).get(userId);
593
+ return { ...user, sites, grants, payments, stripeCustomer };
594
+ }
595
+
596
+ // ─── Platform Settings ───────────────────────────────────────────────
597
+ function getPlatformSetting(key) {
598
+ const row = db.prepare(`SELECT value FROM platform_settings WHERE key = ?`).get(key);
599
+ return row ? row.value : null;
600
+ }
601
+
602
+ function setPlatformSetting(key, value) {
603
+ db.prepare(`INSERT OR REPLACE INTO platform_settings (key, value, updated_at) VALUES (?, ?, datetime('now'))`).run(key, value);
604
+ }
605
+
606
+ // ─── Ads Operations ──────────────────────────────────────────────────
607
+ function submitAd({ title, description, imageUrl, targetUrl, advertiserName, advertiserEmail, position, budgetCents, cpcCents, cpiCents, expiresAt }) {
608
+ const id = uuidv4();
609
+ db.prepare(`INSERT INTO wab_ads (id, title, description, image_url, target_url, advertiser_name, advertiser_email, position, budget_cents, cpc_cents, cpi_cents, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, title, description || '', imageUrl || '', targetUrl, advertiserName, advertiserEmail, position || 'new-tab', budgetCents || 0, cpcCents || 5, cpiCents || 1, expiresAt || null);
610
+ return { id, title, advertiserName, status: 'pending' };
611
+ }
612
+
613
+ function getActiveAds(position) {
614
+ let q = `SELECT id, title, description, image_url, target_url, advertiser_name, position FROM wab_ads WHERE status = 'approved' AND (expires_at IS NULL OR expires_at > datetime('now')) AND (budget_cents <= 0 OR spent_cents < budget_cents)`;
615
+ const params = [];
616
+ if (position) { q += ` AND position = ?`; params.push(position); }
617
+ q += ` ORDER BY created_at DESC LIMIT 10`;
618
+ return db.prepare(q).all(...params);
619
+ }
620
+
621
+ function getAllAds() {
622
+ return db.prepare(`SELECT * FROM wab_ads ORDER BY created_at DESC`).all();
623
+ }
624
+
625
+ function getPendingAds() {
626
+ return db.prepare(`SELECT * FROM wab_ads WHERE status = 'pending' ORDER BY created_at ASC`).all();
627
+ }
628
+
629
+ function getAdById(id) {
630
+ return db.prepare(`SELECT * FROM wab_ads WHERE id = ?`).get(id);
631
+ }
632
+
633
+ function updateAdStatus(id, status, adminId) {
634
+ const sets = ['status = ?'];
635
+ const params = [status];
636
+ if (status === 'approved') {
637
+ sets.push('approved_by = ?', 'approved_at = datetime(\'now\')');
638
+ params.push(adminId);
639
+ }
640
+ params.push(id);
641
+ db.prepare(`UPDATE wab_ads SET ${sets.join(', ')} WHERE id = ?`).run(...params);
642
+ }
643
+
644
+ function deleteAd(id) {
645
+ db.prepare(`DELETE FROM ad_events WHERE ad_id = ?`).run(id);
646
+ db.prepare(`DELETE FROM wab_ads WHERE id = ?`).run(id);
647
+ }
648
+
649
+ function recordAdEvent(adId, eventType, ipHash) {
650
+ // Deduplicate: skip if same ip+ad+event in last 60s
651
+ const recent = db.prepare(`SELECT 1 FROM ad_events WHERE ad_id = ? AND event_type = ? AND ip_hash = ? AND created_at > datetime('now', '-60 seconds') LIMIT 1`).get(adId, eventType, ipHash || '');
652
+ if (recent) return;
653
+ db.prepare(`INSERT INTO ad_events (ad_id, event_type, ip_hash) VALUES (?, ?, ?)`).run(adId, eventType, ipHash || null);
654
+ if (eventType === 'click') {
655
+ const ad = db.prepare(`SELECT cpc_cents FROM wab_ads WHERE id = ?`).get(adId);
656
+ if (ad) {
657
+ db.prepare(`UPDATE wab_ads SET clicks = clicks + 1, spent_cents = spent_cents + ? WHERE id = ?`).run(ad.cpc_cents, adId);
658
+ }
659
+ } else {
660
+ const ad = db.prepare(`SELECT cpi_cents FROM wab_ads WHERE id = ?`).get(adId);
661
+ if (ad) {
662
+ db.prepare(`UPDATE wab_ads SET impressions = impressions + 1, spent_cents = spent_cents + ? WHERE id = ?`).run(ad.cpi_cents, adId);
663
+ }
664
+ }
665
+ }
666
+
667
+ function getAdStats() {
668
+ const total = db.prepare(`SELECT COUNT(*) as c FROM wab_ads`).get().c;
669
+ const pending = db.prepare(`SELECT COUNT(*) as c FROM wab_ads WHERE status = 'pending'`).get().c;
670
+ const approved = db.prepare(`SELECT COUNT(*) as c FROM wab_ads WHERE status = 'approved'`).get().c;
671
+ const totalImpressions = db.prepare(`SELECT COALESCE(SUM(impressions), 0) as c FROM wab_ads`).get().c;
672
+ const totalClicks = db.prepare(`SELECT COALESCE(SUM(clicks), 0) as c FROM wab_ads`).get().c;
673
+ const totalRevenueCents = db.prepare(`SELECT COALESCE(SUM(spent_cents), 0) as c FROM wab_ads`).get().c;
674
+ return { total, pending, approved, totalImpressions, totalClicks, totalRevenueCents };
675
+ }
676
+
677
+ module.exports = {
678
+ db,
679
+ registerUser,
680
+ loginUser,
681
+ findUserById,
682
+ findUserByEmail,
683
+ addSite,
684
+ findSitesByUser,
685
+ findSiteById,
686
+ findSiteByLicense,
687
+ findSiteByDomain,
688
+ updateSiteConfig,
689
+ updateSiteTier,
690
+ deleteSite,
691
+ recordAnalytic,
692
+ getAnalyticsBySite,
693
+ getAnalyticsTimeline,
694
+ verifyLicense,
695
+ generateLicenseKey,
696
+ generateApiKey,
697
+ // Admin
698
+ createAdmin,
699
+ loginAdmin,
700
+ findAdminById,
701
+ maybeBootstrapAdmin,
702
+ getAllUsers,
703
+ getAllSites,
704
+ getAdminStats,
705
+ getPlatformAnalytics,
706
+ adminUpdateUserTier,
707
+ adminUpdateSite,
708
+ adminDeleteUser,
709
+ getUserFullDetails,
710
+ // Free Grants
711
+ grantFreeTier,
712
+ revokeGrant,
713
+ getActiveGrants,
714
+ // Stripe
715
+ saveStripeCustomer,
716
+ getStripeCustomer,
717
+ saveStripeSubscription,
718
+ updateStripeSubscription,
719
+ getStripeSubscriptionBySubId,
720
+ savePayment,
721
+ getPayments,
722
+ // SMTP
723
+ getSmtpSettings,
724
+ updateSmtpSettings,
725
+ logNotification,
726
+ getNotificationLogs,
727
+ // Platform
728
+ getPlatformSetting,
729
+ setPlatformSetting,
730
+ // Ads
731
+ submitAd,
732
+ getActiveAds,
733
+ getAllAds,
734
+ getPendingAds,
735
+ getAdById,
736
+ updateAdStatus,
737
+ deleteAd,
738
+ recordAdEvent,
739
+ getAdStats
740
+ };