neoagent 3.2.1-beta.2 → 3.2.1-beta.3

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 (144) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +511 -89
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +29 -20
  6. package/flutter_app/lib/main_models.dart +3 -0
  7. package/flutter_app/lib/main_operations.dart +334 -321
  8. package/flutter_app/lib/main_settings.dart +4 -3
  9. package/flutter_app/lib/main_shared.dart +14 -11
  10. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  11. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  12. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  13. package/lib/manager.js +106 -89
  14. package/lib/schema_migrations.js +67 -13
  15. package/package.json +20 -13
  16. package/runtime/paths.js +49 -5
  17. package/server/guest_agent.js +52 -30
  18. package/server/http/middleware.js +24 -0
  19. package/server/http/routes.js +4 -6
  20. package/server/public/.last_build_id +1 -1
  21. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  22. package/server/public/flutter_bootstrap.js +1 -1
  23. package/server/public/main.dart.js +30803 -30805
  24. package/server/routes/admin.js +1 -1
  25. package/server/routes/android.js +30 -34
  26. package/server/routes/browser.js +23 -15
  27. package/server/routes/desktop.js +18 -1
  28. package/server/routes/integrations.js +5 -1
  29. package/server/routes/memory.js +1 -0
  30. package/server/routes/settings.js +16 -5
  31. package/server/routes/social_reach.js +12 -3
  32. package/server/routes/social_video.js +4 -0
  33. package/server/services/ai/compaction.js +7 -2
  34. package/server/services/ai/history.js +44 -5
  35. package/server/services/ai/integrated_tools/http_request.js +8 -0
  36. package/server/services/ai/loop/agent_engine_core.js +347 -162
  37. package/server/services/ai/loop/callbacks.js +1 -0
  38. package/server/services/ai/loop/completion_judge.js +12 -0
  39. package/server/services/ai/loop/conversation_loop.js +348 -242
  40. package/server/services/ai/loop/messaging_delivery.js +129 -57
  41. package/server/services/ai/loop/model_call_guard.js +91 -0
  42. package/server/services/ai/loop/model_io.js +8 -41
  43. package/server/services/ai/loop/tool_dispatch.js +19 -8
  44. package/server/services/ai/loopPolicy.js +24 -19
  45. package/server/services/ai/model_discovery.js +227 -0
  46. package/server/services/ai/model_identity.js +71 -0
  47. package/server/services/ai/models.js +67 -162
  48. package/server/services/ai/providerRetry.js +17 -59
  49. package/server/services/ai/provider_selector.js +111 -0
  50. package/server/services/ai/providers/anthropic.js +2 -2
  51. package/server/services/ai/providers/claudeCode.js +15 -28
  52. package/server/services/ai/providers/githubCopilot.js +36 -16
  53. package/server/services/ai/providers/google.js +23 -5
  54. package/server/services/ai/providers/grok.js +4 -3
  55. package/server/services/ai/providers/grokOauth.js +13 -22
  56. package/server/services/ai/providers/nvidia.js +10 -5
  57. package/server/services/ai/providers/ollama.js +102 -82
  58. package/server/services/ai/providers/ollama_stream.js +142 -0
  59. package/server/services/ai/providers/openai.js +39 -5
  60. package/server/services/ai/providers/openaiCodex.js +11 -4
  61. package/server/services/ai/providers/openrouter.js +29 -7
  62. package/server/services/ai/providers/provider_error.js +36 -0
  63. package/server/services/ai/settings.js +26 -2
  64. package/server/services/ai/taskAnalysis.js +5 -1
  65. package/server/services/ai/terminal_reply.js +45 -0
  66. package/server/services/ai/toolEvidence.js +58 -29
  67. package/server/services/ai/tools.js +124 -111
  68. package/server/services/android/controller.js +770 -237
  69. package/server/services/android/process.js +140 -0
  70. package/server/services/android/sdk_download.js +143 -0
  71. package/server/services/android/uia.js +6 -5
  72. package/server/services/artifacts/store.js +24 -0
  73. package/server/services/browser/controller.js +736 -385
  74. package/server/services/browser/extension/gateway.js +40 -16
  75. package/server/services/browser/extension/protocol.js +12 -1
  76. package/server/services/browser/extension/provider.js +59 -47
  77. package/server/services/browser/extension/registry.js +155 -34
  78. package/server/services/cli/executor.js +62 -9
  79. package/server/services/desktop/gateway.js +41 -4
  80. package/server/services/desktop/protocol.js +3 -0
  81. package/server/services/desktop/provider.js +39 -42
  82. package/server/services/desktop/registry.js +137 -52
  83. package/server/services/integrations/figma/provider.js +78 -12
  84. package/server/services/integrations/github/common.js +11 -6
  85. package/server/services/integrations/github/provider.js +52 -53
  86. package/server/services/integrations/google/provider.js +55 -19
  87. package/server/services/integrations/home_assistant/network.js +17 -20
  88. package/server/services/integrations/home_assistant/provider.js +7 -5
  89. package/server/services/integrations/home_assistant/tools.js +17 -5
  90. package/server/services/integrations/http.js +51 -0
  91. package/server/services/integrations/manager.js +158 -53
  92. package/server/services/integrations/microsoft/provider.js +80 -13
  93. package/server/services/integrations/neoarchive/provider.js +55 -29
  94. package/server/services/integrations/neorecall/client.js +17 -10
  95. package/server/services/integrations/neorecall/provider.js +20 -11
  96. package/server/services/integrations/notion/provider.js +16 -13
  97. package/server/services/integrations/oauth_provider.js +115 -51
  98. package/server/services/integrations/slack/provider.js +98 -9
  99. package/server/services/integrations/spotify/provider.js +67 -71
  100. package/server/services/integrations/trello/provider.js +21 -7
  101. package/server/services/integrations/weather/provider.js +18 -12
  102. package/server/services/integrations/whatsapp/provider.js +76 -16
  103. package/server/services/manager.js +87 -1
  104. package/server/services/memory/embedding_index.js +20 -8
  105. package/server/services/memory/embeddings.js +151 -90
  106. package/server/services/memory/ingestion.js +50 -9
  107. package/server/services/memory/ingestion_documents.js +13 -3
  108. package/server/services/memory/manager.js +52 -19
  109. package/server/services/messaging/automation.js +84 -9
  110. package/server/services/messaging/http_platforms.js +33 -13
  111. package/server/services/messaging/inbound_queue.js +78 -24
  112. package/server/services/messaging/inbound_store.js +224 -0
  113. package/server/services/messaging/manager.js +326 -51
  114. package/server/services/messaging/typing_keepalive.js +5 -2
  115. package/server/services/network/http.js +210 -0
  116. package/server/services/network/safe_request.js +307 -0
  117. package/server/services/runtime/backends/local-vm.js +214 -66
  118. package/server/services/runtime/manager.js +17 -12
  119. package/server/services/social_reach/channels/github.js +10 -4
  120. package/server/services/social_reach/channels/reddit.js +4 -4
  121. package/server/services/social_reach/channels/rss.js +2 -2
  122. package/server/services/social_reach/channels/social_video.js +12 -7
  123. package/server/services/social_reach/channels/v2ex.js +21 -8
  124. package/server/services/social_reach/channels/x.js +2 -2
  125. package/server/services/social_reach/channels/xueqiu.js +5 -5
  126. package/server/services/social_reach/service.js +9 -6
  127. package/server/services/social_reach/utils.js +65 -14
  128. package/server/services/social_video/service.js +160 -50
  129. package/server/services/tasks/integration_runtime.js +18 -8
  130. package/server/services/tasks/runtime.js +39 -4
  131. package/server/services/voice/agentBridge.js +17 -4
  132. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  133. package/server/services/voice/liveSession.js +31 -0
  134. package/server/services/voice/openaiSpeech.js +33 -8
  135. package/server/services/voice/providers.js +233 -151
  136. package/server/services/voice/runtimeManager.js +118 -20
  137. package/server/services/voice/turnRunner.js +6 -0
  138. package/server/services/wearable/firmware_manifest.js +51 -13
  139. package/server/services/wearable/service.js +1 -0
  140. package/server/utils/abort.js +96 -0
  141. package/server/utils/cloud-security.js +110 -3
  142. package/server/utils/files.js +31 -0
  143. package/server/utils/image_payload.js +95 -0
  144. package/server/utils/retry.js +107 -0
