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/index.js
CHANGED
|
@@ -1,827 +1,1193 @@
|
|
|
1
|
-
require('dotenv').config();
|
|
2
|
-
|
|
3
|
-
const { assertSecretsAtStartup } = require('./config/secrets');
|
|
4
|
-
assertSecretsAtStartup();
|
|
5
|
-
|
|
6
|
-
const express = require('express');
|
|
7
|
-
const http = require('http');
|
|
8
|
-
const cors = require('cors');
|
|
9
|
-
const helmet = require('helmet');
|
|
10
|
-
const rateLimit = require('express-rate-limit');
|
|
11
|
-
const path = require('path');
|
|
12
|
-
const { setupWebSocket } = require('./ws');
|
|
13
|
-
const { runMigrations } = require('./utils/migrate');
|
|
14
|
-
const {
|
|
15
|
-
const {
|
|
16
|
-
const {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
"
|
|
124
|
-
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
res.
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
app.use((
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
res.set('
|
|
251
|
-
res.set('
|
|
252
|
-
res.set('
|
|
253
|
-
res.set('X-
|
|
254
|
-
res.set('Content-
|
|
255
|
-
res.set('
|
|
256
|
-
res.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
app.
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
//
|
|
369
|
-
app.get('
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
res.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
app.
|
|
428
|
-
res.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
res.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
res.
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
res.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
app.get('/
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
app.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
app.
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
app.
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
app.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
app.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
//
|
|
584
|
-
|
|
585
|
-
app.
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
app.
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
app.get('/
|
|
605
|
-
res.sendFile(path.join(__dirname, '..', 'public', '
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
// /
|
|
626
|
-
app.get('/
|
|
627
|
-
res.sendFile(path.join(__dirname, '..', 'public', '
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
//
|
|
634
|
-
app.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
})
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
})
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
})
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
//
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1
|
+
require('dotenv').config();
|
|
2
|
+
|
|
3
|
+
const { assertSecretsAtStartup } = require('./config/secrets');
|
|
4
|
+
assertSecretsAtStartup();
|
|
5
|
+
|
|
6
|
+
const express = require('express');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const cors = require('cors');
|
|
9
|
+
const helmet = require('helmet');
|
|
10
|
+
const rateLimit = require('express-rate-limit');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { setupWebSocket } = require('./ws');
|
|
13
|
+
const { runMigrations } = require('./utils/migrate');
|
|
14
|
+
const { safeFetch } = require('./utils/safe-fetch');
|
|
15
|
+
const { maybeBootstrapAdmin, db } = require('./models/db');
|
|
16
|
+
const { initSearchEngine, search, getSuggestions, getTrendingSearches, getSearchStats, purgeOldCache } = require('./services/search-engine');
|
|
17
|
+
const { processMessage: agentChat } = require('./services/agent-chat');
|
|
18
|
+
const agentTasks = require('./services/agent-tasks');
|
|
19
|
+
const { cluster } = require('./services/cluster');
|
|
20
|
+
|
|
21
|
+
const authRoutes = require('./routes/auth');
|
|
22
|
+
const apiRoutes = require('./routes/api');
|
|
23
|
+
const licenseRoutes = require('./routes/license');
|
|
24
|
+
const adminRoutes = require('./routes/admin');
|
|
25
|
+
const billingRoutes = require('./routes/billing');
|
|
26
|
+
const geniusGateway = require('./routes/genius-gateway');
|
|
27
|
+
const sovereignRoutes = require('./routes/sovereign');
|
|
28
|
+
const meshRoutes = require('./routes/mesh');
|
|
29
|
+
const commanderRoutes = require('./routes/commander');
|
|
30
|
+
const adsRoutes = require('./routes/ads');
|
|
31
|
+
const wabApiRoutes = require('./routes/wab-api');
|
|
32
|
+
const noscriptRoutes = require('./routes/noscript');
|
|
33
|
+
const discoveryRoutes = require('./routes/discovery');
|
|
34
|
+
const providerRoutes = require('./routes/providers');
|
|
35
|
+
const governanceRoutes = require('./routes/governance');
|
|
36
|
+
const premiumRoutes = require('./routes/premium');
|
|
37
|
+
const adminPremiumRoutes = require('./routes/admin-premium');
|
|
38
|
+
const workspaceRoutes = require('./routes/agent-workspace');
|
|
39
|
+
const universalRoutes = require('./routes/universal');
|
|
40
|
+
const runtimeRoutes = require('./routes/runtime');
|
|
41
|
+
const demoShowcaseRoutes = require('./routes/demo-showcase');
|
|
42
|
+
const demoStoreRoutes = require('./routes/demo-store');
|
|
43
|
+
const gatewayRoutes = require('./routes/gateway');
|
|
44
|
+
let growthRoutes;
|
|
45
|
+
try { growthRoutes = require('./routes/growth'); } catch { growthRoutes = require('express').Router(); }
|
|
46
|
+
const { handleWebhookRequest } = require('./services/stripe');
|
|
47
|
+
const { runtime } = require('./runtime');
|
|
48
|
+
|
|
49
|
+
const app = express();
|
|
50
|
+
const PORT = process.env.PORT || 3000;
|
|
51
|
+
|
|
52
|
+
app.set('trust proxy', 1);
|
|
53
|
+
|
|
54
|
+
const corsOrigins = (process.env.ALLOWED_ORIGINS
|
|
55
|
+
|| 'http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173')
|
|
56
|
+
.split(',')
|
|
57
|
+
.map((s) => s.trim())
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
|
|
60
|
+
app.use(
|
|
61
|
+
cors({
|
|
62
|
+
origin(origin, callback) {
|
|
63
|
+
if (!origin) return callback(null, true);
|
|
64
|
+
if (corsOrigins.includes(origin)) return callback(null, true);
|
|
65
|
+
if (process.env.NODE_ENV !== 'production' && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(origin)) {
|
|
66
|
+
return callback(null, true);
|
|
67
|
+
}
|
|
68
|
+
return callback(null, false);
|
|
69
|
+
},
|
|
70
|
+
credentials: true
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const scriptSrc = process.env.CSP_ALLOW_UNSAFE_INLINE === 'false'
|
|
75
|
+
? ["'self'", 'https://unpkg.com', 'https://cdn.jsdelivr.net']
|
|
76
|
+
: ["'self'", "'unsafe-inline'", 'https://unpkg.com', 'https://cdn.jsdelivr.net'];
|
|
77
|
+
const styleSrc = process.env.CSP_ALLOW_UNSAFE_INLINE === 'false'
|
|
78
|
+
? ["'self'"]
|
|
79
|
+
: ["'self'", "'unsafe-inline'"];
|
|
80
|
+
|
|
81
|
+
// Per-request CSP nonce — exposed as res.locals.cspNonce for new pages opting into strict CSP.
|
|
82
|
+
app.use((req, res, next) => {
|
|
83
|
+
res.locals.cspNonce = require('crypto').randomBytes(16).toString('base64');
|
|
84
|
+
next();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// CSP — tightened: HTTPS-only iframes, upgrade-insecure-requests, report endpoint.
|
|
88
|
+
const cspReportUri = '/api/security/csp-report';
|
|
89
|
+
app.use(
|
|
90
|
+
helmet({
|
|
91
|
+
contentSecurityPolicy: {
|
|
92
|
+
directives: {
|
|
93
|
+
defaultSrc: ["'self'"],
|
|
94
|
+
// NOTE: Adding a nonce alongside 'unsafe-inline' makes browsers ignore
|
|
95
|
+
// 'unsafe-inline' (CSP3 spec). All existing public/admin pages still
|
|
96
|
+
// rely on inline <script> blocks, so we keep 'unsafe-inline' enforced
|
|
97
|
+
// here and use the Report-Only policy below to track nonce migration.
|
|
98
|
+
scriptSrc: scriptSrc,
|
|
99
|
+
scriptSrcAttr: [...scriptSrc, "'unsafe-hashes'"],
|
|
100
|
+
styleSrc: [...styleSrc, 'https://fonts.googleapis.com'],
|
|
101
|
+
imgSrc: ["'self'", 'data:', 'https:'],
|
|
102
|
+
connectSrc: ["'self'", 'https:', 'ws:', 'wss:'],
|
|
103
|
+
fontSrc: ["'self'", 'https://fonts.gstatic.com', 'https:', 'data:'],
|
|
104
|
+
frameSrc: ["'self'", 'https:'],
|
|
105
|
+
frameAncestors: ["'none'"],
|
|
106
|
+
objectSrc: ["'none'"],
|
|
107
|
+
baseUri: ["'self'"],
|
|
108
|
+
formAction: ["'self'"],
|
|
109
|
+
upgradeInsecureRequests: process.env.NODE_ENV === 'production' ? [] : null,
|
|
110
|
+
reportUri: [cspReportUri]
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
crossOriginEmbedderPolicy: false,
|
|
114
|
+
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Companion strict Report-Only CSP — surfaces every inline-script violation
|
|
119
|
+
// without breaking existing pages, so we can migrate page-by-page to nonces.
|
|
120
|
+
app.use((req, res, next) => {
|
|
121
|
+
const nonce = res.locals.cspNonce;
|
|
122
|
+
const strict = [
|
|
123
|
+
"default-src 'self'",
|
|
124
|
+
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
|
|
125
|
+
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
|
126
|
+
"img-src 'self' data: https:",
|
|
127
|
+
"connect-src 'self' https: wss:",
|
|
128
|
+
"font-src 'self' https://fonts.gstatic.com data:",
|
|
129
|
+
"frame-src 'self' https:",
|
|
130
|
+
"frame-ancestors 'none'",
|
|
131
|
+
"object-src 'none'",
|
|
132
|
+
"base-uri 'self'",
|
|
133
|
+
"form-action 'self'",
|
|
134
|
+
"upgrade-insecure-requests",
|
|
135
|
+
`report-uri ${cspReportUri}`
|
|
136
|
+
].join('; ');
|
|
137
|
+
res.setHeader('Content-Security-Policy-Report-Only', strict);
|
|
138
|
+
next();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// CSP violation report sink (capped, in-memory ring buffer + console).
|
|
142
|
+
const _cspReports = [];
|
|
143
|
+
app.post('/api/security/csp-report', express.json({ type: ['application/csp-report', 'application/json'], limit: '32kb' }), (req, res) => {
|
|
144
|
+
const report = req.body && (req.body['csp-report'] || req.body);
|
|
145
|
+
if (report) {
|
|
146
|
+
_cspReports.push({ at: new Date().toISOString(), ip: req.ip, report });
|
|
147
|
+
if (_cspReports.length > 500) _cspReports.shift();
|
|
148
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
149
|
+
console.warn('[CSP]', report['violated-directive'] || report.violatedDirective, '→', report['blocked-uri'] || report.blockedURI);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
res.status(204).end();
|
|
153
|
+
});
|
|
154
|
+
app.get('/api/security/csp-report/recent', (req, res) => {
|
|
155
|
+
res.json({ count: _cspReports.length, reports: _cspReports.slice(-50) });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ── Reward-guard + cross-site redactor admin views (token-gated) ──
|
|
159
|
+
function _adminAuth(req, res, next) {
|
|
160
|
+
const { safeEqual } = require('./utils/safe-compare');
|
|
161
|
+
const want = process.env.WAB_ADMIN_TOKEN;
|
|
162
|
+
if (!want) return res.status(503).json({ error: 'WAB_ADMIN_TOKEN not configured' });
|
|
163
|
+
const got = req.headers['x-wab-admin-token'] || req.query.token;
|
|
164
|
+
if (!safeEqual(got, want)) return res.status(401).json({ error: 'admin token required' });
|
|
165
|
+
next();
|
|
166
|
+
}
|
|
167
|
+
app.get('/api/security/reward-audit/recent', _adminAuth, (req, res) => {
|
|
168
|
+
try {
|
|
169
|
+
const guard = require('./security/reward-guard');
|
|
170
|
+
res.json({ stats: guard.getStats(), recent: guard.getRecentAudits(50, req.query.decision || null) });
|
|
171
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
172
|
+
});
|
|
173
|
+
app.get('/api/security/cross-site-transfers/recent', _adminAuth, (req, res) => {
|
|
174
|
+
try {
|
|
175
|
+
const r = require('./security/cross-site-redactor');
|
|
176
|
+
res.json({ recent: r.getRecentTransfers(50, req.query.from || null) });
|
|
177
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
178
|
+
});
|
|
179
|
+
app.get('/api/security/url-policy/recent', _adminAuth, (req, res) => {
|
|
180
|
+
try {
|
|
181
|
+
const p = require('./security/url-policy');
|
|
182
|
+
res.json({ recent: p.getRecentAudits(50, req.query.decision || null) });
|
|
183
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
app.post('/api/billing/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
187
|
+
try {
|
|
188
|
+
await handleWebhookRequest(req);
|
|
189
|
+
res.json({ received: true });
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.error('Webhook error:', err.message);
|
|
192
|
+
res.status(400).json({ error: err.message });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
app.use(express.json());
|
|
197
|
+
|
|
198
|
+
// Global JSON parse error handler (catches malformed JSON from bots/scanners)
|
|
199
|
+
app.use((err, req, res, next) => {
|
|
200
|
+
if (err.type === 'entity.parse.failed' || err instanceof SyntaxError) {
|
|
201
|
+
return res.status(400).json({ error: 'Invalid JSON', details: err.message });
|
|
202
|
+
}
|
|
203
|
+
next(err);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Global error handler — catches all unhandled route errors
|
|
207
|
+
// global-error-handler
|
|
208
|
+
app.use((err, req, res, next) => {
|
|
209
|
+
const status = err.status || err.statusCode || 500;
|
|
210
|
+
const message = err.message || 'Internal Server Error';
|
|
211
|
+
if (status >= 500) {
|
|
212
|
+
console.error('[server] Unhandled error:', err.message, err.stack?.split('\n')[1] || '');
|
|
213
|
+
}
|
|
214
|
+
if (!res.headersSent) {
|
|
215
|
+
res.status(status).json({ error: message });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const apiLimiter = rateLimit({
|
|
220
|
+
windowMs: 15 * 60 * 1000,
|
|
221
|
+
max: 200,
|
|
222
|
+
standardHeaders: true,
|
|
223
|
+
legacyHeaders: false,
|
|
224
|
+
message: { error: 'Too many requests, please try again later' }
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const licenseLimiter = rateLimit({
|
|
228
|
+
windowMs: 60 * 1000,
|
|
229
|
+
max: 120,
|
|
230
|
+
standardHeaders: true,
|
|
231
|
+
legacyHeaders: false,
|
|
232
|
+
keyGenerator: (req) => {
|
|
233
|
+
const key = req.body?.licenseKey || req.body?.siteId || req.ip;
|
|
234
|
+
return `${req.ip}:${key}`;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Visitor analytics — record every public page hit (HTML routes only) before
|
|
239
|
+
// they're served by express.static. Skips assets, /api, /admin and other noise.
|
|
240
|
+
try {
|
|
241
|
+
const visitorTracker = require('./services/visitor-tracker');
|
|
242
|
+
app.use(visitorTracker.middleware());
|
|
243
|
+
} catch (e) {
|
|
244
|
+
console.warn('[wab] visitor-tracker disabled:', e.message);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Whitepaper guard — must run BEFORE express.static so we can apply strict headers
|
|
248
|
+
// and intercept both /whitepaper and /whitepaper.html with the same protections.
|
|
249
|
+
const whitepaperHandler = (req, res) => {
|
|
250
|
+
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
251
|
+
res.set('Pragma', 'no-cache');
|
|
252
|
+
res.set('Expires', '0');
|
|
253
|
+
res.set('X-Frame-Options', 'DENY');
|
|
254
|
+
res.set('X-Content-Type-Options', 'nosniff');
|
|
255
|
+
res.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
256
|
+
res.set('X-Robots-Tag', 'index, follow, noarchive, nosnippet, noimageindex');
|
|
257
|
+
res.set('Content-Security-Policy', "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'");
|
|
258
|
+
res.set('X-Copyright', 'All Rights Reserved (c) 2026 Web Agent Bridge - Reproduction Prohibited');
|
|
259
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'whitepaper.html'));
|
|
260
|
+
};
|
|
261
|
+
app.get(['/whitepaper', '/whitepaper.html'], whitepaperHandler);
|
|
262
|
+
|
|
263
|
+
// WAB Trust artifact (signed Ed25519 wab.json) — served explicitly because
|
|
264
|
+
// express.static skips dotfile directories like /.well-known by default.
|
|
265
|
+
// We compose: signed trust payload (untouched, from disk) + a top-level
|
|
266
|
+
// `actions` map so structural-agent platforms (e.g. The Code Genius) can
|
|
267
|
+
// discover and execute the public API surface without DOM scraping.
|
|
268
|
+
const WAB_ACTIONS_MANIFEST = {
|
|
269
|
+
v: '1.0',
|
|
270
|
+
name: 'Web Agent Bridge',
|
|
271
|
+
description: 'Structural API surface for agents — registry discovery, trust verification, reputation queries, and ShieldQR scanning.',
|
|
272
|
+
endpoint: 'https://webagentbridge.com',
|
|
273
|
+
actions: {
|
|
274
|
+
discover_sites: {
|
|
275
|
+
id: 'discover_sites',
|
|
276
|
+
description: 'Search the WAB registry for sites by intent tag, ring, or domain pattern.',
|
|
277
|
+
url: '/api/registry/discover',
|
|
278
|
+
method: 'GET',
|
|
279
|
+
inputs: {
|
|
280
|
+
intent: { type: 'string', required: false, description: 'Intent tag to filter by (e.g. "shop", "news")' },
|
|
281
|
+
ring: { type: 'number', required: false, description: 'Minimum trust ring (0–4)' },
|
|
282
|
+
limit: { type: 'number', required: false, description: 'Max results (default 20)' },
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
list_sites: {
|
|
286
|
+
id: 'list_sites',
|
|
287
|
+
description: 'List all active WAB-enabled sites in the public registry.',
|
|
288
|
+
url: '/api/registry/list',
|
|
289
|
+
method: 'GET',
|
|
290
|
+
inputs: {
|
|
291
|
+
limit: { type: 'number', required: false, description: 'Page size (default 50)' },
|
|
292
|
+
offset: { type: 'number', required: false, description: 'Page offset' },
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
get_registry_stats: {
|
|
296
|
+
id: 'get_registry_stats',
|
|
297
|
+
description: 'Get aggregated stats about the WAB network (total sites, rings distribution, top intents).',
|
|
298
|
+
url: '/api/registry/stats',
|
|
299
|
+
method: 'GET',
|
|
300
|
+
},
|
|
301
|
+
suggest_peers: {
|
|
302
|
+
id: 'suggest_peers',
|
|
303
|
+
description: 'Get peer-site suggestions for cross-discovery (gossip protocol).',
|
|
304
|
+
url: '/api/registry/suggest',
|
|
305
|
+
method: 'GET',
|
|
306
|
+
inputs: {
|
|
307
|
+
domain: { type: 'string', required: false, description: 'Seed domain for similarity search' },
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
list_plans: {
|
|
311
|
+
id: 'list_plans',
|
|
312
|
+
description: 'List all available WAB subscription plans with prices and features.',
|
|
313
|
+
url: '/api/plans',
|
|
314
|
+
method: 'GET',
|
|
315
|
+
},
|
|
316
|
+
get_plan: {
|
|
317
|
+
id: 'get_plan',
|
|
318
|
+
description: 'Fetch a specific plan by ID.',
|
|
319
|
+
url: '/api/plans/:id',
|
|
320
|
+
method: 'GET',
|
|
321
|
+
inputs: {
|
|
322
|
+
id: { type: 'string', required: true, description: 'Plan ID' },
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
scan_qr: {
|
|
326
|
+
id: 'scan_qr',
|
|
327
|
+
description: 'Verify a ShieldQR code — returns trust ring, issuer, and risk score for the encoded URL.',
|
|
328
|
+
url: '/api/shieldqr/scan',
|
|
329
|
+
method: 'POST',
|
|
330
|
+
inputs: {
|
|
331
|
+
url: { type: 'string', required: true, description: 'URL decoded from the QR' },
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
recent_scans: {
|
|
335
|
+
id: 'recent_scans',
|
|
336
|
+
description: 'Get the most recent public ShieldQR scan reports.',
|
|
337
|
+
url: '/api/shieldqr/recent',
|
|
338
|
+
method: 'GET',
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
privacy: {
|
|
342
|
+
allowed: ['registry queries', 'public trust metadata', 'plan listings'],
|
|
343
|
+
disallowed: ['admin endpoints', 'billing webhooks', 'individual user data'],
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
let _trustPayloadCache = null;
|
|
348
|
+
function loadTrustPayload() {
|
|
349
|
+
if (_trustPayloadCache) return _trustPayloadCache;
|
|
350
|
+
try {
|
|
351
|
+
const raw = require('fs').readFileSync(
|
|
352
|
+
path.join(__dirname, '..', 'public', '.well-known', 'wab.json'), 'utf8');
|
|
353
|
+
_trustPayloadCache = JSON.parse(raw);
|
|
354
|
+
} catch { _trustPayloadCache = {}; }
|
|
355
|
+
return _trustPayloadCache;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
app.get('/.well-known/wab.json', (req, res) => {
|
|
359
|
+
res.set('Cache-Control', 'public, max-age=300');
|
|
360
|
+
res.set('Access-Control-Allow-Origin', '*');
|
|
361
|
+
res.type('application/json');
|
|
362
|
+
// Merge signed trust artifact (untouched) with action manifest
|
|
363
|
+
res.json({ ...loadTrustPayload(), ...WAB_ACTIONS_MANIFEST });
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// WAB Beacon — /.wab — compact machine-readable trust signal for AI agents.
|
|
367
|
+
// Agents following the Gossip / Spider Protocol read this to learn:
|
|
368
|
+
// ring, score, manifest, registry, and a list of peer WAB-enabled sites.
|
|
369
|
+
app.get('/.wab', (req, res) => {
|
|
370
|
+
const { currentPublicKey, currentFingerprint } = require('./routes/notary');
|
|
371
|
+
const registry = require('./routes/registry');
|
|
372
|
+
// top 10 verified or highest-scored registry entries as peer hints
|
|
373
|
+
let peers = [];
|
|
374
|
+
try {
|
|
375
|
+
const regData = JSON.parse(
|
|
376
|
+
require('fs').readFileSync(
|
|
377
|
+
require('path').join(__dirname, '..', 'data', 'registry.json'), 'utf8')
|
|
378
|
+
);
|
|
379
|
+
if (Array.isArray(regData)) {
|
|
380
|
+
peers = regData
|
|
381
|
+
.filter(e => e.active !== false)
|
|
382
|
+
.sort((a, b) => (b.score || 0) - (a.score || 0) || (b.trust_ring || 0) - (a.trust_ring || 0))
|
|
383
|
+
.slice(0, 10)
|
|
384
|
+
.map(e => ({ domain: e.domain, trust_ring: e.trust_ring, intent_tags: (e.intent_tags || []).slice(0, 5) }));
|
|
385
|
+
}
|
|
386
|
+
} catch { /* registry may be empty */ }
|
|
387
|
+
res.set('Cache-Control', 'public, max-age=120');
|
|
388
|
+
res.set('Access-Control-Allow-Origin', '*');
|
|
389
|
+
res.json({
|
|
390
|
+
v: 1,
|
|
391
|
+
protocol: 'wab/3.19',
|
|
392
|
+
ring: 4,
|
|
393
|
+
score: 94,
|
|
394
|
+
manifest: 'https://webagentbridge.com/.well-known/wab.json',
|
|
395
|
+
registry: 'https://webagentbridge.com/api/registry/discover',
|
|
396
|
+
spider_report: 'https://webagentbridge.com/api/registry/report',
|
|
397
|
+
notary_fp: typeof currentFingerprint === 'function' ? currentFingerprint() : null,
|
|
398
|
+
next: peers,
|
|
399
|
+
generated_at: new Date().toISOString(),
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Discovery headers — advertised on every HTML/JSON response so AI agents,
|
|
404
|
+
// crawlers and MCP-style clients can find the WAB manifest and llms.txt
|
|
405
|
+
// without having to scrape. Mirrors the Mintlify X-Llms-Txt convention and
|
|
406
|
+
// adds X-WAB-Discovery alongside RFC 8288 Link rels.
|
|
407
|
+
// Phase 8: X-WAB-Enabled + X-WAB-Trust-Ring + X-WAB-Score + X-WAB-Registry
|
|
408
|
+
// signal preference to any agent that reads HTTP response headers.
|
|
409
|
+
// Phase 9: X-WAB-Next-Sites — WAB DNA header, top peers from registry (cached 60s)
|
|
410
|
+
// so every HTTP response carries network topology. This is the WAB Gossip DNA.
|
|
411
|
+
let _nextSitesCache = { value: '', exp: 0 };
|
|
412
|
+
function getNextSitesDNA() {
|
|
413
|
+
if (Date.now() < _nextSitesCache.exp) return _nextSitesCache.value;
|
|
414
|
+
try {
|
|
415
|
+
const reg = JSON.parse(
|
|
416
|
+
require('fs').readFileSync(path.join(__dirname, '..', 'data', 'registry.json'), 'utf8')
|
|
417
|
+
);
|
|
418
|
+
const top = Array.isArray(reg)
|
|
419
|
+
? reg.filter(e => e.active !== false)
|
|
420
|
+
.sort((a, b) => (b.trust_ring || 0) - (a.trust_ring || 0) || (b.score || 0) - (a.score || 0))
|
|
421
|
+
.slice(0, 5).map(e => e.domain).join(', ')
|
|
422
|
+
: '';
|
|
423
|
+
_nextSitesCache = { value: top, exp: Date.now() + 60000 };
|
|
424
|
+
return top;
|
|
425
|
+
} catch { return ''; }
|
|
426
|
+
}
|
|
427
|
+
app.use((req, res, next) => {
|
|
428
|
+
res.set('X-WAB-Discovery', '/.well-known/wab.json');
|
|
429
|
+
res.set('X-WAB-Enabled', 'true');
|
|
430
|
+
res.set('X-WAB-Trust-Ring', '4');
|
|
431
|
+
res.set('X-WAB-Score', '94');
|
|
432
|
+
res.set('X-WAB-Registry', '/api/registry/discover');
|
|
433
|
+
res.set('X-Llms-Txt', '/llms.txt');
|
|
434
|
+
const nextSites = getNextSitesDNA();
|
|
435
|
+
if (nextSites) res.set('X-WAB-Next-Sites', nextSites);
|
|
436
|
+
res.append('Link', '</.well-known/wab.json>; rel="wab-manifest"; type="application/json"');
|
|
437
|
+
res.append('Link', '</.wab>; rel="wab-beacon"; type="application/json"');
|
|
438
|
+
res.append('Link', '</llms.txt>; rel="llms-txt"; type="text/plain"');
|
|
439
|
+
res.append('Link', '</llms-full.txt>; rel="llms-full-txt"; type="text/plain"');
|
|
440
|
+
next();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// WAB compliance badge — embeddable SVG. Usage:
|
|
444
|
+
// <img src="https://webagentbridge.com/badge/example.com.svg">
|
|
445
|
+
// Returns green/amber/red based on whether the domain publishes a reachable
|
|
446
|
+
// /.well-known/wab.json (and optionally an Ed25519 signature). Result is
|
|
447
|
+
// cached in-process for 10 minutes to keep this endpoint cheap and DoS-safe.
|
|
448
|
+
const _badgeCache = new Map();
|
|
449
|
+
function _badgeSvg(label, value, color) {
|
|
450
|
+
const labelW = 56;
|
|
451
|
+
const valueW = Math.max(48, value.length * 7 + 14);
|
|
452
|
+
const total = labelW + valueW;
|
|
453
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${total}" height="20" role="img" aria-label="${label}: ${value}">
|
|
454
|
+
<linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-opacity=".3"/><stop offset="1" stop-opacity=".5"/></linearGradient>
|
|
455
|
+
<mask id="m"><rect width="${total}" height="20" rx="3" fill="#fff"/></mask>
|
|
456
|
+
<g mask="url(#m)">
|
|
457
|
+
<rect width="${labelW}" height="20" fill="#1f2937"/>
|
|
458
|
+
<rect x="${labelW}" width="${valueW}" height="20" fill="${color}"/>
|
|
459
|
+
<rect width="${total}" height="20" fill="url(#b)"/>
|
|
460
|
+
</g>
|
|
461
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
|
462
|
+
<text x="${labelW/2}" y="14">${label}</text>
|
|
463
|
+
<text x="${labelW + valueW/2}" y="14">${value}</text>
|
|
464
|
+
</g>
|
|
465
|
+
</svg>`;
|
|
466
|
+
}
|
|
467
|
+
app.get('/badge/:domain', async (req, res) => {
|
|
468
|
+
let host = String(req.params.domain || '').replace(/\.svg$/i, '').trim().toLowerCase();
|
|
469
|
+
host = host.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
|
470
|
+
if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(host)) {
|
|
471
|
+
res.type('image/svg+xml').set('Cache-Control', 'public, max-age=60');
|
|
472
|
+
return res.send(_badgeSvg('WAB', 'invalid', '#9ca3af'));
|
|
473
|
+
}
|
|
474
|
+
const cached = _badgeCache.get(host);
|
|
475
|
+
if (cached && cached.exp > Date.now()) {
|
|
476
|
+
res.type('image/svg+xml').set('Cache-Control', 'public, max-age=600').set('Access-Control-Allow-Origin', '*');
|
|
477
|
+
return res.send(cached.svg);
|
|
478
|
+
}
|
|
479
|
+
let value = 'unknown', color = '#9ca3af';
|
|
480
|
+
try {
|
|
481
|
+
// SSRF-safe: safeFetch resolves DNS and blocks private/loopback/link-local IPs,
|
|
482
|
+
// re-validates on each redirect hop. This defeats nip.io-style tricks where
|
|
483
|
+
// the hostname "looks public" but resolves to 127.0.0.1.
|
|
484
|
+
const r = await safeFetch(`https://${host}/.well-known/wab.json`, {}, { timeoutMs: 3500, maxBytes: 256 * 1024, requireHttps: true });
|
|
485
|
+
if (r.ok) {
|
|
486
|
+
const j = await r.json().catch(() => null);
|
|
487
|
+
const signed = !!(j && (j.signature || (j.trust && j.trust.signed)));
|
|
488
|
+
value = signed ? 'verified' : 'enabled';
|
|
489
|
+
color = signed ? '#10b981' : '#f59e0b';
|
|
490
|
+
} else {
|
|
491
|
+
value = 'missing'; color = '#ef4444';
|
|
492
|
+
}
|
|
493
|
+
} catch (_) {
|
|
494
|
+
value = 'missing'; color = '#ef4444';
|
|
495
|
+
}
|
|
496
|
+
const svg = _badgeSvg('WAB', value, color);
|
|
497
|
+
_badgeCache.set(host, { svg, exp: Date.now() + 10 * 60 * 1000 });
|
|
498
|
+
res.type('image/svg+xml').set('Cache-Control', 'public, max-age=600').set('Access-Control-Allow-Origin', '*');
|
|
499
|
+
return res.send(svg);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
app.use(express.static(path.join(__dirname, '..', 'public'), {
|
|
503
|
+
setHeaders(res, filePath) {
|
|
504
|
+
if (filePath.endsWith('.html')) {
|
|
505
|
+
res.setHeader('Cache-Control', 'no-cache, must-revalidate');
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}));
|
|
509
|
+
app.use('/script', express.static(path.join(__dirname, '..', 'script')));
|
|
510
|
+
|
|
511
|
+
app.use('/api/auth', apiLimiter, authRoutes);
|
|
512
|
+
app.use('/api', apiLimiter, apiRoutes);
|
|
513
|
+
app.use('/api/license', licenseLimiter, licenseRoutes);
|
|
514
|
+
app.use('/api/admin', apiLimiter, adminRoutes);
|
|
515
|
+
app.use('/api/billing', apiLimiter, billingRoutes);
|
|
516
|
+
// genius-platform payment gateway — uses WAB's Stripe service (internal proxy)
|
|
517
|
+
app.use('/api/genius', geniusGateway);
|
|
518
|
+
app.use('/api/sovereign', apiLimiter, sovereignRoutes);
|
|
519
|
+
app.use('/api/mesh', apiLimiter, meshRoutes);
|
|
520
|
+
app.use('/api/commander', apiLimiter, commanderRoutes);
|
|
521
|
+
app.use('/api/ads', apiLimiter, adsRoutes);
|
|
522
|
+
app.use('/api/wab', wabApiRoutes);
|
|
523
|
+
app.use('/api/noscript', apiLimiter, noscriptRoutes);
|
|
524
|
+
app.use('/api/discovery', apiLimiter, discoveryRoutes);
|
|
525
|
+
app.use('/api/activate', apiLimiter, require('./routes/activate'));
|
|
526
|
+
|
|
527
|
+
// ── WAB Advanced Features v1.0 ──────────────────────────────────────────────
|
|
528
|
+
const { reputationRouter, collectiveRouter } = require('./routes/reputation');
|
|
529
|
+
const { intentRouter, privacyRouter } = require('./routes/intent');
|
|
530
|
+
const { cacheRouter, offlineRouter } = require('./routes/wab-cache');
|
|
531
|
+
// Trust Graph tier gate — tags & meters anonymous + keyed traffic.
|
|
532
|
+
// Mounted BEFORE the routers so it sees their requests.
|
|
533
|
+
const { apiTierMiddleware } = require('./middleware/api-tier');
|
|
534
|
+
app.use(['/api/reputation', '/api/truth', '/api/ring4/status'], apiTierMiddleware);
|
|
535
|
+
app.use('/api/reputation', apiLimiter, reputationRouter);
|
|
536
|
+
app.use('/api/collective', apiLimiter, collectiveRouter);
|
|
537
|
+
app.use('/api/intent', apiLimiter, intentRouter);
|
|
538
|
+
app.use('/api/privacy', apiLimiter, privacyRouter);
|
|
539
|
+
app.use('/api/cache', apiLimiter, cacheRouter);
|
|
540
|
+
app.use('/api/offline', apiLimiter, offlineRouter);
|
|
541
|
+
|
|
542
|
+
// ── WAB Truth Layer v1.0 (Semantic Memory + Temporal Trust + Action Graphs + Reality Anchor) ──
|
|
543
|
+
const { truthRouter } = require('./routes/truth-layer');
|
|
544
|
+
app.use('/api/truth', apiLimiter, truthRouter);
|
|
545
|
+
|
|
546
|
+
// ── WAB Ring 4 External Trust Verification (sovereign-agent trust API) ──
|
|
547
|
+
const { ring4Router } = require('./routes/ring4');
|
|
548
|
+
const { wabTrustMiddleware } = require('./middleware/wab-trust');
|
|
549
|
+
app.use(wabTrustMiddleware);
|
|
550
|
+
app.use('/api/ring4', apiLimiter, ring4Router);
|
|
551
|
+
|
|
552
|
+
// ── Agent Transaction Primitive (ATP) v3.9.0 — intents · transactions · signed receipts ──
|
|
553
|
+
app.use('/api/atp', apiLimiter, require('./routes/transactions'));
|
|
554
|
+
|
|
555
|
+
// ── Site Revocations & Appeals v3.11.0 — public transparency + owner appeals ──
|
|
556
|
+
app.use('/api/revocations', apiLimiter, require('./routes/revocations'));
|
|
557
|
+
|
|
558
|
+
// ── Agent-Driven Adoption v3.12.0 — canonical LLM agent system prompt ──
|
|
559
|
+
app.use('/api/agent', apiLimiter, require('./routes/agent-prompt'));
|
|
560
|
+
|
|
561
|
+
// ── Network Effect v3.14.0 — trusted-domains snapshot + revocations feeds ──
|
|
562
|
+
// (apiLimiter already applies via /api mount above; do not stack it here.)
|
|
563
|
+
app.use('/api', require('./routes/network'));
|
|
564
|
+
|
|
565
|
+
// ── Webhook Subscriptions v3.16.0 (Phase 4) — instant push for revocations ──
|
|
566
|
+
app.use('/api/webhooks', apiLimiter, require('./routes/webhooks'));
|
|
567
|
+
|
|
568
|
+
// ── WAB Commercial Foundations v3.8.0 (Partners · Trust Graph API · Governance SaaS · Enterprise Mesh) ──
|
|
569
|
+
app.use('/api/partners', apiLimiter, require('./routes/partners'));
|
|
570
|
+
app.use('/api/keys', apiLimiter, require('./routes/api-keys'));
|
|
571
|
+
app.use('/api/governance-saas', apiLimiter, require('./routes/governance-saas'));
|
|
572
|
+
app.use('/api/enterprise-mesh', apiLimiter, require('./routes/enterprise-mesh'));
|
|
573
|
+
// Trust Graph tier gate is mounted earlier (before /api/reputation et al.)
|
|
574
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
575
|
+
|
|
576
|
+
app.use('/api/providers', apiLimiter, providerRoutes);
|
|
577
|
+
app.use('/api/governance', apiLimiter, governanceRoutes);
|
|
578
|
+
app.use('/api/plans', apiLimiter, require('./routes/plans'));
|
|
579
|
+
app.use('/api/admin/plans', apiLimiter, require('./routes/admin-plans'));
|
|
580
|
+
app.use('/api/admin/shieldqr', apiLimiter, require('./routes/admin-shieldqr'));
|
|
581
|
+
app.use('/api/admin/trust-monitor', apiLimiter, require('./routes/admin-trust-monitor'));
|
|
582
|
+
// Optional premium modules — mounted only when present (open-source repo
|
|
583
|
+
// excludes the ShieldLink stack which is a paid feature).
|
|
584
|
+
function mountOptional(prefix, modPath) {
|
|
585
|
+
try { app.use(prefix, apiLimiter, require(modPath)); }
|
|
586
|
+
catch (e) {
|
|
587
|
+
if (e.code === 'MODULE_NOT_FOUND' && e.message.includes(modPath)) {
|
|
588
|
+
console.log(`[optional] ${prefix} not mounted (${modPath} not present)`);
|
|
589
|
+
} else { throw e; }
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
mountOptional('/api/admin/shieldlink', './routes/admin-shieldlink');
|
|
593
|
+
app.use('/api/shieldqr', apiLimiter, require('./routes/shieldqr'));
|
|
594
|
+
mountOptional('/api/shieldlink', './routes/shieldlink');
|
|
595
|
+
mountOptional('/api/customer/shieldlink','./routes/customer-shieldlink');
|
|
596
|
+
app.use('/api/adopt', apiLimiter, require('./routes/adopt'));
|
|
597
|
+
app.use('/api/diagnose', apiLimiter, require('./routes/diagnose'));
|
|
598
|
+
app.use('/api/admin/outreach', apiLimiter, require('./routes/admin-outreach'));
|
|
599
|
+
app.use('/', apiLimiter, require('./routes/unsubscribe'));
|
|
600
|
+
// Also expose well-known discovery endpoints at the canonical root paths so
|
|
601
|
+
// agents can find them without the /api/discovery prefix (RFC 8615).
|
|
602
|
+
|
|
603
|
+
// /activate — WAB DNS Discovery activation guide (bilingual)
|
|
604
|
+
app.get('/activate', noCache, (req, res) => {
|
|
605
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'activate.html'));
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// /one-click — interactive self-serve activation wizard (key-gen, sign, deploy via API)
|
|
609
|
+
app.get(['/one-click', '/one-click.html', '/activate/one-click'], noCache, (req, res) => {
|
|
610
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'one-click.html'));
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// /wab-features — WAB Advanced Features showcase (Reputation, Cache, Intent, Privacy, Collective, Offline)
|
|
614
|
+
app.get(['/wab-features', '/features'], noCache, (req, res) => {
|
|
615
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'wab-features.html'));
|
|
616
|
+
});
|
|
617
|
+
// /wab-truth — WAB Truth Layer showcase (Semantic Memory + Temporal Trust + Action Graphs + Reality Anchor)
|
|
618
|
+
app.get(['/wab-truth', '/truth'], noCache, (req, res) => {
|
|
619
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'wab-truth.html'));
|
|
620
|
+
});
|
|
621
|
+
// /milestones — Partners & Milestones (VEXR Ultra × WAB Ring 4 integration)
|
|
622
|
+
app.get(['/milestones'], noCache, (req, res) => {
|
|
623
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'milestones.html'));
|
|
624
|
+
});
|
|
625
|
+
// /partners — Certified Partner Program (3 tiers · self-serve)
|
|
626
|
+
app.get(['/partners', '/partners.html'], noCache, (req, res) => {
|
|
627
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'partners.html'));
|
|
628
|
+
});
|
|
629
|
+
// /trust-graph-api — Trust Graph API docs & self-serve key issuance
|
|
630
|
+
app.get(['/trust-graph-api', '/trust-graph-api.html'], noCache, (req, res) => {
|
|
631
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'trust-graph-api.html'));
|
|
632
|
+
});
|
|
633
|
+
// /governance — Governance SaaS landing (EU AI Act audit trail)
|
|
634
|
+
app.get(['/governance', '/governance.html'], noCache, (req, res) => {
|
|
635
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'governance.html'));
|
|
636
|
+
});
|
|
637
|
+
// /enterprise-mesh — Self-hosted Enterprise Mesh contact
|
|
638
|
+
app.get(['/enterprise-mesh', '/enterprise-mesh.html', '/enterprise'], noCache, (req, res) => {
|
|
639
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'enterprise-mesh.html'));
|
|
640
|
+
});
|
|
641
|
+
// /ring4 — Ring 4 Trust Handshake protocol docs
|
|
642
|
+
app.get(['/ring4', '/trust-handshake'], noCache, (req, res) => {
|
|
643
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'ring4.html'));
|
|
644
|
+
});
|
|
645
|
+
// /refusals — Public refusal log (anonymized constitutional refusal stats)
|
|
646
|
+
app.get('/refusals', noCache, (req, res) => {
|
|
647
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'refusals.html'));
|
|
648
|
+
});
|
|
649
|
+
// Trust & protocol pages
|
|
650
|
+
app.get(['/security', '/security.html'], noCache, (req, res) => {
|
|
651
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'security.html'));
|
|
652
|
+
});
|
|
653
|
+
app.get(['/threat-model', '/threat-model.html'], noCache, (req, res) => {
|
|
654
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'threat-model.html'));
|
|
655
|
+
});
|
|
656
|
+
app.get(['/responsible-disclosure', '/responsible-disclosure.html'], noCache, (req, res) => {
|
|
657
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'responsible-disclosure.html'));
|
|
658
|
+
});
|
|
659
|
+
app.get(['/researchers', '/researchers.html', '/hall-of-fame'], noCache, (req, res) => {
|
|
660
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'researchers.html'));
|
|
661
|
+
});
|
|
662
|
+
app.get(['/key-rotation', '/key-rotation.html'], noCache, (req, res) => {
|
|
663
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'key-rotation.html'));
|
|
664
|
+
});
|
|
665
|
+
app.get(['/atp-semantics', '/atp-semantics.html'], noCache, (req, res) => {
|
|
666
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'atp-semantics.html'));
|
|
667
|
+
});
|
|
668
|
+
app.get(['/benchmarks', '/benchmarks.html'], noCache, (req, res) => {
|
|
669
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'benchmarks.html'));
|
|
670
|
+
});
|
|
671
|
+
app.get(['/wab-today', '/wab-today.html', '/architecture'], noCache, (req, res) => {
|
|
672
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'wab-today.html'));
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// ── WAB Ecosystem v3.18.0 — Observatory · Notary · Research · URI scheme · Lens ──
|
|
676
|
+
app.use('/api/notary', apiLimiter, require('./routes/notary'));
|
|
677
|
+
app.use('/api/observatory', apiLimiter, require('./routes/observatory'));
|
|
678
|
+
app.use('/api/research', apiLimiter, require('./routes/research'));
|
|
679
|
+
app.use('/api/security-researchers', apiLimiter, require('./routes/security-researchers'));
|
|
680
|
+
|
|
681
|
+
// ── WAB Spider Network v3.19.0 — Public Registry + Spider Protocol ──
|
|
682
|
+
app.use('/api/registry', apiLimiter, require('./routes/registry'));
|
|
683
|
+
|
|
684
|
+
// ── WAB Self-Propagating Protocol v3.20.0 — Training Signal + Viral Stats ──
|
|
685
|
+
app.use('/api/traces', apiLimiter, require('./routes/traces'));
|
|
686
|
+
|
|
687
|
+
app.get(['/observatory', '/observatory.html'], noCache, (req, res) => {
|
|
688
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'observatory.html'));
|
|
689
|
+
});
|
|
690
|
+
app.get(['/notary', '/notary.html'], noCache, (req, res) => {
|
|
691
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'notary.html'));
|
|
692
|
+
});
|
|
693
|
+
app.get(['/research', '/research.html'], noCache, (req, res) => {
|
|
694
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'research.html'));
|
|
695
|
+
});
|
|
696
|
+
app.get(['/wab-uri', '/wab-uri.html'], noCache, (req, res) => {
|
|
697
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'wab-uri.html'));
|
|
698
|
+
});
|
|
699
|
+
app.get(['/wab-email', '/wab-email.html'], noCache, (req, res) => {
|
|
700
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'wab-email.html'));
|
|
701
|
+
});
|
|
702
|
+
app.get(['/wab-p2p', '/wab-p2p.html'], noCache, (req, res) => {
|
|
703
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'wab-p2p.html'));
|
|
704
|
+
});
|
|
705
|
+
app.get(['/wab-lens', '/wab-lens.html'], noCache, (req, res) => {
|
|
706
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'wab-lens.html'));
|
|
707
|
+
});
|
|
708
|
+
app.get(['/wab-registry', '/wab-registry.html', '/registry'], noCache, (req, res) => {
|
|
709
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'wab-registry.html'));
|
|
710
|
+
});
|
|
711
|
+
app.get(['/wab-dataset', '/wab-dataset.html', '/dataset'], noCache, (req, res) => {
|
|
712
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'wab-dataset.html'));
|
|
713
|
+
});
|
|
714
|
+
app.get(['/viral-coefficient', '/viral-coefficient.html', '/viral'], noCache, (req, res) => {
|
|
715
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'viral-coefficient.html'));
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// /resolve?u=wab://host/action?... — universal handler for the wab:// URI scheme.
|
|
719
|
+
// Parses the URI, fetches the target manifest, validates the action and shows
|
|
720
|
+
// a confirmation page. Renders an inline HTML response so it works without JS.
|
|
721
|
+
app.get('/resolve', async (req, res) => {
|
|
722
|
+
const raw = String(req.query.u || '');
|
|
723
|
+
const esc = (s) => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
724
|
+
function page(title, body) {
|
|
725
|
+
res.type('html').send(`<!doctype html><html><head><meta charset="utf-8"><title>${esc(title)}</title>
|
|
726
|
+
<style>body{font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0b0f17;color:#e5e7eb;margin:0;padding:48px 24px;max-width:640px;margin-inline:auto}
|
|
727
|
+
h1{font-size:22px}a{color:#60a5fa}code,pre{background:#0d1320;border:1px solid #1f2937;border-radius:6px;padding:2px 6px}
|
|
728
|
+
pre{padding:12px;overflow-x:auto;font-size:12px}.err{color:#ef4444}.ok{color:#10b981}
|
|
729
|
+
button,a.btn{display:inline-block;background:#60a5fa;color:#0b0f17;border:0;padding:10px 18px;border-radius:6px;font:inherit;font-weight:600;cursor:pointer;text-decoration:none;margin-top:12px}
|
|
730
|
+
</style></head><body>${body}<p style="margin-top:32px;font-size:12px;color:#9ca3af"><a href="/wab-uri">About the wab:// URI scheme</a></p></body></html>`);
|
|
731
|
+
}
|
|
732
|
+
if (!/^wab:\/\//i.test(raw)) {
|
|
733
|
+
return page('Invalid wab:// URI', `<h1 class="err">Invalid wab:// URI</h1><p>The <code>u</code> parameter must start with <code>wab://</code>.</p>`);
|
|
734
|
+
}
|
|
735
|
+
let host, action, params;
|
|
736
|
+
try {
|
|
737
|
+
const u = new URL(raw.replace(/^wab:\/\//i, 'https://'));
|
|
738
|
+
host = u.hostname.toLowerCase();
|
|
739
|
+
action = u.pathname.replace(/^\/+/, '').split('/')[0] || '';
|
|
740
|
+
params = Object.fromEntries(u.searchParams.entries());
|
|
741
|
+
if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(host) || !action) throw new Error('bad uri');
|
|
742
|
+
} catch (e) {
|
|
743
|
+
return page('Invalid wab:// URI', `<h1 class="err">Could not parse</h1><pre>${esc(raw)}</pre>`);
|
|
744
|
+
}
|
|
745
|
+
let manifest = null;
|
|
746
|
+
try {
|
|
747
|
+
// SSRF-safe: routes through safeFetch which DNS-resolves and blocks
|
|
748
|
+
// private/loopback/link-local addresses (nip.io, AWS metadata, etc.)
|
|
749
|
+
const r = await safeFetch(`https://${host}/.well-known/wab.json`, {}, { timeoutMs: 4000, maxBytes: 256 * 1024, requireHttps: true });
|
|
750
|
+
if (r.ok) manifest = await r.json().catch(() => null);
|
|
751
|
+
} catch (_) {}
|
|
752
|
+
if (!manifest) {
|
|
753
|
+
return page('Manifest not found', `<h1 class="err">${esc(host)} does not publish a WAB manifest</h1>
|
|
754
|
+
<p>The wab:// URI cannot be resolved because <code>https://${esc(host)}/.well-known/wab.json</code> is not reachable.</p>`);
|
|
755
|
+
}
|
|
756
|
+
const actions = Array.isArray(manifest.actions) ? manifest.actions : [];
|
|
757
|
+
const match = actions.find(a => a && a.id === action);
|
|
758
|
+
if (!match) {
|
|
759
|
+
return page('Action not found', `<h1 class="err">Unknown action <code>${esc(action)}</code></h1>
|
|
760
|
+
<p>${esc(host)} publishes a manifest, but no action with id <code>${esc(action)}</code> is declared.</p>
|
|
761
|
+
<p>Known actions: ${actions.map(a => `<code>${esc(a.id||'')}</code>`).join(', ') || '<em>none</em>'}</p>`);
|
|
762
|
+
}
|
|
763
|
+
const signed = !!(manifest.signature || (manifest.trust && manifest.trust.signed));
|
|
764
|
+
return page(`Confirm: ${action} on ${host}`, `
|
|
765
|
+
<h1>Confirm action</h1>
|
|
766
|
+
<p>You are about to invoke <code>${esc(action)}</code> on <strong>${esc(host)}</strong>${signed ? ' <span class="ok">✓ signed manifest</span>' : ''}.</p>
|
|
767
|
+
<h3 style="margin-top:24px;font-size:14px">Parameters</h3>
|
|
768
|
+
<pre>${esc(JSON.stringify(params, null, 2))}</pre>
|
|
769
|
+
<h3 style="margin-top:24px;font-size:14px">Endpoint</h3>
|
|
770
|
+
<pre>${esc(match.method || 'POST')} ${esc(match.endpoint || '')}</pre>
|
|
771
|
+
<form method="${esc(match.safe ? 'GET' : 'POST')}" action="${esc(match.endpoint || '#')}">
|
|
772
|
+
${Object.entries(params).map(([k,v]) => `<input type="hidden" name="${esc(k)}" value="${esc(v)}">`).join('')}
|
|
773
|
+
<button type="submit">Proceed</button>
|
|
774
|
+
<a class="btn" style="background:transparent;color:#e5e7eb;border:1px solid #1f2937;margin-left:6px" href="javascript:history.back()">Cancel</a>
|
|
775
|
+
</form>`);
|
|
776
|
+
});
|
|
777
|
+
// /.well-known/jwks.json — standard JWKS discovery for OIDC/JWT ecosystem
|
|
778
|
+
app.get('/.well-known/jwks.json', (req, res) => {
|
|
779
|
+
try {
|
|
780
|
+
const { _internals } = require('./routes/ring4');
|
|
781
|
+
return res.json(_internals.buildJwks());
|
|
782
|
+
} catch (e) {
|
|
783
|
+
return res.status(503).json({ error: 'jwks_unavailable', detail: e.message });
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
app.get('/shieldqr', noCache, (req, res) => {
|
|
787
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'shieldqr.html'));
|
|
788
|
+
});
|
|
789
|
+
// ── ShieldLink landing + Trust Preview redirect ──
|
|
790
|
+
app.get('/shieldlink', noCache, (req, res) => {
|
|
791
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'shieldlink.html'));
|
|
792
|
+
});
|
|
793
|
+
app.get('/l/:token', noCache, (req, res) => {
|
|
794
|
+
// Serve the Trust Preview page; the page calls /api/shieldlink/verify?token=
|
|
795
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'l-preview.html'));
|
|
796
|
+
});
|
|
797
|
+
app.get('/dashboard/shieldlink', noCache, (req, res) => {
|
|
798
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'dashboard-shieldlink.html'));
|
|
799
|
+
});
|
|
800
|
+
app.get('/activate-dns', noCache, (req, res) => {
|
|
801
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'activate.html'));
|
|
802
|
+
});
|
|
803
|
+
app.get('/provider-onboarding', noCache, (req, res) => {
|
|
804
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'provider-onboarding.html'));
|
|
805
|
+
});
|
|
806
|
+
app.get('/provider-sandbox', noCache, (req, res) => {
|
|
807
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'provider-sandbox.html'));
|
|
808
|
+
});
|
|
809
|
+
app.get('/cloudflare-integration', noCache, (req, res) => {
|
|
810
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'cloudflare-integration.html'));
|
|
811
|
+
});
|
|
812
|
+
app.get('/cpanel-integration', noCache, (req, res) => {
|
|
813
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'cpanel-integration.html'));
|
|
814
|
+
});
|
|
815
|
+
app.get('/route53-integration', noCache, (req, res) => {
|
|
816
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'route53-integration.html'));
|
|
817
|
+
});
|
|
818
|
+
app.get('/plesk-integration', noCache, (req, res) => {
|
|
819
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'plesk-integration.html'));
|
|
820
|
+
});
|
|
821
|
+
app.get('/gcp-dns-integration', noCache, (req, res) => {
|
|
822
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'gcp-dns-integration.html'));
|
|
823
|
+
});
|
|
824
|
+
app.get('/azure-dns-integration', noCache, (req, res) => {
|
|
825
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'azure-dns-integration.html'));
|
|
826
|
+
});
|
|
827
|
+
app.get('/registrar-integrations', noCache, (req, res) => {
|
|
828
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'registrar-integrations.html'));
|
|
829
|
+
});
|
|
830
|
+
app.get('/adoption-metrics', noCache, (req, res) => {
|
|
831
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'adoption-metrics.html'));
|
|
832
|
+
});
|
|
833
|
+
app.get('/adopt', noCache, (req, res) => {
|
|
834
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'adopt.html'));
|
|
835
|
+
});
|
|
836
|
+
app.get('/wab-trust', noCache, (req, res) => {
|
|
837
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'wab-trust.html'));
|
|
838
|
+
});
|
|
839
|
+
app.get('/wab-vs-protocols', noCache, (req, res) => {
|
|
840
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'wab-vs-protocols.html'));
|
|
841
|
+
});
|
|
842
|
+
app.use('/', apiLimiter, discoveryRoutes);
|
|
843
|
+
app.use('/api/premium', apiLimiter, premiumRoutes);
|
|
844
|
+
app.use('/api/admin/premium', apiLimiter, adminPremiumRoutes);
|
|
845
|
+
app.use('/api/workspace', apiLimiter, workspaceRoutes);
|
|
846
|
+
app.use('/api/universal', apiLimiter, universalRoutes);
|
|
847
|
+
app.use('/api/os', apiLimiter, runtimeRoutes);
|
|
848
|
+
app.use('/api/demo', apiLimiter, demoShowcaseRoutes);
|
|
849
|
+
app.use('/api/growth', apiLimiter, growthRoutes);
|
|
850
|
+
app.use('/api/v1', gatewayRoutes);
|
|
851
|
+
|
|
852
|
+
// Convenience alias: /api/negotiate/* → /api/sovereign/negotiation/*
|
|
853
|
+
app.get('/api/negotiate', apiLimiter, (req, res) => {
|
|
854
|
+
res.json({
|
|
855
|
+
engine: 'WAB Negotiation Engine',
|
|
856
|
+
endpoints: {
|
|
857
|
+
'POST /api/negotiate/rules': 'Create negotiation rules (auth required)',
|
|
858
|
+
'GET /api/negotiate/rules/:siteId': 'Get rules for a site',
|
|
859
|
+
'PUT /api/negotiate/rules/:ruleId': 'Update a rule (auth required)',
|
|
860
|
+
'POST /api/negotiate/sessions': 'Open negotiation session',
|
|
861
|
+
'POST /api/negotiate/sessions/:id/propose': 'Agent counter-offer',
|
|
862
|
+
'POST /api/negotiate/sessions/:id/confirm': 'Confirm deal',
|
|
863
|
+
'GET /api/negotiate/stats/:siteId': 'Negotiation stats',
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
app.use('/api/negotiate', apiLimiter, (req, res, next) => {
|
|
868
|
+
req.url = '/negotiation' + req.url;
|
|
869
|
+
sovereignRoutes(req, res, next);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
// ─── WAB Search Engine ────────────────────────────────────────────────
|
|
873
|
+
|
|
874
|
+
const searchLimiter = rateLimit({
|
|
875
|
+
windowMs: 60 * 1000,
|
|
876
|
+
max: 30,
|
|
877
|
+
standardHeaders: true,
|
|
878
|
+
legacyHeaders: false,
|
|
879
|
+
message: { error: 'Too many search requests, please slow down' }
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
app.get('/api/search', searchLimiter, async (req, res) => {
|
|
883
|
+
const q = (req.query.q || '').trim();
|
|
884
|
+
if (!q) return res.json({ results: [], cached: false });
|
|
885
|
+
if (q.length > 200) return res.status(400).json({ error: 'Query too long' });
|
|
886
|
+
const crypto = require('crypto');
|
|
887
|
+
const ipHash = crypto.createHash('sha256').update(req.ip || '').digest('hex').slice(0, 16);
|
|
888
|
+
const result = await search(q, ipHash);
|
|
889
|
+
res.json(result);
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
app.get('/api/search/suggest', searchLimiter, (req, res) => {
|
|
893
|
+
const q = (req.query.q || '').trim();
|
|
894
|
+
if (!q) return res.json({ suggestions: [] });
|
|
895
|
+
const suggestions = getSuggestions(q, 8);
|
|
896
|
+
res.json({ suggestions });
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
app.get('/api/search/trending', apiLimiter, (req, res) => {
|
|
900
|
+
const trending = getTrendingSearches(10);
|
|
901
|
+
res.json({ trending });
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
app.get('/api/search/stats', apiLimiter, (req, res) => {
|
|
905
|
+
const stats = getSearchStats();
|
|
906
|
+
res.json(stats);
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
// Prevent browsers from caching HTML page routes
|
|
910
|
+
function noCache(req, res, next) {
|
|
911
|
+
res.set('Cache-Control', 'no-cache, must-revalidate');
|
|
912
|
+
next();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
app.get('/dashboard', noCache, (req, res) => {
|
|
916
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'dashboard.html'));
|
|
917
|
+
});
|
|
918
|
+
app.get('/providers', noCache, (req, res) => {
|
|
919
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'providers.html'));
|
|
920
|
+
});
|
|
921
|
+
app.get('/mesh-dashboard', noCache, (req, res) => {
|
|
922
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'mesh-dashboard.html'));
|
|
923
|
+
});
|
|
924
|
+
app.get('/commander-dashboard', noCache, (req, res) => {
|
|
925
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'commander-dashboard.html'));
|
|
926
|
+
});
|
|
927
|
+
app.get('/docs', noCache, (req, res) => {
|
|
928
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'docs.html'));
|
|
929
|
+
});
|
|
930
|
+
app.get('/login', noCache, (req, res) => {
|
|
931
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'login.html'));
|
|
932
|
+
});
|
|
933
|
+
app.get('/register', noCache, (req, res) => {
|
|
934
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'register.html'));
|
|
935
|
+
});
|
|
936
|
+
app.get('/admin/login', noCache, (req, res) => {
|
|
937
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'admin', 'login.html'));
|
|
938
|
+
});
|
|
939
|
+
app.get('/admin', noCache, (req, res) => {
|
|
940
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'admin', 'dashboard.html'));
|
|
941
|
+
});
|
|
942
|
+
app.get('/admin/snapshots', noCache, (req, res) => {
|
|
943
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'admin', 'snapshots.html'));
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// ─── Admin sub-pages (each backed by real API endpoints in /api/admin/*) ──
|
|
947
|
+
['users','sites','analytics','grants','payments','stripe','smtp','notifications','governance','discovery','trust','providers','plans','shieldqr','shieldlink','trust-monitor','outreach'].forEach((page) => {
|
|
948
|
+
app.get('/admin/' + page, noCache, (req, res) => {
|
|
949
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'admin', page + '.html'));
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
app.get('/privacy', noCache, (req, res) => {
|
|
953
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'privacy.html'));
|
|
954
|
+
});
|
|
955
|
+
app.get('/terms', noCache, (req, res) => {
|
|
956
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'terms.html'));
|
|
957
|
+
});
|
|
958
|
+
app.get('/cookies', noCache, (req, res) => {
|
|
959
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'cookies.html'));
|
|
960
|
+
});
|
|
961
|
+
app.get('/browser', noCache, (req, res) => {
|
|
962
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'browser.html'));
|
|
963
|
+
});
|
|
964
|
+
app.get('/workspace', noCache, (req, res) => {
|
|
965
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'agent-workspace.html'));
|
|
966
|
+
});
|
|
967
|
+
app.get('/growth', noCache, (req, res) => {
|
|
968
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'growth.html'));
|
|
969
|
+
});
|
|
970
|
+
app.get('/score', noCache, (req, res) => {
|
|
971
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'score.html'));
|
|
972
|
+
});
|
|
973
|
+
app.get('/sovereign', noCache, (req, res) => {
|
|
974
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'sovereign.html'));
|
|
975
|
+
});
|
|
976
|
+
app.get('/api', noCache, (req, res) => {
|
|
977
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'api.html'));
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
app.get('/phone-shield', noCache, (req, res) => {
|
|
981
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'phone-shield.html'));
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
app.get('/dns', noCache, (req, res) => {
|
|
985
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'dns.html'));
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
// /integrations — bilingual deploy landing page
|
|
989
|
+
app.get('/integrations', noCache, (req, res) => {
|
|
990
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'integrations.html'));
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// /demo — interactive WAB Demo Store (new)
|
|
994
|
+
app.use('/demo', demoStoreRoutes);
|
|
995
|
+
|
|
996
|
+
// Browser downloads
|
|
997
|
+
app.use('/downloads', express.static(path.join(__dirname, '..', 'downloads'), {
|
|
998
|
+
maxAge: '1d',
|
|
999
|
+
setHeaders: (res, filePath) => {
|
|
1000
|
+
// Shell scripts served as plain text for curl | bash usage
|
|
1001
|
+
if (filePath.endsWith('.sh')) {
|
|
1002
|
+
res.set('Content-Type', 'text/plain; charset=utf-8');
|
|
1003
|
+
} else {
|
|
1004
|
+
res.set('Content-Disposition', 'attachment');
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}));
|
|
1008
|
+
|
|
1009
|
+
// WAB Discovery install shortcut: curl -fsSL https://webagentbridge.com/install | bash
|
|
1010
|
+
app.get('/install', (req, res) => {
|
|
1011
|
+
res.set('Content-Type', 'text/plain; charset=utf-8');
|
|
1012
|
+
res.sendFile(path.join(__dirname, '..', 'downloads', 'quick-wab.sh'));
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
// Agent chat endpoint for WAB Browser — Real AI Agent
|
|
1016
|
+
const chatLimiter = rateLimit({
|
|
1017
|
+
windowMs: 60 * 1000,
|
|
1018
|
+
max: 20,
|
|
1019
|
+
standardHeaders: true,
|
|
1020
|
+
legacyHeaders: false,
|
|
1021
|
+
message: { error: 'Too many messages, please slow down' }
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
app.post('/api/wab/agent-chat', chatLimiter, async (req, res) => {
|
|
1025
|
+
const { message, context, sessionId, taskId, taskAction } = req.body || {};
|
|
1026
|
+
if (!message || typeof message !== 'string') {
|
|
1027
|
+
return res.status(400).json({ error: 'Message required' });
|
|
1028
|
+
}
|
|
1029
|
+
if (message.length > 3000) {
|
|
1030
|
+
return res.status(400).json({ error: 'Message too long' });
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const sid = sessionId || req.ip || 'anonymous';
|
|
1034
|
+
|
|
1035
|
+
try {
|
|
1036
|
+
// ── Task actions (user responding to an active task) ──
|
|
1037
|
+
if (taskId && taskAction) {
|
|
1038
|
+
if (taskAction === 'answer') {
|
|
1039
|
+
const result = agentTasks.answerClarification(taskId, message);
|
|
1040
|
+
if (result.status === 'planning') {
|
|
1041
|
+
// Auto-execute after planning
|
|
1042
|
+
const execResult = await agentTasks.executeTask(taskId);
|
|
1043
|
+
return res.json({ ...execResult, type: 'task' });
|
|
1044
|
+
}
|
|
1045
|
+
return res.json({ ...result, type: 'task' });
|
|
1046
|
+
}
|
|
1047
|
+
if (taskAction === 'select') {
|
|
1048
|
+
const idx = parseInt(message.replace(/\D/g, '')) - 1;
|
|
1049
|
+
const result = agentTasks.selectOffer(taskId, idx);
|
|
1050
|
+
return res.json({ ...result, type: 'task' });
|
|
1051
|
+
}
|
|
1052
|
+
if (taskAction === 'cancel') {
|
|
1053
|
+
const result = agentTasks.cancelTask(taskId);
|
|
1054
|
+
return res.json({ ...result, type: 'task' });
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// ── Check if user wants to select from existing offers ──
|
|
1059
|
+
if (!taskId) {
|
|
1060
|
+
const selectMatch = message.match(/(?:اختر|اخت(?:ا|ي)ر|select|choose|pick)\s*(\d+)/i);
|
|
1061
|
+
if (selectMatch) {
|
|
1062
|
+
const tasks = agentTasks.getSessionTasks(sid, 1);
|
|
1063
|
+
if (tasks.length > 0 && tasks[0].status === 'presenting') {
|
|
1064
|
+
const idx = parseInt(selectMatch[1]) - 1;
|
|
1065
|
+
const result = agentTasks.selectOffer(tasks[0].id, idx);
|
|
1066
|
+
return res.json({ ...result, type: 'task' });
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// ── Detect URL paste — create URL negotiation task ──
|
|
1072
|
+
const urlData = agentTasks.parseBookingUrl(message);
|
|
1073
|
+
if (urlData) {
|
|
1074
|
+
const task = agentTasks.createUrlTask(sid, message, urlData);
|
|
1075
|
+
const execResult = await agentTasks.executeUrlTask(task.taskId);
|
|
1076
|
+
return res.json({ ...execResult, type: 'task', urlData });
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// ── Detect if this is a task-type request (booking, shopping, etc.) ──
|
|
1080
|
+
const intent = agentTasks.detectIntent(message);
|
|
1081
|
+
if (intent.confidence >= 0.7 && intent.intent !== 'general') {
|
|
1082
|
+
const task = agentTasks.createTask(sid, message);
|
|
1083
|
+
|
|
1084
|
+
if (task.status === 'clarifying') {
|
|
1085
|
+
return res.json({ ...task, type: 'task' });
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// If requirements are complete, auto-execute
|
|
1089
|
+
const execResult = await agentTasks.executeTask(task.taskId);
|
|
1090
|
+
return res.json({ ...execResult, type: 'task' });
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// ── Regular chat (not a task) ──
|
|
1094
|
+
const chatContext = {
|
|
1095
|
+
url: context?.url || '',
|
|
1096
|
+
platform: context?.platform || 'unknown',
|
|
1097
|
+
sessionId: sid,
|
|
1098
|
+
};
|
|
1099
|
+
const result = await agentChat(message, chatContext);
|
|
1100
|
+
res.json(result);
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
console.error('[agent-chat] Error:', err.message);
|
|
1103
|
+
res.json({ reply: '🤖 عذراً، حدث خطأ. حاول مرة أخرى.', type: 'text' });
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// Agent task status & history
|
|
1108
|
+
app.get('/api/wab/agent-task/:id', chatLimiter, (req, res) => {
|
|
1109
|
+
const state = agentTasks.getTaskState(req.params.id);
|
|
1110
|
+
if (!state) return res.status(404).json({ error: 'Task not found' });
|
|
1111
|
+
res.json(state);
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
app.get('/api/wab/agent-tasks', chatLimiter, (req, res) => {
|
|
1115
|
+
const sid = req.query.sessionId || req.ip || 'anonymous';
|
|
1116
|
+
const tasks = agentTasks.getSessionTasks(sid, 20);
|
|
1117
|
+
res.json({ tasks });
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
const pkg = require('../package.json');
|
|
1121
|
+
app.use(`/v${pkg.version.split('.')[0]}`, express.static(path.join(__dirname, '..', 'script')));
|
|
1122
|
+
app.use('/latest', express.static(path.join(__dirname, '..', 'script')));
|
|
1123
|
+
|
|
1124
|
+
app.get('*', (req, res) => {
|
|
1125
|
+
// API routes always return JSON 404
|
|
1126
|
+
if (req.path.startsWith('/api/')) {
|
|
1127
|
+
return res.status(404).json({ error: 'Not found', path: req.path });
|
|
1128
|
+
}
|
|
1129
|
+
if (req.accepts('html')) {
|
|
1130
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
|
|
1131
|
+
} else {
|
|
1132
|
+
res.status(404).json({ error: 'Not found' });
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
// Prevent PM2 restarts from uncaught errors — log and continue
|
|
1138
|
+
process.on('uncaughtException', (err) => {
|
|
1139
|
+
console.error('[process] uncaughtException:', err.message);
|
|
1140
|
+
});
|
|
1141
|
+
process.on('unhandledRejection', (reason) => {
|
|
1142
|
+
console.error('[process] unhandledRejection:', reason?.message || reason);
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
// Run migrations on every load (including tests) so worker-isolated DBs have
|
|
1146
|
+
// a complete schema before the first request.
|
|
1147
|
+
runMigrations();
|
|
1148
|
+
|
|
1149
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
1150
|
+
console.log('Running database migrations...');
|
|
1151
|
+
maybeBootstrapAdmin();
|
|
1152
|
+
initSearchEngine(db);
|
|
1153
|
+
|
|
1154
|
+
// Purge old search cache every hour
|
|
1155
|
+
setInterval(purgeOldCache, 60 * 60 * 1000);
|
|
1156
|
+
|
|
1157
|
+
const server = http.createServer(app);
|
|
1158
|
+
setupWebSocket(server);
|
|
1159
|
+
|
|
1160
|
+
// Start Agent OS runtime
|
|
1161
|
+
runtime.start();
|
|
1162
|
+
|
|
1163
|
+
// Start Cluster Orchestrator
|
|
1164
|
+
cluster.start();
|
|
1165
|
+
|
|
1166
|
+
// Start the SSL Health Monitor cron (Extended Trust Layer).
|
|
1167
|
+
try { require('./services/ssl-monitor').start(); } catch (e) { console.warn('[ssl-monitor] start failed:', e.message); }
|
|
1168
|
+
|
|
1169
|
+
// Start the Certificate Transparency Monitor (opt-in via WAB_CT_MONITOR=true).
|
|
1170
|
+
try { require('./services/ssl-ct-monitor').start(); } catch (e) { console.warn('[ct-monitor] start failed:', e.message); }
|
|
1171
|
+
|
|
1172
|
+
// Start the ATP commission billing timer (opt-in via WAB_COMMISSION_BILLING_INTERVAL_HOURS).
|
|
1173
|
+
try {
|
|
1174
|
+
const r = require('./services/commission-billing').startPeriodicBilling();
|
|
1175
|
+
if (r) console.log(`[commission-billing] periodic cycle every ${r.intervalHours}h`);
|
|
1176
|
+
} catch (e) { console.warn('[commission-billing] start failed:', e.message); }
|
|
1177
|
+
|
|
1178
|
+
// Start the revocation appeal-window sweep (opt-in via WAB_REVOCATION_SWEEP_INTERVAL_HOURS).
|
|
1179
|
+
try {
|
|
1180
|
+
const r = require('./services/revocations').startPeriodicSweep();
|
|
1181
|
+
if (r) console.log(`[revocations] periodic sweep every ${r.intervalHours}h`);
|
|
1182
|
+
} catch (e) { console.warn('[revocations] sweep start failed:', e.message); }
|
|
1183
|
+
|
|
1184
|
+
server.listen(PORT, () => {
|
|
1185
|
+
console.log(`\n ╔══════════════════════════════════════════╗`);
|
|
1186
|
+
console.log(` ║ Web Agent Bridge v${pkg.version} ║`);
|
|
1187
|
+
console.log(` ║ Server running on http://localhost:${PORT} ║`);
|
|
1188
|
+
console.log(` ║ WebSocket: ws://localhost:${PORT}/ws/analytics ║`);
|
|
1189
|
+
console.log(` ╚══════════════════════════════════════════╝\n`);
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
module.exports = app;
|