web-agent-bridge 3.9.1 → 3.10.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/bin/wab.js +54 -0
- package/package.json +93 -93
- package/public/forgot-password.html +68 -0
- package/public/login.html +3 -2
- package/public/reset-password.html +84 -0
- package/public/verify-email.html +76 -0
- package/server/index.js +9 -0
- package/server/middleware/auth.js +42 -1
- package/server/migrations/021_visitor_analytics.sql +31 -0
- package/server/migrations/022_auth_recovery_verification.sql +27 -0
- package/server/migrations/023_atp_merchant_commission.sql +43 -0
- package/server/models/db.js +76 -1
- package/server/routes/admin.js +111 -0
- package/server/routes/auth.js +106 -3
- package/server/routes/premium.js +18 -18
- package/server/routes/transactions.js +32 -0
- package/server/services/commissions.js +209 -0
- package/server/services/email.js +53 -0
- package/server/services/stripe.js +108 -0
- package/server/services/transactions.js +15 -0
- package/server/services/visitor-tracker.js +250 -0
package/bin/wab.js
CHANGED
|
@@ -64,6 +64,60 @@ switch (command) {
|
|
|
64
64
|
fs.writeFileSync(envTarget, defaultEnv);
|
|
65
65
|
console.log(' Created default .env file.');
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
// Generate wab.json — site manifest for agents
|
|
69
|
+
const wabJsonTarget = path.join(process.cwd(), 'wab.json');
|
|
70
|
+
if (fs.existsSync(wabJsonTarget)) {
|
|
71
|
+
console.log(' wab.json already exists. Skipping.');
|
|
72
|
+
} else {
|
|
73
|
+
const projectName = (() => {
|
|
74
|
+
try {
|
|
75
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
76
|
+
if (fs.existsSync(pkgPath)) {
|
|
77
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
78
|
+
return pkg.name || path.basename(process.cwd());
|
|
79
|
+
}
|
|
80
|
+
} catch { /* ignore */ }
|
|
81
|
+
return path.basename(process.cwd());
|
|
82
|
+
})();
|
|
83
|
+
|
|
84
|
+
const wabJson = {
|
|
85
|
+
$schema: 'https://webagentbridge.com/schemas/wab.schema.json',
|
|
86
|
+
version: '1.0',
|
|
87
|
+
site: {
|
|
88
|
+
name: projectName,
|
|
89
|
+
domain: 'example.com',
|
|
90
|
+
description: 'A WAB-enabled site. Replace this with your real description.'
|
|
91
|
+
},
|
|
92
|
+
agentPermissions: {
|
|
93
|
+
readContent: true,
|
|
94
|
+
click: true,
|
|
95
|
+
fillForms: false,
|
|
96
|
+
scroll: true,
|
|
97
|
+
navigate: false,
|
|
98
|
+
apiAccess: false,
|
|
99
|
+
automatedLogin: false,
|
|
100
|
+
extractData: false
|
|
101
|
+
},
|
|
102
|
+
restrictions: {
|
|
103
|
+
allowedSelectors: [],
|
|
104
|
+
blockedSelectors: ['.private', '[data-private]'],
|
|
105
|
+
rateLimit: { maxCallsPerMinute: 60 }
|
|
106
|
+
},
|
|
107
|
+
actions: [
|
|
108
|
+
{
|
|
109
|
+
name: 'example_action',
|
|
110
|
+
description: 'Example action — replace with your real actions',
|
|
111
|
+
selector: '#example',
|
|
112
|
+
type: 'click'
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
logging: { enabled: true, level: 'basic' }
|
|
116
|
+
};
|
|
117
|
+
fs.writeFileSync(wabJsonTarget, JSON.stringify(wabJson, null, 2) + '\n');
|
|
118
|
+
console.log(' Created wab.json site manifest.');
|
|
119
|
+
console.log(' Edit wab.json to describe your site and actions.');
|
|
120
|
+
}
|
|
67
121
|
break;
|
|
68
122
|
}
|
|
69
123
|
|
package/package.json
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "web-agent-bridge",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Agent Transaction Bridge — the trust + transaction layer for agentic commerce. Signed intent contracts, idempotent transactions, Ed25519-verifiable receipts, explicit compensation. Plus the original WAB stack: sovereign browser, ShieldQR, SSL health, DNS discovery, agent mesh, and unified gateway for safe AI–website interaction.",
|
|
5
|
-
"author": "Web Agent Bridge <dev@webagentbridge.com>",
|
|
6
|
-
"main": "server/index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"web-agent-bridge": "./bin/cli.js",
|
|
9
|
-
"wab": "./bin/cli.js",
|
|
10
|
-
"wab-agent": "./bin/cli.js",
|
|
11
|
-
"wab-init": "./bin/wab-init.js"
|
|
12
|
-
},
|
|
13
|
-
"scripts": {
|
|
14
|
-
"start": "node server/index.js",
|
|
15
|
-
"dev": "node server/index.js",
|
|
16
|
-
"test": "jest --forceExit --detectOpenHandles",
|
|
17
|
-
"build:script": "node scripts/build.js",
|
|
18
|
-
"prepublishOnly": "npm test"
|
|
19
|
-
},
|
|
20
|
-
"keywords": [
|
|
21
|
-
"ai",
|
|
22
|
-
"agent",
|
|
23
|
-
"bridge",
|
|
24
|
-
"protocol",
|
|
25
|
-
"platform",
|
|
26
|
-
"automation",
|
|
27
|
-
"web",
|
|
28
|
-
"ai-agent",
|
|
29
|
-
"agent-mesh",
|
|
30
|
-
"sovereign-browser",
|
|
31
|
-
"phone-shield",
|
|
32
|
-
"dns-discovery",
|
|
33
|
-
"api-gateway",
|
|
34
|
-
"browser-automation",
|
|
35
|
-
"webdriver-bidi"
|
|
36
|
-
],
|
|
37
|
-
"repository": {
|
|
38
|
-
"type": "git",
|
|
39
|
-
"url": "git+https://github.com/abokenan444/web-agent-bridge.git"
|
|
40
|
-
},
|
|
41
|
-
"homepage": "https://github.com/abokenan444/web-agent-bridge#readme",
|
|
42
|
-
"bugs": {
|
|
43
|
-
"url": "https://github.com/abokenan444/web-agent-bridge/issues"
|
|
44
|
-
},
|
|
45
|
-
"files": [
|
|
46
|
-
"bin/",
|
|
47
|
-
"server/",
|
|
48
|
-
"public/*.html",
|
|
49
|
-
"public/*.txt",
|
|
50
|
-
"public/*.xml",
|
|
51
|
-
"public/*.json",
|
|
52
|
-
"public/css/",
|
|
53
|
-
"public/js/",
|
|
54
|
-
"public/script/",
|
|
55
|
-
"public/assets/",
|
|
56
|
-
"public/.well-known/",
|
|
57
|
-
"script/",
|
|
58
|
-
"sdk/",
|
|
59
|
-
"templates/",
|
|
60
|
-
"examples/",
|
|
61
|
-
"README.md",
|
|
62
|
-
"README.ar.md",
|
|
63
|
-
"LICENSE"
|
|
64
|
-
],
|
|
65
|
-
"engines": {
|
|
66
|
-
"node": ">=18.0.0"
|
|
67
|
-
},
|
|
68
|
-
"license": "MIT",
|
|
69
|
-
"dependencies": {
|
|
70
|
-
"bcryptjs": "^3.0.3",
|
|
71
|
-
"better-sqlite3": "^11.6.0",
|
|
72
|
-
"cors": "^2.8.5",
|
|
73
|
-
"dotenv": "^16.4.5",
|
|
74
|
-
"express": "^4.21.0",
|
|
75
|
-
"express-rate-limit": "^7.4.1",
|
|
76
|
-
"helmet": "^8.0.0",
|
|
77
|
-
"jsonwebtoken": "^9.0.2",
|
|
78
|
-
"nodemailer": "^8.0.7",
|
|
79
|
-
"stripe": "^20.4.1",
|
|
80
|
-
"ws": "^8.20.0"
|
|
81
|
-
},
|
|
82
|
-
"devDependencies": {
|
|
83
|
-
"all-contributors-cli": "^6.26.1",
|
|
84
|
-
"jest": "^30.3.0",
|
|
85
|
-
"supertest": "^7.2.2"
|
|
86
|
-
},
|
|
87
|
-
"jest": {
|
|
88
|
-
"testPathIgnorePatterns": [
|
|
89
|
-
"/node_modules/",
|
|
90
|
-
"/packages/"
|
|
91
|
-
]
|
|
92
|
-
}
|
|
93
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "web-agent-bridge",
|
|
3
|
+
"version": "3.10.0",
|
|
4
|
+
"description": "Agent Transaction Bridge — the trust + transaction layer for agentic commerce. Signed intent contracts, idempotent transactions, Ed25519-verifiable receipts, explicit compensation. Plus the original WAB stack: sovereign browser, ShieldQR, SSL health, DNS discovery, agent mesh, and unified gateway for safe AI–website interaction.",
|
|
5
|
+
"author": "Web Agent Bridge <dev@webagentbridge.com>",
|
|
6
|
+
"main": "server/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"web-agent-bridge": "./bin/cli.js",
|
|
9
|
+
"wab": "./bin/cli.js",
|
|
10
|
+
"wab-agent": "./bin/cli.js",
|
|
11
|
+
"wab-init": "./bin/wab-init.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node server/index.js",
|
|
15
|
+
"dev": "node server/index.js",
|
|
16
|
+
"test": "jest --forceExit --detectOpenHandles",
|
|
17
|
+
"build:script": "node scripts/build.js",
|
|
18
|
+
"prepublishOnly": "npm test"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ai",
|
|
22
|
+
"agent",
|
|
23
|
+
"bridge",
|
|
24
|
+
"protocol",
|
|
25
|
+
"platform",
|
|
26
|
+
"automation",
|
|
27
|
+
"web",
|
|
28
|
+
"ai-agent",
|
|
29
|
+
"agent-mesh",
|
|
30
|
+
"sovereign-browser",
|
|
31
|
+
"phone-shield",
|
|
32
|
+
"dns-discovery",
|
|
33
|
+
"api-gateway",
|
|
34
|
+
"browser-automation",
|
|
35
|
+
"webdriver-bidi"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/abokenan444/web-agent-bridge.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/abokenan444/web-agent-bridge#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/abokenan444/web-agent-bridge/issues"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"bin/",
|
|
47
|
+
"server/",
|
|
48
|
+
"public/*.html",
|
|
49
|
+
"public/*.txt",
|
|
50
|
+
"public/*.xml",
|
|
51
|
+
"public/*.json",
|
|
52
|
+
"public/css/",
|
|
53
|
+
"public/js/",
|
|
54
|
+
"public/script/",
|
|
55
|
+
"public/assets/",
|
|
56
|
+
"public/.well-known/",
|
|
57
|
+
"script/",
|
|
58
|
+
"sdk/",
|
|
59
|
+
"templates/",
|
|
60
|
+
"examples/",
|
|
61
|
+
"README.md",
|
|
62
|
+
"README.ar.md",
|
|
63
|
+
"LICENSE"
|
|
64
|
+
],
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=18.0.0"
|
|
67
|
+
},
|
|
68
|
+
"license": "MIT",
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"bcryptjs": "^3.0.3",
|
|
71
|
+
"better-sqlite3": "^11.6.0",
|
|
72
|
+
"cors": "^2.8.5",
|
|
73
|
+
"dotenv": "^16.4.5",
|
|
74
|
+
"express": "^4.21.0",
|
|
75
|
+
"express-rate-limit": "^7.4.1",
|
|
76
|
+
"helmet": "^8.0.0",
|
|
77
|
+
"jsonwebtoken": "^9.0.2",
|
|
78
|
+
"nodemailer": "^8.0.7",
|
|
79
|
+
"stripe": "^20.4.1",
|
|
80
|
+
"ws": "^8.20.0"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"all-contributors-cli": "^6.26.1",
|
|
84
|
+
"jest": "^30.3.0",
|
|
85
|
+
"supertest": "^7.2.2"
|
|
86
|
+
},
|
|
87
|
+
"jest": {
|
|
88
|
+
"testPathIgnorePatterns": [
|
|
89
|
+
"/node_modules/",
|
|
90
|
+
"/packages/"
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Forgot password — Web Agent Bridge</title>
|
|
7
|
+
<style>body{background:#0a0e1a;color:#f0f4ff;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;margin:0;min-height:100vh}</style>
|
|
8
|
+
<link rel="stylesheet" href="/css/styles.css?v=3.0.1">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="auth-page">
|
|
12
|
+
<div class="auth-card fade-in">
|
|
13
|
+
<div style="text-align:center; margin-bottom:32px;">
|
|
14
|
+
<a href="/" class="navbar-brand" style="justify-content:center;">
|
|
15
|
+
<div class="brand-icon">⚡</div>
|
|
16
|
+
<span>WAB</span>
|
|
17
|
+
</a>
|
|
18
|
+
</div>
|
|
19
|
+
<h1 style="text-align:center;">Forgot password</h1>
|
|
20
|
+
<p class="subtitle" style="text-align:center;">Enter your email and we'll send you a reset link.</p>
|
|
21
|
+
|
|
22
|
+
<div class="alert alert-error" id="errorAlert"></div>
|
|
23
|
+
<div class="alert alert-success" id="successAlert" style="display:none;">If that email is registered, a reset link has been sent. Check your inbox.</div>
|
|
24
|
+
|
|
25
|
+
<form id="forgotForm">
|
|
26
|
+
<div class="form-group">
|
|
27
|
+
<label for="email">Email</label>
|
|
28
|
+
<input type="email" id="email" class="form-input" placeholder="you@example.com" required>
|
|
29
|
+
</div>
|
|
30
|
+
<button type="submit" class="btn btn-primary btn-lg">Send reset link</button>
|
|
31
|
+
</form>
|
|
32
|
+
|
|
33
|
+
<div class="auth-footer" style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;">
|
|
34
|
+
<a href="/login">Back to sign in</a>
|
|
35
|
+
<a href="/register">Create account</a>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<script>
|
|
41
|
+
document.getElementById('forgotForm').addEventListener('submit', async (e) => {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
const errorEl = document.getElementById('errorAlert');
|
|
44
|
+
const okEl = document.getElementById('successAlert');
|
|
45
|
+
errorEl.style.display = 'none';
|
|
46
|
+
okEl.style.display = 'none';
|
|
47
|
+
const email = document.getElementById('email').value;
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch('/api/auth/forgot-password', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({ email })
|
|
53
|
+
});
|
|
54
|
+
const data = await res.json();
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
errorEl.textContent = data.error || 'Request failed';
|
|
57
|
+
errorEl.style.display = 'block';
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
okEl.style.display = 'block';
|
|
61
|
+
} catch (err) {
|
|
62
|
+
errorEl.textContent = 'Connection error. Please try again.';
|
|
63
|
+
errorEl.style.display = 'block';
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
</script>
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
package/public/login.html
CHANGED
|
@@ -38,8 +38,9 @@
|
|
|
38
38
|
<button type="submit" class="btn btn-primary btn-lg">Sign In</button>
|
|
39
39
|
</form>
|
|
40
40
|
|
|
41
|
-
<div class="auth-footer">
|
|
42
|
-
|
|
41
|
+
<div class="auth-footer" style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;">
|
|
42
|
+
<a href="/forgot-password.html">Forgot password?</a>
|
|
43
|
+
<span>Don't have an account? <a href="/register">Create one</a></span>
|
|
43
44
|
</div>
|
|
44
45
|
</div>
|
|
45
46
|
</div>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Reset password — Web Agent Bridge</title>
|
|
7
|
+
<style>body{background:#0a0e1a;color:#f0f4ff;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;margin:0;min-height:100vh}</style>
|
|
8
|
+
<link rel="stylesheet" href="/css/styles.css?v=3.0.1">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="auth-page">
|
|
12
|
+
<div class="auth-card fade-in">
|
|
13
|
+
<div style="text-align:center; margin-bottom:32px;">
|
|
14
|
+
<a href="/" class="navbar-brand" style="justify-content:center;">
|
|
15
|
+
<div class="brand-icon">⚡</div>
|
|
16
|
+
<span>WAB</span>
|
|
17
|
+
</a>
|
|
18
|
+
</div>
|
|
19
|
+
<h1 style="text-align:center;">Reset password</h1>
|
|
20
|
+
<p class="subtitle" style="text-align:center;">Choose a new password for your account.</p>
|
|
21
|
+
|
|
22
|
+
<div class="alert alert-error" id="errorAlert"></div>
|
|
23
|
+
<div class="alert alert-success" id="successAlert" style="display:none;">Password reset. Redirecting to sign-in…</div>
|
|
24
|
+
|
|
25
|
+
<form id="resetForm">
|
|
26
|
+
<div class="form-group">
|
|
27
|
+
<label for="password">New password</label>
|
|
28
|
+
<input type="password" id="password" class="form-input" placeholder="At least 8 characters" minlength="8" maxlength="128" required>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="form-group">
|
|
31
|
+
<label for="password2">Confirm new password</label>
|
|
32
|
+
<input type="password" id="password2" class="form-input" minlength="8" maxlength="128" required>
|
|
33
|
+
</div>
|
|
34
|
+
<button type="submit" class="btn btn-primary btn-lg">Reset password</button>
|
|
35
|
+
</form>
|
|
36
|
+
|
|
37
|
+
<div class="auth-footer">
|
|
38
|
+
<a href="/login">Back to sign in</a>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<script>
|
|
44
|
+
const params = new URLSearchParams(window.location.search);
|
|
45
|
+
const token = params.get('token');
|
|
46
|
+
const errorEl = document.getElementById('errorAlert');
|
|
47
|
+
if (!token) {
|
|
48
|
+
errorEl.textContent = 'Missing reset token in URL.';
|
|
49
|
+
errorEl.style.display = 'block';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
document.getElementById('resetForm').addEventListener('submit', async (e) => {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
errorEl.style.display = 'none';
|
|
55
|
+
const okEl = document.getElementById('successAlert');
|
|
56
|
+
const pw = document.getElementById('password').value;
|
|
57
|
+
const pw2 = document.getElementById('password2').value;
|
|
58
|
+
if (pw !== pw2) {
|
|
59
|
+
errorEl.textContent = 'Passwords do not match.';
|
|
60
|
+
errorEl.style.display = 'block';
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch('/api/auth/reset-password', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify({ token, password: pw })
|
|
68
|
+
});
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
errorEl.textContent = data.error || 'Reset failed';
|
|
72
|
+
errorEl.style.display = 'block';
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
okEl.style.display = 'block';
|
|
76
|
+
setTimeout(() => { window.location.href = '/login'; }, 1500);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
errorEl.textContent = 'Connection error. Please try again.';
|
|
79
|
+
errorEl.style.display = 'block';
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
</script>
|
|
83
|
+
</body>
|
|
84
|
+
</html>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Verify email — Web Agent Bridge</title>
|
|
7
|
+
<style>body{background:#0a0e1a;color:#f0f4ff;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;margin:0;min-height:100vh}</style>
|
|
8
|
+
<link rel="stylesheet" href="/css/styles.css?v=3.0.1">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="auth-page">
|
|
12
|
+
<div class="auth-card fade-in">
|
|
13
|
+
<div style="text-align:center; margin-bottom:32px;">
|
|
14
|
+
<a href="/" class="navbar-brand" style="justify-content:center;">
|
|
15
|
+
<div class="brand-icon">⚡</div>
|
|
16
|
+
<span>WAB</span>
|
|
17
|
+
</a>
|
|
18
|
+
</div>
|
|
19
|
+
<h1 style="text-align:center;" id="title">Verifying…</h1>
|
|
20
|
+
<p class="subtitle" style="text-align:center;" id="message">Please wait while we confirm your email.</p>
|
|
21
|
+
|
|
22
|
+
<div class="alert alert-error" id="errorAlert"></div>
|
|
23
|
+
<div class="alert alert-success" id="successAlert" style="display:none;">Email verified. You can now use all features.</div>
|
|
24
|
+
|
|
25
|
+
<div class="auth-footer" style="text-align:center;">
|
|
26
|
+
<a href="/dashboard" id="dashLink" style="display:none;">Go to dashboard</a>
|
|
27
|
+
<a href="/login" id="loginLink">Back to sign in</a>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<script>
|
|
33
|
+
(async function () {
|
|
34
|
+
const params = new URLSearchParams(window.location.search);
|
|
35
|
+
const token = params.get('token');
|
|
36
|
+
const titleEl = document.getElementById('title');
|
|
37
|
+
const msgEl = document.getElementById('message');
|
|
38
|
+
const errorEl = document.getElementById('errorAlert');
|
|
39
|
+
const okEl = document.getElementById('successAlert');
|
|
40
|
+
const dashLink = document.getElementById('dashLink');
|
|
41
|
+
|
|
42
|
+
if (!token) {
|
|
43
|
+
titleEl.textContent = 'Missing token';
|
|
44
|
+
msgEl.textContent = 'This link is invalid. Sign in and request a new verification email.';
|
|
45
|
+
errorEl.textContent = 'No verification token in URL.';
|
|
46
|
+
errorEl.style.display = 'block';
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch('/api/auth/verify-email', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({ token })
|
|
54
|
+
});
|
|
55
|
+
const data = await res.json();
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
titleEl.textContent = 'Verification failed';
|
|
58
|
+
msgEl.textContent = data.error || 'Token invalid or expired.';
|
|
59
|
+
errorEl.textContent = data.error || 'Verification failed';
|
|
60
|
+
errorEl.style.display = 'block';
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
titleEl.textContent = 'Verified ✓';
|
|
64
|
+
msgEl.textContent = 'Your email has been confirmed.';
|
|
65
|
+
okEl.style.display = 'block';
|
|
66
|
+
dashLink.style.display = 'inline';
|
|
67
|
+
} catch (err) {
|
|
68
|
+
titleEl.textContent = 'Network error';
|
|
69
|
+
msgEl.textContent = 'Please try again later.';
|
|
70
|
+
errorEl.textContent = 'Connection error.';
|
|
71
|
+
errorEl.style.display = 'block';
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
</script>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
package/server/index.js
CHANGED
|
@@ -232,6 +232,15 @@ const licenseLimiter = rateLimit({
|
|
|
232
232
|
}
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
+
// Visitor analytics — record every public page hit (HTML routes only) before
|
|
236
|
+
// they're served by express.static. Skips assets, /api, /admin and other noise.
|
|
237
|
+
try {
|
|
238
|
+
const visitorTracker = require('./services/visitor-tracker');
|
|
239
|
+
app.use(visitorTracker.middleware());
|
|
240
|
+
} catch (e) {
|
|
241
|
+
console.warn('[wab] visitor-tracker disabled:', e.message);
|
|
242
|
+
}
|
|
243
|
+
|
|
235
244
|
// Whitepaper guard — must run BEFORE express.static so we can apply strict headers
|
|
236
245
|
// and intercept both /whitepaper and /whitepaper.html with the same protections.
|
|
237
246
|
const whitepaperHandler = (req, res) => {
|
|
@@ -47,4 +47,45 @@ function optionalAuth(req, res, next) {
|
|
|
47
47
|
next();
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
// Tier hierarchy for requireTier()
|
|
51
|
+
const TIER_ORDER = { free: 0, starter: 1, pro: 2, business: 3, enterprise: 4 };
|
|
52
|
+
|
|
53
|
+
function tierRank(t) {
|
|
54
|
+
return TIER_ORDER[String(t || 'free').toLowerCase()] ?? 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// requireTier('pro') — must be used AFTER a middleware that puts a site on req.site
|
|
58
|
+
// (e.g. requireSiteOwnership). If no req.site exists, falls back to the user's
|
|
59
|
+
// highest tier across their owned sites.
|
|
60
|
+
function requireTier(minTier) {
|
|
61
|
+
const required = tierRank(minTier);
|
|
62
|
+
return (req, res, next) => {
|
|
63
|
+
let actualTier = 'free';
|
|
64
|
+
if (req.site && req.site.tier) {
|
|
65
|
+
actualTier = req.site.tier;
|
|
66
|
+
} else if (req.user && req.user.id) {
|
|
67
|
+
try {
|
|
68
|
+
const { findSitesByUser } = require('../models/db');
|
|
69
|
+
const sites = findSitesByUser.all(req.user.id) || [];
|
|
70
|
+
for (const s of sites) {
|
|
71
|
+
if (tierRank(s.tier) > tierRank(actualTier)) actualTier = s.tier;
|
|
72
|
+
}
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// DB layer may not be ready in tests — be permissive there.
|
|
75
|
+
if (process.env.NODE_ENV === 'test') return next();
|
|
76
|
+
return res.status(500).json({ error: 'Tier lookup failed' });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (tierRank(actualTier) < required) {
|
|
80
|
+
return res.status(402).json({
|
|
81
|
+
error: 'Plan upgrade required',
|
|
82
|
+
required_tier: minTier,
|
|
83
|
+
current_tier: actualTier,
|
|
84
|
+
upgrade_url: '/premium.html'
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
next();
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { generateToken, authenticateToken, optionalAuth, requireTier };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
-- Migration 021 — Visitor analytics (page_visits)
|
|
3
|
+
--
|
|
4
|
+
-- Captures every public page request (registered or anonymous) so the admin
|
|
5
|
+
-- panel can show real traffic data. IPs are hashed for privacy.
|
|
6
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS page_visits (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
path TEXT NOT NULL,
|
|
11
|
+
query_string TEXT,
|
|
12
|
+
referrer TEXT,
|
|
13
|
+
host TEXT,
|
|
14
|
+
user_agent TEXT,
|
|
15
|
+
ip_hash TEXT, -- sha256(ip + salt), first 32 chars
|
|
16
|
+
country TEXT, -- best-effort from Cloudflare/CF-IPCountry; nullable
|
|
17
|
+
device TEXT, -- desktop | mobile | tablet | bot
|
|
18
|
+
is_bot INTEGER NOT NULL DEFAULT 0,
|
|
19
|
+
session_id TEXT, -- random per-visitor cookie or derived from ip_hash+UA
|
|
20
|
+
user_id TEXT, -- nullable; populated if request carried an auth cookie/token
|
|
21
|
+
status_code INTEGER, -- HTTP status that was returned
|
|
22
|
+
duration_ms INTEGER, -- server-side handler time
|
|
23
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_pv_created ON page_visits(created_at);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_pv_path ON page_visits(path);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_pv_ip ON page_visits(ip_hash);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_pv_session ON page_visits(session_id);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_pv_is_bot ON page_visits(is_bot);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_pv_user ON page_visits(user_id);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- Email verification + password reset
|
|
2
|
+
ALTER TABLE users ADD COLUMN email_verified INTEGER DEFAULT 0;
|
|
3
|
+
ALTER TABLE users ADD COLUMN email_verified_at TEXT;
|
|
4
|
+
|
|
5
|
+
CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
|
6
|
+
token_hash TEXT PRIMARY KEY,
|
|
7
|
+
user_id TEXT NOT NULL,
|
|
8
|
+
expires_at TEXT NOT NULL,
|
|
9
|
+
used_at TEXT,
|
|
10
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
11
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_prt_user ON password_reset_tokens(user_id);
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_prt_expires ON password_reset_tokens(expires_at);
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS email_verification_tokens (
|
|
18
|
+
token_hash TEXT PRIMARY KEY,
|
|
19
|
+
user_id TEXT NOT NULL,
|
|
20
|
+
expires_at TEXT NOT NULL,
|
|
21
|
+
used_at TEXT,
|
|
22
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
23
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_evt_user ON email_verification_tokens(user_id);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_evt_expires ON email_verification_tokens(expires_at);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
-- Migration 023 — ATP Merchant Commission (v3.10.0)
|
|
3
|
+
--
|
|
4
|
+
-- WAB takes a small platform commission (default 0.1% / 10 bps) on every
|
|
5
|
+
-- successful merchant transaction settled through ATP on a paid plan.
|
|
6
|
+
-- Free-tier sites and platform self-payments are exempt.
|
|
7
|
+
--
|
|
8
|
+
-- One row per settled atp_transactions.id. State machine:
|
|
9
|
+
-- pending → newly recorded
|
|
10
|
+
-- invoiced → rolled into a Stripe invoice / payout cycle
|
|
11
|
+
-- collected → billed and paid by merchant
|
|
12
|
+
-- refunded → underlying tx was compensated
|
|
13
|
+
-- waived → manually waived by an admin
|
|
14
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
CREATE TABLE IF NOT EXISTS atp_commissions (
|
|
17
|
+
id TEXT PRIMARY KEY, -- atp_com_<ulid>
|
|
18
|
+
transaction_id TEXT NOT NULL UNIQUE, -- one commission per tx
|
|
19
|
+
intent_id TEXT NOT NULL,
|
|
20
|
+
merchant_user_id TEXT NOT NULL, -- the site owner
|
|
21
|
+
merchant_site_id TEXT,
|
|
22
|
+
merchant_tier TEXT NOT NULL, -- snapshot at charge time
|
|
23
|
+
gross_amount_cents INTEGER NOT NULL,
|
|
24
|
+
currency TEXT NOT NULL DEFAULT 'EUR',
|
|
25
|
+
commission_bps INTEGER NOT NULL DEFAULT 10, -- 10 bps = 0.10%
|
|
26
|
+
commission_cents INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
28
|
+
CHECK (status IN ('pending','invoiced','collected','refunded','waived')),
|
|
29
|
+
external_ref TEXT, -- payment gateway ref (PI id, etc.)
|
|
30
|
+
notes TEXT,
|
|
31
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
32
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
33
|
+
FOREIGN KEY (transaction_id) REFERENCES atp_transactions(id) ON DELETE CASCADE
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_atp_commissions_merchant
|
|
37
|
+
ON atp_commissions(merchant_user_id, created_at DESC);
|
|
38
|
+
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_atp_commissions_site
|
|
40
|
+
ON atp_commissions(merchant_site_id, created_at DESC);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_atp_commissions_status
|
|
43
|
+
ON atp_commissions(status, created_at DESC);
|