@agentunion/fastaun 0.4.9 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/_packed_docs/CHANGELOG.md +51 -0
  3. package/_packed_docs/INDEX.md +31 -14
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +20 -14
  5. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +244 -16
  6. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +114 -28
  7. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +7 -4
  8. package/_packed_docs/sdk/09-group-rpc-manual.md +238 -2
  9. package/_packed_docs/sdk/09-proxy-rpc-manual.md +231 -0
  10. package/_packed_docs/sdk/09-storage-rpc-manual.md +354 -22
  11. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +15 -11
  12. package/_packed_docs/sdk/INDEX.md +14 -8
  13. package/_packed_docs/sdk/Notify/351/200/232/347/237/245/346/226/271/346/241/210.md +214 -0
  14. package/_packed_docs/sdk/README.md +8 -6
  15. package/dist/client/delivery.d.ts +9 -1
  16. package/dist/client/delivery.js +255 -44
  17. package/dist/client/delivery.js.map +1 -1
  18. package/dist/client/rpc-pipeline.js +31 -7
  19. package/dist/client/rpc-pipeline.js.map +1 -1
  20. package/dist/client/v2-e2ee.d.ts +2 -0
  21. package/dist/client/v2-e2ee.js +32 -1
  22. package/dist/client/v2-e2ee.js.map +1 -1
  23. package/dist/client.d.ts +22 -1
  24. package/dist/client.js +132 -17
  25. package/dist/client.js.map +1 -1
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/keystore/local-identity-store.js +10 -0
  30. package/dist/keystore/local-identity-store.js.map +1 -1
  31. package/dist/secret-store/file-store.js +35 -2
  32. package/dist/secret-store/file-store.js.map +1 -1
  33. package/dist/service-proxy.d.ts +197 -0
  34. package/dist/service-proxy.js +1387 -0
  35. package/dist/service-proxy.js.map +1 -0
  36. package/dist/transport.d.ts +2 -0
  37. package/dist/transport.js +45 -0
  38. package/dist/transport.js.map +1 -1
  39. package/dist/v2/session/keystore.js +6 -6
  40. package/dist/v2/session/keystore.js.map +1 -1
  41. package/dist/version.d.ts +1 -1
  42. package/dist/version.js +1 -1
  43. package/dist/version.js.map +1 -1
  44. package/package.json +1 -1
