web-agent-bridge 3.16.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.
Files changed (61) hide show
  1. package/README.ar.md +27 -8
  2. package/README.md +95 -0
  3. package/bin/wab-init.js +38 -0
  4. package/package.json +1 -1
  5. package/public/atp-semantics.html +216 -0
  6. package/public/benchmarks.html +151 -0
  7. package/public/dashboard.html +1 -0
  8. package/public/docs.html +113 -43
  9. package/public/index.html +142 -8
  10. package/public/key-rotation.html +184 -0
  11. package/public/llms.txt +54 -0
  12. package/public/notary.html +94 -0
  13. package/public/observatory.html +103 -0
  14. package/public/research.html +57 -0
  15. package/public/researchers.html +113 -0
  16. package/public/responsible-disclosure.html +294 -0
  17. package/public/robots.txt +17 -0
  18. package/public/security.html +157 -0
  19. package/public/threat-model.html +153 -0
  20. package/public/viral-coefficient.html +533 -0
  21. package/public/wab-dataset.html +501 -0
  22. package/public/wab-email.html +78 -0
  23. package/public/wab-lens.html +61 -0
  24. package/public/wab-p2p.html +96 -0
  25. package/public/wab-registry.html +481 -0
  26. package/public/wab-today.html +448 -0
  27. package/public/wab-uri.html +88 -0
  28. package/public/webhooks.html +181 -0
  29. package/script/ai-agent-bridge.js +24 -4
  30. package/server/index.js +1193 -827
  31. package/server/models/db.js +2 -1
  32. package/server/routes/admin-shieldlink.js +1 -1
  33. package/server/routes/admin-shieldqr.js +1 -1
  34. package/server/routes/admin-trust-monitor.js +1 -1
  35. package/server/routes/api-keys.js +2 -1
  36. package/server/routes/customer-shieldlink.js +1 -1
  37. package/server/routes/enterprise-mesh.js +2 -1
  38. package/server/routes/genius-bridge.js +256 -0
  39. package/server/routes/genius-gateway.js +137 -0
  40. package/server/routes/governance-saas.js +2 -1
  41. package/server/routes/notary.js +309 -0
  42. package/server/routes/observatory.js +109 -0
  43. package/server/routes/partners.js +2 -1
  44. package/server/routes/registry.js +352 -0
  45. package/server/routes/research.js +83 -0
  46. package/server/routes/ring4.js +2 -1
  47. package/server/routes/runtime.js +98 -25
  48. package/server/routes/security-researchers.js +161 -0
  49. package/server/routes/shieldqr.js +1 -1
  50. package/server/routes/traces.js +247 -0
  51. package/server/services/agent-tasks.js +9 -7
  52. package/server/services/email.js +50 -2
  53. package/server/services/marketplace.js +27 -8
  54. package/server/services/plans.js +1 -1
  55. package/server/services/shieldlink.js +1 -1
  56. package/server/services/ssl-ct-monitor.js +1 -1
  57. package/server/services/ssl-monitor.js +1 -1
  58. package/server/services/stripe.js +29 -4
  59. package/server/services/webhooks.js +61 -1
  60. package/server/utils/migrate.js +1 -1
  61. package/server/utils/safe-compare.js +26 -0
@@ -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
- const dbFile = isTest ? 'wab-test.db' : 'wab.db';
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' ? 'wab-test.db' : 'wab.db';
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' ? 'wab-test.db' : 'wab.db';
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' ? 'wab-test.db' : 'wab.db';
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'] || '') !== expected) return res.status(401).json({ error: 'unauthorized' });
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' ? 'wab-test.db' : 'wab.db';
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'] || '') !== expected) return res.status(401).json({ error: 'unauthorized' });
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'] || '') !== expected) return res.status(401).json({ error: 'unauthorized' });
44
+ if (!safeEqual(req.headers['x-admin-token'] || '', expected)) return res.status(401).json({ error: 'unauthorized' });
44
45
  next();
45
46
  }
46
47