@@ -0,0 +1,210 @@
1
+ 'use strict';
2
+
3
+ const { createAbortError } = require('../../utils/abort');
4
+
5
+ const DEFAULT_HTTP_TIMEOUT_MS = 15000;
6
+ const DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
7
+
8
+ function timeoutError(serviceName, timeoutMs, code = 'HTTP_TIMEOUT') {
9
+ const error = new Error(`${serviceName} request timed out after ${timeoutMs}ms.`);
10
+ error.code = code;
11
+ return error;
12
+ }
13
+
14
+ function responseTooLargeError(serviceName, maxBytes, code = 'HTTP_RESPONSE_TOO_LARGE') {
15
+ const error = new Error(
16
+ `${serviceName} response exceeded the ${maxBytes}-byte safety limit.`,
17
+ );
18
+ error.code = code;
19
+ return error;
20
+ }
21
+
22
+ function waitForAbortableResult(promise, signal, fallback = 'Request aborted.') {
23
+ if (!signal) return Promise.resolve(promise);
24
+ if (signal.aborted) return Promise.reject(createAbortError(signal, fallback));
25
+
26
+ return new Promise((resolve, reject) => {
27
+ const onAbort = () => reject(createAbortError(signal, fallback));
28
+ signal.addEventListener('abort', onAbort, { once: true });
29
+ Promise.resolve(promise).then(resolve, reject).finally(() => {
30
+ signal.removeEventListener('abort', onAbort);
31
+ });
32
+ });
33
+ }
34
+
35
+ async function waitForBoundedResult(promise, options = {}) {
36
+ const timeoutMs = Number(options.timeoutMs) > 0
37
+ ? Number(options.timeoutMs)
38
+ : DEFAULT_HTTP_TIMEOUT_MS;
39
+ const serviceName = String(options.serviceName || 'Remote service').trim()
40
+ || 'Remote service';
41
+ let timer = null;
42
+ const deadline = new Promise((_, reject) => {
43
+ timer = setTimeout(() => reject(timeoutError(
44
+ serviceName,
45
+ timeoutMs,
46
+ options.timeoutCode,
47
+ )), timeoutMs);
48
+ });
49
+ try {
50
+ return await waitForAbortableResult(
51
+ Promise.race([Promise.resolve(promise), deadline]),
52
+ options.signal,
53
+ );
54
+ } finally {
55
+ clearTimeout(timer);
56
+ }
57
+ }
58
+
59
+ async function readResponseBuffer(response, options = {}) {
60
+ const maxBytes = Number(options.maxResponseBytes) > 0
61
+ ? Number(options.maxResponseBytes)
62
+ : DEFAULT_MAX_RESPONSE_BYTES;
63
+ const serviceName = String(options.serviceName || 'Remote service').trim()
64
+ || 'Remote service';
65
+ const tooLarge = () => responseTooLargeError(
66
+ serviceName,
67
+ maxBytes,
68
+ options.tooLargeCode,
69
+ );
70
+ const signal = options.signal || null;
71
+ if (signal?.aborted) {
72
+ await response?.body?.cancel?.().catch(() => {});
73
+ throw createAbortError(signal);
74
+ }
75
+ const contentLength = Number(response?.headers?.get?.('content-length'));
76
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
77
+ await response?.body?.cancel?.().catch(() => {});
78
+ throw tooLarge();
79
+ }
80
+
81
+ const reader = response?.body?.getReader?.();
82
+ if (!reader) {
83
+ let buffer;
84
+ if (typeof response?.arrayBuffer === 'function') {
85
+ buffer = Buffer.from(await waitForAbortableResult(response.arrayBuffer(), signal));
86
+ } else {
87
+ buffer = Buffer.from(await waitForAbortableResult(response.text(), signal), 'utf8');
88
+ }
89
+ if (buffer.byteLength > maxBytes) throw tooLarge();
90
+ return buffer;
91
+ }
92
+
93
+ const chunks = [];
94
+ let totalBytes = 0;
95
+ const cancelReader = () => {
96
+ Promise.resolve(reader.cancel(signal?.reason)).catch(() => {});
97
+ };
98
+ signal?.addEventListener('abort', cancelReader, { once: true });
99
+ try {
100
+ while (true) {
101
+ const { done, value } = await waitForAbortableResult(reader.read(), signal);
102
+ if (done) break;
103
+ const chunk = Buffer.from(value);
104
+ totalBytes += chunk.byteLength;
105
+ if (totalBytes > maxBytes) {
106
+ await reader.cancel().catch(() => {});
107
+ throw tooLarge();
108
+ }
109
+ chunks.push(chunk);
110
+ }
111
+ } finally {
112
+ signal?.removeEventListener('abort', cancelReader);
113
+ reader.releaseLock?.();
114
+ }
115
+ return Buffer.concat(chunks, totalBytes);
116
+ }
117
+
118
+ async function readResponseText(response, options = {}) {
119
+ return (await readResponseBuffer(response, options)).toString('utf8');
120
+ }
121
+
122
+ async function fetchResponse(url, options = {}, reader = readResponseBuffer) {
123
+ const serviceName = String(options.serviceName || 'Remote service').trim()
124
+ || 'Remote service';
125
+ const timeoutMs = Number(options.timeoutMs) > 0
126
+ ? Number(options.timeoutMs)
127
+ : DEFAULT_HTTP_TIMEOUT_MS;
128
+ const maxResponseBytes = Number(options.maxResponseBytes) > 0
129
+ ? Number(options.maxResponseBytes)
130
+ : DEFAULT_MAX_RESPONSE_BYTES;
131
+ const callerSignal = options.signal || null;
132
+ const fetchImpl = typeof options.fetchImpl === 'function' ? options.fetchImpl : fetch;
133
+ if (callerSignal?.aborted) throw createAbortError(callerSignal);
134
+
135
+ const controller = new AbortController();
136
+ let rejectDeadline;
137
+ let timeoutFailure = null;
138
+ const deadline = new Promise((_, reject) => {
139
+ rejectDeadline = reject;
140
+ });
141
+ const abortFromCaller = () => {
142
+ const error = createAbortError(callerSignal);
143
+ controller.abort(error);
144
+ rejectDeadline(error);
145
+ };
146
+ callerSignal?.addEventListener('abort', abortFromCaller, { once: true });
147
+
148
+ const timer = setTimeout(() => {
149
+ timeoutFailure = timeoutError(serviceName, timeoutMs, options.timeoutCode);
150
+ controller.abort(timeoutFailure);
151
+ rejectDeadline(timeoutFailure);
152
+ }, timeoutMs);
153
+
154
+ const {
155
+ fetchImpl: _fetchImpl,
156
+ maxResponseBytes: _maxResponseBytes,
157
+ serviceName: _serviceName,
158
+ signal: _signal,
159
+ timeoutCode: _timeoutCode,
160
+ timeoutMs: _timeoutMs,
161
+ tooLargeCode: _tooLargeCode,
162
+ ...fetchOptions
163
+ } = options;
164
+ const operation = Promise.resolve().then(async () => {
165
+ const response = await fetchImpl(url, {
166
+ ...fetchOptions,
167
+ signal: controller.signal,
168
+ });
169
+ const body = await reader(response, {
170
+ maxResponseBytes,
171
+ serviceName,
172
+ signal: controller.signal,
173
+ tooLargeCode: options.tooLargeCode,
174
+ });
175
+ return { response, body };
176
+ });
177
+
178
+ try {
179
+ return await Promise.race([operation, deadline]);
180
+ } catch (error) {
181
+ if (callerSignal?.aborted) throw createAbortError(callerSignal);
182
+ if (timeoutFailure) throw timeoutFailure;
183
+ throw error;
184
+ } finally {
185
+ clearTimeout(timer);
186
+ callerSignal?.removeEventListener('abort', abortFromCaller);
187
+ }
188
+ }
189
+
190
+ async function fetchResponseBuffer(url, options = {}) {
191
+ return fetchResponse(url, options, readResponseBuffer);
192
+ }
193
+
194
+ async function fetchResponseText(url, options = {}) {
195
+ const { response, body } = await fetchResponse(url, options, readResponseBuffer);
196
+ return { response, text: body.toString('utf8') };
197
+ }
198
+
199
+ module.exports = {
200
+ DEFAULT_HTTP_TIMEOUT_MS,
201
+ DEFAULT_MAX_RESPONSE_BYTES,
202
+ fetchResponseBuffer,
203
+ fetchResponseText,
204
+ readResponseBuffer,
205
+ readResponseText,
206
+ responseTooLargeError,
207
+ timeoutError,
208
+ waitForAbortableResult,
209
+ waitForBoundedResult,
210
+ };
@@ -0,0 +1,307 @@
1
+ 'use strict';
2
+
3
+ const dns = require('node:dns');
4
+ const http = require('node:http');
5
+ const https = require('node:https');
6
+ const net = require('node:net');
7
+ const {
8
+ createAbortError,
9
+ createLinkedAbortController,
10
+ throwIfAborted,
11
+ } = require('../../utils/abort');
12
+ const { isPrivateHost } = require('../../utils/cloud-security');
13
+
14
+ const ALLOWED_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']);
15
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
16
+ const SENSITIVE_REDIRECT_HEADERS = new Set([
17
+ 'authorization',
18
+ 'cookie',
19
+ 'proxy-authorization',
20
+ ]);
21
+ const HIDDEN_RESPONSE_HEADERS = new Set([
22
+ 'proxy-authenticate',
23
+ 'set-cookie',
24
+ 'www-authenticate',
25
+ ]);
26
+ const DEFAULT_TIMEOUT_MS = 30000;
27
+ const MAX_TIMEOUT_MS = 120000;
28
+ const MAX_RESPONSE_BYTES = 512 * 1024;
29
+ const MAX_REDIRECTS = 5;
30
+
31
+ function timeoutError(timeoutMs) {
32
+ const error = new Error(`Request timed out after ${timeoutMs}ms.`);
33
+ error.code = 'HTTP_REQUEST_TIMEOUT';
34
+ return error;
35
+ }
36
+
37
+ function normalizeHeaders(value) {
38
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
39
+ const headers = {};
40
+ for (const [rawName, rawValue] of Object.entries(value)) {
41
+ const name = String(rawName || '').trim().toLowerCase();
42
+ if (!name || rawValue == null) continue;
43
+ if (name === 'host' || name === 'connection' || name === 'transfer-encoding') continue;
44
+ headers[name] = Array.isArray(rawValue)
45
+ ? rawValue.map((item) => String(item))
46
+ : String(rawValue);
47
+ }
48
+ return headers;
49
+ }
50
+
51
+ function parseHttpUrl(value) {
52
+ let parsed;
53
+ try {
54
+ parsed = new URL(String(value || '').trim());
55
+ } catch {
56
+ throw new Error('Invalid URL.');
57
+ }
58
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
59
+ throw new Error('URL scheme not allowed. Only http and https are permitted.');
60
+ }
61
+ if (parsed.username || parsed.password) {
62
+ throw new Error('Credentials in request URLs are not permitted. Use headers instead.');
63
+ }
64
+ return parsed;
65
+ }
66
+
67
+ async function waitForLookup(lookupPromise, signal) {
68
+ throwIfAborted(signal, 'HTTP request aborted.');
69
+ if (!signal) return lookupPromise;
70
+ return new Promise((resolve, reject) => {
71
+ const onAbort = () => reject(createAbortError(signal));
72
+ signal.addEventListener('abort', onAbort, { once: true });
73
+ Promise.resolve(lookupPromise).then(resolve, reject).finally(() => {
74
+ signal.removeEventListener('abort', onAbort);
75
+ });
76
+ });
77
+ }
78
+
79
+ async function resolveHttpTarget(url, options = {}) {
80
+ const parsed = url instanceof URL ? url : parseHttpUrl(url);
81
+ const hostname = parsed.hostname.replace(/^\[|\]$/g, '');
82
+ const allowPrivate = options.allowPrivate === true;
83
+ if (!allowPrivate && isPrivateHost(hostname)) {
84
+ throw new Error('Private, loopback, and reserved network addresses are not permitted.');
85
+ }
86
+ if (net.isIP(hostname)) {
87
+ return { address: hostname, family: net.isIP(hostname), parsed };
88
+ }
89
+
90
+ const lookup = options.lookup || dns.promises.lookup;
91
+ let records;
92
+ try {
93
+ records = await waitForLookup(
94
+ lookup(hostname, { all: true, verbatim: true }),
95
+ options.signal,
96
+ );
97
+ } catch (error) {
98
+ throwIfAborted(options.signal, 'HTTP request aborted.');
99
+ throw new Error(`Could not resolve request host: ${error?.message || 'DNS lookup failed'}`);
100
+ }
101
+ const addresses = (Array.isArray(records) ? records : [records])
102
+ .map((record) => ({
103
+ address: String(record?.address || record || ''),
104
+ family: Number(record?.family) || net.isIP(record?.address || record),
105
+ }))
106
+ .filter((record) => record.address && record.family);
107
+ if (addresses.length === 0) throw new Error('Could not resolve request host.');
108
+ if (!allowPrivate && addresses.some((record) => isPrivateHost(record.address))) {
109
+ throw new Error('Request host resolves to a private, loopback, or reserved address.');
110
+ }
111
+ return { ...addresses[0], parsed };
112
+ }
113
+
114
+ function sanitizeResponseHeaders(headers = {}) {
115
+ const safe = {};
116
+ for (const [name, value] of Object.entries(headers)) {
117
+ const normalized = String(name || '').toLowerCase();
118
+ if (!normalized || HIDDEN_RESPONSE_HEADERS.has(normalized)) continue;
119
+ safe[normalized] = Array.isArray(value) ? value.join(', ') : String(value ?? '');
120
+ }
121
+ return safe;
122
+ }
123
+
124
+ function collectResponseBody(chunks, totalBytes, responseType) {
125
+ const buffer = Buffer.concat(chunks, totalBytes);
126
+ return responseType === 'buffer' ? buffer : buffer.toString('utf8');
127
+ }
128
+
129
+ function requestOnce(target, options = {}) {
130
+ const { parsed, address, family } = target;
131
+ const requestImpl = options.requestImpl
132
+ || (parsed.protocol === 'https:' ? https.request : http.request);
133
+ const headers = { ...normalizeHeaders(options.headers), host: parsed.host };
134
+ if (options.body != null) {
135
+ headers['content-length'] = String(Buffer.byteLength(options.body));
136
+ } else {
137
+ delete headers['content-length'];
138
+ }
139
+
140
+ return new Promise((resolve, reject) => {
141
+ let settled = false;
142
+ const finish = (callback, value) => {
143
+ if (settled) return;
144
+ settled = true;
145
+ callback(value);
146
+ };
147
+ const request = requestImpl({
148
+ protocol: parsed.protocol,
149
+ hostname: address,
150
+ family,
151
+ port: parsed.port || undefined,
152
+ method: options.method,
153
+ path: `${parsed.pathname}${parsed.search}`,
154
+ headers,
155
+ maxHeaderSize: 32 * 1024,
156
+ servername: net.isIP(parsed.hostname.replace(/^\[|\]$/g, ''))
157
+ ? undefined
158
+ : parsed.hostname,
159
+ signal: options.signal,
160
+ }, (response) => {
161
+ const status = Number(response.statusCode) || 0;
162
+ if (REDIRECT_STATUSES.has(status) && response.headers?.location) {
163
+ response.destroy();
164
+ finish(resolve, { status, headers: response.headers, body: '', truncated: false });
165
+ return;
166
+ }
167
+ const chunks = [];
168
+ let totalBytes = 0;
169
+ let truncated = false;
170
+ response.on('data', (chunkValue) => {
171
+ if (settled) return;
172
+ const chunk = Buffer.from(chunkValue);
173
+ const remaining = options.maxResponseBytes - totalBytes;
174
+ if (remaining > 0) {
175
+ chunks.push(chunk.subarray(0, remaining));
176
+ totalBytes += Math.min(chunk.byteLength, remaining);
177
+ }
178
+ if (chunk.byteLength > remaining) {
179
+ truncated = true;
180
+ response.destroy();
181
+ finish(resolve, {
182
+ status,
183
+ headers: response.headers,
184
+ body: collectResponseBody(chunks, totalBytes, options.responseType),
185
+ truncated,
186
+ });
187
+ }
188
+ });
189
+ response.on('end', () => finish(resolve, {
190
+ status,
191
+ headers: response.headers,
192
+ body: collectResponseBody(chunks, totalBytes, options.responseType),
193
+ truncated,
194
+ }));
195
+ response.on('error', (error) => finish(reject, error));
196
+ });
197
+ request.on('error', (error) => finish(reject, error));
198
+ if (options.body != null) request.write(options.body);
199
+ request.end();
200
+ });
201
+ }
202
+
203
+ function redirectedRequest(previousUrl, nextUrl, method, headers, body, status) {
204
+ let nextMethod = method;
205
+ let nextBody = body;
206
+ const nextHeaders = { ...headers };
207
+ if (status === 303 || ((status === 301 || status === 302) && method === 'POST')) {
208
+ nextMethod = 'GET';
209
+ nextBody = null;
210
+ delete nextHeaders['content-length'];
211
+ delete nextHeaders['content-type'];
212
+ }
213
+ if (previousUrl.origin !== nextUrl.origin) {
214
+ for (const name of SENSITIVE_REDIRECT_HEADERS) delete nextHeaders[name];
215
+ }
216
+ return { method: nextMethod, headers: nextHeaders, body: nextBody };
217
+ }
218
+
219
+ async function executeSafeHttpRequest(args = {}, context = {}) {
220
+ const method = String(args.method || 'GET').trim().toUpperCase();
221
+ if (!ALLOWED_METHODS.has(method)) throw new Error(`Unsupported HTTP method: ${method}`);
222
+ const timeoutMs = Math.max(
223
+ 100,
224
+ Math.min(Number(args.timeout_ms) || DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS),
225
+ );
226
+ const timeoutController = new AbortController();
227
+ const linked = createLinkedAbortController([context.signal, timeoutController.signal]);
228
+ const timer = setTimeout(() => timeoutController.abort(timeoutError(timeoutMs)), timeoutMs);
229
+ let currentUrl = parseHttpUrl(args.url);
230
+ let requestState = {
231
+ method,
232
+ headers: normalizeHeaders(args.headers),
233
+ body: args.body != null && !['GET', 'DELETE'].includes(method)
234
+ ? String(args.body)
235
+ : null,
236
+ };
237
+ if (requestState.body != null && !requestState.headers['content-type']) {
238
+ requestState.headers['content-type'] = 'application/json';
239
+ }
240
+ const redirects = [];
241
+
242
+ try {
243
+ for (let redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount += 1) {
244
+ throwIfAborted(linked.signal, 'HTTP request aborted.');
245
+ const target = await resolveHttpTarget(currentUrl, {
246
+ allowPrivate: context.allowPrivate === true,
247
+ lookup: context.lookup,
248
+ signal: linked.signal,
249
+ });
250
+ const response = await requestOnce(target, {
251
+ ...requestState,
252
+ maxResponseBytes: Number(context.maxResponseBytes) > 0
253
+ ? Number(context.maxResponseBytes)
254
+ : MAX_RESPONSE_BYTES,
255
+ requestImpl: context.requestImpl,
256
+ responseType: context.responseType,
257
+ signal: linked.signal,
258
+ });
259
+ const location = response.headers?.location;
260
+ if (!REDIRECT_STATUSES.has(response.status) || !location) {
261
+ return {
262
+ status: response.status,
263
+ headers: sanitizeResponseHeaders(response.headers),
264
+ body: response.truncated && typeof response.body === 'string'
265
+ ? `${response.body}\n...[truncated at response safety limit]`
266
+ : response.body,
267
+ finalUrl: currentUrl.toString(),
268
+ redirects,
269
+ truncated: response.truncated,
270
+ };
271
+ }
272
+ if (redirectCount >= MAX_REDIRECTS) {
273
+ throw new Error(`Request exceeded the ${MAX_REDIRECTS}-redirect limit.`);
274
+ }
275
+ const nextUrl = parseHttpUrl(new URL(String(location), currentUrl).toString());
276
+ redirects.push(nextUrl.toString());
277
+ requestState = redirectedRequest(
278
+ currentUrl,
279
+ nextUrl,
280
+ requestState.method,
281
+ requestState.headers,
282
+ requestState.body,
283
+ response.status,
284
+ );
285
+ currentUrl = nextUrl;
286
+ }
287
+ throw new Error(`Request exceeded the ${MAX_REDIRECTS}-redirect limit.`);
288
+ } catch (error) {
289
+ if (context.signal?.aborted) throw createAbortError(context.signal);
290
+ if (timeoutController.signal.aborted) throw createAbortError(timeoutController.signal);
291
+ throw error;
292
+ } finally {
293
+ clearTimeout(timer);
294
+ linked.cleanup();
295
+ }
296
+ }
297
+
298
+ module.exports = {
299
+ MAX_REDIRECTS,
300
+ MAX_RESPONSE_BYTES,
301
+ executeSafeHttpRequest,
302
+ normalizeHeaders,
303
+ parseHttpUrl,
304
+ redirectedRequest,
305
+ resolveHttpTarget,
306
+ sanitizeResponseHeaders,
307
+ };