@@ -0,0 +1,1387 @@
1
+ import * as http from 'node:http';
2
+ import * as https from 'node:https';
3
+ import { URL } from 'node:url';
4
+ import WebSocket from 'ws';
5
+ import { AuthError, ConnectionError, ValidationError } from './errors.js';
6
+ const LOG_MODULE = 'service_proxy';
7
+ const PROXY_DISCOVERY_CACHE_KEY = 'service_proxy_discovery';
8
+ const PROXY_DISCOVERY_CACHE_TTL_MS = 3600_000;
9
+ const TOKEN_EXPIRY_SKEW_SECONDS = 30;
10
+ const HOP_BY_HOP_HEADERS = new Set([
11
+ 'connection',
12
+ 'upgrade',
13
+ 'keep-alive',
14
+ 'proxy-authenticate',
15
+ 'proxy-authorization',
16
+ 'te',
17
+ 'trailer',
18
+ 'transfer-encoding',
19
+ ]);
20
+ const AUTO_RESPONSE_HEADERS = new Set(['content-length', 'date', 'server']);
21
+ const ALLOWED_SCHEMES = new Set(['http:', 'https:', 'ws:', 'wss:']);
22
+ const RESERVED_SERVICE_NAMES = new Set([
23
+ 'api',
24
+ 'health',
25
+ 'metrics',
26
+ 'status',
27
+ 'proxy',
28
+ 'admin',
29
+ 'ws',
30
+ 'wss',
31
+ 'static',
32
+ 'favicon.ico',
33
+ ]);
34
+ const SENSITIVE_METADATA_KEYS = new Set([
35
+ 'endpoint',
36
+ 'url',
37
+ 'uri',
38
+ 'token',
39
+ 'access_token',
40
+ 'authorization',
41
+ 'cookie',
42
+ 'secret',
43
+ 'password',
44
+ 'private_key',
45
+ 'key',
46
+ 'cert',
47
+ 'certificate',
48
+ ]);
49
+ const SERVICE_NAME_RE = /^[a-z0-9_-]+$/;
50
+ const STREAMING_SERVICE_TYPES = new Set(['mcp', 'mcp-sse', 'mcp-streamable-http', 'sse', 'stream', 'file', 'ws', 'websocket']);
51
+ const VALID_STREAM_MODES = new Set(['auto', 'stream', 'always', 'no_stream']);
52
+ const FILE_CONTENT_TYPES = new Set([
53
+ 'application/octet-stream',
54
+ 'application/pdf',
55
+ 'application/zip',
56
+ 'application/x-zip-compressed',
57
+ 'application/gzip',
58
+ 'application/x-tar',
59
+ ]);
60
+ export class ServiceRecord {
61
+ service_name;
62
+ endpoint;
63
+ service_type;
64
+ visibility;
65
+ metadata;
66
+ constructor(params) {
67
+ this.service_name = params.service_name;
68
+ this.endpoint = params.endpoint;
69
+ this.service_type = String(params.service_type ?? 'http').trim() || 'http';
70
+ this.visibility = String(params.visibility ?? 'private').trim() || 'private';
71
+ this.metadata = sanitizeMetadata(params.metadata ?? {});
72
+ }
73
+ summary() {
74
+ return {
75
+ service_name: this.service_name,
76
+ service_type: this.service_type,
77
+ visibility: this.visibility,
78
+ metadata: sanitizeMetadata(this.metadata),
79
+ };
80
+ }
81
+ }
82
+ export class EndpointPolicy {
83
+ allowedHosts;
84
+ constructor(opts = {}) {
85
+ this.allowedHosts = new Set(Array.from(opts.allowedHosts ?? []).map(normalizeHost).filter(Boolean));
86
+ }
87
+ isAllowed(endpoint) {
88
+ let parsed;
89
+ try {
90
+ parsed = new URL(String(endpoint ?? '').trim());
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ if (!ALLOWED_SCHEMES.has(parsed.protocol))
96
+ return false;
97
+ const host = normalizeHost(parsed.hostname);
98
+ if (!host)
99
+ return false;
100
+ if (this.allowedHosts.has(host))
101
+ return true;
102
+ if (host === 'localhost')
103
+ return true;
104
+ return isIPv4LoopbackHost(host);
105
+ }
106
+ }
107
+ export class EmbeddedServiceRegistry {
108
+ _endpointPolicy;
109
+ _replaceExisting;
110
+ _records = new Map();
111
+ constructor(opts = {}) {
112
+ this._endpointPolicy = opts.endpointPolicy ?? new EndpointPolicy();
113
+ this._replaceExisting = opts.replaceExisting ?? true;
114
+ }
115
+ register(serviceName, endpoint, opts = {}) {
116
+ const normalizedName = normalizeServiceName(serviceName);
117
+ const endpointText = String(endpoint ?? '').trim();
118
+ if (!this._endpointPolicy.isAllowed(endpointText)) {
119
+ throw new ValidationError('endpoint is not allowed');
120
+ }
121
+ if (this._records.has(normalizedName) && !this._replaceExisting) {
122
+ throw new ValidationError(`service already registered: ${normalizedName}`);
123
+ }
124
+ const record = new ServiceRecord({
125
+ service_name: normalizedName,
126
+ endpoint: endpointText,
127
+ service_type: opts.serviceType,
128
+ visibility: opts.visibility,
129
+ metadata: opts.metadata,
130
+ });
131
+ this._records.set(normalizedName, record);
132
+ return record;
133
+ }
134
+ unregister(serviceName) {
135
+ return this._records.delete(normalizeServiceName(serviceName));
136
+ }
137
+ get(serviceName) {
138
+ return this._records.get(normalizeServiceName(serviceName)) ?? null;
139
+ }
140
+ listRecords() {
141
+ return Array.from(this._records.values()).sort((a, b) => a.service_name.localeCompare(b.service_name));
142
+ }
143
+ listSummaries() {
144
+ return this.listRecords().map((record) => record.summary());
145
+ }
146
+ }
147
+ class AsyncQueue {
148
+ _items = [];
149
+ _waiters = [];
150
+ _closed = false;
151
+ push(value) {
152
+ if (this._closed)
153
+ return;
154
+ const waiter = this._waiters.shift();
155
+ if (waiter)
156
+ waiter(value);
157
+ else
158
+ this._items.push(value);
159
+ }
160
+ close() {
161
+ this._closed = true;
162
+ for (const waiter of this._waiters.splice(0))
163
+ waiter(null);
164
+ }
165
+ shift(timeoutMs) {
166
+ if (this._items.length > 0)
167
+ return Promise.resolve(this._items.shift());
168
+ if (this._closed)
169
+ return Promise.resolve(null);
170
+ return new Promise((resolve) => {
171
+ let timer = null;
172
+ const done = (value) => {
173
+ if (timer !== null)
174
+ clearTimeout(timer);
175
+ resolve(value);
176
+ };
177
+ this._waiters.push(done);
178
+ if (timeoutMs !== undefined) {
179
+ timer = setTimeout(() => {
180
+ const idx = this._waiters.indexOf(done);
181
+ if (idx >= 0)
182
+ this._waiters.splice(idx, 1);
183
+ resolve(null);
184
+ }, Math.max(0, timeoutMs));
185
+ }
186
+ });
187
+ }
188
+ }
189
+ class TunnelSocket {
190
+ _ws;
191
+ _queue = new AsyncQueue();
192
+ constructor(ws) {
193
+ this._ws = ws;
194
+ ws.on('message', (data) => {
195
+ if (typeof data === 'string') {
196
+ this._queue.push(data);
197
+ }
198
+ else if (Buffer.isBuffer(data)) {
199
+ this._queue.push(data.toString('utf-8'));
200
+ }
201
+ else if (Array.isArray(data)) {
202
+ this._queue.push(Buffer.concat(data).toString('utf-8'));
203
+ }
204
+ else {
205
+ this._queue.push(Buffer.from(data).toString('utf-8'));
206
+ }
207
+ });
208
+ ws.on('close', () => this._queue.close());
209
+ ws.on('error', () => this._queue.close());
210
+ }
211
+ async send(message) {
212
+ const payload = JSON.stringify(message);
213
+ await new Promise((resolve, reject) => {
214
+ this._ws.send(payload, (err) => (err ? reject(err) : resolve()));
215
+ });
216
+ }
217
+ recv(timeoutMs) {
218
+ return this._queue.shift(timeoutMs);
219
+ }
220
+ close() {
221
+ try {
222
+ this._ws.close();
223
+ }
224
+ catch { /* noop */ }
225
+ this._queue.close();
226
+ }
227
+ }
228
+ export class ServiceProxyClient {
229
+ providerAid;
230
+ registry;
231
+ maxResponseBodyBytes;
232
+ maxTunnelMessageBytes;
233
+ _logger;
234
+ _aunClient;
235
+ _running = false;
236
+ _activeTunnel = null;
237
+ constructor(opts) {
238
+ this.providerAid = String(opts.providerAid ?? '').trim();
239
+ this.registry = opts.registry ?? new EmbeddedServiceRegistry({ endpointPolicy: opts.endpointPolicy });
240
+ this._logger = opts.logger ?? null;
241
+ this._aunClient = opts.aunClient ?? null;
242
+ this.maxResponseBodyBytes = Math.max(1, Math.floor(opts.maxResponseBodyBytes ?? 16 * 1024 * 1024));
243
+ this.maxTunnelMessageBytes = Math.max(1, Math.floor(opts.maxTunnelMessageBytes ?? 64 * 1024 * 1024));
244
+ }
245
+ get isRunning() {
246
+ return this._running;
247
+ }
248
+ get is_running() {
249
+ return this.isRunning;
250
+ }
251
+ stop() {
252
+ this._running = false;
253
+ this._activeTunnel?.close();
254
+ }
255
+ registerService(serviceName, endpoint, opts = {}) {
256
+ return this.registry.register(serviceName, endpoint, {
257
+ serviceType: opts.serviceType ?? opts.service_type,
258
+ visibility: opts.visibility,
259
+ metadata: opts.metadata,
260
+ });
261
+ }
262
+ register_service(serviceName, endpoint, opts = {}) {
263
+ return this.registerService(serviceName, endpoint, opts);
264
+ }
265
+ unregisterService(serviceName) {
266
+ return this.registry.unregister(serviceName);
267
+ }
268
+ unregister_service(serviceName) {
269
+ return this.unregisterService(serviceName);
270
+ }
271
+ listServiceSummaries() {
272
+ return this.registry.listSummaries();
273
+ }
274
+ list_service_summaries() {
275
+ return this.listServiceSummaries();
276
+ }
277
+ async registerServicesWithGateway(services) {
278
+ const call = this._gatewayCallMethod(true);
279
+ const result = await call('proxy.register_services', {
280
+ provider_aid: this.providerAid,
281
+ services: services ?? this.listServiceSummaries(),
282
+ });
283
+ if (!isRecord(result))
284
+ return {};
285
+ if (result.ok === false) {
286
+ throw new ValidationError(String(result.error ?? 'Gateway service registration failed'));
287
+ }
288
+ return result;
289
+ }
290
+ register_services_with_gateway(services) {
291
+ return this.registerServicesWithGateway(services);
292
+ }
293
+ async unregisterServicesFromGateway(serviceNames) {
294
+ const call = this._gatewayCallMethod(true);
295
+ const params = { provider_aid: this.providerAid };
296
+ if (typeof serviceNames === 'string')
297
+ params.service_names = [serviceNames];
298
+ else if (Array.isArray(serviceNames))
299
+ params.service_names = serviceNames.map(String);
300
+ const result = await call('proxy.unregister_services', params);
301
+ return isRecord(result) ? result : {};
302
+ }
303
+ unregister_services_from_gateway(serviceNames) {
304
+ return this.unregisterServicesFromGateway(serviceNames);
305
+ }
306
+ async listGatewayServices() {
307
+ const call = this._gatewayCallMethod(true);
308
+ const result = await call('proxy.list_services', { provider_aid: this.providerAid });
309
+ return isRecord(result) ? result : {};
310
+ }
311
+ list_gateway_services() {
312
+ return this.listGatewayServices();
313
+ }
314
+ async discoverProxyServer(opts = {}) {
315
+ const forceRefresh = Boolean(opts.forceRefresh ?? opts.force_refresh ?? false);
316
+ if (!forceRefresh) {
317
+ const cached = await this._loadCachedProxyDiscovery();
318
+ if (cached)
319
+ return cached;
320
+ }
321
+ const errors = [];
322
+ for (const url of this._proxyWellKnownUrls()) {
323
+ try {
324
+ const discovery = await this._fetchProxyWellKnown(url, opts.timeout ?? 5);
325
+ await this._persistProxyDiscovery(discovery);
326
+ return discovery;
327
+ }
328
+ catch (exc) {
329
+ errors.push(`${url}: ${formatError(exc)}`);
330
+ this._logWarn(`Service Proxy discovery failed: url=${url} err=${formatError(exc)}`);
331
+ }
332
+ }
333
+ throw new ConnectionError(`Service Proxy discovery failed: ${errors.join('; ')}`, { retryable: true });
334
+ }
335
+ discover_proxy_server(opts = {}) {
336
+ return this.discoverProxyServer(opts);
337
+ }
338
+ async discoverProxyWsUrl(opts = {}) {
339
+ const discovery = await this.discoverProxyServer(opts);
340
+ return String(discovery.ws_url ?? '').trim();
341
+ }
342
+ discover_proxy_ws_url(opts = {}) {
343
+ return this.discoverProxyWsUrl(opts);
344
+ }
345
+ async connectOnce(opts = {}) {
346
+ this._running = true;
347
+ try {
348
+ await this._autoRegisterServicesWithGateway();
349
+ const tunnel = await this._connectProxyWs();
350
+ this._activeTunnel = tunnel;
351
+ await tunnel.send({
352
+ type: 'service_proxy_auth',
353
+ request_id: opts.authRequestId ?? 'auth',
354
+ provider_aid: this.providerAid,
355
+ client_version: 'ts',
356
+ });
357
+ const authResponse = parseTunnelMessage(await tunnel.recv());
358
+ if (!authResponse.ok) {
359
+ const err = isRecord(authResponse.error) ? authResponse.error : {};
360
+ throw new AuthError(String(err.message ?? 'Service Proxy auth failed'));
361
+ }
362
+ const registered = await this.registerServicesWithProxyServer(tunnel, {
363
+ registerRequestId: opts.registerRequestId ?? 'register-services',
364
+ });
365
+ let heartbeat = false;
366
+ if (opts.heartbeatRequestId) {
367
+ await tunnel.send({ type: 'heartbeat', request_id: opts.heartbeatRequestId });
368
+ const hb = parseTunnelMessage(await tunnel.recv());
369
+ heartbeat = Boolean(hb.ok);
370
+ }
371
+ return { registered, heartbeat };
372
+ }
373
+ finally {
374
+ this._running = false;
375
+ this._activeTunnel?.close();
376
+ this._activeTunnel = null;
377
+ }
378
+ }
379
+ connect_once(opts = {}) {
380
+ return this.connectOnce({
381
+ authRequestId: opts.auth_request_id,
382
+ registerRequestId: opts.register_request_id,
383
+ heartbeatRequestId: opts.heartbeat_request_id,
384
+ });
385
+ }
386
+ async serveOnce(opts = {}) {
387
+ this._running = true;
388
+ try {
389
+ await this._autoRegisterServicesWithGateway();
390
+ const tunnel = await this._connectProxyWs();
391
+ this._activeTunnel = tunnel;
392
+ return await this._serveTunnel(tunnel, {
393
+ authRequestId: opts.authRequestId ?? 'auth',
394
+ registerRequestId: opts.registerRequestId ?? 'register-services',
395
+ maxRequests: opts.maxRequests ?? 1,
396
+ });
397
+ }
398
+ finally {
399
+ this._running = false;
400
+ this._activeTunnel?.close();
401
+ this._activeTunnel = null;
402
+ }
403
+ }
404
+ serve_once(opts = {}) {
405
+ return this.serveOnce({
406
+ authRequestId: opts.auth_request_id,
407
+ registerRequestId: opts.register_request_id,
408
+ maxRequests: opts.max_requests,
409
+ });
410
+ }
411
+ async serveForever(opts = {}) {
412
+ const mode = opts.connectionMode ?? 'persistent';
413
+ if (mode !== 'persistent' && mode !== 'on_demand') {
414
+ throw new ValidationError('connectionMode must be persistent or on_demand');
415
+ }
416
+ this._running = true;
417
+ const stats = {
418
+ connection_mode: mode,
419
+ connections: 0,
420
+ registered: 0,
421
+ handled_requests: 0,
422
+ wakeup_count: 0,
423
+ };
424
+ try {
425
+ if (mode === 'persistent') {
426
+ while (this._running) {
427
+ try {
428
+ await this._autoRegisterServicesWithGateway();
429
+ const tunnel = await this._connectProxyWs();
430
+ this._activeTunnel = tunnel;
431
+ const result = await this._serveTunnel(tunnel, {
432
+ authRequestId: opts.authRequestId ?? 'auth',
433
+ registerRequestId: opts.registerRequestId ?? 'register-services',
434
+ idleTimeoutSeconds: undefined,
435
+ });
436
+ stats.connections = Number(stats.connections) + 1;
437
+ stats.registered = Number(result.registered ?? stats.registered);
438
+ stats.handled_requests = Number(stats.handled_requests) + Number(result.handled_requests ?? 0);
439
+ }
440
+ catch (exc) {
441
+ if (!this._running)
442
+ break;
443
+ this._logWarn(`persistent tunnel reconnect scheduled after error: ${formatError(exc)}`);
444
+ await sleep(Math.max(0, opts.reconnectDelaySeconds ?? 1) * 1000);
445
+ }
446
+ finally {
447
+ this._activeTunnel?.close();
448
+ this._activeTunnel = null;
449
+ }
450
+ }
451
+ return stats;
452
+ }
453
+ return await this._serveOnDemand(stats, opts);
454
+ }
455
+ finally {
456
+ this._running = false;
457
+ this._activeTunnel?.close();
458
+ this._activeTunnel = null;
459
+ }
460
+ }
461
+ serve_forever(opts = {}) {
462
+ return this.serveForever({
463
+ connectionMode: opts.connection_mode,
464
+ authRequestId: opts.auth_request_id,
465
+ registerRequestId: opts.register_request_id,
466
+ idleTimeoutSeconds: opts.idle_timeout_seconds,
467
+ reconnectDelaySeconds: opts.reconnect_delay_seconds,
468
+ });
469
+ }
470
+ async registerServicesWithProxyServer(tunnel, opts = {}) {
471
+ const services = opts.services ?? this.listServiceSummaries();
472
+ await tunnel.send({
473
+ type: 'register_services',
474
+ request_id: opts.registerRequestId ?? 'register-services',
475
+ services,
476
+ });
477
+ const response = parseTunnelMessage(await tunnel.recv());
478
+ if (!response.ok) {
479
+ throw new ValidationError('Service Proxy service registration failed');
480
+ }
481
+ return Number(response.count ?? services.length);
482
+ }
483
+ register_services_with_proxy_server(tunnel, opts = {}) {
484
+ return this.registerServicesWithProxyServer(tunnel, {
485
+ registerRequestId: opts.register_request_id,
486
+ services: opts.services,
487
+ });
488
+ }
489
+ async _serveOnDemand(stats, opts) {
490
+ const client = this._aunClient;
491
+ if (!client || typeof client.on !== 'function') {
492
+ throw new ValidationError('on_demand mode requires aunClient with on()');
493
+ }
494
+ await this._autoRegisterServicesWithGateway();
495
+ const queue = new AsyncQueue();
496
+ const subscription = client.on('app.service_proxy.wakeup', (payload) => {
497
+ if (!isRecord(payload))
498
+ return;
499
+ if (String(payload.type ?? '') !== 'aun.service_proxy.wakeup')
500
+ return;
501
+ const providerAid = String(payload.provider_aid ?? '').trim();
502
+ if (providerAid && providerAid !== this.providerAid)
503
+ return;
504
+ queue.push({ ...payload });
505
+ });
506
+ try {
507
+ while (this._running) {
508
+ const wakeup = await queue.shift(100);
509
+ if (!this._running)
510
+ break;
511
+ if (!wakeup)
512
+ continue;
513
+ stats.wakeup_count = Number(stats.wakeup_count) + 1;
514
+ try {
515
+ await this._autoRegisterServicesWithGateway();
516
+ const tunnel = await this._connectProxyWs();
517
+ this._activeTunnel = tunnel;
518
+ const result = await this._serveTunnel(tunnel, {
519
+ authRequestId: opts.authRequestId ?? 'auth',
520
+ registerRequestId: opts.registerRequestId ?? 'register-services',
521
+ idleTimeoutSeconds: opts.idleTimeoutSeconds ?? 60,
522
+ });
523
+ stats.connections = Number(stats.connections) + 1;
524
+ stats.registered = Number(result.registered ?? stats.registered);
525
+ stats.handled_requests = Number(stats.handled_requests) + Number(result.handled_requests ?? 0);
526
+ }
527
+ catch (exc) {
528
+ if (!this._running)
529
+ break;
530
+ this._logWarn(`on-demand tunnel connection failed after wakeup: ${formatError(exc)}`);
531
+ await sleep(Math.max(0, opts.reconnectDelaySeconds ?? 1) * 1000);
532
+ }
533
+ finally {
534
+ this._activeTunnel?.close();
535
+ this._activeTunnel = null;
536
+ }
537
+ }
538
+ return stats;
539
+ }
540
+ finally {
541
+ subscription?.unsubscribe?.();
542
+ queue.close();
543
+ }
544
+ }
545
+ async _serveTunnel(tunnel, opts) {
546
+ let handledRequests = 0;
547
+ const activeWsQueues = new Map();
548
+ const activeWsTasks = new Map();
549
+ const registered = await this._authAndRegister(tunnel, opts.authRequestId, opts.registerRequestId);
550
+ const cleanupTasks = () => {
551
+ for (const [connectionId, task] of activeWsTasks) {
552
+ void task.then(() => {
553
+ activeWsTasks.delete(connectionId);
554
+ activeWsQueues.delete(connectionId);
555
+ }, (exc) => {
556
+ activeWsTasks.delete(connectionId);
557
+ activeWsQueues.delete(connectionId);
558
+ this._logError(`websocket backend task failed: connection_id=${connectionId} err=${formatError(exc)}`, exc);
559
+ });
560
+ }
561
+ };
562
+ try {
563
+ while (this._running) {
564
+ cleanupTasks();
565
+ if (opts.maxRequests !== undefined && handledRequests >= opts.maxRequests && activeWsTasks.size === 0)
566
+ break;
567
+ const waitForWsTasks = opts.maxRequests !== undefined && handledRequests >= opts.maxRequests && activeWsTasks.size > 0;
568
+ const timeoutMs = waitForWsTasks
569
+ ? 50
570
+ : opts.idleTimeoutSeconds === undefined
571
+ ? undefined
572
+ : Math.max(0, opts.idleTimeoutSeconds * 1000);
573
+ const raw = await tunnel.recv(timeoutMs);
574
+ if (raw === null) {
575
+ if (timeoutMs !== undefined && activeWsTasks.size > 0)
576
+ continue;
577
+ break;
578
+ }
579
+ let message;
580
+ try {
581
+ const parsed = JSON.parse(raw);
582
+ if (!isRecord(parsed))
583
+ continue;
584
+ message = parsed;
585
+ }
586
+ catch {
587
+ continue;
588
+ }
589
+ const msgType = String(message.type ?? '');
590
+ if (msgType === 'service_proxy_request') {
591
+ const requestId = String(message.request_id ?? '');
592
+ const bodyIter = message.body_stream === true
593
+ ? this._iterRequestBodyChunks(tunnel, requestId, activeWsQueues)
594
+ : undefined;
595
+ for await (const response of this.iterRequestMessages(message, { bodyIter })) {
596
+ await tunnel.send(response);
597
+ }
598
+ handledRequests += 1;
599
+ }
600
+ else if (msgType === 'ws_connect') {
601
+ const connectionId = String(message.connection_id ?? '');
602
+ if (!connectionId) {
603
+ await tunnel.send(wsErrorMessage('', 'missing_connection_id', 'connection_id is required'));
604
+ continue;
605
+ }
606
+ const queue = new AsyncQueue();
607
+ activeWsQueues.set(connectionId, queue);
608
+ activeWsTasks.set(connectionId, this.handleWsConnectMessage(message, tunnel, queue));
609
+ handledRequests += 1;
610
+ }
611
+ else if (msgType === 'ws_message' || msgType === 'ws_close' || msgType === 'ws_error') {
612
+ const connectionId = String(message.connection_id ?? '');
613
+ const queue = activeWsQueues.get(connectionId);
614
+ if (queue)
615
+ queue.push(message);
616
+ else if (connectionId)
617
+ await tunnel.send(wsErrorMessage(connectionId, 'unknown_ws_connection', 'WebSocket connection is not active'));
618
+ }
619
+ else if (msgType === 'heartbeat_ack') {
620
+ continue;
621
+ }
622
+ else {
623
+ await tunnel.send(errorMessage(String(message.request_id ?? ''), 'unsupported_message', 'unsupported Service Proxy tunnel message'));
624
+ }
625
+ }
626
+ return { registered, handled_requests: handledRequests };
627
+ }
628
+ finally {
629
+ for (const queue of activeWsQueues.values())
630
+ queue.close();
631
+ }
632
+ }
633
+ async _authAndRegister(tunnel, authRequestId, registerRequestId) {
634
+ await tunnel.send({
635
+ type: 'service_proxy_auth',
636
+ request_id: authRequestId,
637
+ provider_aid: this.providerAid,
638
+ client_version: 'ts',
639
+ });
640
+ const authResponse = parseTunnelMessage(await tunnel.recv());
641
+ if (!authResponse.ok) {
642
+ const err = isRecord(authResponse.error) ? authResponse.error : {};
643
+ throw new AuthError(String(err.message ?? 'Service Proxy auth failed'));
644
+ }
645
+ return this.registerServicesWithProxyServer(tunnel, { registerRequestId });
646
+ }
647
+ async *iterRequestMessages(message, opts = {}) {
648
+ const requestId = String(message.request_id ?? '');
649
+ const serviceName = String(message.service_name ?? '');
650
+ let record = null;
651
+ try {
652
+ record = this.registry.get(serviceName);
653
+ }
654
+ catch {
655
+ record = null;
656
+ }
657
+ if (!record) {
658
+ yield errorMessage(requestId, 'service_not_registered', 'service is not registered');
659
+ return;
660
+ }
661
+ const method = String(message.method ?? 'GET').toUpperCase();
662
+ const path = normalizePath(String(message.path ?? '/'));
663
+ const queryString = String(message.query_string ?? '');
664
+ const targetUrl = buildTargetUrl(record.endpoint, path, queryString);
665
+ let requestBody;
666
+ if (message.body_stream === true) {
667
+ if (!opts.bodyIter) {
668
+ yield errorMessage(requestId, 'missing_body_stream', 'request body stream is missing');
669
+ return;
670
+ }
671
+ requestBody = opts.bodyIter;
672
+ }
673
+ else {
674
+ try {
675
+ requestBody = message.body_base64 ? decodeBase64Strict(String(message.body_base64)) : Buffer.alloc(0);
676
+ }
677
+ catch {
678
+ yield errorMessage(requestId, 'invalid_body', 'body_base64 is invalid');
679
+ return;
680
+ }
681
+ }
682
+ const headers = backendHeaders(isRecord(message.headers) ? message.headers : {});
683
+ let response;
684
+ try {
685
+ response = await requestBackend(targetUrl, {
686
+ method,
687
+ headers,
688
+ body: requestBody,
689
+ timeoutMs: 30_000,
690
+ verifySsl: this._shouldVerifySsl(),
691
+ });
692
+ }
693
+ catch (exc) {
694
+ this._logWarn(`backend request failed: request_id=${requestId} service_name=${serviceName} err=${formatError(exc)}`);
695
+ yield errorMessage(requestId, 'backend_unreachable', 'backend request failed');
696
+ return;
697
+ }
698
+ const responseHeaders = responseHeadersMap(response.headers);
699
+ const detection = detectRequestProtocol(message, record);
700
+ const responseHeaderStream = detection.streamMode !== 'no_stream' && isStreamResponseHeaders(responseHeaders);
701
+ const shouldStream = detection.isStream || responseHeaderStream;
702
+ if (!shouldStream) {
703
+ try {
704
+ const body = await readResponseBodyLimited(response, this.maxResponseBodyBytes);
705
+ yield {
706
+ type: 'service_proxy_response',
707
+ request_id: requestId,
708
+ status: Number(response.statusCode ?? 0),
709
+ headers: responseHeaders,
710
+ body_base64: body.toString('base64'),
711
+ };
712
+ }
713
+ catch {
714
+ yield errorMessage(requestId, 'response_body_too_large', 'backend response body is too large');
715
+ }
716
+ return;
717
+ }
718
+ const streamType = streamTypeFromResponse(responseHeaders, detection.serviceType);
719
+ if (!responseHeaders['x-stream-type'])
720
+ responseHeaders['x-stream-type'] = streamType;
721
+ const chunkSize = Math.max(1, Math.floor(opts.chunkSize ?? 65536));
722
+ let index = 0;
723
+ let pending = null;
724
+ for await (const raw of response) {
725
+ const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
726
+ for (let offset = 0; offset < chunk.length; offset += chunkSize) {
727
+ const part = chunk.subarray(offset, Math.min(chunk.length, offset + chunkSize));
728
+ if (pending) {
729
+ yield streamMessage(requestId, index, Number(response.statusCode ?? 0), responseHeaders, pending, false);
730
+ index += 1;
731
+ }
732
+ pending = part;
733
+ }
734
+ }
735
+ if (pending) {
736
+ yield streamMessage(requestId, index, Number(response.statusCode ?? 0), responseHeaders, pending, true);
737
+ }
738
+ else if (index === 0) {
739
+ yield {
740
+ type: 'service_proxy_stream',
741
+ request_id: requestId,
742
+ index: 0,
743
+ status: Number(response.statusCode ?? 0),
744
+ headers: responseHeaders,
745
+ data_base64: '',
746
+ done: true,
747
+ };
748
+ }
749
+ }
750
+ async handleWsConnectMessage(message, tunnel, inboundQueue) {
751
+ const connectionId = String(message.connection_id ?? '');
752
+ const serviceName = String(message.service_name ?? '');
753
+ let record = null;
754
+ try {
755
+ record = this.registry.get(serviceName);
756
+ }
757
+ catch {
758
+ record = null;
759
+ }
760
+ if (!record) {
761
+ await tunnel.send(wsErrorMessage(connectionId, 'service_not_registered', 'service is not registered'));
762
+ return;
763
+ }
764
+ const path = normalizePath(String(message.path ?? '/'));
765
+ const queryString = String(message.query_string ?? '');
766
+ const targetUrl = buildTargetUrl(record.endpoint, path, queryString);
767
+ const headers = backendHeaders(isRecord(message.headers) ? message.headers : {});
768
+ const subprotocols = Array.isArray(message.subprotocols)
769
+ ? message.subprotocols.map(String).map((item) => item.trim()).filter(Boolean)
770
+ : [];
771
+ let backend;
772
+ try {
773
+ backend = new WebSocket(targetUrl, subprotocols, {
774
+ headers,
775
+ rejectUnauthorized: this._shouldVerifySsl(),
776
+ });
777
+ await waitForWsOpen(backend);
778
+ await tunnel.send({ type: 'ws_connected', connection_id: connectionId, subprotocol: backend.protocol || '' });
779
+ }
780
+ catch (exc) {
781
+ this._logWarn(`backend websocket bridge failed: connection_id=${connectionId} err=${formatError(exc)}`);
782
+ await tunnel.send(wsErrorMessage(connectionId, 'backend_ws_unreachable', 'backend websocket request failed'));
783
+ return;
784
+ }
785
+ const backendClosed = new Promise((resolve) => {
786
+ backend.on('message', (data, isBinary) => {
787
+ const payload = isBinary
788
+ ? { type: 'ws_message', connection_id: connectionId, data_base64: rawDataToBuffer(data).toString('base64') }
789
+ : { type: 'ws_message', connection_id: connectionId, text: rawDataToBuffer(data).toString('utf-8') };
790
+ tunnel.send(payload).catch(() => { });
791
+ });
792
+ backend.on('close', (code) => {
793
+ tunnel.send({ type: 'ws_close', connection_id: connectionId, code: Number(code || 1000), reason: '' }).catch(() => { });
794
+ resolve();
795
+ });
796
+ backend.on('error', () => resolve());
797
+ });
798
+ const tunnelToBackend = (async () => {
799
+ while (this._running) {
800
+ const item = await inboundQueue.shift();
801
+ if (!item)
802
+ return;
803
+ const msgType = String(item.type ?? '');
804
+ if (msgType === 'ws_message') {
805
+ if (item.text !== undefined && item.text !== null) {
806
+ backend.send(String(item.text));
807
+ }
808
+ else if (item.data_base64 !== undefined) {
809
+ let data;
810
+ try {
811
+ data = decodeBase64Strict(String(item.data_base64 ?? ''));
812
+ }
813
+ catch {
814
+ await tunnel.send(wsErrorMessage(connectionId, 'invalid_ws_frame', 'data_base64 is invalid'));
815
+ backend.close();
816
+ return;
817
+ }
818
+ backend.send(data);
819
+ }
820
+ }
821
+ else if (msgType === 'ws_close' || msgType === 'ws_error') {
822
+ backend.close(Number(item.code ?? 1000), String(item.reason ?? ''));
823
+ return;
824
+ }
825
+ }
826
+ })();
827
+ await Promise.race([backendClosed, tunnelToBackend]);
828
+ try {
829
+ backend.close();
830
+ }
831
+ catch { /* noop */ }
832
+ }
833
+ async *_iterRequestBodyChunks(tunnel, requestId, activeWsQueues) {
834
+ while (true) {
835
+ const raw = await tunnel.recv();
836
+ const message = parseTunnelMessage(raw);
837
+ const msgType = String(message.type ?? '');
838
+ if (msgType === 'ws_message' || msgType === 'ws_close' || msgType === 'ws_error') {
839
+ const queue = activeWsQueues.get(String(message.connection_id ?? ''));
840
+ if (queue) {
841
+ queue.push(message);
842
+ continue;
843
+ }
844
+ }
845
+ if (msgType !== 'service_proxy_request_body') {
846
+ throw { code: 'invalid_body_stream', message: 'unexpected tunnel message while reading body' };
847
+ }
848
+ if (String(message.request_id ?? '') !== requestId) {
849
+ throw { code: 'invalid_body_stream', message: 'request body stream request_id mismatch' };
850
+ }
851
+ if (isRecord(message.error)) {
852
+ throw {
853
+ code: String(message.error.code ?? 'request_body_stream_error'),
854
+ message: String(message.error.message ?? 'request body stream failed'),
855
+ };
856
+ }
857
+ const dataText = String(message.data_base64 ?? '');
858
+ if (dataText)
859
+ yield decodeBase64Strict(dataText);
860
+ if (message.done === true)
861
+ return;
862
+ }
863
+ }
864
+ _gatewayCallMethod(required) {
865
+ const call = this._aunClient?.call;
866
+ if (typeof call === 'function') {
867
+ return (method, params) => Promise.resolve(call.call(this._aunClient, method, params ?? {}));
868
+ }
869
+ if (required)
870
+ throw new ValidationError('Gateway service registration requires aunClient with call()');
871
+ return async () => ({ skipped: true });
872
+ }
873
+ async _autoRegisterServicesWithGateway() {
874
+ const call = this._aunClient?.call;
875
+ if (typeof call !== 'function')
876
+ return { skipped: true };
877
+ return this.registerServicesWithGateway();
878
+ }
879
+ _issuerDomainForAid(aid) {
880
+ const target = String(aid ?? '').trim().toLowerCase();
881
+ if (!target.includes('.'))
882
+ return '';
883
+ return target.split('.').slice(1).join('.').replace(/^\.+|\.+$/g, '');
884
+ }
885
+ _proxyWellKnownUrls() {
886
+ const issuer = this._issuerDomainForAid(this.providerAid);
887
+ if (!this.providerAid || !issuer) {
888
+ throw new ValidationError('providerAid must be a full AID for Service Proxy discovery');
889
+ }
890
+ return [
891
+ `https://${this.providerAid}/.well-known/aun-proxy`,
892
+ `https://proxy.${issuer}/.well-known/aun-proxy`,
893
+ ];
894
+ }
895
+ _normalizeProxyWsUrl(rawUrl) {
896
+ const value = String(rawUrl ?? '').trim();
897
+ if (!value)
898
+ return '';
899
+ let parsed;
900
+ try {
901
+ parsed = new URL(value);
902
+ }
903
+ catch {
904
+ return '';
905
+ }
906
+ if (parsed.protocol === 'ws:' && this._shouldVerifySsl())
907
+ return '';
908
+ if (parsed.protocol !== 'wss:' && parsed.protocol !== 'ws:')
909
+ return '';
910
+ if (parsed.username || parsed.password || !parsed.hostname || parsed.pathname === '/')
911
+ return '';
912
+ parsed.hash = '';
913
+ return parsed.toString();
914
+ }
915
+ _selectProxyWsUrl(payload) {
916
+ const direct = this._normalizeProxyWsUrl(String(payload.ws_url ?? ''));
917
+ if (direct)
918
+ return direct;
919
+ const servers = Array.isArray(payload.proxy_servers) ? payload.proxy_servers.filter(isRecord) : [];
920
+ servers.sort((a, b) => Number(a.priority ?? 999) - Number(b.priority ?? 999));
921
+ for (const item of servers) {
922
+ const url = this._normalizeProxyWsUrl(String(item.ws_url ?? ''));
923
+ if (url)
924
+ return url;
925
+ }
926
+ return '';
927
+ }
928
+ async _fetchProxyWellKnown(wellKnownUrl, timeoutSeconds) {
929
+ const payload = await httpGetJson(wellKnownUrl, {
930
+ timeoutMs: Math.max(100, timeoutSeconds * 1000),
931
+ verifySsl: this._shouldVerifySsl(),
932
+ });
933
+ const wsUrl = this._selectProxyWsUrl(payload);
934
+ if (!wsUrl)
935
+ throw new ValidationError('Service Proxy well-known missing valid ws_url');
936
+ return {
937
+ ...payload,
938
+ ws_url: wsUrl,
939
+ source_url: wellKnownUrl,
940
+ discovered_at: Date.now() / 1000,
941
+ };
942
+ }
943
+ async _loadCachedProxyDiscovery() {
944
+ const tokenStore = this._aunClient?._tokenStore;
945
+ if (!tokenStore || typeof tokenStore.loadMetadata !== 'function')
946
+ return null;
947
+ try {
948
+ const metadata = tokenStore.loadMetadata(this.providerAid);
949
+ const raw = metadata?.[PROXY_DISCOVERY_CACHE_KEY];
950
+ const cached = typeof raw === 'string' ? JSON.parse(raw) : raw;
951
+ if (!isRecord(cached))
952
+ return null;
953
+ const wsUrl = this._normalizeProxyWsUrl(String(cached.ws_url ?? ''));
954
+ if (!wsUrl)
955
+ return null;
956
+ const discoveredAt = Number(cached.discovered_at ?? 0);
957
+ if (!Number.isFinite(discoveredAt) || Date.now() - discoveredAt * 1000 >= PROXY_DISCOVERY_CACHE_TTL_MS)
958
+ return null;
959
+ return { ...cached, ws_url: wsUrl, cached: true };
960
+ }
961
+ catch {
962
+ return null;
963
+ }
964
+ }
965
+ async _persistProxyDiscovery(discovery) {
966
+ const tokenStore = this._aunClient?._tokenStore;
967
+ if (!tokenStore || typeof tokenStore.saveMetadata !== 'function' || !this.providerAid)
968
+ return;
969
+ try {
970
+ tokenStore.saveMetadata(this.providerAid, {
971
+ [PROXY_DISCOVERY_CACHE_KEY]: JSON.stringify(discovery),
972
+ });
973
+ }
974
+ catch (exc) {
975
+ this._logWarn(`Service Proxy discovery cache write failed: ${formatError(exc)}`);
976
+ }
977
+ }
978
+ _shouldVerifySsl() {
979
+ const client = this._aunClient;
980
+ const cfg = client?._configModel;
981
+ if (cfg && (typeof cfg.verifySsl === 'boolean' || typeof cfg.verify_ssl === 'boolean')) {
982
+ return Boolean(cfg.verifySsl ?? cfg.verify_ssl);
983
+ }
984
+ const aid = client?.currentAid ?? client?._currentAid;
985
+ if (aid && (typeof aid.verifySsl === 'boolean' || typeof aid.verify_ssl === 'boolean')) {
986
+ return Boolean(aid.verifySsl ?? aid.verify_ssl);
987
+ }
988
+ return true;
989
+ }
990
+ _mappingAccessToken(mapping) {
991
+ if (!mapping)
992
+ return '';
993
+ const token = String(mapping.access_token ?? mapping.token ?? mapping.kite_token ?? '').trim();
994
+ if (!token)
995
+ return '';
996
+ const expiresAt = Number(mapping.access_token_expires_at ?? mapping.expires_at ?? 0);
997
+ if (Number.isFinite(expiresAt) && expiresAt > 0 && expiresAt <= Date.now() / 1000 + TOKEN_EXPIRY_SKEW_SECONDS) {
998
+ return '';
999
+ }
1000
+ return token;
1001
+ }
1002
+ _resolveCachedAccessToken() {
1003
+ const client = this._aunClient;
1004
+ if (!client)
1005
+ return '';
1006
+ const direct = this._mappingAccessToken(client);
1007
+ if (direct)
1008
+ return direct;
1009
+ if (isRecord(client._identity)) {
1010
+ const token = this._mappingAccessToken(client._identity);
1011
+ if (token)
1012
+ return token;
1013
+ }
1014
+ const auth = client._auth;
1015
+ if (auth && typeof auth.loadIdentityOrNone === 'function') {
1016
+ try {
1017
+ const token = this._mappingAccessToken(auth.loadIdentityOrNone(this.providerAid));
1018
+ if (token)
1019
+ return token;
1020
+ }
1021
+ catch { /* noop */ }
1022
+ }
1023
+ const tokenStore = client._tokenStore;
1024
+ if (tokenStore && typeof tokenStore.loadInstanceState === 'function') {
1025
+ try {
1026
+ const deviceId = String(client.deviceId ?? client.device_id ?? client._deviceId ?? client._device_id ?? '');
1027
+ const slotId = String(client.slotId ?? client.slot_id ?? client._slotId ?? client._slot_id ?? '');
1028
+ const token = this._mappingAccessToken(tokenStore.loadInstanceState(this.providerAid, deviceId, slotId));
1029
+ if (token)
1030
+ return token;
1031
+ }
1032
+ catch { /* noop */ }
1033
+ }
1034
+ return '';
1035
+ }
1036
+ async _authenticateForAccessToken() {
1037
+ const authenticate = this._aunClient?.authenticate;
1038
+ if (typeof authenticate !== 'function') {
1039
+ throw new AuthError('Service Proxy tunnel requires aunClient.authenticate() for AUN token authentication');
1040
+ }
1041
+ let result;
1042
+ try {
1043
+ result = await authenticate.call(this._aunClient);
1044
+ }
1045
+ catch (exc) {
1046
+ throw new AuthError(`AUNClient authenticate failed for Service Proxy tunnel: ${formatError(exc)}`);
1047
+ }
1048
+ const token = this._mappingAccessToken(isRecord(result) ? result : null);
1049
+ if (token)
1050
+ return token;
1051
+ throw new AuthError('AUNClient authenticate did not return a valid access_token');
1052
+ }
1053
+ async _ensureAccessToken() {
1054
+ return this._resolveCachedAccessToken() || await this._authenticateForAccessToken();
1055
+ }
1056
+ async _connectProxyWs() {
1057
+ const proxyUrl = await this.discoverProxyWsUrl();
1058
+ const token = await this._ensureAccessToken();
1059
+ if (!token)
1060
+ throw new AuthError('AUN access_token is required for Service Proxy tunnel');
1061
+ const ws = new WebSocket(proxyUrl, {
1062
+ headers: { Authorization: `Bearer ${token}` },
1063
+ maxPayload: this.maxTunnelMessageBytes,
1064
+ rejectUnauthorized: this._shouldVerifySsl(),
1065
+ });
1066
+ await waitForWsOpen(ws);
1067
+ return new TunnelSocket(ws);
1068
+ }
1069
+ _logWarn(message) {
1070
+ try {
1071
+ this._logger?.warn(message);
1072
+ }
1073
+ catch { /* noop */ }
1074
+ }
1075
+ _logError(message, err) {
1076
+ try {
1077
+ this._logger?.error(message, err instanceof Error ? err : undefined);
1078
+ }
1079
+ catch { /* noop */ }
1080
+ }
1081
+ }
1082
+ function normalizeServiceName(serviceName) {
1083
+ const value = String(serviceName ?? '').trim();
1084
+ if (!value)
1085
+ throw new ValidationError('service_name is required');
1086
+ if (RESERVED_SERVICE_NAMES.has(value))
1087
+ throw new ValidationError('service_name is reserved');
1088
+ if (!SERVICE_NAME_RE.test(value))
1089
+ throw new ValidationError('service_name must match [a-z0-9_-]+');
1090
+ return value;
1091
+ }
1092
+ function normalizeHost(host) {
1093
+ return String(host ?? '').trim().toLowerCase().replace(/\.+$/g, '');
1094
+ }
1095
+ function isIPv4LoopbackHost(host) {
1096
+ const parts = host.split('.');
1097
+ if (parts.length !== 4 || parts[0] !== '127')
1098
+ return false;
1099
+ return parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) >= 0 && Number(part) <= 255);
1100
+ }
1101
+ function isSensitiveMetadataKey(key) {
1102
+ const normalized = String(key ?? '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
1103
+ return SENSITIVE_METADATA_KEYS.has(normalized) || /(_token|_secret|_password|_private_key)$/.test(normalized);
1104
+ }
1105
+ function sanitizeMetadata(metadata) {
1106
+ const out = {};
1107
+ for (const [key, value] of Object.entries(metadata ?? {})) {
1108
+ if (isSensitiveMetadataKey(key))
1109
+ continue;
1110
+ if (isRecord(value))
1111
+ out[key] = sanitizeMetadata(value);
1112
+ else if (Array.isArray(value))
1113
+ out[key] = value.map((item) => (isRecord(item) ? sanitizeMetadata(item) : item));
1114
+ else
1115
+ out[key] = value;
1116
+ }
1117
+ return out;
1118
+ }
1119
+ function isRecord(value) {
1120
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
1121
+ }
1122
+ function headersMap(headers) {
1123
+ const result = {};
1124
+ if (!isRecord(headers))
1125
+ return result;
1126
+ for (const [key, value] of Object.entries(headers))
1127
+ result[key.toLowerCase()] = String(value);
1128
+ return result;
1129
+ }
1130
+ function streamModeFrom(headers, record, message) {
1131
+ let value = String(message.stream_mode ?? '').trim().toLowerCase();
1132
+ if (!value)
1133
+ value = String(headers['x-stream-mode'] ?? '').trim().toLowerCase();
1134
+ if (!value)
1135
+ value = String(record.metadata.stream_mode ?? '').trim().toLowerCase();
1136
+ if (value === 'always')
1137
+ return 'stream';
1138
+ return VALID_STREAM_MODES.has(value) ? value : 'auto';
1139
+ }
1140
+ function detectRequestProtocol(message, record) {
1141
+ const headers = headersMap(isRecord(message.headers) ? message.headers : {});
1142
+ const streamMode = streamModeFrom(headers, record, message);
1143
+ let serviceType = String(message.service_type ?? '').trim().toLowerCase() || record.service_type.toLowerCase() || 'http';
1144
+ if (streamMode === 'no_stream') {
1145
+ serviceType = 'http';
1146
+ }
1147
+ else if (!message.service_type) {
1148
+ const explicitType = String(headers['x-service-type'] ?? '').trim().toLowerCase();
1149
+ const method = String(message.method ?? '').toUpperCase();
1150
+ const path = String(message.path ?? '').toLowerCase();
1151
+ const accept = String(headers.accept ?? '').toLowerCase();
1152
+ const contentType = String(headers['content-type'] ?? '').toLowerCase();
1153
+ if (explicitType)
1154
+ serviceType = explicitType;
1155
+ else if (accept.includes('text/event-stream'))
1156
+ serviceType = 'sse';
1157
+ else if ('mcp-session-id' in headers)
1158
+ serviceType = 'mcp';
1159
+ else if (method === 'POST' && bodyHasJsonRpc(message))
1160
+ serviceType = 'mcp';
1161
+ else if (contentType.startsWith('application/grpc'))
1162
+ serviceType = 'ws';
1163
+ else if (path.includes('/mcp'))
1164
+ serviceType = 'mcp';
1165
+ else if (path.includes('/sse') || path.includes('/events'))
1166
+ serviceType = 'sse';
1167
+ else if (path.includes('/download') || path.includes('/files/'))
1168
+ serviceType = 'file';
1169
+ }
1170
+ let isStream;
1171
+ if (streamMode === 'stream')
1172
+ isStream = true;
1173
+ else if (streamMode === 'no_stream')
1174
+ isStream = false;
1175
+ else if ('is_stream' in message)
1176
+ isStream = Boolean(message.is_stream);
1177
+ else if ('stream' in message)
1178
+ isStream = Boolean(message.stream);
1179
+ else
1180
+ isStream = STREAMING_SERVICE_TYPES.has(serviceType);
1181
+ return { serviceType, streamMode, isStream };
1182
+ }
1183
+ function bodyHasJsonRpc(message) {
1184
+ const raw = String(message.body_base64 ?? '');
1185
+ if (!raw)
1186
+ return false;
1187
+ let text = '';
1188
+ try {
1189
+ text = decodeBase64Strict(raw).toString('utf-8');
1190
+ }
1191
+ catch {
1192
+ return false;
1193
+ }
1194
+ if (text.includes('"jsonrpc"') || text.includes("'jsonrpc'"))
1195
+ return true;
1196
+ try {
1197
+ const parsed = JSON.parse(text);
1198
+ if (isRecord(parsed))
1199
+ return String(parsed.jsonrpc ?? '') === '2.0';
1200
+ if (Array.isArray(parsed))
1201
+ return parsed.some((item) => isRecord(item) && String(item.jsonrpc ?? '') === '2.0');
1202
+ }
1203
+ catch { /* noop */ }
1204
+ return false;
1205
+ }
1206
+ function isStreamResponseHeaders(headers) {
1207
+ const contentType = String(headers['content-type'] ?? '').split(';', 1)[0].trim().toLowerCase();
1208
+ const contentDisposition = String(headers['content-disposition'] ?? '').toLowerCase();
1209
+ if (String(headers['content-type'] ?? '').toLowerCase().includes('text/event-stream'))
1210
+ return true;
1211
+ if (FILE_CONTENT_TYPES.has(contentType))
1212
+ return true;
1213
+ if (contentType.startsWith('image/') || contentType.startsWith('video/'))
1214
+ return true;
1215
+ return contentDisposition.includes('attachment');
1216
+ }
1217
+ function streamTypeFromResponse(headers, fallback) {
1218
+ const contentType = String(headers['content-type'] ?? '').toLowerCase();
1219
+ if (contentType.includes('text/event-stream'))
1220
+ return 'sse';
1221
+ if (isStreamResponseHeaders(headers))
1222
+ return 'file';
1223
+ return String(fallback || 'stream').trim().toLowerCase() || 'stream';
1224
+ }
1225
+ function backendHeaders(headers) {
1226
+ const result = {};
1227
+ for (const [key, value] of Object.entries(headers)) {
1228
+ const name = key.toLowerCase();
1229
+ if (HOP_BY_HOP_HEADERS.has(name) || name === 'host')
1230
+ continue;
1231
+ result[name] = String(value);
1232
+ }
1233
+ return result;
1234
+ }
1235
+ function responseHeadersMap(headers) {
1236
+ const result = {};
1237
+ for (const [key, value] of Object.entries(headers)) {
1238
+ const name = key.toLowerCase();
1239
+ if (HOP_BY_HOP_HEADERS.has(name) || AUTO_RESPONSE_HEADERS.has(name))
1240
+ continue;
1241
+ if (Array.isArray(value))
1242
+ result[name] = value.join(', ');
1243
+ else if (value !== undefined)
1244
+ result[name] = String(value);
1245
+ }
1246
+ return result;
1247
+ }
1248
+ function normalizePath(path) {
1249
+ const text = String(path || '/');
1250
+ return text.startsWith('/') ? text : `/${text}`;
1251
+ }
1252
+ function buildTargetUrl(endpoint, path, queryString) {
1253
+ const base = endpoint.replace(/\/+$/g, '') + '/';
1254
+ const url = new URL(path.replace(/^\/+/g, ''), base);
1255
+ if (queryString)
1256
+ url.search = queryString.startsWith('?') ? queryString : `?${queryString}`;
1257
+ return url.toString();
1258
+ }
1259
+ function decodeBase64Strict(value) {
1260
+ const text = String(value ?? '').trim();
1261
+ if (!text)
1262
+ return Buffer.alloc(0);
1263
+ if (text.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(text)) {
1264
+ throw new Error('invalid base64');
1265
+ }
1266
+ return Buffer.from(text, 'base64');
1267
+ }
1268
+ function errorMessage(requestId, code, message) {
1269
+ return { type: 'service_proxy_error', request_id: requestId, error: { code, message } };
1270
+ }
1271
+ function wsErrorMessage(connectionId, code, message) {
1272
+ return { type: 'ws_error', connection_id: connectionId, error: { code, message } };
1273
+ }
1274
+ function streamMessage(requestId, index, status, headers, data, done) {
1275
+ return {
1276
+ type: 'service_proxy_stream',
1277
+ request_id: requestId,
1278
+ index,
1279
+ status: index === 0 ? status : null,
1280
+ headers: index === 0 ? headers : {},
1281
+ data_base64: data.toString('base64'),
1282
+ done,
1283
+ };
1284
+ }
1285
+ function parseTunnelMessage(raw) {
1286
+ if (raw === null)
1287
+ throw new ConnectionError('Service Proxy tunnel closed');
1288
+ const parsed = JSON.parse(raw);
1289
+ return isRecord(parsed) ? parsed : {};
1290
+ }
1291
+ function waitForWsOpen(ws) {
1292
+ return new Promise((resolve, reject) => {
1293
+ const cleanup = () => {
1294
+ ws.off('open', onOpen);
1295
+ ws.off('error', onError);
1296
+ };
1297
+ const onOpen = () => { cleanup(); resolve(); };
1298
+ const onError = (err) => { cleanup(); reject(err); };
1299
+ ws.once('open', onOpen);
1300
+ ws.once('error', onError);
1301
+ });
1302
+ }
1303
+ function requestBackend(targetUrl, opts) {
1304
+ return new Promise((resolve, reject) => {
1305
+ const parsed = new URL(targetUrl);
1306
+ const mod = parsed.protocol === 'https:' ? https : http;
1307
+ const req = mod.request(parsed, {
1308
+ method: opts.method,
1309
+ headers: opts.headers,
1310
+ rejectUnauthorized: opts.verifySsl,
1311
+ timeout: opts.timeoutMs,
1312
+ }, (res) => resolve(res));
1313
+ req.on('error', reject);
1314
+ req.on('timeout', () => {
1315
+ req.destroy(new Error('backend request timeout'));
1316
+ });
1317
+ void (async () => {
1318
+ try {
1319
+ if (Buffer.isBuffer(opts.body)) {
1320
+ if (opts.body.length > 0)
1321
+ req.write(opts.body);
1322
+ }
1323
+ else {
1324
+ for await (const chunk of opts.body) {
1325
+ if (chunk.length > 0)
1326
+ req.write(chunk);
1327
+ }
1328
+ }
1329
+ req.end();
1330
+ }
1331
+ catch (exc) {
1332
+ req.destroy(exc instanceof Error ? exc : new Error(String(exc)));
1333
+ }
1334
+ })();
1335
+ });
1336
+ }
1337
+ async function readResponseBodyLimited(response, limit) {
1338
+ const chunks = [];
1339
+ let total = 0;
1340
+ for await (const raw of response) {
1341
+ const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
1342
+ total += chunk.length;
1343
+ if (total > limit)
1344
+ throw new Error('response body too large');
1345
+ chunks.push(chunk);
1346
+ }
1347
+ return Buffer.concat(chunks);
1348
+ }
1349
+ async function httpGetJson(url, opts) {
1350
+ const body = await new Promise((resolve, reject) => {
1351
+ const parsed = new URL(url);
1352
+ const mod = parsed.protocol === 'https:' ? https : http;
1353
+ const req = mod.get(parsed, {
1354
+ rejectUnauthorized: opts.verifySsl,
1355
+ timeout: opts.timeoutMs,
1356
+ }, (res) => {
1357
+ if ((res.statusCode ?? 0) < 200 || (res.statusCode ?? 0) >= 300) {
1358
+ res.resume();
1359
+ reject(new Error(`HTTP ${res.statusCode}`));
1360
+ return;
1361
+ }
1362
+ const chunks = [];
1363
+ res.on('data', (chunk) => chunks.push(chunk));
1364
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
1365
+ });
1366
+ req.on('error', reject);
1367
+ req.on('timeout', () => req.destroy(new Error('request timeout')));
1368
+ });
1369
+ const payload = JSON.parse(body);
1370
+ if (!isRecord(payload))
1371
+ throw new ValidationError('Service Proxy well-known returned invalid payload');
1372
+ return payload;
1373
+ }
1374
+ function rawDataToBuffer(data) {
1375
+ if (Buffer.isBuffer(data))
1376
+ return data;
1377
+ if (Array.isArray(data))
1378
+ return Buffer.concat(data);
1379
+ return Buffer.from(data);
1380
+ }
1381
+ function sleep(ms) {
1382
+ return new Promise((resolve) => setTimeout(resolve, ms));
1383
+ }
1384
+ function formatError(error) {
1385
+ return error instanceof Error ? error.message : String(error);
1386
+ }
1387
+ //# sourceMappingURL=service-proxy.js.map