web-agent-bridge 1.0.0 → 1.1.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 (52) hide show
  1. package/README.ar.md +1 -1
  2. package/README.md +336 -36
  3. package/docs/DEPLOY.md +118 -0
  4. package/docs/SPEC.md +1540 -0
  5. package/examples/mcp-agent.js +85 -0
  6. package/examples/vision-agent.js +12 -0
  7. package/package.json +14 -3
  8. package/public/admin/dashboard.html +848 -0
  9. package/public/admin/login.html +84 -0
  10. package/public/cookies.html +208 -0
  11. package/public/css/premium.css +317 -0
  12. package/public/dashboard.html +138 -0
  13. package/public/docs.html +5 -2
  14. package/public/index.html +54 -28
  15. package/public/js/auth-nav.js +31 -0
  16. package/public/js/auth-redirect.js +12 -0
  17. package/public/js/cookie-consent.js +56 -0
  18. package/public/js/ws-client.js +74 -0
  19. package/public/login.html +4 -2
  20. package/public/premium-dashboard.html +2075 -0
  21. package/public/premium.html +791 -0
  22. package/public/privacy.html +295 -0
  23. package/public/register.html +11 -2
  24. package/public/terms.html +254 -0
  25. package/script/ai-agent-bridge.js +253 -22
  26. package/sdk/index.js +36 -0
  27. package/server/config/secrets.js +92 -0
  28. package/server/index.js +100 -26
  29. package/server/middleware/adminAuth.js +30 -0
  30. package/server/middleware/auth.js +4 -7
  31. package/server/middleware/rateLimits.js +24 -0
  32. package/server/migrations/001_add_analytics_indexes.sql +7 -0
  33. package/server/migrations/002_premium_features.sql +418 -0
  34. package/server/models/db.js +360 -4
  35. package/server/routes/admin.js +247 -0
  36. package/server/routes/api.js +26 -9
  37. package/server/routes/billing.js +45 -0
  38. package/server/routes/discovery.js +324 -0
  39. package/server/routes/license.js +200 -11
  40. package/server/routes/noscript.js +543 -0
  41. package/server/routes/premium.js +724 -0
  42. package/server/services/email.js +204 -0
  43. package/server/services/fairness.js +420 -0
  44. package/server/services/premium.js +1680 -0
  45. package/server/services/stripe.js +192 -0
  46. package/server/utils/cache.js +125 -0
  47. package/server/utils/migrate.js +81 -0
  48. package/server/utils/secureFields.js +50 -0
  49. package/server/ws.js +33 -13
  50. package/wab-mcp-adapter/README.md +136 -0
  51. package/wab-mcp-adapter/index.js +528 -0
  52. package/wab-mcp-adapter/package.json +17 -0
@@ -1,14 +1,16 @@
1
1
  /**
2
- * Web Agent Bridge v1.0.0
3
- * Open-source middleware for AI agent ↔ website interaction
2
+ * Web Agent Bridge v1.1.0
3
+ * Open protocol + runtime for AI agent ↔ website interaction
4
4
  * https://github.com/web-agent-bridge
5
5
  * License: MIT
6
6
  */
