adp-agent 0.2.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/LICENSE +21 -0
- package/README.md +141 -0
- package/config.example.json +32 -0
- package/dist/agent-id.d.ts +9 -0
- package/dist/agent-id.js +118 -0
- package/dist/agent-id.js.map +1 -0
- package/dist/agent-id.test.d.ts +1 -0
- package/dist/agent-id.test.js +36 -0
- package/dist/agent-id.test.js.map +1 -0
- package/dist/canonical.d.ts +5 -0
- package/dist/canonical.js +35 -0
- package/dist/canonical.js.map +1 -0
- package/dist/canonical.test.d.ts +1 -0
- package/dist/canonical.test.js +33 -0
- package/dist/canonical.test.js.map +1 -0
- package/dist/capability-test.d.ts +1 -0
- package/dist/capability-test.js +108 -0
- package/dist/capability-test.js.map +1 -0
- package/dist/chat.d.ts +1 -0
- package/dist/chat.js +166 -0
- package/dist/chat.js.map +1 -0
- package/dist/contacts-test.d.ts +1 -0
- package/dist/contacts-test.js +225 -0
- package/dist/contacts-test.js.map +1 -0
- package/dist/crypto.d.ts +11 -0
- package/dist/crypto.js +103 -0
- package/dist/crypto.js.map +1 -0
- package/dist/crypto.test.d.ts +1 -0
- package/dist/crypto.test.js +40 -0
- package/dist/crypto.test.js.map +1 -0
- package/dist/discovery.d.ts +36 -0
- package/dist/discovery.js +291 -0
- package/dist/discovery.js.map +1 -0
- package/dist/envelope.d.ts +42 -0
- package/dist/envelope.js +58 -0
- package/dist/envelope.js.map +1 -0
- package/dist/gateway.d.ts +44 -0
- package/dist/gateway.js +255 -0
- package/dist/gateway.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +75 -0
- package/dist/index.js.map +1 -0
- package/dist/integration-test.d.ts +1 -0
- package/dist/integration-test.js +206 -0
- package/dist/integration-test.js.map +1 -0
- package/dist/key-store.d.ts +10 -0
- package/dist/key-store.js +81 -0
- package/dist/key-store.js.map +1 -0
- package/dist/manifest.d.ts +35 -0
- package/dist/manifest.js +24 -0
- package/dist/manifest.js.map +1 -0
- package/dist/mdns-test.d.ts +1 -0
- package/dist/mdns-test.js +93 -0
- package/dist/mdns-test.js.map +1 -0
- package/dist/relay-peer-test.d.ts +1 -0
- package/dist/relay-peer-test.js +220 -0
- package/dist/relay-peer-test.js.map +1 -0
- package/dist/relay-test.d.ts +1 -0
- package/dist/relay-test.js +92 -0
- package/dist/relay-test.js.map +1 -0
- package/dist/relay.d.ts +60 -0
- package/dist/relay.js +277 -0
- package/dist/relay.js.map +1 -0
- package/dist/src/agent-id.d.ts +9 -0
- package/dist/src/agent-id.js +44 -0
- package/dist/src/agent-id.js.map +1 -0
- package/dist/src/agent-id.test.d.ts +1 -0
- package/dist/src/agent-id.test.js +36 -0
- package/dist/src/agent-id.test.js.map +1 -0
- package/dist/src/canonical.d.ts +5 -0
- package/dist/src/canonical.js +37 -0
- package/dist/src/canonical.js.map +1 -0
- package/dist/src/canonical.test.d.ts +1 -0
- package/dist/src/canonical.test.js +33 -0
- package/dist/src/canonical.test.js.map +1 -0
- package/dist/src/capabilities.d.ts +3 -0
- package/dist/src/capabilities.js +39 -0
- package/dist/src/capabilities.js.map +1 -0
- package/dist/src/config.d.ts +35 -0
- package/dist/src/config.js +3 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/contacts.d.ts +28 -0
- package/dist/src/contacts.js +118 -0
- package/dist/src/contacts.js.map +1 -0
- package/dist/src/crypto.d.ts +11 -0
- package/dist/src/crypto.js +83 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/crypto.test.d.ts +1 -0
- package/dist/src/crypto.test.js +40 -0
- package/dist/src/crypto.test.js.map +1 -0
- package/dist/src/discovery.d.ts +39 -0
- package/dist/src/discovery.js +317 -0
- package/dist/src/discovery.js.map +1 -0
- package/dist/src/envelope.d.ts +55 -0
- package/dist/src/envelope.js +95 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/gateway.d.ts +78 -0
- package/dist/src/gateway.js +540 -0
- package/dist/src/gateway.js.map +1 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.js +81 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/key-rotation.d.ts +27 -0
- package/dist/src/key-rotation.js +41 -0
- package/dist/src/key-rotation.js.map +1 -0
- package/dist/src/key-store.d.ts +10 -0
- package/dist/src/key-store.js +81 -0
- package/dist/src/key-store.js.map +1 -0
- package/dist/src/logger.d.ts +9 -0
- package/dist/src/logger.js +18 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/manifest.d.ts +37 -0
- package/dist/src/manifest.js +24 -0
- package/dist/src/manifest.js.map +1 -0
- package/dist/src/mcp-server.d.ts +38 -0
- package/dist/src/mcp-server.js +408 -0
- package/dist/src/mcp-server.js.map +1 -0
- package/dist/src/net-utils.d.ts +3 -0
- package/dist/src/net-utils.js +71 -0
- package/dist/src/net-utils.js.map +1 -0
- package/dist/src/registry/cache.d.ts +12 -0
- package/dist/src/registry/cache.js +45 -0
- package/dist/src/registry/cache.js.map +1 -0
- package/dist/src/registry/client.d.ts +43 -0
- package/dist/src/registry/client.js +245 -0
- package/dist/src/registry/client.js.map +1 -0
- package/dist/src/registry/config.d.ts +32 -0
- package/dist/src/registry/config.js +79 -0
- package/dist/src/registry/config.js.map +1 -0
- package/dist/src/registry/db.d.ts +10 -0
- package/dist/src/registry/db.js +97 -0
- package/dist/src/registry/db.js.map +1 -0
- package/dist/src/registry/index.d.ts +5 -0
- package/dist/src/registry/index.js +22 -0
- package/dist/src/registry/index.js.map +1 -0
- package/dist/src/registry/service.d.ts +45 -0
- package/dist/src/registry/service.js +802 -0
- package/dist/src/registry/service.js.map +1 -0
- package/dist/src/relay.d.ts +69 -0
- package/dist/src/relay.js +399 -0
- package/dist/src/relay.js.map +1 -0
- package/dist/src/task-manager.d.ts +55 -0
- package/dist/src/task-manager.js +150 -0
- package/dist/src/task-manager.js.map +1 -0
- package/dist/src/trust-store.d.ts +24 -0
- package/dist/src/trust-store.js +144 -0
- package/dist/src/trust-store.js.map +1 -0
- package/dist/src/webhook-client.d.ts +30 -0
- package/dist/src/webhook-client.js +78 -0
- package/dist/src/webhook-client.js.map +1 -0
- package/dist/start-mcp.d.ts +2 -0
- package/dist/start-mcp.js +126 -0
- package/dist/start-mcp.js.map +1 -0
- package/dist/start-registry.d.ts +2 -0
- package/dist/start-registry.js +33 -0
- package/dist/start-registry.js.map +1 -0
- package/dist/start-relay.d.ts +1 -0
- package/dist/start-relay.js +35 -0
- package/dist/start-relay.js.map +1 -0
- package/dist/start.d.ts +1 -0
- package/dist/start.js +364 -0
- package/dist/start.js.map +1 -0
- package/dist/task-manager.d.ts +55 -0
- package/dist/task-manager.js +145 -0
- package/dist/task-manager.js.map +1 -0
- package/dist/task-test.d.ts +1 -0
- package/dist/task-test.js +188 -0
- package/dist/task-test.js.map +1 -0
- package/dist/test-auth.d.ts +1 -0
- package/dist/test-auth.js +166 -0
- package/dist/test-auth.js.map +1 -0
- package/dist/test-key-rotation.d.ts +2 -0
- package/dist/test-key-rotation.js +114 -0
- package/dist/test-key-rotation.js.map +1 -0
- package/dist/test-registry.d.ts +2 -0
- package/dist/test-registry.js +123 -0
- package/dist/test-registry.js.map +1 -0
- package/dist/trust-store.d.ts +23 -0
- package/dist/trust-store.js +111 -0
- package/dist/trust-store.js.map +1 -0
- package/package.json +96 -0
- package/schema.sql +54 -0
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RegistryService = void 0;
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const cors_1 = __importDefault(require("cors"));
|
|
9
|
+
const crypto_1 = require("../crypto");
|
|
10
|
+
const agent_id_1 = require("../agent-id");
|
|
11
|
+
const canonical_1 = require("../canonical");
|
|
12
|
+
class RegistryService {
|
|
13
|
+
constructor(config, db, cache) {
|
|
14
|
+
this.heartbeatQueue = new Set();
|
|
15
|
+
this.heartbeatDrainTimer = null;
|
|
16
|
+
this.heartbeatBatchSize = 500;
|
|
17
|
+
this.rateLimitMap = new Map();
|
|
18
|
+
this.rateLimitMax = 100;
|
|
19
|
+
this.rateLimitWindowMs = 60000;
|
|
20
|
+
this.rateLimitCleanupTimer = null;
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.db = db;
|
|
23
|
+
this.cache = cache;
|
|
24
|
+
this.app = (0, express_1.default)();
|
|
25
|
+
this.setupMiddleware();
|
|
26
|
+
this.setupRoutes();
|
|
27
|
+
this.startHeartbeatDrain();
|
|
28
|
+
this.startRateLimitCleanup();
|
|
29
|
+
}
|
|
30
|
+
startRateLimitCleanup() {
|
|
31
|
+
this.rateLimitCleanupTimer = setInterval(() => {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
for (const [ip, entry] of this.rateLimitMap) {
|
|
34
|
+
if (now > entry.resetAt) {
|
|
35
|
+
this.rateLimitMap.delete(ip);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}, this.rateLimitWindowMs);
|
|
39
|
+
}
|
|
40
|
+
startHeartbeatDrain() {
|
|
41
|
+
this.heartbeatDrainTimer = setInterval(() => {
|
|
42
|
+
this.drainHeartbeats().catch(err => {
|
|
43
|
+
console.error('Heartbeat drain error:', err);
|
|
44
|
+
});
|
|
45
|
+
}, 5000);
|
|
46
|
+
}
|
|
47
|
+
setupMiddleware() {
|
|
48
|
+
if (this.config.cors.enabled) {
|
|
49
|
+
this.app.use((0, cors_1.default)({
|
|
50
|
+
origin: this.config.cors.origins
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
this.app.use(express_1.default.json({ limit: '1mb' }));
|
|
54
|
+
this.app.use((req, res, next) => {
|
|
55
|
+
console.log(`${req.method} ${req.path}`);
|
|
56
|
+
next();
|
|
57
|
+
});
|
|
58
|
+
this.app.use((req, res, next) => {
|
|
59
|
+
if (!this.checkRateLimit(req, res))
|
|
60
|
+
return;
|
|
61
|
+
next();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
checkRateLimit(req, res) {
|
|
65
|
+
const ip = req.ip || req.socket.remoteAddress || 'unknown';
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const entry = this.rateLimitMap.get(ip);
|
|
68
|
+
if (!entry || now > entry.resetAt) {
|
|
69
|
+
this.rateLimitMap.set(ip, { count: 1, resetAt: now + this.rateLimitWindowMs });
|
|
70
|
+
}
|
|
71
|
+
else if (entry.count >= this.rateLimitMax) {
|
|
72
|
+
res.status(429).json({
|
|
73
|
+
error: { code: 'RATE_LIMITED', message: 'Too many requests' }
|
|
74
|
+
});
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
entry.count++;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
tokenAuth(req, res, next) {
|
|
83
|
+
if (!this.config.token.enabled) {
|
|
84
|
+
next();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const token = req.body?.token || req.headers.authorization?.replace(/^Bearer\s+/i, '');
|
|
88
|
+
if (!token) {
|
|
89
|
+
res.status(401).json({
|
|
90
|
+
error: {
|
|
91
|
+
code: 'UNAUTHORIZED',
|
|
92
|
+
message: 'Token is required'
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const tokenEntries = this.config.token.tokens || {};
|
|
98
|
+
const tokenEntry = tokenEntries[token];
|
|
99
|
+
if (!tokenEntry) {
|
|
100
|
+
res.status(401).json({
|
|
101
|
+
error: {
|
|
102
|
+
code: 'UNAUTHORIZED',
|
|
103
|
+
message: 'Invalid token'
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
req.tokenNamespace =
|
|
109
|
+
tokenEntry.namespace;
|
|
110
|
+
req.tokenCapabilities =
|
|
111
|
+
tokenEntry.capabilities;
|
|
112
|
+
next();
|
|
113
|
+
}
|
|
114
|
+
signatureAuth(req, res, next) {
|
|
115
|
+
const signatureHeader = req.headers['x-adp-signature'];
|
|
116
|
+
if (!signatureHeader) {
|
|
117
|
+
res.status(401).json({
|
|
118
|
+
error: {
|
|
119
|
+
code: 'UNAUTHORIZED',
|
|
120
|
+
message: 'X-ADP-Signature header is required for this operation'
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const body = req.body;
|
|
126
|
+
if (!body?.agent_id) {
|
|
127
|
+
res.status(400).json({
|
|
128
|
+
error: {
|
|
129
|
+
code: 'INVALID_PARAMS',
|
|
130
|
+
message: 'agent_id is required in request body for signature verification'
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const sigBytes = (0, crypto_1.decodeBase64URL)(signatureHeader);
|
|
137
|
+
if (sigBytes.length !== 64) {
|
|
138
|
+
res.status(401).json({
|
|
139
|
+
error: {
|
|
140
|
+
code: 'UNAUTHORIZED',
|
|
141
|
+
message: 'Invalid signature'
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const publicKey = (() => {
|
|
147
|
+
try {
|
|
148
|
+
return (0, agent_id_1.extractPublicKey)(body.agent_id);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
})();
|
|
154
|
+
if (!publicKey) {
|
|
155
|
+
res.status(401).json({
|
|
156
|
+
error: {
|
|
157
|
+
code: 'INVALID_PARAMS',
|
|
158
|
+
message: 'Invalid agent_id format'
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const signedPayload = {};
|
|
164
|
+
signedPayload.agent_id = body.agent_id;
|
|
165
|
+
signedPayload.manifest = body.manifest;
|
|
166
|
+
signedPayload.routes = body.routes;
|
|
167
|
+
if (body.rotation)
|
|
168
|
+
signedPayload.rotation = body.rotation;
|
|
169
|
+
signedPayload.timestamp = req.headers['x-adp-timestamp'] || body.timestamp;
|
|
170
|
+
const canonical = (0, canonical_1.canonicalize)(signedPayload);
|
|
171
|
+
const messageBytes = new TextEncoder().encode(canonical);
|
|
172
|
+
const isValid = (0, crypto_1.verify)(publicKey, messageBytes, sigBytes);
|
|
173
|
+
if (!isValid) {
|
|
174
|
+
res.status(401).json({
|
|
175
|
+
error: {
|
|
176
|
+
code: 'UNAUTHORIZED',
|
|
177
|
+
message: 'X-ADP-Signature verification failed'
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
res.status(401).json({
|
|
185
|
+
error: {
|
|
186
|
+
code: 'UNAUTHORIZED',
|
|
187
|
+
message: 'Invalid signature format'
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
next();
|
|
193
|
+
}
|
|
194
|
+
setupRoutes() {
|
|
195
|
+
this.app.get('/health', this.healthCheck.bind(this));
|
|
196
|
+
this.app.post('/v1/agents', this.tokenAuth.bind(this), this.signatureAuth.bind(this), this.registerAgent.bind(this));
|
|
197
|
+
this.app.put('/v1/agents/:initialId', this.tokenAuth.bind(this), this.signatureAuth.bind(this), this.updateAgent.bind(this));
|
|
198
|
+
this.app.post('/v1/agents/:initialId/heartbeat', this.tokenAuth.bind(this), this.signatureAuth.bind(this), this.heartbeat.bind(this));
|
|
199
|
+
this.app.get('/v1/agents/:initialId', this.getAgent.bind(this));
|
|
200
|
+
this.app.delete('/v1/agents/:initialId', this.tokenAuth.bind(this), this.signatureAuth.bind(this), this.deleteAgent.bind(this));
|
|
201
|
+
this.app.get('/v1/agents', this.searchAgents.bind(this));
|
|
202
|
+
}
|
|
203
|
+
normalizeInitialId(raw) {
|
|
204
|
+
const decoded = decodeURIComponent(Array.isArray(raw) ? raw[0] : raw);
|
|
205
|
+
if (decoded.startsWith('adp://')) {
|
|
206
|
+
try {
|
|
207
|
+
return (0, crypto_1.encodeBase64URL)((0, agent_id_1.extractPublicKey)(decoded));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return decoded;
|
|
214
|
+
}
|
|
215
|
+
healthCheck(req, res) {
|
|
216
|
+
res.json({
|
|
217
|
+
status: 'ok',
|
|
218
|
+
version: '0.2.0',
|
|
219
|
+
timestamp: new Date().toISOString()
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async registerAgent(req, res) {
|
|
223
|
+
try {
|
|
224
|
+
const request = req.body;
|
|
225
|
+
const validationError = this.validateRegistrationRequest(request);
|
|
226
|
+
if (validationError) {
|
|
227
|
+
res.status(400).json({
|
|
228
|
+
error: {
|
|
229
|
+
code: 'INVALID_PARAMS',
|
|
230
|
+
message: validationError
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const initialId = (() => {
|
|
236
|
+
try {
|
|
237
|
+
return (0, crypto_1.encodeBase64URL)((0, agent_id_1.extractPublicKey)(request.agent_id));
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
})();
|
|
243
|
+
if (!initialId) {
|
|
244
|
+
res.status(400).json({
|
|
245
|
+
error: {
|
|
246
|
+
code: 'INVALID_PARAMS',
|
|
247
|
+
message: 'Invalid agent_id format'
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const parsed = (() => {
|
|
253
|
+
try {
|
|
254
|
+
return (0, agent_id_1.parseAgentId)(request.agent_id);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
})();
|
|
260
|
+
if (!parsed) {
|
|
261
|
+
res.status(400).json({
|
|
262
|
+
error: {
|
|
263
|
+
code: 'INVALID_PARAMS',
|
|
264
|
+
message: 'Invalid agent_id format'
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const connection = await this.db.getConnection();
|
|
270
|
+
try {
|
|
271
|
+
const [existing] = await connection.execute('SELECT expires_at FROM agents WHERE initial_id = ?', [initialId]);
|
|
272
|
+
const rows = existing;
|
|
273
|
+
if (rows.length > 0) {
|
|
274
|
+
const row = rows[0];
|
|
275
|
+
const isExpired = new Date(row.expires_at) <= new Date();
|
|
276
|
+
if (!isExpired) {
|
|
277
|
+
res.status(409).json({
|
|
278
|
+
error: {
|
|
279
|
+
code: 'AGENT_ALREADY_EXISTS',
|
|
280
|
+
message: 'Agent already registered, use PUT to update'
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const expiresAt = new Date(Date.now() + this.config.registration.ttlSeconds * 1000);
|
|
287
|
+
await connection.execute(`INSERT INTO agents (initial_id, current_agent_id, namespace, manifest, routes, last_seen, expires_at)
|
|
288
|
+
VALUES (?, ?, ?, ?, ?, NOW(), ?)
|
|
289
|
+
ON DUPLICATE KEY UPDATE
|
|
290
|
+
current_agent_id = VALUES(current_agent_id),
|
|
291
|
+
namespace = VALUES(namespace),
|
|
292
|
+
manifest = VALUES(manifest),
|
|
293
|
+
routes = VALUES(routes),
|
|
294
|
+
last_seen = NOW(),
|
|
295
|
+
expires_at = VALUES(expires_at)`, [
|
|
296
|
+
initialId,
|
|
297
|
+
request.agent_id,
|
|
298
|
+
parsed.namespace,
|
|
299
|
+
JSON.stringify(request.manifest),
|
|
300
|
+
JSON.stringify(request.routes),
|
|
301
|
+
expiresAt
|
|
302
|
+
]);
|
|
303
|
+
await connection.execute('DELETE FROM agent_capabilities WHERE initial_id = ?', [initialId]);
|
|
304
|
+
const capabilities = request.manifest.capabilities || [];
|
|
305
|
+
for (const cap of capabilities) {
|
|
306
|
+
const capName = typeof cap === 'string' ? cap : cap.capability;
|
|
307
|
+
await connection.execute('INSERT IGNORE INTO agent_capabilities (initial_id, capability) VALUES (?, ?)', [initialId, capName]);
|
|
308
|
+
}
|
|
309
|
+
const agentData = {
|
|
310
|
+
initial_id: initialId,
|
|
311
|
+
current_agent_id: request.agent_id,
|
|
312
|
+
manifest: request.manifest,
|
|
313
|
+
routes: request.routes,
|
|
314
|
+
last_seen: new Date().toISOString(),
|
|
315
|
+
expires_at: expiresAt.toISOString(),
|
|
316
|
+
rotation_chain: []
|
|
317
|
+
};
|
|
318
|
+
await this.cache.setAgent(initialId, agentData, this.config.registration.ttlSeconds);
|
|
319
|
+
const alreadyExisted = rows.length > 0;
|
|
320
|
+
res.status(alreadyExisted ? 200 : 201).json({
|
|
321
|
+
initial_id: initialId,
|
|
322
|
+
current_agent_id: request.agent_id,
|
|
323
|
+
status: 'ok',
|
|
324
|
+
expires_at: expiresAt.toISOString()
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
connection.release();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
console.error('Registration error:', error);
|
|
333
|
+
res.status(500).json({
|
|
334
|
+
error: {
|
|
335
|
+
code: 'INTERNAL_ERROR',
|
|
336
|
+
message: 'Internal server error'
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async updateAgent(req, res) {
|
|
342
|
+
try {
|
|
343
|
+
const initialId = this.normalizeInitialId(req.params.initialId);
|
|
344
|
+
if (!initialId) {
|
|
345
|
+
res.status(400).json({
|
|
346
|
+
error: {
|
|
347
|
+
code: 'INVALID_PARAMS',
|
|
348
|
+
message: 'Invalid agent_id format in URL'
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const request = req.body;
|
|
354
|
+
// Validate request
|
|
355
|
+
const validationError = this.validateRegistrationRequest(request);
|
|
356
|
+
if (validationError) {
|
|
357
|
+
res.status(400).json({
|
|
358
|
+
error: {
|
|
359
|
+
code: 'INVALID_PARAMS',
|
|
360
|
+
message: validationError
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const connection = await this.db.getConnection();
|
|
366
|
+
try {
|
|
367
|
+
// Check if agent exists
|
|
368
|
+
const [agents] = await connection.execute('SELECT current_agent_id FROM agents WHERE initial_id = ?', [initialId]);
|
|
369
|
+
if (agents.length === 0) {
|
|
370
|
+
res.status(404).json({
|
|
371
|
+
error: {
|
|
372
|
+
code: 'AGENT_NOT_FOUND',
|
|
373
|
+
message: 'Agent not found'
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const currentAgentId = agents[0].current_agent_id;
|
|
379
|
+
const currentPublicKey = (0, crypto_1.encodeBase64URL)((0, agent_id_1.extractPublicKey)(currentAgentId));
|
|
380
|
+
const newPublicKey = (() => {
|
|
381
|
+
try {
|
|
382
|
+
return (0, crypto_1.encodeBase64URL)((0, agent_id_1.extractPublicKey)(request.agent_id));
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
})();
|
|
388
|
+
if (!newPublicKey) {
|
|
389
|
+
res.status(400).json({
|
|
390
|
+
error: {
|
|
391
|
+
code: 'INVALID_PARAMS',
|
|
392
|
+
message: 'Invalid agent_id format'
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const isRotation = newPublicKey !== currentPublicKey;
|
|
398
|
+
if (isRotation && !request.rotation) {
|
|
399
|
+
res.status(400).json({
|
|
400
|
+
error: {
|
|
401
|
+
code: 'INVALID_PARAMS',
|
|
402
|
+
message: 'Key rotation requires rotation envelope'
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (isRotation && request.rotation) {
|
|
408
|
+
// Add to rotation chain
|
|
409
|
+
const [maxSeq] = await connection.execute('SELECT MAX(sequence) as max_seq FROM rotation_chain WHERE initial_id = ?', [initialId]);
|
|
410
|
+
const nextSequence = (maxSeq[0].max_seq ?? -1) + 1;
|
|
411
|
+
await connection.execute(`INSERT INTO rotation_chain (initial_id, sequence, from_agent_id, to_agent_id, envelope)
|
|
412
|
+
VALUES (?, ?, ?, ?, ?)`, [
|
|
413
|
+
initialId,
|
|
414
|
+
nextSequence,
|
|
415
|
+
currentAgentId,
|
|
416
|
+
request.agent_id,
|
|
417
|
+
JSON.stringify(request.rotation)
|
|
418
|
+
]);
|
|
419
|
+
// Clear rotation cache
|
|
420
|
+
await this.cache.setRotationChain(initialId, []);
|
|
421
|
+
}
|
|
422
|
+
// Calculate new expires_at
|
|
423
|
+
const expiresAt = new Date(Date.now() + this.config.registration.ttlSeconds * 1000);
|
|
424
|
+
// Update agent
|
|
425
|
+
const parsed = (() => {
|
|
426
|
+
try {
|
|
427
|
+
return (0, agent_id_1.parseAgentId)(request.agent_id);
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
})();
|
|
433
|
+
if (!parsed) {
|
|
434
|
+
res.status(400).json({
|
|
435
|
+
error: {
|
|
436
|
+
code: 'INVALID_PARAMS',
|
|
437
|
+
message: 'Invalid agent_id format'
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
await connection.execute(`UPDATE agents
|
|
443
|
+
SET current_agent_id = ?, namespace = ?, manifest = ?, routes = ?, last_seen = NOW(), expires_at = ?
|
|
444
|
+
WHERE initial_id = ?`, [
|
|
445
|
+
request.agent_id,
|
|
446
|
+
parsed.namespace,
|
|
447
|
+
JSON.stringify(request.manifest),
|
|
448
|
+
JSON.stringify(request.routes),
|
|
449
|
+
expiresAt,
|
|
450
|
+
initialId
|
|
451
|
+
]);
|
|
452
|
+
// Update capabilities
|
|
453
|
+
await connection.execute('DELETE FROM agent_capabilities WHERE initial_id = ?', [initialId]);
|
|
454
|
+
const capabilities = request.manifest.capabilities || [];
|
|
455
|
+
for (const cap of capabilities) {
|
|
456
|
+
const capName = typeof cap === 'string' ? cap : cap.capability;
|
|
457
|
+
await connection.execute('INSERT IGNORE INTO agent_capabilities (initial_id, capability) VALUES (?, ?)', [initialId, capName]);
|
|
458
|
+
}
|
|
459
|
+
// Get updated rotation chain
|
|
460
|
+
const [chainResult] = await connection.execute('SELECT envelope FROM rotation_chain WHERE initial_id = ? ORDER BY sequence ASC', [initialId]);
|
|
461
|
+
const rotationChain = chainResult.map(row => ({
|
|
462
|
+
envelope: row.envelope
|
|
463
|
+
}));
|
|
464
|
+
// Update cache
|
|
465
|
+
const agentData = {
|
|
466
|
+
initial_id: initialId,
|
|
467
|
+
current_agent_id: request.agent_id,
|
|
468
|
+
manifest: request.manifest,
|
|
469
|
+
routes: request.routes,
|
|
470
|
+
last_seen: new Date().toISOString(),
|
|
471
|
+
expires_at: expiresAt.toISOString(),
|
|
472
|
+
rotation_chain: rotationChain
|
|
473
|
+
};
|
|
474
|
+
await this.cache.setAgent(initialId, agentData, this.config.registration.ttlSeconds);
|
|
475
|
+
await this.cache.setRotationChain(initialId, rotationChain, this.config.registration.ttlSeconds);
|
|
476
|
+
res.json({
|
|
477
|
+
initial_id: initialId,
|
|
478
|
+
current_agent_id: request.agent_id,
|
|
479
|
+
status: 'ok',
|
|
480
|
+
expires_at: expiresAt.toISOString()
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
connection.release();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
console.error('Update error:', error);
|
|
489
|
+
res.status(500).json({
|
|
490
|
+
error: {
|
|
491
|
+
code: 'INTERNAL_ERROR',
|
|
492
|
+
message: 'Internal server error'
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async getAgent(req, res) {
|
|
498
|
+
try {
|
|
499
|
+
const initialId = this.normalizeInitialId(req.params.initialId);
|
|
500
|
+
if (!initialId) {
|
|
501
|
+
res.status(400).json({
|
|
502
|
+
error: { code: 'INVALID_PARAMS', message: 'Invalid agent_id format in URL' }
|
|
503
|
+
});
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
// Check cache first
|
|
507
|
+
const cached = await this.cache.getAgent(initialId);
|
|
508
|
+
if (cached) {
|
|
509
|
+
const now = new Date();
|
|
510
|
+
const expiresAt = new Date(cached.expires_at);
|
|
511
|
+
const online = now < expiresAt;
|
|
512
|
+
if (online) {
|
|
513
|
+
res.json({
|
|
514
|
+
...cached,
|
|
515
|
+
online: true
|
|
516
|
+
});
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const connection = await this.db.getConnection();
|
|
521
|
+
try {
|
|
522
|
+
const [agents] = await connection.execute('SELECT * FROM agents WHERE initial_id = ?', [initialId]);
|
|
523
|
+
if (agents.length === 0) {
|
|
524
|
+
res.status(404).json({
|
|
525
|
+
initial_id: initialId,
|
|
526
|
+
online: false,
|
|
527
|
+
error: {
|
|
528
|
+
code: 'AGENT_NOT_FOUND',
|
|
529
|
+
message: '未注册或注册已过期'
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const agent = agents[0];
|
|
535
|
+
const now = new Date();
|
|
536
|
+
const expiresAt = new Date(agent.expires_at);
|
|
537
|
+
const online = now < expiresAt;
|
|
538
|
+
// Get rotation chain
|
|
539
|
+
const [chainResult] = await connection.execute('SELECT envelope FROM rotation_chain WHERE initial_id = ? ORDER BY sequence ASC', [initialId]);
|
|
540
|
+
const rotationChain = chainResult.map(row => ({
|
|
541
|
+
envelope: row.envelope
|
|
542
|
+
}));
|
|
543
|
+
const agentData = {
|
|
544
|
+
initial_id: agent.initial_id,
|
|
545
|
+
current_agent_id: agent.current_agent_id,
|
|
546
|
+
online,
|
|
547
|
+
manifest: JSON.parse(agent.manifest),
|
|
548
|
+
routes: JSON.parse(agent.routes),
|
|
549
|
+
rotation_chain: rotationChain,
|
|
550
|
+
last_seen: agent.last_seen.toISOString()
|
|
551
|
+
};
|
|
552
|
+
// Cache the result
|
|
553
|
+
if (online) {
|
|
554
|
+
const ttl = Math.max(1, Math.floor((expiresAt.getTime() - now.getTime()) / 1000));
|
|
555
|
+
await this.cache.setAgent(initialId, agentData, ttl);
|
|
556
|
+
await this.cache.setRotationChain(initialId, rotationChain, ttl);
|
|
557
|
+
}
|
|
558
|
+
if (!online) {
|
|
559
|
+
res.status(404).json({
|
|
560
|
+
initial_id: initialId,
|
|
561
|
+
online: false,
|
|
562
|
+
error: {
|
|
563
|
+
code: 'AGENT_NOT_FOUND',
|
|
564
|
+
message: '未注册或注册已过期'
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
res.json(agentData);
|
|
570
|
+
}
|
|
571
|
+
finally {
|
|
572
|
+
connection.release();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
console.error('Get agent error:', error);
|
|
577
|
+
res.status(500).json({
|
|
578
|
+
error: {
|
|
579
|
+
code: 'INTERNAL_ERROR',
|
|
580
|
+
message: 'Internal server error'
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async deleteAgent(req, res) {
|
|
586
|
+
try {
|
|
587
|
+
const initialId = this.normalizeInitialId(req.params.initialId);
|
|
588
|
+
if (!initialId) {
|
|
589
|
+
res.status(400).json({
|
|
590
|
+
error: { code: 'INVALID_PARAMS', message: 'Invalid agent_id format in URL' }
|
|
591
|
+
});
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const connection = await this.db.getConnection();
|
|
595
|
+
try {
|
|
596
|
+
await connection.execute('DELETE FROM agents WHERE initial_id = ?', [initialId]);
|
|
597
|
+
await this.cache.deleteAgent(initialId);
|
|
598
|
+
res.json({ status: 'ok' });
|
|
599
|
+
}
|
|
600
|
+
finally {
|
|
601
|
+
connection.release();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
console.error('Delete agent error:', error);
|
|
606
|
+
res.status(500).json({
|
|
607
|
+
error: {
|
|
608
|
+
code: 'INTERNAL_ERROR',
|
|
609
|
+
message: 'Internal server error'
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
async heartbeat(req, res) {
|
|
615
|
+
try {
|
|
616
|
+
const initialId = this.normalizeInitialId(req.params.initialId);
|
|
617
|
+
if (!initialId) {
|
|
618
|
+
res.status(400).json({
|
|
619
|
+
error: { code: 'INVALID_PARAMS', message: 'Invalid agent_id format in URL' }
|
|
620
|
+
});
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const cached = await this.cache.getAgent(initialId);
|
|
624
|
+
if (!cached) {
|
|
625
|
+
const connection = await this.db.getConnection();
|
|
626
|
+
try {
|
|
627
|
+
const [agents] = await connection.execute('SELECT 1 FROM agents WHERE initial_id = ? AND expires_at > NOW()', [initialId]);
|
|
628
|
+
if (agents.length === 0) {
|
|
629
|
+
res.status(404).json({
|
|
630
|
+
error: {
|
|
631
|
+
code: 'AGENT_NOT_FOUND',
|
|
632
|
+
message: 'Agent not registered'
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
connection.release();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
this.heartbeatQueue.add(initialId);
|
|
643
|
+
res.json({
|
|
644
|
+
status: 'ok',
|
|
645
|
+
expires_at: new Date(Date.now() + this.config.registration.ttlSeconds * 1000).toISOString()
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
console.error('Heartbeat error:', error);
|
|
650
|
+
res.status(500).json({
|
|
651
|
+
error: {
|
|
652
|
+
code: 'INTERNAL_ERROR',
|
|
653
|
+
message: 'Internal server error'
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async drainHeartbeats() {
|
|
659
|
+
if (this.heartbeatQueue.size === 0)
|
|
660
|
+
return;
|
|
661
|
+
const oldQueue = this.heartbeatQueue;
|
|
662
|
+
this.heartbeatQueue = new Set();
|
|
663
|
+
const allIds = Array.from(oldQueue);
|
|
664
|
+
const expiresAt = new Date(Date.now() + this.config.registration.ttlSeconds * 1000);
|
|
665
|
+
for (let i = 0; i < allIds.length; i += this.heartbeatBatchSize) {
|
|
666
|
+
const batch = allIds.slice(i, i + this.heartbeatBatchSize);
|
|
667
|
+
const connection = await this.db.getConnection();
|
|
668
|
+
try {
|
|
669
|
+
const placeholders = batch.map(() => '?').join(',');
|
|
670
|
+
await connection.execute(`UPDATE agents SET last_seen = NOW(), expires_at = DATE_ADD(NOW(), INTERVAL ? SECOND) WHERE initial_id IN (${placeholders})`, [this.config.registration.ttlSeconds, ...batch]);
|
|
671
|
+
}
|
|
672
|
+
finally {
|
|
673
|
+
connection.release();
|
|
674
|
+
}
|
|
675
|
+
for (const id of batch) {
|
|
676
|
+
const cached = await this.cache.getAgent(id);
|
|
677
|
+
if (cached) {
|
|
678
|
+
cached.last_seen = new Date().toISOString();
|
|
679
|
+
cached.expires_at = expiresAt.toISOString();
|
|
680
|
+
await this.cache.setAgent(id, cached, this.config.registration.ttlSeconds);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async searchAgents(req, res) {
|
|
686
|
+
try {
|
|
687
|
+
const namespace = req.query.namespace;
|
|
688
|
+
const capability = req.query.capability;
|
|
689
|
+
const cursor = req.query.cursor;
|
|
690
|
+
const limit = Math.min(100, parseInt(req.query.limit || '20'));
|
|
691
|
+
const offset = cursor ? parseInt(Buffer.from(cursor, 'base64').toString(), 10) : 0;
|
|
692
|
+
const connection = await this.db.getConnection();
|
|
693
|
+
try {
|
|
694
|
+
let query = `
|
|
695
|
+
SELECT DISTINCT a.*
|
|
696
|
+
FROM agents a
|
|
697
|
+
WHERE a.expires_at > NOW()
|
|
698
|
+
`;
|
|
699
|
+
const params = [];
|
|
700
|
+
if (namespace) {
|
|
701
|
+
query += ' AND a.namespace = ?';
|
|
702
|
+
params.push(namespace);
|
|
703
|
+
}
|
|
704
|
+
if (capability) {
|
|
705
|
+
query += `
|
|
706
|
+
AND EXISTS (
|
|
707
|
+
SELECT 1 FROM agent_capabilities ac
|
|
708
|
+
WHERE ac.initial_id = a.initial_id
|
|
709
|
+
AND ac.capability = ?
|
|
710
|
+
)
|
|
711
|
+
`;
|
|
712
|
+
params.push(capability);
|
|
713
|
+
}
|
|
714
|
+
query += ` ORDER BY a.last_seen DESC LIMIT ? OFFSET ?`;
|
|
715
|
+
params.push(limit, offset);
|
|
716
|
+
const [agents] = await connection.execute(query, params);
|
|
717
|
+
const agentList = agents;
|
|
718
|
+
const results = [];
|
|
719
|
+
if (agentList.length > 0) {
|
|
720
|
+
const initialIds = agentList.map((a) => a.initial_id);
|
|
721
|
+
const placeholders = initialIds.map(() => '?').join(',');
|
|
722
|
+
const [chainResults] = await connection.execute(`SELECT initial_id, envelope FROM rotation_chain WHERE initial_id IN (${placeholders}) ORDER BY sequence ASC`, initialIds);
|
|
723
|
+
const chainMap = new Map();
|
|
724
|
+
for (const row of chainResults) {
|
|
725
|
+
if (!chainMap.has(row.initial_id)) {
|
|
726
|
+
chainMap.set(row.initial_id, []);
|
|
727
|
+
}
|
|
728
|
+
chainMap.get(row.initial_id).push({ envelope: row.envelope });
|
|
729
|
+
}
|
|
730
|
+
for (const agent of agentList) {
|
|
731
|
+
results.push({
|
|
732
|
+
initial_id: agent.initial_id,
|
|
733
|
+
current_agent_id: agent.current_agent_id,
|
|
734
|
+
manifest: JSON.parse(agent.manifest),
|
|
735
|
+
routes: JSON.parse(agent.routes),
|
|
736
|
+
rotation_chain: chainMap.get(agent.initial_id) || [],
|
|
737
|
+
last_seen: agent.last_seen.toISOString()
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const nextCursor = agentList.length < limit ? null :
|
|
742
|
+
Buffer.from(String(offset + limit)).toString('base64');
|
|
743
|
+
res.json({
|
|
744
|
+
agents: results,
|
|
745
|
+
next_cursor: nextCursor
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
finally {
|
|
749
|
+
connection.release();
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
console.error('Search agents error:', error);
|
|
754
|
+
res.status(500).json({
|
|
755
|
+
error: {
|
|
756
|
+
code: 'INTERNAL_ERROR',
|
|
757
|
+
message: 'Internal server error'
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
validateRegistrationRequest(request) {
|
|
763
|
+
if (!request.agent_id) {
|
|
764
|
+
return 'agent_id is required';
|
|
765
|
+
}
|
|
766
|
+
if (!request.manifest) {
|
|
767
|
+
return 'manifest is required';
|
|
768
|
+
}
|
|
769
|
+
if (!request.routes || !Array.isArray(request.routes) || request.routes.length === 0) {
|
|
770
|
+
return 'routes is required and must be a non-empty array';
|
|
771
|
+
}
|
|
772
|
+
if (request.manifest.agent_id !== request.agent_id) {
|
|
773
|
+
return 'manifest.agent_id must match request.agent_id';
|
|
774
|
+
}
|
|
775
|
+
if (!request.manifest.protocol || request.manifest.protocol !== 'adp/0.2') {
|
|
776
|
+
return 'manifest.protocol must be adp/0.2';
|
|
777
|
+
}
|
|
778
|
+
// Validate agent_id format (basic check)
|
|
779
|
+
if (!request.agent_id.startsWith('adp://')) {
|
|
780
|
+
return 'agent_id must start with adp://';
|
|
781
|
+
}
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
start() {
|
|
785
|
+
this.app.listen(this.config.port, this.config.host, () => {
|
|
786
|
+
console.log(`Registry server started on ${this.config.host}:${this.config.port}`);
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
async stop() {
|
|
790
|
+
if (this.heartbeatDrainTimer) {
|
|
791
|
+
clearInterval(this.heartbeatDrainTimer);
|
|
792
|
+
this.heartbeatDrainTimer = null;
|
|
793
|
+
}
|
|
794
|
+
if (this.rateLimitCleanupTimer) {
|
|
795
|
+
clearInterval(this.rateLimitCleanupTimer);
|
|
796
|
+
this.rateLimitCleanupTimer = null;
|
|
797
|
+
}
|
|
798
|
+
await this.drainHeartbeats();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
exports.RegistryService = RegistryService;
|
|
802
|
+
//# sourceMappingURL=service.js.map
|