web-agent-bridge 3.17.0 → 3.20.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/README.ar.md +27 -8
- package/README.md +95 -0
- package/bin/wab-init.js +38 -0
- package/package.json +1 -1
- package/public/atp-semantics.html +216 -0
- package/public/benchmarks.html +151 -0
- package/public/docs.html +113 -43
- package/public/index.html +142 -8
- package/public/key-rotation.html +184 -0
- package/public/llms.txt +54 -0
- package/public/notary.html +94 -0
- package/public/observatory.html +103 -0
- package/public/research.html +57 -0
- package/public/researchers.html +113 -0
- package/public/responsible-disclosure.html +294 -0
- package/public/robots.txt +17 -0
- package/public/security.html +157 -0
- package/public/threat-model.html +153 -0
- package/public/viral-coefficient.html +533 -0
- package/public/wab-dataset.html +501 -0
- package/public/wab-email.html +78 -0
- package/public/wab-lens.html +61 -0
- package/public/wab-p2p.html +96 -0
- package/public/wab-registry.html +481 -0
- package/public/wab-today.html +448 -0
- package/public/wab-uri.html +88 -0
- package/script/ai-agent-bridge.js +24 -4
- package/server/index.js +1193 -827
- package/server/models/db.js +2 -1
- package/server/routes/admin-shieldlink.js +1 -1
- package/server/routes/admin-shieldqr.js +1 -1
- package/server/routes/admin-trust-monitor.js +1 -1
- package/server/routes/api-keys.js +2 -1
- package/server/routes/customer-shieldlink.js +1 -1
- package/server/routes/enterprise-mesh.js +2 -1
- package/server/routes/genius-bridge.js +256 -0
- package/server/routes/genius-gateway.js +137 -0
- package/server/routes/governance-saas.js +2 -1
- package/server/routes/notary.js +309 -0
- package/server/routes/observatory.js +109 -0
- package/server/routes/partners.js +2 -1
- package/server/routes/registry.js +352 -0
- package/server/routes/research.js +83 -0
- package/server/routes/ring4.js +2 -1
- package/server/routes/runtime.js +98 -25
- package/server/routes/security-researchers.js +161 -0
- package/server/routes/shieldqr.js +1 -1
- package/server/routes/traces.js +247 -0
- package/server/services/agent-tasks.js +9 -7
- package/server/services/email.js +50 -2
- package/server/services/marketplace.js +27 -8
- package/server/services/plans.js +1 -1
- package/server/services/shieldlink.js +1 -1
- package/server/services/ssl-ct-monitor.js +1 -1
- package/server/services/ssl-monitor.js +1 -1
- package/server/services/stripe.js +29 -4
- package/server/utils/migrate.js +1 -1
- package/server/utils/safe-compare.js +26 -0
package/server/models/db.js
CHANGED
|
@@ -12,7 +12,8 @@ const DATA_DIR = isTest
|
|
|
12
12
|
: (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
|
|
13
13
|
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// In test mode, isolate per Jest worker so parallel suites can't trample each other's data.
|
|
16
|
+
const dbFile = isTest ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
|
|
16
17
|
const db = new Database(path.join(DATA_DIR, dbFile));
|
|
17
18
|
|
|
18
19
|
db.pragma('journal_mode = WAL');
|
|
@@ -25,7 +25,7 @@ router.use(authenticateAdmin);
|
|
|
25
25
|
const DATA_DIR = process.env.NODE_ENV === 'test'
|
|
26
26
|
? path.join(__dirname, '..', '..', 'data-test')
|
|
27
27
|
: (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
|
|
28
|
-
const DB_FILE = process.env.NODE_ENV === 'test' ?
|
|
28
|
+
const DB_FILE = process.env.NODE_ENV === 'test' ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
|
|
29
29
|
let _db = null;
|
|
30
30
|
function db() { if (!_db) _db = new Database(path.join(DATA_DIR, DB_FILE)); return _db; }
|
|
31
31
|
|
|
@@ -19,7 +19,7 @@ router.use(authenticateAdmin);
|
|
|
19
19
|
const DATA_DIR = process.env.NODE_ENV === 'test'
|
|
20
20
|
? path.join(__dirname, '..', '..', 'data-test')
|
|
21
21
|
: (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
|
|
22
|
-
const DB_FILE = process.env.NODE_ENV === 'test' ?
|
|
22
|
+
const DB_FILE = process.env.NODE_ENV === 'test' ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
|
|
23
23
|
|
|
24
24
|
let _db = null;
|
|
25
25
|
function db() {
|
|
@@ -22,7 +22,7 @@ router.use(authenticateAdmin);
|
|
|
22
22
|
const DATA_DIR = process.env.NODE_ENV === 'test'
|
|
23
23
|
? path.join(__dirname, '..', '..', 'data-test')
|
|
24
24
|
: (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
|
|
25
|
-
const DB_FILE = process.env.NODE_ENV === 'test' ?
|
|
25
|
+
const DB_FILE = process.env.NODE_ENV === 'test' ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
|
|
26
26
|
|
|
27
27
|
let _db = null;
|
|
28
28
|
function db() { if (!_db) { _db = new Database(path.join(DATA_DIR, DB_FILE)); } return _db; }
|
|
@@ -38,9 +38,10 @@ function issueOk(ip) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function adminGate(req, res, next) {
|
|
41
|
+
const { safeEqual } = require('../utils/safe-compare');
|
|
41
42
|
const expected = process.env.WAB_API_KEYS_ADMIN_TOKEN || process.env.WAB_RING4_ADMIN_TOKEN;
|
|
42
43
|
if (!expected) return res.status(503).json({ error: 'admin_disabled' });
|
|
43
|
-
if ((req.headers['x-admin-token'] || ''
|
|
44
|
+
if (!safeEqual(req.headers['x-admin-token'] || '', expected)) return res.status(401).json({ error: 'unauthorized' });
|
|
44
45
|
next();
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -26,7 +26,7 @@ const router = express.Router();
|
|
|
26
26
|
const DATA_DIR = process.env.NODE_ENV === 'test'
|
|
27
27
|
? path.join(__dirname, '..', '..', 'data-test')
|
|
28
28
|
: (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
|
|
29
|
-
const DB_FILE = process.env.NODE_ENV === 'test' ?
|
|
29
|
+
const DB_FILE = process.env.NODE_ENV === 'test' ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
|
|
30
30
|
let _db = null;
|
|
31
31
|
function db() { if (!_db) _db = new Database(path.join(DATA_DIR, DB_FILE)); return _db; }
|
|
32
32
|
|
|
@@ -26,9 +26,10 @@ const { db } = require('../models/db');
|
|
|
26
26
|
const router = express.Router();
|
|
27
27
|
|
|
28
28
|
function adminGate(req, res, next) {
|
|
29
|
+
const { safeEqual } = require('../utils/safe-compare');
|
|
29
30
|
const expected = process.env.WAB_LICENSE_ADMIN_TOKEN || process.env.WAB_RING4_ADMIN_TOKEN;
|
|
30
31
|
if (!expected) return res.status(503).json({ error: 'admin_disabled' });
|
|
31
|
-
if ((req.headers['x-admin-token'] || ''
|
|
32
|
+
if (!safeEqual(req.headers['x-admin-token'] || '', expected)) return res.status(401).json({ error: 'unauthorized' });
|
|
32
33
|
next();
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Genius Platform Payment Bridge (v1.0.0)
|
|
3
|
+
*
|
|
4
|
+
* Backend-only proxy — genius-platform sends billing requests here using a
|
|
5
|
+
* shared internal secret. WAB owns the Stripe keys; genius never needs them.
|
|
6
|
+
*
|
|
7
|
+
* Endpoints:
|
|
8
|
+
* POST /api/genius/checkout — create Stripe checkout session
|
|
9
|
+
* POST /api/genius/portal — create Stripe customer portal session
|
|
10
|
+
* POST /api/genius/stripe-webhook — Stripe webhook for genius-tagged events
|
|
11
|
+
*
|
|
12
|
+
* WAB env vars required:
|
|
13
|
+
* GENIUS_BRIDGE_SECRET — shared secret (also in genius as WAB_GENIUS_SECRET)
|
|
14
|
+
* GENIUS_APP_URL — https://thecodegenius.com
|
|
15
|
+
* GENIUS_CALLBACK_URL — http://localhost:3004 (internal only)
|
|
16
|
+
* STRIPE_GENIUS_WEBHOOK_SECRET — separate Stripe webhook secret for this endpoint
|
|
17
|
+
* STRIPE_GENIUS_PRICE_PRO — Stripe price ID for genius PRO (falls back to STRIPE_PRICE_PRO)
|
|
18
|
+
* STRIPE_GENIUS_PRICE_BUSINESS — Stripe price ID for genius BUSINESS (falls back to STRIPE_PRICE_BUSINESS)
|
|
19
|
+
* STRIPE_GENIUS_PRICE_STARTER — (optional) Stripe price ID for genius STARTER
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const express = require('express');
|
|
25
|
+
const router = express.Router();
|
|
26
|
+
|
|
27
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const BRIDGE_SECRET = () => process.env.GENIUS_BRIDGE_SECRET;
|
|
30
|
+
const GENIUS_APP_URL = () => process.env.GENIUS_APP_URL || 'https://thecodegenius.com';
|
|
31
|
+
const GENIUS_CALLBACK_URL = () => process.env.GENIUS_CALLBACK_URL || 'http://localhost:3004';
|
|
32
|
+
|
|
33
|
+
function getStripe() {
|
|
34
|
+
const key = process.env.STRIPE_SECRET_KEY;
|
|
35
|
+
if (!key || key.startsWith('sk_disabled')) return null;
|
|
36
|
+
// Cache the instance to avoid re-init on every request
|
|
37
|
+
if (!getStripe._instance) {
|
|
38
|
+
getStripe._instance = require('stripe')(key);
|
|
39
|
+
}
|
|
40
|
+
return getStripe._instance;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Invalidate cached Stripe instance when key changes (hot reload)
|
|
44
|
+
let _lastKey = null;
|
|
45
|
+
function stripe() {
|
|
46
|
+
const key = process.env.STRIPE_SECRET_KEY;
|
|
47
|
+
if (key !== _lastKey) { getStripe._instance = null; _lastKey = key; }
|
|
48
|
+
return getStripe();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolvePriceId(tier) {
|
|
52
|
+
const t = (tier || '').toUpperCase();
|
|
53
|
+
const lookup = {
|
|
54
|
+
STARTER: process.env.STRIPE_GENIUS_PRICE_STARTER || process.env.STRIPE_PRICE_STARTER,
|
|
55
|
+
PRO: process.env.STRIPE_GENIUS_PRICE_PRO || process.env.STRIPE_PRICE_PRO,
|
|
56
|
+
BUSINESS: process.env.STRIPE_GENIUS_PRICE_BUSINESS || process.env.STRIPE_PRICE_BUSINESS,
|
|
57
|
+
};
|
|
58
|
+
return lookup[t] || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Middleware — validate shared internal secret
|
|
62
|
+
function bridgeAuth(req, res, next) {
|
|
63
|
+
const secret = BRIDGE_SECRET();
|
|
64
|
+
if (!secret) {
|
|
65
|
+
// Bridge secret not configured → treat as disabled
|
|
66
|
+
return res.status(503).json({ error: 'Genius payment bridge not configured on this server' });
|
|
67
|
+
}
|
|
68
|
+
if (req.headers['x-internal-secret'] !== secret) {
|
|
69
|
+
return res.status(401).json({ error: 'unauthorized' });
|
|
70
|
+
}
|
|
71
|
+
next();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── POST /api/genius/checkout ─────────────────────────────────────────────────
|
|
75
|
+
router.post('/checkout', bridgeAuth, express.json({ limit: '8kb' }), async (req, res) => {
|
|
76
|
+
const s = stripe();
|
|
77
|
+
if (!s) return res.status(503).json({ error: 'Stripe not configured on this server' });
|
|
78
|
+
|
|
79
|
+
const { orgId, userId, email, name, tier, existingCustomerId } = req.body;
|
|
80
|
+
|
|
81
|
+
if (!orgId || !tier || !email) {
|
|
82
|
+
return res.status(400).json({ error: 'orgId, tier, and email are required' });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const priceId = resolvePriceId(tier);
|
|
86
|
+
if (!priceId) {
|
|
87
|
+
return res.status(400).json({ error: `No Stripe price ID configured for tier: ${tier}` });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Re-use existing customer or create a new one
|
|
92
|
+
let customerId = existingCustomerId || null;
|
|
93
|
+
if (!customerId) {
|
|
94
|
+
const customer = await s.customers.create({
|
|
95
|
+
email,
|
|
96
|
+
name: name || email,
|
|
97
|
+
metadata: {
|
|
98
|
+
genius_org_id: orgId,
|
|
99
|
+
genius_user_id: userId || '',
|
|
100
|
+
source: 'genius-platform',
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
customerId = customer.id;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const baseUrl = GENIUS_APP_URL();
|
|
107
|
+
const session = await s.checkout.sessions.create({
|
|
108
|
+
customer: customerId,
|
|
109
|
+
mode: 'subscription',
|
|
110
|
+
payment_method_types: ['card'],
|
|
111
|
+
line_items: [{ price: priceId, quantity: 1 }],
|
|
112
|
+
metadata: {
|
|
113
|
+
genius_org_id: orgId,
|
|
114
|
+
genius_user_id: userId || '',
|
|
115
|
+
tier: tier.toUpperCase(),
|
|
116
|
+
source: 'genius-platform',
|
|
117
|
+
},
|
|
118
|
+
subscription_data: {
|
|
119
|
+
metadata: {
|
|
120
|
+
genius_org_id: orgId,
|
|
121
|
+
genius_user_id: userId || '',
|
|
122
|
+
tier: tier.toUpperCase(),
|
|
123
|
+
source: 'genius-platform',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
success_url: `${baseUrl}/dashboard/user/billing?success=1&plan=${tier.toUpperCase()}`,
|
|
127
|
+
cancel_url: `${baseUrl}/pricing?canceled=1`,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
res.json({ url: session.url, customerId });
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error('[genius-bridge] checkout error:', err.message);
|
|
133
|
+
res.status(500).json({ error: err.message });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ── POST /api/genius/portal ───────────────────────────────────────────────────
|
|
138
|
+
router.post('/portal', bridgeAuth, express.json({ limit: '8kb' }), async (req, res) => {
|
|
139
|
+
const s = stripe();
|
|
140
|
+
if (!s) return res.status(503).json({ error: 'Stripe not configured on this server' });
|
|
141
|
+
|
|
142
|
+
const { customerId } = req.body;
|
|
143
|
+
if (!customerId) {
|
|
144
|
+
return res.status(400).json({ error: 'customerId is required' });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const baseUrl = GENIUS_APP_URL();
|
|
149
|
+
const session = await s.billingPortal.sessions.create({
|
|
150
|
+
customer: customerId,
|
|
151
|
+
return_url: `${baseUrl}/dashboard/user/billing`,
|
|
152
|
+
});
|
|
153
|
+
res.json({ url: session.url });
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error('[genius-bridge] portal error:', err.message);
|
|
156
|
+
res.status(500).json({ error: err.message });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ── POST /api/genius/stripe-webhook ──────────────────────────────────────────
|
|
161
|
+
// Register a SEPARATE Stripe webhook pointing here:
|
|
162
|
+
// https://webagentbridge.com/api/genius/stripe-webhook
|
|
163
|
+
// Use STRIPE_GENIUS_WEBHOOK_SECRET (different from WAB's own webhook secret).
|
|
164
|
+
router.post('/stripe-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
165
|
+
const s = stripe();
|
|
166
|
+
if (!s) return res.status(503).json({ error: 'Stripe not configured' });
|
|
167
|
+
|
|
168
|
+
const sig = req.headers['stripe-signature'];
|
|
169
|
+
const webhookSecret = process.env.STRIPE_GENIUS_WEBHOOK_SECRET;
|
|
170
|
+
|
|
171
|
+
if (!sig || !webhookSecret) {
|
|
172
|
+
return res.status(400).json({ error: 'Missing Stripe signature or STRIPE_GENIUS_WEBHOOK_SECRET' });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let event;
|
|
176
|
+
try {
|
|
177
|
+
event = s.webhooks.constructEvent(req.body, sig, webhookSecret);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error('[genius-bridge] webhook signature failed:', err.message);
|
|
180
|
+
return res.status(400).json({ error: `Webhook signature verification failed: ${err.message}` });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Only process events originating from genius-platform
|
|
184
|
+
const meta = event.data.object?.metadata ?? {};
|
|
185
|
+
if (meta.source !== 'genius-platform') {
|
|
186
|
+
return res.json({ received: true, skipped: 'not a genius event' });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let payload = null;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
switch (event.type) {
|
|
193
|
+
case 'checkout.session.completed': {
|
|
194
|
+
const sess = event.data.object;
|
|
195
|
+
payload = {
|
|
196
|
+
event: 'checkout.completed',
|
|
197
|
+
orgId: sess.metadata?.genius_org_id,
|
|
198
|
+
tier: sess.metadata?.tier,
|
|
199
|
+
customerId: sess.customer,
|
|
200
|
+
subscriptionId: sess.subscription,
|
|
201
|
+
};
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case 'customer.subscription.updated': {
|
|
205
|
+
const sub = event.data.object;
|
|
206
|
+
payload = {
|
|
207
|
+
event: 'subscription.updated',
|
|
208
|
+
orgId: sub.metadata?.genius_org_id,
|
|
209
|
+
tier: sub.metadata?.tier,
|
|
210
|
+
subscriptionId: sub.id,
|
|
211
|
+
status: sub.status,
|
|
212
|
+
};
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
case 'customer.subscription.deleted': {
|
|
216
|
+
const sub = event.data.object;
|
|
217
|
+
payload = {
|
|
218
|
+
event: 'subscription.deleted',
|
|
219
|
+
orgId: sub.metadata?.genius_org_id,
|
|
220
|
+
subscriptionId: sub.id,
|
|
221
|
+
};
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
default:
|
|
225
|
+
return res.json({ received: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (payload?.orgId) {
|
|
229
|
+
const callbackUrl = `${GENIUS_CALLBACK_URL()}/api/wab/billing-callback`;
|
|
230
|
+
const secret = BRIDGE_SECRET();
|
|
231
|
+
const r = await fetch(callbackUrl, {
|
|
232
|
+
method: 'POST',
|
|
233
|
+
headers: {
|
|
234
|
+
'Content-Type': 'application/json',
|
|
235
|
+
'X-Internal-Secret': secret,
|
|
236
|
+
},
|
|
237
|
+
body: JSON.stringify(payload),
|
|
238
|
+
signal: AbortSignal.timeout(8000),
|
|
239
|
+
}).catch(err => {
|
|
240
|
+
console.error('[genius-bridge] callback to genius failed:', err.message);
|
|
241
|
+
return null;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (r && !r.ok) {
|
|
245
|
+
console.error('[genius-bridge] callback returned', r.status);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error('[genius-bridge] webhook handler error:', err);
|
|
250
|
+
return res.status(500).json({ error: 'Internal error processing event' });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
res.json({ received: true });
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
module.exports = router;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Genius Platform Payment Gateway
|
|
3
|
+
*
|
|
4
|
+
* Exposes WAB's existing Stripe service to genius-platform over a shared
|
|
5
|
+
* secret. Uses WAB's configured Stripe keys and price IDs — no separate
|
|
6
|
+
* Stripe account or duplicated logic required.
|
|
7
|
+
*
|
|
8
|
+
* Endpoints (prefix: /api/genius):
|
|
9
|
+
* POST /checkout → create Stripe Checkout session for a genius org
|
|
10
|
+
* POST /portal → create Customer Portal session for a genius org
|
|
11
|
+
*
|
|
12
|
+
* Stripe Webhook:
|
|
13
|
+
* Use the EXISTING WAB webhook: POST /api/billing/webhook
|
|
14
|
+
* Same STRIPE_WEBHOOK_SECRET — no separate endpoint or secret needed.
|
|
15
|
+
* Events tagged with genius_org_id in metadata are automatically forwarded
|
|
16
|
+
* to genius-platform's /api/wab/billing-callback by stripe.js.
|
|
17
|
+
*
|
|
18
|
+
* Auth: X-Internal-Secret header must match GENIUS_BRIDGE_SECRET env var.
|
|
19
|
+
* All endpoints are reachable only from localhost (blocked by nginx for external).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const express = require('express');
|
|
25
|
+
const router = express.Router();
|
|
26
|
+
|
|
27
|
+
// ── Use WAB's existing Stripe service — single Stripe account for everything ──
|
|
28
|
+
// Note: customer storage stays in genius-platform's own DB (no WAB DB foreign key needed)
|
|
29
|
+
const { getStripe, getStripePrices, isStripeConfigured } = require('../services/stripe');
|
|
30
|
+
|
|
31
|
+
// ── Auth middleware — shared secret ──────────────────────────────────────────
|
|
32
|
+
function requireInternalSecret(req, res, next) {
|
|
33
|
+
const secret = process.env.GENIUS_BRIDGE_SECRET;
|
|
34
|
+
if (!secret) return res.status(503).json({ error: 'GENIUS_BRIDGE_SECRET not configured on WAB' });
|
|
35
|
+
if (req.headers['x-internal-secret'] !== secret) {
|
|
36
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
37
|
+
}
|
|
38
|
+
next();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Resolve price ID for genius tiers ─────────────────────────────────────────
|
|
42
|
+
// Priority: STRIPE_GENIUS_PRICE_<TIER> → WAB's existing prices → null
|
|
43
|
+
function resolveGeniusPrice(tier) {
|
|
44
|
+
const t = String(tier || '').toUpperCase();
|
|
45
|
+
// Genius-specific override (optional)
|
|
46
|
+
const override = process.env[`STRIPE_GENIUS_PRICE_${t}`];
|
|
47
|
+
if (override) return override;
|
|
48
|
+
// Fall back to WAB's own price IDs (same products, shared account)
|
|
49
|
+
const prices = getStripePrices();
|
|
50
|
+
return prices[t.toLowerCase()] || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── POST /checkout ────────────────────────────────────────────────────────────
|
|
54
|
+
router.post('/checkout', requireInternalSecret, express.json(), async (req, res) => {
|
|
55
|
+
if (!isStripeConfigured()) {
|
|
56
|
+
return res.status(503).json({ error: 'Stripe not configured on this server' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { orgId, userId, email, name, tier, existingCustomerId } = req.body || {};
|
|
60
|
+
if (!orgId || !tier || !email) {
|
|
61
|
+
return res.status(400).json({ error: 'orgId, tier, and email are required' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const priceId = resolveGeniusPrice(tier);
|
|
65
|
+
if (!priceId) {
|
|
66
|
+
return res.status(400).json({ error: `No Stripe price configured for tier: ${tier}` });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const s = getStripe();
|
|
71
|
+
|
|
72
|
+
// genius-platform stores and passes back its own Stripe customer ID
|
|
73
|
+
let stripeCustomerId = existingCustomerId || null;
|
|
74
|
+
|
|
75
|
+
if (!stripeCustomerId) {
|
|
76
|
+
const stripeCustomer = await s.customers.create({
|
|
77
|
+
email,
|
|
78
|
+
name: name || email,
|
|
79
|
+
metadata: { genius_org_id: orgId, source: 'thecodegenius' }
|
|
80
|
+
});
|
|
81
|
+
stripeCustomerId = stripeCustomer.id;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const appUrl = process.env.GENIUS_APP_URL || 'https://thecodegenius.com';
|
|
85
|
+
const session = await s.checkout.sessions.create({
|
|
86
|
+
customer: stripeCustomerId,
|
|
87
|
+
mode: 'subscription',
|
|
88
|
+
payment_method_types: ['card'],
|
|
89
|
+
line_items: [{ price: priceId, quantity: 1 }],
|
|
90
|
+
metadata: { genius_org_id: orgId, genius_user_id: userId || '', tier },
|
|
91
|
+
success_url: `${appUrl}/dashboard/user/billing?payment=success&session_id={CHECKOUT_SESSION_ID}`,
|
|
92
|
+
cancel_url: `${appUrl}/pricing?canceled=1`,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Return customerId so genius-platform can persist it in its own DB
|
|
96
|
+
res.json({ sessionId: session.id, url: session.url, customerId: stripeCustomerId });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error('[genius-gateway] checkout error:', err.message);
|
|
99
|
+
res.status(500).json({ error: err.message });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── POST /portal ──────────────────────────────────────────────────────────────
|
|
104
|
+
router.post('/portal', requireInternalSecret, express.json(), async (req, res) => {
|
|
105
|
+
if (!isStripeConfigured()) {
|
|
106
|
+
return res.status(503).json({ error: 'Stripe not configured on this server' });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { orgId, customerId } = req.body || {};
|
|
110
|
+
if (!orgId && !customerId) {
|
|
111
|
+
return res.status(400).json({ error: 'orgId or customerId is required' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const s = getStripe();
|
|
116
|
+
|
|
117
|
+
// genius-platform is expected to pass customerId from its own DB
|
|
118
|
+
let stripeCustomerId = customerId;
|
|
119
|
+
|
|
120
|
+
if (!stripeCustomerId) {
|
|
121
|
+
return res.status(404).json({ error: 'No Stripe customer found for this org' });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const appUrl = process.env.GENIUS_APP_URL || 'https://thecodegenius.com';
|
|
125
|
+
const session = await s.billingPortal.sessions.create({
|
|
126
|
+
customer: stripeCustomerId,
|
|
127
|
+
return_url: `${appUrl}/dashboard/user/billing`,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
res.json({ url: session.url });
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error('[genius-gateway] portal error:', err.message);
|
|
133
|
+
res.status(500).json({ error: err.message });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
module.exports = router;
|
|
@@ -38,9 +38,10 @@ const EVENT_TYPES = new Set(['refusal','approval','override','policy','audit','c
|
|
|
38
38
|
const SEVERITIES = new Set(['info','low','medium','high','critical']);
|
|
39
39
|
|
|
40
40
|
function adminGate(req, res, next) {
|
|
41
|
+
const { safeEqual } = require('../utils/safe-compare');
|
|
41
42
|
const expected = process.env.WAB_GOVERNANCE_ADMIN_TOKEN || process.env.WAB_RING4_ADMIN_TOKEN;
|
|
42
43
|
if (!expected) return res.status(503).json({ error: 'admin_disabled' });
|
|
43
|
-
if ((req.headers['x-admin-token'] || ''
|
|
44
|
+
if (!safeEqual(req.headers['x-admin-token'] || '', expected)) return res.status(401).json({ error: 'unauthorized' });
|
|
44
45
|
next();
|
|
45
46
|
}
|
|
46
47
|
|