7
7
  (function (global) {
8
8
  'use strict';
9
9
 
10
- const VERSION = '1.0.0';
10
+ const VERSION = '1.1.0';
11
+ const PROTOCOL_VERSION = '1.0';
11
12
  const LICENSING_SERVER = 'https://api.webagentbridge.com';
13
+ const DISCOVERY_PATHS = ['/agent-bridge.json', '/.well-known/wab.json'];
12
14
 
13
15
  // ─── Default Configuration ────────────────────────────────────────────
14
16
  const DEFAULT_CONFIG = {
@@ -22,12 +24,6 @@
22
24
  automatedLogin: false,
23
25
  extractData: false
24
26
  },
25
- features: {
26
- advancedAnalytics: false,
27
- realTimeUpdates: false,
28
- customActions: false,
29
- webhooks: false
30
- },
31
27
  restrictions: {
32
28
  allowedSelectors: [],
33
29
  blockedSelectors: ['.private', '[data-private]', '[data-no-agent]'],
@@ -39,7 +35,19 @@
39
35
  level: 'basic'
40
36
  },
41
37
  subscriptionTier: 'free',
42
- licenseKey: null
38
+ licenseKey: null,
39
+ /** Public site id from dashboard (preferred). Used with configEndpoint for token exchange — license key stays off the page. */
40
+ siteId: null,
41
+ /** Base URL for /api/license/* (verify, track). Default: current origin, else LICENSING_SERVER. */
42
+ apiBaseUrl: null,
43
+ features: {
44
+ advancedAnalytics: false,
45
+ realTimeUpdates: false,
46
+ customActions: false,
47
+ webhooks: false,
48
+ /** Send execute events to POST /api/license/track (populates admin analytics). */
49
+ reportUsage: true
50
+ }
43
51
  };
44
52
 
45
53
  // ─── Rate Limiter ─────────────────────────────────────────────────────
@@ -348,15 +356,28 @@
348
356
  }
349
357
 
350
358
  // ─── Stealth / Human-like Interaction ─────────────────────────────────
