@agentunion/fastaun-browser 0.4.8 → 0.4.10

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