web-agent-bridge 2.5.0 → 2.7.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.md +14 -0
- package/package.json +79 -79
- package/public/.well-known/agent-tools.json +180 -0
- package/sdk/package.json +22 -14
- package/server/adapters/index.js +520 -0
- package/server/config/plans.js +367 -0
- package/server/index.js +4 -0
- package/server/middleware/featureGate.js +88 -0
- package/server/migrations/004_agent_os.sql +158 -0
- package/server/migrations/005_marketplace_metering.sql +126 -0
- package/server/observability/failure-analysis.js +337 -0
- package/server/registry/certification.js +271 -0
- package/server/routes/runtime.js +724 -3
- package/server/runtime/replay.js +264 -0
- package/server/runtime/session-engine.js +293 -0
- package/server/security/index.js +13 -0
- package/server/services/hosted-runtime.js +205 -0
- package/server/services/marketplace.js +270 -0
- package/server/services/metering.js +182 -0
package/server/routes/runtime.js
CHANGED
|
@@ -19,11 +19,94 @@ const router = express.Router();
|
|
|
19
19
|
const protocol = require('../protocol');
|
|
20
20
|
const { runtime, bus } = require('../runtime');
|
|
21
21
|
const { logger, tracer, metrics } = require('../observability');
|
|
22
|
+
const { failureAnalyzer } = require('../observability/failure-analysis');
|
|
22
23
|
const { identity, signer, isolation } = require('../security');
|
|
23
24
|
const { agentManager, policyEngine } = require('../control-plane');
|
|
24
25
|
const { executor } = require('../data-plane');
|
|
25
26
|
const { llm } = require('../llm');
|
|
26
27
|
const { commandRegistry, siteRegistry, templateRegistry } = require('../registry');
|
|
28
|
+
const { certificationEngine } = require('../registry/certification');
|
|
29
|
+
const { adapterManager, mcpAdapter, restAdapter, browserAdapter } = require('../adapters');
|
|
30
|
+
const { replayEngine } = require('../runtime/replay');
|
|
31
|
+
const { featureGate, usageLimit } = require('../middleware/featureGate');
|
|
32
|
+
const { listPlans, getPlan, USAGE_PRICING, MARKETPLACE } = require('../config/plans');
|
|
33
|
+
const metering = require('../services/metering');
|
|
34
|
+
const { marketplace } = require('../services/marketplace');
|
|
35
|
+
const { hostedRuntime } = require('../services/hosted-runtime');
|
|
36
|
+
const { sessionEngine } = require('../runtime/session-engine');
|
|
37
|
+
|
|
38
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
39
|
+
// AUTH MIDDLEWARE
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Authenticate requests via API key or session token.
|
|
44
|
+
* Public endpoints (protocol info, agent registration, health) bypass auth.
|
|
45
|
+
*/
|
|
46
|
+
const PUBLIC_PATHS = [
|
|
47
|
+
'/protocol',
|
|
48
|
+
'/agents/register',
|
|
49
|
+
'/agents/authenticate',
|
|
50
|
+
'/observability/health',
|
|
51
|
+
'/llm/models',
|
|
52
|
+
'/llm/status',
|
|
53
|
+
'/registry/commands',
|
|
54
|
+
'/registry/sites',
|
|
55
|
+
'/registry/templates',
|
|
56
|
+
'/plans',
|
|
57
|
+
'/marketplace',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
function authMiddleware(req, res, next) {
|
|
61
|
+
// Allow public GET endpoints
|
|
62
|
+
const matchesPublic = PUBLIC_PATHS.some(p =>
|
|
63
|
+
req.path === p || (req.method === 'GET' && req.path.startsWith(p))
|
|
64
|
+
);
|
|
65
|
+
if (matchesPublic) return next();
|
|
66
|
+
|
|
67
|
+
// Check session token
|
|
68
|
+
const authHeader = req.headers['authorization'];
|
|
69
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
70
|
+
const token = authHeader.slice(7);
|
|
71
|
+
const session = identity.validateSession(token);
|
|
72
|
+
if (session) {
|
|
73
|
+
req.agentId = session.agentId;
|
|
74
|
+
req.session = session;
|
|
75
|
+
return next();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check API key
|
|
80
|
+
const apiKey = req.headers['x-wab-key'];
|
|
81
|
+
if (apiKey) {
|
|
82
|
+
const ip = req.ip || req.connection?.remoteAddress;
|
|
83
|
+
const session = identity.authenticate(apiKey, ip);
|
|
84
|
+
if (session) {
|
|
85
|
+
req.agentId = session.agentId;
|
|
86
|
+
req.session = session;
|
|
87
|
+
return next();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check agent ID header (for internal/trusted calls)
|
|
92
|
+
const agentHeader = req.headers['x-wab-agent'];
|
|
93
|
+
if (agentHeader) {
|
|
94
|
+
const agent = identity.getAgent(agentHeader);
|
|
95
|
+
if (agent && agent.status === 'active') {
|
|
96
|
+
req.agentId = agentHeader;
|
|
97
|
+
return next();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// No auth on non-mutation GET requests (read-only)
|
|
102
|
+
if (req.method === 'GET') return next();
|
|
103
|
+
|
|
104
|
+
metrics.increment('auth.rejected');
|
|
105
|
+
return res.status(401).json({ error: 'Authentication required. Provide X-WAB-Key or Authorization: Bearer <token>' });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
router.use(authMiddleware);
|
|
109
|
+
router.use(featureGate);
|
|
27
110
|
|
|
28
111
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
29
112
|
// PROTOCOL ENDPOINTS
|
|
@@ -166,7 +249,7 @@ router.delete('/agents/:agentId', (req, res) => {
|
|
|
166
249
|
/**
|
|
167
250
|
* Submit a task
|
|
168
251
|
*/
|
|
169
|
-
router.post('/tasks', (req, res) => {
|
|
252
|
+
router.post('/tasks', usageLimit('tasksPerDay'), (req, res) => {
|
|
170
253
|
try {
|
|
171
254
|
const result = runtime.submitTask(req.body);
|
|
172
255
|
metrics.increment('tasks.submitted', 1, { type: req.body.type });
|
|
@@ -224,7 +307,7 @@ router.post('/tasks/:taskId/resume', (req, res) => {
|
|
|
224
307
|
/**
|
|
225
308
|
* Execute a semantic action
|
|
226
309
|
*/
|
|
227
|
-
router.post('/execute', async (req, res) => {
|
|
310
|
+
router.post('/execute', usageLimit('executionsPerDay'), async (req, res) => {
|
|
228
311
|
try {
|
|
229
312
|
const result = await executor.execute(req.body);
|
|
230
313
|
res.json(result);
|
|
@@ -443,6 +526,14 @@ router.get('/observability/health', (req, res) => {
|
|
|
443
526
|
};
|
|
444
527
|
health.executor = executor.getStats();
|
|
445
528
|
health.llm = llm.getStatus();
|
|
529
|
+
health.adapters = adapterManager.getStats();
|
|
530
|
+
health.replay = replayEngine.getStats();
|
|
531
|
+
health.sessions = sessionEngine.getStats();
|
|
532
|
+
health.failures = failureAnalyzer.getStats();
|
|
533
|
+
health.certification = certificationEngine.getStats();
|
|
534
|
+
health.marketplace = marketplace.getStats();
|
|
535
|
+
health.hostedRuntime = hostedRuntime.getStats();
|
|
536
|
+
health.metering = metering.getStats();
|
|
446
537
|
res.json(health);
|
|
447
538
|
});
|
|
448
539
|
|
|
@@ -554,7 +645,7 @@ router.get('/registry/templates/:templateId', (req, res) => {
|
|
|
554
645
|
/**
|
|
555
646
|
* LLM completion
|
|
556
647
|
*/
|
|
557
|
-
router.post('/llm/complete', async (req, res) => {
|
|
648
|
+
router.post('/llm/complete', usageLimit('executionsPerDay'), async (req, res) => {
|
|
558
649
|
try {
|
|
559
650
|
const result = await llm.complete(req.body.prompt, req.body.options || req.body);
|
|
560
651
|
metrics.increment('llm.api.requests');
|
|
@@ -722,4 +813,634 @@ protocolHandler.handle('wab.commerce.compare', async (payload) => {
|
|
|
722
813
|
});
|
|
723
814
|
});
|
|
724
815
|
|
|
816
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
817
|
+
// ADAPTERS
|
|
818
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* List adapters
|
|
822
|
+
*/
|
|
823
|
+
router.get('/adapters', (req, res) => {
|
|
824
|
+
res.json({ adapters: adapterManager.list() });
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Adapter stats
|
|
829
|
+
*/
|
|
830
|
+
router.get('/adapters/stats', (req, res) => {
|
|
831
|
+
res.json(adapterManager.getStats());
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* MCP: list tools
|
|
836
|
+
*/
|
|
837
|
+
router.get('/adapters/mcp/tools', (req, res) => {
|
|
838
|
+
const commands = protocol.schema.listCommands();
|
|
839
|
+
res.json(mcpAdapter.handleListTools(commands));
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* MCP: call tool
|
|
844
|
+
*/
|
|
845
|
+
router.post('/adapters/mcp/call', async (req, res) => {
|
|
846
|
+
try {
|
|
847
|
+
const result = await mcpAdapter.handleCallTool(req.body, async (wapReq) => {
|
|
848
|
+
const request = protocol.createRequest(wapReq.command, wapReq.payload);
|
|
849
|
+
return protocolHandler.process(request);
|
|
850
|
+
});
|
|
851
|
+
res.json(result);
|
|
852
|
+
} catch (err) {
|
|
853
|
+
res.status(500).json({ error: err.message });
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* REST adapter: register endpoint
|
|
859
|
+
*/
|
|
860
|
+
router.post('/adapters/rest/endpoints', (req, res) => {
|
|
861
|
+
try {
|
|
862
|
+
const endpoint = restAdapter.registerEndpoint(req.body.id, req.body);
|
|
863
|
+
res.json(endpoint);
|
|
864
|
+
} catch (err) {
|
|
865
|
+
res.status(400).json({ error: err.message });
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* REST adapter: list endpoints
|
|
871
|
+
*/
|
|
872
|
+
router.get('/adapters/rest/endpoints', (req, res) => {
|
|
873
|
+
res.json({ endpoints: restAdapter.listEndpoints() });
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* REST adapter: execute
|
|
878
|
+
*/
|
|
879
|
+
router.post('/adapters/rest/execute', async (req, res) => {
|
|
880
|
+
try {
|
|
881
|
+
const result = await restAdapter.execute(req.body.endpoint, req.body.params);
|
|
882
|
+
res.json(result);
|
|
883
|
+
} catch (err) {
|
|
884
|
+
res.status(500).json({ error: err.message });
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Browser adapter: list semantic mappings
|
|
890
|
+
*/
|
|
891
|
+
router.get('/adapters/browser/mappings', (req, res) => {
|
|
892
|
+
res.json({ mappings: browserAdapter.listMappings() });
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Browser adapter: resolve semantic action
|
|
897
|
+
*/
|
|
898
|
+
router.post('/adapters/browser/resolve', (req, res) => {
|
|
899
|
+
const { domain, action, params } = req.body;
|
|
900
|
+
const plan = browserAdapter.fromWAP({ domain, action, params });
|
|
901
|
+
if (!plan) return res.status(404).json({ error: 'No mapping for this semantic action' });
|
|
902
|
+
res.json(plan);
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Browser adapter: register mapping
|
|
907
|
+
*/
|
|
908
|
+
router.post('/adapters/browser/mappings', (req, res) => {
|
|
909
|
+
const { domainAction, plan } = req.body;
|
|
910
|
+
if (!domainAction || !plan) return res.status(400).json({ error: 'domainAction and plan required' });
|
|
911
|
+
browserAdapter.registerMapping(domainAction, plan);
|
|
912
|
+
res.json({ success: true });
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
916
|
+
// REPLAY ENGINE
|
|
917
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* List recordings
|
|
921
|
+
*/
|
|
922
|
+
router.get('/replay/recordings', (req, res) => {
|
|
923
|
+
res.json({ recordings: replayEngine.listRecordings(parseInt(req.query.limit) || 50) });
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Get recording
|
|
928
|
+
*/
|
|
929
|
+
router.get('/replay/recordings/:taskId', (req, res) => {
|
|
930
|
+
const rec = replayEngine.getRecording(req.params.taskId);
|
|
931
|
+
if (!rec) return res.status(404).json({ error: 'Recording not found' });
|
|
932
|
+
res.json(rec);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Replay a task
|
|
937
|
+
*/
|
|
938
|
+
router.post('/replay/:taskId', async (req, res) => {
|
|
939
|
+
try {
|
|
940
|
+
const result = await replayEngine.replay(req.params.taskId, {
|
|
941
|
+
verify: req.body.verify !== false,
|
|
942
|
+
continueOnMismatch: !!req.body.continueOnMismatch,
|
|
943
|
+
});
|
|
944
|
+
res.json(result);
|
|
945
|
+
} catch (err) {
|
|
946
|
+
res.status(400).json({ error: err.message });
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Diff two recordings
|
|
952
|
+
*/
|
|
953
|
+
router.get('/replay/diff/:taskId1/:taskId2', (req, res) => {
|
|
954
|
+
const diff = replayEngine.diff(req.params.taskId1, req.params.taskId2);
|
|
955
|
+
if (!diff) return res.status(404).json({ error: 'One or both recordings not found' });
|
|
956
|
+
res.json(diff);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Replay stats
|
|
961
|
+
*/
|
|
962
|
+
router.get('/replay/stats', (req, res) => {
|
|
963
|
+
res.json(replayEngine.getStats());
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
967
|
+
// SESSION ENGINE
|
|
968
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Create browser session
|
|
972
|
+
*/
|
|
973
|
+
router.post('/sessions', (req, res) => {
|
|
974
|
+
const session = sessionEngine.create(req.body);
|
|
975
|
+
res.json(session);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* List sessions
|
|
980
|
+
*/
|
|
981
|
+
router.get('/sessions', (req, res) => {
|
|
982
|
+
const sessions = sessionEngine.list({
|
|
983
|
+
agentId: req.query.agentId,
|
|
984
|
+
siteId: req.query.siteId,
|
|
985
|
+
state: req.query.state,
|
|
986
|
+
}, parseInt(req.query.limit) || 50);
|
|
987
|
+
res.json({ sessions, total: sessions.length });
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Get session
|
|
992
|
+
*/
|
|
993
|
+
router.get('/sessions/:sessionId', (req, res) => {
|
|
994
|
+
const session = sessionEngine.get(req.params.sessionId);
|
|
995
|
+
if (!session) return res.status(404).json({ error: 'Session not found or expired' });
|
|
996
|
+
res.json(session);
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Export session
|
|
1001
|
+
*/
|
|
1002
|
+
router.get('/sessions/:sessionId/export', (req, res) => {
|
|
1003
|
+
const data = sessionEngine.export(req.params.sessionId);
|
|
1004
|
+
if (!data) return res.status(404).json({ error: 'Session not found' });
|
|
1005
|
+
res.json(data);
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Import session
|
|
1010
|
+
*/
|
|
1011
|
+
router.post('/sessions/import', (req, res) => {
|
|
1012
|
+
const session = sessionEngine.import(req.body);
|
|
1013
|
+
res.json(session);
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Set cookies
|
|
1018
|
+
*/
|
|
1019
|
+
router.post('/sessions/:sessionId/cookies', (req, res) => {
|
|
1020
|
+
sessionEngine.setCookies(req.params.sessionId, req.body.cookies || []);
|
|
1021
|
+
res.json({ success: true });
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Get cookies
|
|
1026
|
+
*/
|
|
1027
|
+
router.get('/sessions/:sessionId/cookies', (req, res) => {
|
|
1028
|
+
const cookies = sessionEngine.getCookies(req.params.sessionId, req.query.domain);
|
|
1029
|
+
res.json({ cookies });
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Set storage
|
|
1034
|
+
*/
|
|
1035
|
+
router.post('/sessions/:sessionId/storage', (req, res) => {
|
|
1036
|
+
const { key, value, type } = req.body;
|
|
1037
|
+
sessionEngine.setStorage(req.params.sessionId, key, value, type);
|
|
1038
|
+
res.json({ success: true });
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Destroy session
|
|
1043
|
+
*/
|
|
1044
|
+
router.delete('/sessions/:sessionId', (req, res) => {
|
|
1045
|
+
sessionEngine.destroy(req.params.sessionId);
|
|
1046
|
+
res.json({ success: true });
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1050
|
+
// FAILURE ANALYSIS
|
|
1051
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Query failures
|
|
1055
|
+
*/
|
|
1056
|
+
router.get('/failures', (req, res) => {
|
|
1057
|
+
const failures = failureAnalyzer.query({
|
|
1058
|
+
classification: req.query.classification,
|
|
1059
|
+
severity: req.query.severity,
|
|
1060
|
+
agentId: req.query.agentId,
|
|
1061
|
+
taskId: req.query.taskId,
|
|
1062
|
+
retryable: req.query.retryable === 'true' ? true : req.query.retryable === 'false' ? false : undefined,
|
|
1063
|
+
since: parseInt(req.query.since) || undefined,
|
|
1064
|
+
}, parseInt(req.query.limit) || 50);
|
|
1065
|
+
res.json({ failures, total: failures.length });
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Get failure
|
|
1070
|
+
*/
|
|
1071
|
+
router.get('/failures/:failureId', (req, res) => {
|
|
1072
|
+
const failure = failureAnalyzer.getFailure(req.params.failureId);
|
|
1073
|
+
if (!failure) return res.status(404).json({ error: 'Failure not found' });
|
|
1074
|
+
res.json(failure);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Get failure patterns
|
|
1079
|
+
*/
|
|
1080
|
+
router.get('/failures/analysis/patterns', (req, res) => {
|
|
1081
|
+
res.json({ patterns: failureAnalyzer.getPatterns() });
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Get failure summary
|
|
1086
|
+
*/
|
|
1087
|
+
router.get('/failures/analysis/summary', (req, res) => {
|
|
1088
|
+
res.json(failureAnalyzer.getSummary(parseInt(req.query.since) || 0));
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Classify a failure manually
|
|
1093
|
+
*/
|
|
1094
|
+
router.post('/failures/classify', (req, res) => {
|
|
1095
|
+
const { error, context } = req.body;
|
|
1096
|
+
if (!error) return res.status(400).json({ error: 'error object required' });
|
|
1097
|
+
const classification = failureAnalyzer.classify(error, context || {});
|
|
1098
|
+
res.json(classification);
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1102
|
+
// CERTIFICATION
|
|
1103
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Verify a site
|
|
1107
|
+
*/
|
|
1108
|
+
router.post('/certification/verify', async (req, res) => {
|
|
1109
|
+
try {
|
|
1110
|
+
const { domain, probeData } = req.body;
|
|
1111
|
+
if (!domain) return res.status(400).json({ error: 'domain required' });
|
|
1112
|
+
const result = await certificationEngine.verify(domain, probeData || {});
|
|
1113
|
+
res.json(result);
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
res.status(500).json({ error: err.message });
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Get certificate
|
|
1121
|
+
*/
|
|
1122
|
+
router.get('/certification/:domain', (req, res) => {
|
|
1123
|
+
const cert = certificationEngine.getCertificate(req.params.domain);
|
|
1124
|
+
if (!cert) return res.status(404).json({ error: 'No active certificate for this domain' });
|
|
1125
|
+
res.json(cert);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* List certificates
|
|
1130
|
+
*/
|
|
1131
|
+
router.get('/certification', (req, res) => {
|
|
1132
|
+
const certs = certificationEngine.listCertificates({
|
|
1133
|
+
level: req.query.level,
|
|
1134
|
+
minScore: parseInt(req.query.minScore) || undefined,
|
|
1135
|
+
}, parseInt(req.query.limit) || 50);
|
|
1136
|
+
res.json({ certificates: certs, total: certs.length });
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Revoke certificate
|
|
1141
|
+
*/
|
|
1142
|
+
router.delete('/certification/:domain', (req, res) => {
|
|
1143
|
+
certificationEngine.revoke(req.params.domain);
|
|
1144
|
+
res.json({ success: true });
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1148
|
+
// PLANS & PRICING
|
|
1149
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* List available plans
|
|
1153
|
+
*/
|
|
1154
|
+
router.get('/plans', (req, res) => {
|
|
1155
|
+
const plans = listPlans().map(p => ({
|
|
1156
|
+
id: p.id,
|
|
1157
|
+
name: p.name,
|
|
1158
|
+
price: p.price,
|
|
1159
|
+
interval: p.interval,
|
|
1160
|
+
description: p.description,
|
|
1161
|
+
limits: p.limits,
|
|
1162
|
+
features: Object.entries(p.features)
|
|
1163
|
+
.filter(([, v]) => v === true)
|
|
1164
|
+
.map(([k]) => k),
|
|
1165
|
+
}));
|
|
1166
|
+
res.json({ plans, usagePricing: USAGE_PRICING });
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Get specific plan details
|
|
1171
|
+
*/
|
|
1172
|
+
router.get('/plans/:planId', (req, res) => {
|
|
1173
|
+
const plan = getPlan(req.params.planId);
|
|
1174
|
+
if (!plan || plan.id === 'free' && req.params.planId !== 'free') {
|
|
1175
|
+
return res.status(404).json({ error: 'Plan not found' });
|
|
1176
|
+
}
|
|
1177
|
+
res.json(plan);
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1181
|
+
// USAGE METERING
|
|
1182
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Get usage for current agent
|
|
1186
|
+
*/
|
|
1187
|
+
router.get('/usage', (req, res) => {
|
|
1188
|
+
const entityId = req.agentId || req.ip;
|
|
1189
|
+
const tier = req.agentTier || req.session?.tier || 'free';
|
|
1190
|
+
res.json(metering.getUsage(entityId, tier));
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Get billing summary (overages)
|
|
1195
|
+
*/
|
|
1196
|
+
router.get('/usage/billing', (req, res) => {
|
|
1197
|
+
const entityId = req.agentId || req.ip;
|
|
1198
|
+
res.json(metering.getBillingSummary(entityId));
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* Get metering stats (admin)
|
|
1203
|
+
*/
|
|
1204
|
+
router.get('/usage/stats', (req, res) => {
|
|
1205
|
+
res.json(metering.getStats());
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1209
|
+
// MARKETPLACE
|
|
1210
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Search marketplace
|
|
1214
|
+
*/
|
|
1215
|
+
router.get('/marketplace', (req, res) => {
|
|
1216
|
+
const listings = marketplace.search({
|
|
1217
|
+
type: req.query.type,
|
|
1218
|
+
category: req.query.category,
|
|
1219
|
+
query: req.query.q,
|
|
1220
|
+
tag: req.query.tag,
|
|
1221
|
+
free: req.query.free === 'true',
|
|
1222
|
+
paid: req.query.paid === 'true',
|
|
1223
|
+
minRating: req.query.minRating ? parseFloat(req.query.minRating) : undefined,
|
|
1224
|
+
sortBy: req.query.sortBy,
|
|
1225
|
+
}, parseInt(req.query.limit) || 50);
|
|
1226
|
+
res.json({ listings, total: listings.length });
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Get listing
|
|
1231
|
+
*/
|
|
1232
|
+
router.get('/marketplace/:listingId', (req, res) => {
|
|
1233
|
+
const listing = marketplace.getListing(req.params.listingId);
|
|
1234
|
+
if (!listing) return res.status(404).json({ error: 'Listing not found' });
|
|
1235
|
+
res.json(listing);
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Get reviews
|
|
1240
|
+
*/
|
|
1241
|
+
router.get('/marketplace/:listingId/reviews', (req, res) => {
|
|
1242
|
+
res.json({ reviews: marketplace.getReviews(req.params.listingId) });
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Publish listing
|
|
1247
|
+
*/
|
|
1248
|
+
router.post('/marketplace/publish', (req, res) => {
|
|
1249
|
+
try {
|
|
1250
|
+
const listing = marketplace.publish({
|
|
1251
|
+
...req.body,
|
|
1252
|
+
sellerId: req.agentId || req.body.sellerId,
|
|
1253
|
+
});
|
|
1254
|
+
res.json(listing);
|
|
1255
|
+
} catch (err) {
|
|
1256
|
+
res.status(400).json({ error: err.message });
|
|
1257
|
+
}
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Purchase/install listing
|
|
1262
|
+
*/
|
|
1263
|
+
router.post('/marketplace/:listingId/purchase', (req, res) => {
|
|
1264
|
+
try {
|
|
1265
|
+
const buyerId = req.agentId || req.body.buyerId;
|
|
1266
|
+
if (!buyerId) return res.status(400).json({ error: 'buyerId required' });
|
|
1267
|
+
const purchase = marketplace.purchase(req.params.listingId, buyerId);
|
|
1268
|
+
res.json(purchase);
|
|
1269
|
+
} catch (err) {
|
|
1270
|
+
res.status(400).json({ error: err.message });
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Add review
|
|
1276
|
+
*/
|
|
1277
|
+
router.post('/marketplace/:listingId/review', (req, res) => {
|
|
1278
|
+
try {
|
|
1279
|
+
const review = marketplace.addReview(req.params.listingId, {
|
|
1280
|
+
userId: req.agentId || req.body.userId,
|
|
1281
|
+
rating: req.body.rating,
|
|
1282
|
+
comment: req.body.comment,
|
|
1283
|
+
});
|
|
1284
|
+
res.json(review);
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
res.status(400).json({ error: err.message });
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Get my purchases
|
|
1292
|
+
*/
|
|
1293
|
+
router.get('/marketplace/my/purchases', (req, res) => {
|
|
1294
|
+
const buyerId = req.agentId || req.query.buyerId;
|
|
1295
|
+
res.json({ purchases: marketplace.getPurchases(buyerId) });
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* Get seller earnings
|
|
1300
|
+
*/
|
|
1301
|
+
router.get('/marketplace/my/earnings', (req, res) => {
|
|
1302
|
+
const sellerId = req.agentId || req.query.sellerId;
|
|
1303
|
+
res.json(marketplace.getEarnings(sellerId));
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Admin: pending listings
|
|
1308
|
+
*/
|
|
1309
|
+
router.get('/marketplace/admin/pending', (req, res) => {
|
|
1310
|
+
res.json({ listings: marketplace.getPendingListings() });
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Admin: approve listing
|
|
1315
|
+
*/
|
|
1316
|
+
router.post('/marketplace/admin/:listingId/approve', (req, res) => {
|
|
1317
|
+
try {
|
|
1318
|
+
const listing = marketplace.approve(req.params.listingId);
|
|
1319
|
+
res.json(listing);
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
res.status(400).json({ error: err.message });
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Admin: reject listing
|
|
1327
|
+
*/
|
|
1328
|
+
router.post('/marketplace/admin/:listingId/reject', (req, res) => {
|
|
1329
|
+
try {
|
|
1330
|
+
const listing = marketplace.reject(req.params.listingId, req.body.reason);
|
|
1331
|
+
res.json(listing);
|
|
1332
|
+
} catch (err) {
|
|
1333
|
+
res.status(400).json({ error: err.message });
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Marketplace stats
|
|
1339
|
+
*/
|
|
1340
|
+
router.get('/marketplace/stats', (req, res) => {
|
|
1341
|
+
res.json(marketplace.getStats());
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1345
|
+
// HOSTED RUNTIME
|
|
1346
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Launch hosted instance
|
|
1350
|
+
*/
|
|
1351
|
+
router.post('/hosted/launch', (req, res) => {
|
|
1352
|
+
try {
|
|
1353
|
+
const instance = hostedRuntime.launch({
|
|
1354
|
+
agentId: req.agentId || req.body.agentId,
|
|
1355
|
+
tier: req.agentTier || req.session?.tier || 'starter',
|
|
1356
|
+
region: req.body.region,
|
|
1357
|
+
cpu: req.body.cpu,
|
|
1358
|
+
memory: req.body.memory,
|
|
1359
|
+
timeout: req.body.timeout,
|
|
1360
|
+
});
|
|
1361
|
+
res.json(instance);
|
|
1362
|
+
} catch (err) {
|
|
1363
|
+
res.status(400).json({ error: err.message });
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Execute on hosted instance
|
|
1369
|
+
*/
|
|
1370
|
+
router.post('/hosted/:instanceId/execute', async (req, res) => {
|
|
1371
|
+
try {
|
|
1372
|
+
const execution = await hostedRuntime.execute(req.params.instanceId, req.body);
|
|
1373
|
+
res.json(execution);
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
res.status(400).json({ error: err.message });
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* Complete execution
|
|
1381
|
+
*/
|
|
1382
|
+
router.post('/hosted/executions/:executionId/complete', (req, res) => {
|
|
1383
|
+
const execution = hostedRuntime.completeExecution(
|
|
1384
|
+
req.params.executionId,
|
|
1385
|
+
req.body.result,
|
|
1386
|
+
req.body.error ? new Error(req.body.error) : null
|
|
1387
|
+
);
|
|
1388
|
+
if (!execution) return res.status(404).json({ error: 'Execution not found' });
|
|
1389
|
+
res.json(execution);
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Stop hosted instance
|
|
1394
|
+
*/
|
|
1395
|
+
router.post('/hosted/:instanceId/stop', (req, res) => {
|
|
1396
|
+
const success = hostedRuntime.stop(req.params.instanceId);
|
|
1397
|
+
res.json({ success });
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Get hosted instance
|
|
1402
|
+
*/
|
|
1403
|
+
router.get('/hosted/:instanceId', (req, res) => {
|
|
1404
|
+
const instance = hostedRuntime.getInstance(req.params.instanceId);
|
|
1405
|
+
if (!instance) return res.status(404).json({ error: 'Instance not found' });
|
|
1406
|
+
res.json(instance);
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* List instances
|
|
1411
|
+
*/
|
|
1412
|
+
router.get('/hosted', (req, res) => {
|
|
1413
|
+
const instances = hostedRuntime.listInstances({
|
|
1414
|
+
agentId: req.query.agentId,
|
|
1415
|
+
status: req.query.status,
|
|
1416
|
+
region: req.query.region,
|
|
1417
|
+
}, parseInt(req.query.limit) || 50);
|
|
1418
|
+
res.json({ instances, total: instances.length });
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* List executions for instance
|
|
1423
|
+
*/
|
|
1424
|
+
router.get('/hosted/:instanceId/executions', (req, res) => {
|
|
1425
|
+
const executions = hostedRuntime.listExecutions(
|
|
1426
|
+
req.params.instanceId,
|
|
1427
|
+
parseInt(req.query.limit) || 50
|
|
1428
|
+
);
|
|
1429
|
+
res.json({ executions, total: executions.length });
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* Get compute usage
|
|
1434
|
+
*/
|
|
1435
|
+
router.get('/hosted/usage/:agentId', (req, res) => {
|
|
1436
|
+
res.json(hostedRuntime.getComputeUsage(req.params.agentId));
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Hosted runtime stats
|
|
1441
|
+
*/
|
|
1442
|
+
router.get('/hosted/stats', (req, res) => {
|
|
1443
|
+
res.json(hostedRuntime.getStats());
|
|
1444
|
+
});
|
|
1445
|
+
|
|
725
1446
|
module.exports = router;
|