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.
- 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/dashboard.html +1 -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/public/webhooks.html +181 -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/services/webhooks.js +61 -1
- package/server/utils/migrate.js +1 -1
- package/server/utils/safe-compare.js +26 -0
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
const crypto = require('crypto');
|
|
18
18
|
const { db } = require('../models/db');
|
|
19
|
+
const { safeFetch } = require('../utils/safe-fetch');
|
|
19
20
|
|
|
20
21
|
// ─── Schema ──────────────────────────────────────────────────────────
|
|
21
22
|
|
|
@@ -362,21 +363,22 @@ async function executeUrlTask(taskId) {
|
|
|
362
363
|
_addMessage(taskId, 'agent', analyzeMsg, { type: 'progress', step: 'analyze' });
|
|
363
364
|
updates.push({ type: 'progress', step: 'analyze', message: analyzeMsg });
|
|
364
365
|
|
|
365
|
-
// Try to fetch the original page to get the current price
|
|
366
|
+
// Try to fetch the original page to get the current price.
|
|
367
|
+
// SSRF-hardened: safeFetch blocks private/internal IPs, caps body size,
|
|
368
|
+
// enforces a timeout, and re-validates every redirect hop.
|
|
366
369
|
let originalPrice = null;
|
|
367
370
|
try {
|
|
368
|
-
const
|
|
369
|
-
const timeout = setTimeout(() => controller.abort(), 12000);
|
|
370
|
-
const res = await fetch(urlData.url, {
|
|
371
|
+
const res = await safeFetch(urlData.url, {
|
|
371
372
|
headers: {
|
|
372
373
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
|
|
373
374
|
'Accept': 'text/html',
|
|
374
375
|
'Accept-Language': 'ar,en-US;q=0.9,en;q=0.8',
|
|
375
376
|
},
|
|
376
|
-
|
|
377
|
-
|
|
377
|
+
}, {
|
|
378
|
+
timeoutMs: 12000,
|
|
379
|
+
maxBytes: 2 * 1024 * 1024,
|
|
380
|
+
allowedContentTypes: ['text/html', 'application/xhtml+xml'],
|
|
378
381
|
});
|
|
379
|
-
clearTimeout(timeout);
|
|
380
382
|
|
|
381
383
|
if (res.ok) {
|
|
382
384
|
const html = await res.text();
|
package/server/services/email.js
CHANGED
|
@@ -4,8 +4,48 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const nodemailer = require('nodemailer');
|
|
7
|
+
const crypto = require('crypto');
|
|
7
8
|
const { getSmtpSettings, logNotification } = require('../models/db');
|
|
8
9
|
|
|
10
|
+
// WAB email headers (RFC 5322 X-* extension headers) — let receiving agents
|
|
11
|
+
// verify that a transactional email came from a WAB-enabled domain. Spec at
|
|
12
|
+
// https://webagentbridge.com/wab-email. Signing is opt-in via env:
|
|
13
|
+
// WAB_EMAIL_HOST — the domain that owns the wab.json manifest
|
|
14
|
+
// WAB_EMAIL_SIGNING_KEY — base64 Ed25519 private key (32 bytes raw)
|
|
15
|
+
const WAB_EMAIL_HOST = process.env.WAB_EMAIL_HOST || 'webagentbridge.com';
|
|
16
|
+
const WAB_EMAIL_KEY = process.env.WAB_EMAIL_SIGNING_KEY || '';
|
|
17
|
+
let _wabSigningKey = null;
|
|
18
|
+
function _getSigningKey() {
|
|
19
|
+
if (_wabSigningKey !== null) return _wabSigningKey;
|
|
20
|
+
if (!WAB_EMAIL_KEY) { _wabSigningKey = false; return false; }
|
|
21
|
+
try {
|
|
22
|
+
const raw = Buffer.from(WAB_EMAIL_KEY, 'base64');
|
|
23
|
+
if (raw.length !== 32) throw new Error('expected 32-byte ed25519 seed');
|
|
24
|
+
const der = Buffer.concat([Buffer.from('302e020100300506032b657004220420', 'hex'), raw]);
|
|
25
|
+
_wabSigningKey = crypto.createPrivateKey({ key: der, format: 'der', type: 'pkcs8' });
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.warn('[wab-email] signing disabled:', e.message);
|
|
28
|
+
_wabSigningKey = false;
|
|
29
|
+
}
|
|
30
|
+
return _wabSigningKey;
|
|
31
|
+
}
|
|
32
|
+
function buildWabHeaders({ from, to, subject, template, messageId, receipt }) {
|
|
33
|
+
const headers = {
|
|
34
|
+
'X-WAB-Manifest': `https://${WAB_EMAIL_HOST}/.well-known/wab.json`,
|
|
35
|
+
'X-WAB-Action': `email.${template || 'transactional'}`
|
|
36
|
+
};
|
|
37
|
+
if (receipt) headers['X-WAB-Receipt'] = receipt;
|
|
38
|
+
const key = _getSigningKey();
|
|
39
|
+
if (key) {
|
|
40
|
+
const digest = crypto.createHash('sha256')
|
|
41
|
+
.update(`${from}\n${to}\n${subject}\n${messageId}`)
|
|
42
|
+
.digest();
|
|
43
|
+
const sig = crypto.sign(null, digest, key).toString('base64');
|
|
44
|
+
headers['X-WAB-Signature'] = `ed25519=${sig}`;
|
|
45
|
+
}
|
|
46
|
+
return headers;
|
|
47
|
+
}
|
|
48
|
+
|
|
9
49
|
function escapeHtml(s) {
|
|
10
50
|
if (s == null) return '';
|
|
11
51
|
return String(s)
|
|
@@ -269,11 +309,19 @@ async function sendEmail({ to, template, data, userId }) {
|
|
|
269
309
|
const { subject, html } = tmpl(data);
|
|
270
310
|
|
|
271
311
|
try {
|
|
312
|
+
const from = `"${settings.from_name}" <${settings.from_email}>`;
|
|
313
|
+
const messageId = `<${crypto.randomBytes(12).toString('hex')}@${WAB_EMAIL_HOST}>`;
|
|
314
|
+
const wabHeaders = buildWabHeaders({
|
|
315
|
+
from: settings.from_email, to, subject, template,
|
|
316
|
+
messageId, receipt: data && data.wab_receipt
|
|
317
|
+
});
|
|
272
318
|
await transport.sendMail({
|
|
273
|
-
from
|
|
319
|
+
from,
|
|
274
320
|
to,
|
|
275
321
|
subject,
|
|
276
|
-
html
|
|
322
|
+
html,
|
|
323
|
+
messageId,
|
|
324
|
+
headers: wabHeaders
|
|
277
325
|
});
|
|
278
326
|
logNotification({ userId, emailTo: to, template, subject, status: 'sent' });
|
|
279
327
|
return { success: true };
|
|
@@ -125,29 +125,48 @@ class MarketplaceEngine {
|
|
|
125
125
|
};
|
|
126
126
|
|
|
127
127
|
this._purchases.set(purchaseId, purchase);
|
|
128
|
-
listing.installs++;
|
|
129
128
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
// Only credit installs / revenue / seller earnings once the purchase is
|
|
130
|
+
// actually completed. For paid listings the purchase starts in
|
|
131
|
+
// 'pending_payment' and must be promoted via completePayment(). Crediting
|
|
132
|
+
// here would let any caller who can reach POST /marketplace/:id/purchase
|
|
133
|
+
// inflate a seller's earnings without ever paying.
|
|
134
|
+
if (purchase.status === 'completed') {
|
|
135
|
+
this._creditCompletedPurchase(listing, purchase);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
listing.revenue += listing.price;
|
|
139
138
|
bus.emit('marketplace.purchased', { purchaseId, listingId, buyerId, price: listing.price });
|
|
140
139
|
return purchase;
|
|
141
140
|
}
|
|
142
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Credit installs / revenue / seller earnings for a completed purchase.
|
|
144
|
+
* Idempotent guard: stamps purchase._credited so a double-call is a no-op.
|
|
145
|
+
*/
|
|
146
|
+
_creditCompletedPurchase(listing, purchase) {
|
|
147
|
+
if (purchase._credited) return;
|
|
148
|
+
listing.installs++;
|
|
149
|
+
listing.revenue += purchase.price;
|
|
150
|
+
if (purchase.sellerEarning > 0) {
|
|
151
|
+
const earnings = this._earnings.get(listing.sellerId) || { total: 0, pending: 0, paid: 0 };
|
|
152
|
+
earnings.total += purchase.sellerEarning;
|
|
153
|
+
earnings.pending += purchase.sellerEarning;
|
|
154
|
+
this._earnings.set(listing.sellerId, earnings);
|
|
155
|
+
}
|
|
156
|
+
purchase._credited = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
143
159
|
/**
|
|
144
160
|
* Complete payment for a purchase
|
|
145
161
|
*/
|
|
146
162
|
completePayment(purchaseId) {
|
|
147
163
|
const purchase = this._purchases.get(purchaseId);
|
|
148
164
|
if (!purchase) throw new Error('Purchase not found');
|
|
165
|
+
if (purchase.status === 'completed') return purchase;
|
|
149
166
|
purchase.status = 'completed';
|
|
150
167
|
purchase.completedAt = Date.now();
|
|
168
|
+
const listing = this._listings.get(purchase.listingId);
|
|
169
|
+
if (listing) this._creditCompletedPurchase(listing, purchase);
|
|
151
170
|
return purchase;
|
|
152
171
|
}
|
|
153
172
|
|
package/server/services/plans.js
CHANGED
|
@@ -24,7 +24,7 @@ function db() {
|
|
|
24
24
|
const DATA_DIR = process.env.NODE_ENV === 'test'
|
|
25
25
|
? path.join(__dirname, '..', '..', 'data-test')
|
|
26
26
|
: (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
|
|
27
|
-
const dbFile = process.env.NODE_ENV === 'test' ?
|
|
27
|
+
const dbFile = process.env.NODE_ENV === 'test' ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
|
|
28
28
|
_db = new Database(path.join(DATA_DIR, dbFile));
|
|
29
29
|
return _db;
|
|
30
30
|
}
|
|
@@ -40,7 +40,7 @@ const { encryptOptional, decryptOptional } = require('../utils/secureFields');
|
|
|
40
40
|
const DATA_DIR = process.env.NODE_ENV === 'test'
|
|
41
41
|
? path.join(__dirname, '..', '..', 'data-test')
|
|
42
42
|
: (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
|
|
43
|
-
const DB_FILE = process.env.NODE_ENV === 'test' ?
|
|
43
|
+
const DB_FILE = process.env.NODE_ENV === 'test' ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
|
|
44
44
|
let _db = null;
|
|
45
45
|
function db() {
|
|
46
46
|
if (!_db) _db = new Database(path.join(DATA_DIR, DB_FILE));
|
|
@@ -21,7 +21,7 @@ const Database = require('better-sqlite3');
|
|
|
21
21
|
const DATA_DIR = process.env.NODE_ENV === 'test'
|
|
22
22
|
? path.join(__dirname, '..', '..', 'data-test')
|
|
23
23
|
: (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
|
|
24
|
-
const DB_FILE = process.env.NODE_ENV === 'test' ?
|
|
24
|
+
const DB_FILE = process.env.NODE_ENV === 'test' ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
|
|
25
25
|
|
|
26
26
|
let _db = null;
|
|
27
27
|
function db() {
|
|
@@ -17,7 +17,7 @@ const ssl = require('./ssl-inspector');
|
|
|
17
17
|
const DATA_DIR = process.env.NODE_ENV === 'test'
|
|
18
18
|
? path.join(__dirname, '..', '..', 'data-test')
|
|
19
19
|
: (process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'));
|
|
20
|
-
const DB_FILE = process.env.NODE_ENV === 'test' ?
|
|
20
|
+
const DB_FILE = process.env.NODE_ENV === 'test' ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
|
|
21
21
|
|
|
22
22
|
let _db = null;
|
|
23
23
|
function db() {
|
|
@@ -26,10 +26,11 @@ function getStripe() {
|
|
|
26
26
|
|
|
27
27
|
function getStripePrices() {
|
|
28
28
|
return {
|
|
29
|
-
starter:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
starter: process.env.STRIPE_PRICE_STARTER || getPlatformSetting('stripe_price_starter'),
|
|
30
|
+
freelancer: process.env.STRIPE_PRICE_FREELANCER || getPlatformSetting('stripe_price_freelancer'),
|
|
31
|
+
pro: process.env.STRIPE_PRICE_PRO || getPlatformSetting('stripe_price_pro'),
|
|
32
|
+
business: process.env.STRIPE_PRICE_BUSINESS || getPlatformSetting('stripe_price_business'),
|
|
33
|
+
enterprise: process.env.STRIPE_PRICE_ENTERPRISE || getPlatformSetting('stripe_price_enterprise'),
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -332,6 +333,30 @@ function handleWebhookRequest(req) {
|
|
|
332
333
|
if (!s) throw new Error('Stripe not configured');
|
|
333
334
|
const event = s.webhooks.constructEvent(raw, sig, whSecret);
|
|
334
335
|
handleWebhookEvent(event);
|
|
336
|
+
|
|
337
|
+
// ── Forward genius-tagged events to genius-platform (same webhook, same secret) ──
|
|
338
|
+
const obj = event.data && event.data.object;
|
|
339
|
+
const geniusOrgId = obj && obj.metadata && obj.metadata.genius_org_id;
|
|
340
|
+
if (geniusOrgId) {
|
|
341
|
+
const callbackUrl = `${process.env.GENIUS_CALLBACK_URL || 'http://localhost:3004'}/api/wab/billing-callback`;
|
|
342
|
+
(async () => {
|
|
343
|
+
try {
|
|
344
|
+
const fetchFn = globalThis.fetch || (await import('node-fetch').then(m => m.default).catch(() => null));
|
|
345
|
+
if (!fetchFn) return;
|
|
346
|
+
await fetchFn(callbackUrl, {
|
|
347
|
+
method: 'POST',
|
|
348
|
+
headers: {
|
|
349
|
+
'Content-Type': 'application/json',
|
|
350
|
+
'X-Internal-Secret': process.env.GENIUS_BRIDGE_SECRET || '',
|
|
351
|
+
},
|
|
352
|
+
body: JSON.stringify({ type: event.type, data: event.data }),
|
|
353
|
+
});
|
|
354
|
+
console.log(`[stripe] forwarded ${event.type} → genius-platform (org: ${geniusOrgId})`);
|
|
355
|
+
} catch (e) {
|
|
356
|
+
console.error('[stripe] genius billing-callback failed (non-fatal):', e.message);
|
|
357
|
+
}
|
|
358
|
+
})();
|
|
359
|
+
}
|
|
335
360
|
}
|
|
336
361
|
|
|
337
362
|
module.exports = {
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
const crypto = require('crypto');
|
|
22
22
|
const { db } = require('../models/db');
|
|
23
|
+
const { canonicalize } = require('./canonical-json');
|
|
24
|
+
const signer = require('./operator-signer');
|
|
23
25
|
|
|
24
26
|
const VALID_EVENTS = new Set([
|
|
25
27
|
'revocation.opened',
|
|
@@ -183,6 +185,10 @@ async function _attemptDelivery(deliveryId, subscription, body) {
|
|
|
183
185
|
'X-WAB-Webhook-Delivery': deliveryId,
|
|
184
186
|
'User-Agent': 'web-agent-bridge-webhooks/1.0',
|
|
185
187
|
};
|
|
188
|
+
if (subscription._operatorSignature) {
|
|
189
|
+
headers['X-WAB-Operator-Signature'] = subscription._operatorSignature;
|
|
190
|
+
headers['X-WAB-Operator-Key-Url'] = '/api/operator-key.json';
|
|
191
|
+
}
|
|
186
192
|
try {
|
|
187
193
|
const res = await _httpPost(subscription.url, body, headers);
|
|
188
194
|
if (res.ok) {
|
|
@@ -264,6 +270,20 @@ function emit(eventType, data) {
|
|
|
264
270
|
created_at: new Date().toISOString(),
|
|
265
271
|
data,
|
|
266
272
|
};
|
|
273
|
+
// Ed25519-sign the canonical form of the payload (RFC 8785). Receivers verify
|
|
274
|
+
// with the public key published at /api/operator-key.json. If no operator key
|
|
275
|
+
// is configured, we still deliver — the HMAC signature alone is sufficient for
|
|
276
|
+
// per-subscription auth.
|
|
277
|
+
const operatorSignature = signer.isConfigured() ? signer.sign(payload) : null;
|
|
278
|
+
if (operatorSignature) {
|
|
279
|
+
payload.signature = {
|
|
280
|
+
alg: signer.ALGORITHM,
|
|
281
|
+
value: operatorSignature,
|
|
282
|
+
key_url: '/api/operator-key.json',
|
|
283
|
+
canonicalization: 'RFC8785',
|
|
284
|
+
signed_fields: ['id', 'type', 'created_at', 'data'],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
267
287
|
const body = JSON.stringify(payload);
|
|
268
288
|
|
|
269
289
|
for (const sub of subs) {
|
|
@@ -273,7 +293,12 @@ function emit(eventType, data) {
|
|
|
273
293
|
(id, subscription_id, event_id, event_type, payload, status, attempts)
|
|
274
294
|
VALUES (?, ?, ?, ?, ?, 'pending', 0)
|
|
275
295
|
`).run(deliveryId, sub.id, eventId, eventType, body);
|
|
276
|
-
const enriched = {
|
|
296
|
+
const enriched = {
|
|
297
|
+
...sub,
|
|
298
|
+
_eventId: eventId,
|
|
299
|
+
_eventType: eventType,
|
|
300
|
+
_operatorSignature: operatorSignature,
|
|
301
|
+
};
|
|
277
302
|
setImmediate(async () => {
|
|
278
303
|
const ok = await _attemptDelivery(deliveryId, enriched, body);
|
|
279
304
|
if (!ok) _scheduleRetry(deliveryId, enriched, body, 1);
|
|
@@ -298,6 +323,40 @@ function verifySignature({ secret, header, body, toleranceSec = 300 }) {
|
|
|
298
323
|
} catch (_) { return false; }
|
|
299
324
|
}
|
|
300
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Verify the operator (Ed25519) signature embedded inside an event body.
|
|
328
|
+
*
|
|
329
|
+
* The receiver passes the parsed JSON envelope (or the raw body string + we
|
|
330
|
+
* parse). We strip the `signature` field, RFC8785-canonicalise the remainder,
|
|
331
|
+
* and verify with the operator public key (32-byte raw, base64).
|
|
332
|
+
*
|
|
333
|
+
* verifyOperatorSignature({ body, publicKeyB64 }) -> true | false
|
|
334
|
+
*/
|
|
335
|
+
function verifyOperatorSignature({ body, publicKeyB64 }) {
|
|
336
|
+
if (!body || !publicKeyB64) return false;
|
|
337
|
+
let envelope;
|
|
338
|
+
try {
|
|
339
|
+
envelope = typeof body === 'string' ? JSON.parse(body) : body;
|
|
340
|
+
} catch (_) { return false; }
|
|
341
|
+
const sigObj = envelope.signature;
|
|
342
|
+
if (!sigObj || sigObj.alg !== 'ed25519' || !sigObj.value) return false;
|
|
343
|
+
|
|
344
|
+
const { signature: _omit, ...signed } = envelope;
|
|
345
|
+
const canon = canonicalize(signed);
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const raw = Buffer.from(publicKeyB64, 'base64');
|
|
349
|
+
if (raw.length !== 32) return false;
|
|
350
|
+
// Reconstruct SPKI DER for Ed25519: 12-byte prefix + 32-byte raw.
|
|
351
|
+
const spki = Buffer.concat([
|
|
352
|
+
Buffer.from('302a300506032b6570032100', 'hex'),
|
|
353
|
+
raw,
|
|
354
|
+
]);
|
|
355
|
+
const pub = crypto.createPublicKey({ key: spki, format: 'der', type: 'spki' });
|
|
356
|
+
return crypto.verify(null, Buffer.from(canon, 'utf8'), pub, Buffer.from(sigObj.value, 'base64'));
|
|
357
|
+
} catch (_) { return false; }
|
|
358
|
+
}
|
|
359
|
+
|
|
301
360
|
module.exports = {
|
|
302
361
|
createSubscription,
|
|
303
362
|
listSubscriptions,
|
|
@@ -307,6 +366,7 @@ module.exports = {
|
|
|
307
366
|
listDeliveries,
|
|
308
367
|
emit,
|
|
309
368
|
verifySignature,
|
|
369
|
+
verifyOperatorSignature,
|
|
310
370
|
VALID_EVENTS,
|
|
311
371
|
// exposed for tests
|
|
312
372
|
_attemptDelivery,
|
package/server/utils/migrate.js
CHANGED
|
@@ -12,7 +12,7 @@ const DATA_DIR = process.env.NODE_ENV === 'test'
|
|
|
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 = process.env.NODE_ENV === 'test' ?
|
|
15
|
+
const dbFile = process.env.NODE_ENV === 'test' ? `wab-test-${process.env.JEST_WORKER_ID || '1'}.db` : 'wab.db';
|
|
16
16
|
const db = new Database(path.join(DATA_DIR, dbFile));
|
|
17
17
|
|
|
18
18
|
// Ensure migrations tracking table exists
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Constant-time comparison for secrets / bearer tokens.
|
|
7
|
+
*
|
|
8
|
+
* Both operands are hashed to a fixed-length SHA-256 digest before the
|
|
9
|
+
* timing-safe compare, so this never throws on length mismatch and does not
|
|
10
|
+
* leak the secret length through early termination. Non-string inputs (missing
|
|
11
|
+
* headers, undefined env vars) return false instead of throwing.
|
|
12
|
+
*
|
|
13
|
+
* Use this for ALL static-token / shared-secret equality checks instead of
|
|
14
|
+
* `a === b` / `a !== b`, which short-circuit on the first differing byte and
|
|
15
|
+
* are therefore vulnerable to remote timing analysis.
|
|
16
|
+
*/
|
|
17
|
+
function safeEqual(a, b) {
|
|
18
|
+
if (typeof a !== 'string' || typeof b !== 'string' || a.length === 0 || b.length === 0) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const ha = crypto.createHash('sha256').update(a, 'utf8').digest();
|
|
22
|
+
const hb = crypto.createHash('sha256').update(b, 'utf8').digest();
|
|
23
|
+
return crypto.timingSafeEqual(ha, hb);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { safeEqual };
|