351
- // Makes automation interactions look natural to anti-bot systems
359
+ // ⚠️ ETHICAL USE POLICY:
360
+ // Stealth mode simulates human-like interaction patterns for LEGITIMATE uses:
361
+ // - Accessibility testing - QA automation on YOUR OWN sites
362
+ // - UX research with consent - Authorized penetration testing
363
+ // DO NOT use to bypass anti-bot protections on sites you do not own or control.
364
+ // Misuse violates the MIT license terms and is the user's legal responsibility.
352
365
  const Stealth = {
353
366
  _enabled: false,
367
+ _consentGiven: false,
354
368
 
355
- enable() { this._enabled = true; },
369
+ enable(consent) {
370
+ if (consent !== true) {
371
+ console.warn('[WAB] Stealth mode requires explicit consent: stealth.enable(true)');
372
+ return;
373
+ }
374
+ this._consentGiven = true;
375
+ this._enabled = true;
376
+ },
356
377
  disable() { this._enabled = false; },
357
378
  get isEnabled() { return this._enabled; },
358
379
 
359
- // Random delay between min and max ms, with optional Gaussian distribution
380
+ // Random delay between min and max ms
360
381
  delay(min = 50, max = 300) {
361
382
  if (!this._enabled) return Promise.resolve();
362
383
  const duration = min + Math.floor(Math.random() * (max - min));
@@ -557,13 +578,15 @@
557
578
  this.authenticated = false;
558
579
  this.agentInfo = null;
559
580
  this._licenseVerified = null;
581
+ this._discoveryDoc = null;
560
582
  this._ready = false;
561
583
  this._readyCallbacks = [];
562
584
  this._mutationObserver = null;
585
+ this._eventSubscriptions = new Map();
563
586
 
564
- // Enable stealth mode if configured
565
- if (this.config.stealth?.enabled) {
566
- this.stealth.enable();
587
+ // Enable stealth mode if configured (requires consent: true)
588
+ if (this.config.stealth?.enabled && this.config.stealth?.consent === true) {
589
+ this.stealth.enable(true);
567
590
  }
568
591
 
569
592
  this._init();
@@ -571,7 +594,11 @@
571
594
 
572
595
  // ── Initialization ──────────────────────────────────────────────────
573
596
  async _init() {
574
- if (this.config.licenseKey) {
597
+ await this._fetchDiscoveryDocument();
598
+
599
+ if (this.config.configEndpoint && (this.config.siteId || this.config._licenseKey || this.config.licenseKey)) {
600
+ await this._secureLicenseExchange();
601
+ } else if (this.config.licenseKey) {
575
602
  await this._verifyLicense();
576
603
  } else {
577
604
  this._licenseVerified = { tier: 'free', valid: true };
@@ -583,8 +610,54 @@
583
610
  this._ready = true;
584
611
  this._readyCallbacks.forEach(cb => cb());
585
612
  this._readyCallbacks = [];
586
- this.events.emit('ready', { version: VERSION, tier: this.getEffectiveTier() });
587
- this.logger.log('init', { version: VERSION, tier: this.getEffectiveTier(), security: 'sandbox-active' });
613
+ this.events.emit('ready', { version: VERSION, protocol: PROTOCOL_VERSION, tier: this.getEffectiveTier() });
614
+ this.logger.log('init', { version: VERSION, protocol: PROTOCOL_VERSION, tier: this.getEffectiveTier(), security: 'sandbox-active' });
615
+ }
616
+
617
+ async _fetchDiscoveryDocument() {
618
+ var base = this._getLicenseApiBase();
619
+ for (var i = 0; i < DISCOVERY_PATHS.length; i++) {
620
+ try {
621
+ var res = await fetch(base + DISCOVERY_PATHS[i], { method: 'GET', credentials: 'omit' });
622
+ if (res.ok) {
623
+ this._discoveryDoc = await res.json();
624
+ this.logger.log('discovery', { path: DISCOVERY_PATHS[i], provider: this._discoveryDoc.provider });
625
+ this.events.emit('discovery', this._discoveryDoc);
626
+ return;
627
+ }
628
+ } catch (_) {}
629
+ }
630
+ }
631
+
632
+ // Secure license exchange: POST license key to server, get session token back
633
+ // License key is transmitted once via POST (not visible in page source)
634
+ async _secureLicenseExchange() {
635
+ try {
636
+ const endpoint = this.config.configEndpoint;
637
+ const body = this.config.siteId
638
+ ? { siteId: this.config.siteId }
639
+ : { licenseKey: this.config._licenseKey || this.config.licenseKey };
640
+ const res = await fetch(endpoint, {
641
+ method: 'POST',
642
+ headers: { 'Content-Type': 'application/json' },
643
+ body: JSON.stringify(body)
644
+ });
645
+ if (res.ok) {
646
+ const data = await res.json();
647
+ this._licenseVerified = { tier: data.tier, valid: true, sessionToken: data.sessionToken };
648
+ delete this.config._licenseKey;
649
+ if (data.expiresIn) {
650
+ if (this._tokenRefreshTimer) clearTimeout(this._tokenRefreshTimer);
651
+ this._tokenRefreshTimer = setTimeout(() => {
652
+ this._secureLicenseExchange();
653
+ }, Math.floor(data.expiresIn * 800));
654
+ }
655
+ } else {
656
+ this._licenseVerified = { tier: 'free', valid: false, error: 'Token exchange failed' };
657
+ }
658
+ } catch (e) {
659
+ this._licenseVerified = { tier: this.config.subscriptionTier || 'free', valid: false, error: 'Offline' };
660
+ }
588
661
  }
589
662
 
590
663
  // Store fingerprints for all discovered actions (self-healing)
@@ -642,10 +715,42 @@
642
715
  return result;
643
716
  }
644
717
 
718
+ _getLicenseApiBase() {
719
+ const c = this.config;
720
+ if (c.apiBaseUrl) return String(c.apiBaseUrl).replace(/\/$/, '');
721
+ if (typeof global.location !== 'undefined' && location && location.origin) return location.origin;
722
+ return LICENSING_SERVER;
723
+ }
724
+
725
+ /**
726
+ * Report action execution to WAB (fills platform analytics in admin).
727
+ */
728
+ _maybeReportUsage(actionName, triggerType, success) {
729
+ try {
730
+ const sessionToken = this._licenseVerified && this._licenseVerified.sessionToken;
731
+ if (!sessionToken || (this.config.features && this.config.features.reportUsage === false)) return;
732
+ const base = this._getLicenseApiBase();
733
+ const payload = JSON.stringify({
734
+ sessionToken,
735
+ actionName,
736
+ triggerType: triggerType || 'unknown',
737
+ success: success !== false
738
+ });
739
+ fetch(`${base}/api/license/track`, {
740
+ method: 'POST',
741
+ headers: { 'Content-Type': 'application/json' },
742
+ body: payload,
743
+ keepalive: true,
744
+ mode: 'cors',
745
+ credentials: 'omit'
746
+ }).catch(function () {});
747
+ } catch (e) { /* ignore */ }
748
+ }
749
+
645
750
  // ── License Verification ────────────────────────────────────────────
646
751
  async _verifyLicense() {
647
752
  try {
648
- const res = await fetch(`${LICENSING_SERVER}/api/license/verify`, {
753
+ const res = await fetch(`${this._getLicenseApiBase()}/api/license/verify`, {
649
754
  method: 'POST',
650
755
  headers: { 'Content-Type': 'application/json' },
651
756
  body: JSON.stringify({
@@ -903,12 +1008,14 @@
903
1008
 
904
1009
  this.events.emit('action:after', { action: actionName, result });
905
1010
  this.logger.log('execute_result', { action: actionName, success: result.success }, 'detailed');
1011
+ this._maybeReportUsage(actionName, action.trigger, !!(result && result.success !== false));
906
1012
  return result;
907
1013
 
908
1014
  } catch (err) {
909
1015
  const error = { success: false, error: err.message };
910
1016
  this.events.emit('error', { action: actionName, error: err.message });
911
1017
  this.logger.log('execute_error', { action: actionName, error: err.message });
1018
+ this._maybeReportUsage(actionName, action.trigger, false);
912
1019
  return error;
913
1020
  }
914
1021
  }
@@ -1090,6 +1197,50 @@
1090
1197
  this.events.emit('action:unregistered', { name });
1091
1198
  }
1092
1199
 
1200
+ // ── Protocol Discovery ──────────────────────────────────────────────
1201
+ discover() {
1202
+ return {
1203
+ wab_version: PROTOCOL_VERSION,
1204
+ runtime_version: VERSION,
1205
+ discovery_document: this._discoveryDoc,
1206
+ page: this.getPageInfo(),
1207
+ actions: this.getActions(),
1208
+ fairness: this._discoveryDoc ? this._discoveryDoc.fairness : null,
1209
+ transport: {
1210
+ js_global: { enabled: true, interface: 'window.AICommands' },
1211
+ bidi: { enabled: true, interface: 'window.__wab_bidi' },
1212
+ noscript: { enabled: !!(this.config.siteId) }
1213
+ }
1214
+ };
1215
+ }
1216
+
1217
+ subscribe(eventName, callback) {
1218
+ if (typeof callback !== 'function') return { success: false, error: 'callback must be a function' };
1219
+ var id = 'sub_' + (++this.security._commandCounter);
1220
+ this._eventSubscriptions.set(id, { event: eventName, callback });
1221
+ this.events.on(eventName, callback);
1222
+ return { success: true, subscriptionId: id, event: eventName };
1223
+ }
1224
+
1225
+ unsubscribe(subscriptionId) {
1226
+ var sub = this._eventSubscriptions.get(subscriptionId);
1227
+ if (!sub) return { success: false, error: 'Subscription not found' };
1228
+ this.events.off(sub.event, sub.callback);
1229
+ this._eventSubscriptions.delete(subscriptionId);
1230
+ return { success: true };
1231
+ }
1232
+
1233
+ ping() {
1234
+ return {
1235
+ pong: true,
1236
+ version: VERSION,
1237
+ protocol: PROTOCOL_VERSION,
1238
+ timestamp: Date.now(),
1239
+ ready: this._ready,
1240
+ locked: this.security.isLocked
1241
+ };
1242
+ }
1243
+
1093
1244
  // ── Discovery / Info ────────────────────────────────────────────────
1094
1245
  getActions(category) {
1095
1246
  if (category) return this.registry.getByCategory(category);
@@ -1167,8 +1318,13 @@
1167
1318
  toJSON() {
1168
1319
  return {
1169
1320
  version: VERSION,
1321
+ protocol: PROTOCOL_VERSION,
1170
1322
  page: this.getPageInfo(),
1171
- actions: this.getActions()
1323
+ actions: this.getActions(),
1324
+ discovery: this._discoveryDoc ? {
1325
+ provider: this._discoveryDoc.provider,
1326
+ fairness: this._discoveryDoc.fairness
1327
+ } : null
1172
1328
  };
1173
1329
  }
1174
1330
 
@@ -1178,6 +1334,7 @@
1178
1334
  return {
1179
1335
  type: 'wab:context',
1180
1336
  version: VERSION,
1337
+ protocol: PROTOCOL_VERSION,
1181
1338
  context: {
1182
1339
  url: location.href,
1183
1340
  title: document.title,
@@ -1192,7 +1349,9 @@
1192
1349
  })),
1193
1350
  permissions: this._getEffectivePermissions(),
1194
1351
  tier: this.getEffectiveTier()
1195
- }
1352
+ },
1353
+ discovery: this._discoveryDoc || null,
1354
+ fairness: this._discoveryDoc ? this._discoveryDoc.fairness : null
1196
1355
  };
1197
1356
  }
1198
1357
 
@@ -1234,6 +1393,24 @@
1234
1393
  case 'wab.getPageInfo':
1235
1394
  return { ...responseBase, result: this.getPageInfo() };
1236
1395
 
1396
+ case 'wab.discover':
1397
+ return { ...responseBase, result: this.discover() };
1398
+
1399
+ case 'wab.authenticate':
1400
+ if (!command.params?.key) {
1401
+ return { id: command.id, error: { code: 'invalid argument', message: 'Agent key required' } };
1402
+ }
1403
+ return { ...responseBase, result: this.authenticate(command.params.key, command.params.meta || {}) };
1404
+
1405
+ case 'wab.subscribe':
1406
+ if (!command.params?.event) {
1407
+ return { id: command.id, error: { code: 'invalid argument', message: 'Event name required' } };
1408
+ }
1409
+ return { ...responseBase, result: this.subscribe(command.params.event, command.params.callback || function() {}) };
1410
+
1411
+ case 'wab.ping':
1412
+ return { ...responseBase, result: this.ping() };
1413
+
1237
1414
  default:
1238
1415
  return { id: command.id, error: { code: 'unknown command', message: `Unknown method: ${command.method}` } };
1239
1416
  }
@@ -1261,18 +1438,72 @@
1261
1438
  global.AICommands = bridge;
1262
1439
  global.WebAgentBridge = WebAgentBridge;
1263
1440
 
1441
+ // WAB Protocol interface
1442
+ global.__wab_protocol = {
1443
+ version: VERSION,
1444
+ protocol: PROTOCOL_VERSION,
1445
+ discover: () => bridge.discover(),
1446
+ ping: () => bridge.ping()
1447
+ };
1448
+
1264
1449
  // WebDriver BiDi compatibility: expose via __wab_bidi channel
1265
1450
  global.__wab_bidi = {
1266
1451
  version: VERSION,
1452
+ protocol: PROTOCOL_VERSION,
1267
1453
  send: async (command) => bridge.executeBiDi(command),
1268
1454
  getContext: () => bridge.toBiDi()
1269
1455
  };
1270
1456
 
1457
+ // Inject NoJS fallback elements for pages that might disable JS later or
1458
+ // for hybrid environments (SSR, partial hydration, headless crawlers).
1459
+ if (config.siteId) {
1460
+ injectNoScriptFallback(config);
1461
+ }
1462
+
1271
1463
  if (typeof CustomEvent !== 'undefined') {
1272
1464
  document.dispatchEvent(new CustomEvent('wab:ready', { detail: { version: VERSION } }));
1273
1465
  }
1274
1466
  }
1275
1467
 
1468
+ function injectNoScriptFallback(config) {
1469
+ try {
1470
+ var siteId = config.siteId;
1471
+ var base = config.apiBaseUrl || '';
1472
+
1473
+ // Add <noscript> block if not already present
1474
+ if (!document.querySelector('noscript [src*="noscript/pixel/' + siteId + '"]')) {
1475
+ var ns = document.createElement('noscript');
1476
+ ns.innerHTML =
1477
+ '<link rel="stylesheet" href="' + base + '/api/noscript/css/' + siteId + '">' +
1478
+ '<img src="' + base + '/api/noscript/pixel/' + siteId + '?action=pageview&t=noscript" width="1" height="1" alt="" style="position:absolute;opacity:0">';
1479
+ document.body.appendChild(ns);
1480
+ }
1481
+
1482
+ // Expose noscript endpoints on the bridge for AI agents
1483
+ global.__wab_noscript = {
1484
+ pixel: base + '/api/noscript/pixel/' + siteId,
1485
+ css: base + '/api/noscript/css/' + siteId,
1486
+ bridge: base + '/api/noscript/bridge/' + siteId,
1487
+ embed: base + '/api/noscript/embed/' + siteId,
1488
+ serverTrack: base + '/api/noscript/server-track',
1489
+ status: base + '/api/noscript/status/' + siteId
1490
+ };
1491
+
1492
+ // Add meta tags for crawlers/agents that read HTML
1493
+ if (!document.querySelector('meta[name="wab:noscript"]')) {
1494
+ var meta1 = document.createElement('meta');
1495
+ meta1.name = 'wab:noscript';
1496
+ meta1.content = 'true';
1497
+ document.head.appendChild(meta1);
1498
+
1499
+ var meta2 = document.createElement('meta');
1500
+ meta2.name = 'wab:bridge';
1501
+ meta2.content = base + '/api/noscript/bridge/' + siteId;
1502
+ document.head.appendChild(meta2);
1503
+ }
1504
+ } catch (_) {}
1505
+ }
1506
+
1276
1507
  if (document.readyState === 'loading') {
1277
1508
  document.addEventListener('DOMContentLoaded', autoInit);
1278
1509
  } else {
package/sdk/index.js CHANGED
@@ -149,6 +149,30 @@ class WABAgent {
149
149
  return results;
150
150
  }
151
151
 
152
+ /**
153
+ * Get the WAB discovery document for the current page.
154
+ * @returns {Promise<object>}
155
+ */
156
+ async discover() {
157
+ if (this.useBiDi) {
158
+ const result = await this._bidiSend('wab.discover');
159
+ return result.result || result;
160
+ }
161
+ return this.page.evaluate(() => window.AICommands.discover());
162
+ }
163
+
164
+ /**
165
+ * Ping the bridge for a health check.
166
+ * @returns {Promise<object>}
167
+ */
168
+ async ping() {
169
+ if (this.useBiDi) {
170
+ const result = await this._bidiSend('wab.ping');
171
+ return result.result || result;
172
+ }
173
+ return this.page.evaluate(() => window.AICommands.ping());
174
+ }
175
+
152
176
  /**
153
177
  * Get BiDi context (only available when useBiDi is true).
154
178
  * @returns {Promise<object>}
@@ -157,6 +181,18 @@ class WABAgent {
157
181
  return this.page.evaluate(() => window.__wab_bidi.getContext());
158
182
  }
159
183
 
184
+ /**
185
+ * Get the WAB protocol interface data.
186
+ * @returns {Promise<object>}
187
+ */
188
+ async getProtocolInfo() {
189
+ return this.page.evaluate(() => window.__wab_protocol ? {
190
+ version: window.__wab_protocol.version,
191
+ protocol: window.__wab_protocol.protocol,
192
+ discovery: window.__wab_protocol.discover()
193
+ } : null);
194
+ }
195
+
160
196
  /** @private */
161
197
  async _bidiSend(method, params = {}) {
162
198
  const cmd = { id: ++this._biDiId, method, params };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Central JWT and startup secret checks.
3
+ * User tokens and admin tokens use different secrets and audiences in production.
4
+ */
5
+
6
+ const jwt = require('jsonwebtoken');
7
+
8
+ const JWT_ISSUER = 'wab';
9
+ const JWT_AUD_USER = 'wab:user';
10
+ const JWT_AUD_ADMIN = 'wab:admin';
11
+
12
+ const jwtVerifyUser = { issuer: JWT_ISSUER, audience: JWT_AUD_USER };
13
+ const jwtVerifyAdmin = { issuer: JWT_ISSUER, audience: JWT_AUD_ADMIN };
14
+
15
+ function isTest() {
16
+ return process.env.NODE_ENV === 'test';
17
+ }
18
+
19
+ function isProd() {
20
+ return process.env.NODE_ENV === 'production';
21
+ }
22
+
23
+ function assertSecretsAtStartup() {
24
+ if (isTest()) return;
25
+ if (isProd()) {
26
+ if (!process.env.JWT_SECRET) {
27
+ throw new Error('FATAL: JWT_SECRET is required in production');
28
+ }
29
+ if (!process.env.JWT_SECRET_ADMIN) {
30
+ throw new Error('FATAL: JWT_SECRET_ADMIN is required in production');
31
+ }
32
+ }
33
+ }
34
+
35
+ function getJwtUserSecret() {
36
+ if (isTest()) {
37
+ return process.env.JWT_SECRET || 'test-secret-key-for-testing';
38
+ }
39
+ if (isProd()) {
40
+ return process.env.JWT_SECRET;
41
+ }
42
+ return process.env.JWT_SECRET || 'dev-user-secret-change-in-development';
43
+ }
44
+
45
+ function getJwtAdminSecret() {
46
+ if (isTest()) {
47
+ return process.env.JWT_SECRET_ADMIN || process.env.JWT_SECRET || 'test-secret-key-for-testing-admin';
48
+ }
49
+ if (isProd()) {
50
+ return process.env.JWT_SECRET_ADMIN;
51
+ }
52
+ return process.env.JWT_SECRET_ADMIN || process.env.JWT_SECRET || 'dev-admin-secret-change-in-development';
53
+ }
54
+
55
+ function signUserToken(payload, options = {}) {
56
+ return jwt.sign(
57
+ { ...payload },
58
+ getJwtUserSecret(),
59
+ { expiresIn: options.expiresIn || '7d', issuer: JWT_ISSUER, audience: JWT_AUD_USER }
60
+ );
61
+ }
62
+
63
+ function signAdminToken(payload, options = {}) {
64
+ return jwt.sign(
65
+ { ...payload },
66
+ getJwtAdminSecret(),
67
+ { expiresIn: options.expiresIn || '12h', issuer: JWT_ISSUER, audience: JWT_AUD_ADMIN }
68
+ );
69
+ }
70
+
71
+ function verifyUserToken(token) {
72
+ return jwt.verify(token, getJwtUserSecret(), jwtVerifyUser);
73
+ }
74
+
75
+ function verifyAdminToken(token) {
76
+ return jwt.verify(token, getJwtAdminSecret(), jwtVerifyAdmin);
77
+ }
78
+
79
+ module.exports = {
80
+ assertSecretsAtStartup,
81
+ getJwtUserSecret,
82
+ getJwtAdminSecret,
83
+ signUserToken,
84
+ signAdminToken,
85
+ verifyUserToken,
86
+ verifyAdminToken,
87
+ JWT_ISSUER,
88
+ JWT_AUD_USER,
89
+ JWT_AUD_ADMIN,
90
+ jwtVerifyUser,
91
+ jwtVerifyAdmin
92
+ };