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
@@ -1,5 +1,28 @@
1
1
  'use strict';
2
2
 
3
+ const dns = require('node:dns');
4
+ const net = require('node:net');
5
+
6
+ const ALLOWED_SCHEMES = new Set(['http', 'https']);
7
+ const BLOCKED_ANDROID_INTENT_SCHEMES = new Set([
8
+ 'about',
9
+ 'chrome',
10
+ 'chrome-extension',
11
+ 'content',
12
+ 'data',
13
+ 'file',
14
+ 'javascript',
15
+ 'vbscript',
16
+ ]);
17
+
18
+ function urlAbortError(signal) {
19
+ if (signal?.reason instanceof Error) return signal.reason;
20
+ const error = new Error('URL validation was aborted.');
21
+ error.name = 'AbortError';
22
+ error.code = 'ABORT_ERR';
23
+ return error;
24
+ }
25
+
3
26
  // URL schemes that must never be navigated to in the cloud browser or Android.
4
27
  const BLOCKED_SCHEMES = new Set([
5
28
  'javascript',
@@ -56,8 +79,26 @@ function isPrivateHost(hostname) {
56
79
 
57
80
  // IPv6 loopback, link-local and unique-local
58
81
  if (h === '::1' || h === '::') return true;
59
- if (h.startsWith('fe80:')) return true;
82
+ if (/^fe[89ab][0-9a-f]:/.test(h)) return true; // fe80::/10 link-local
83
+ if (/^fe[c-f][0-9a-f]:/.test(h)) return true; // fec0::/10 deprecated site-local
60
84
  if (/^f[cd][0-9a-f]*:/.test(h)) return true; // fc00::/7 unique-local
85
+ if (h.startsWith('ff')) return true; // IPv6 multicast
86
+ if (h === '100::' || h.startsWith('100::')) return true; // discard-only prefix
87
+ if (h === '2001:db8::' || h.startsWith('2001:db8:')) return true; // documentation
88
+
89
+ if (net.isIPv4(h)) {
90
+ const [a, b, c] = h.split('.').map(Number);
91
+ if (a === 0 || a === 10 || a === 127 || a >= 224) return true;
92
+ if (a === 100 && b >= 64 && b <= 127) return true;
93
+ if (a === 169 && b === 254) return true;
94
+ if (a === 172 && b >= 16 && b <= 31) return true;
95
+ if (a === 192 && b === 168) return true;
96
+ if (a === 192 && b === 0 && c === 0) return true;
97
+ if (a === 192 && b === 0 && c === 2) return true;
98
+ if (a === 198 && (b === 18 || b === 19)) return true;
99
+ if (a === 198 && b === 51 && c === 100) return true;
100
+ if (a === 203 && b === 0 && c === 113) return true;
101
+ }
61
102
 
62
103
  for (const pattern of PRIVATE_IPV4) {
63
104
  if (pattern.test(h)) return true;
@@ -90,7 +131,7 @@ function validateCloudUrl(urlString) {
90
131
  }
91
132
 
92
133
  const scheme = parsed.protocol.replace(/:$/, '').toLowerCase();
93
- if (BLOCKED_SCHEMES.has(scheme)) return { allowed: false };
134
+ if (BLOCKED_SCHEMES.has(scheme) || !ALLOWED_SCHEMES.has(scheme)) return { allowed: false };
94
135
 
95
136
  const hostname = parsed.hostname;
96
137
  if (isPrivateHost(hostname)) return { allowed: false };
@@ -99,4 +140,70 @@ function validateCloudUrl(urlString) {
99
140
  return { allowed: true };
100
141
  }
101
142
 
102
- module.exports = { validateCloudUrl, isPrivateHost };
143
+ async function validateCloudUrlWithDns(urlString, options = {}) {
144
+ if (options.signal?.aborted) {
145
+ throw urlAbortError(options.signal);
146
+ }
147
+ const syntactic = validateCloudUrl(urlString);
148
+ if (!syntactic.allowed) return syntactic;
149
+
150
+ const hostname = new URL(urlString).hostname.replace(/^\[|\]$/g, '');
151
+ if (net.isIP(hostname)) return { allowed: !isPrivateHost(hostname) };
152
+
153
+ const lookup = options.lookup || dns.promises.lookup;
154
+ const timeoutMs = Math.max(100, Math.min(Number(options.timeoutMs) || 5000, 30_000));
155
+ let timer = null;
156
+ let onAbort = null;
157
+ try {
158
+ const pending = [
159
+ lookup(hostname, { all: true, verbatim: true }),
160
+ new Promise((_, reject) => {
161
+ timer = setTimeout(() => reject(new Error('DNS lookup timed out.')), timeoutMs);
162
+ }),
163
+ ];
164
+ if (options.signal) {
165
+ pending.push(new Promise((_, reject) => {
166
+ onAbort = () => {
167
+ reject(urlAbortError(options.signal));
168
+ };
169
+ options.signal.addEventListener('abort', onAbort, { once: true });
170
+ }));
171
+ }
172
+ const addresses = await Promise.race(pending);
173
+ const resolved = Array.isArray(addresses) ? addresses : [addresses];
174
+ if (resolved.length === 0) return { allowed: false };
175
+ if (resolved.some((entry) => isPrivateHost(entry?.address || entry))) {
176
+ return { allowed: false };
177
+ }
178
+ return { allowed: true };
179
+ } catch (error) {
180
+ if (options.signal?.aborted) throw error;
181
+ return { allowed: false };
182
+ } finally {
183
+ if (timer) clearTimeout(timer);
184
+ if (onAbort) options.signal?.removeEventListener('abort', onAbort);
185
+ }
186
+ }
187
+
188
+ async function validateAndroidIntentUrl(urlString, options = {}) {
189
+ if (!urlString || typeof urlString !== 'string') return { allowed: false };
190
+ let parsed;
191
+ try {
192
+ parsed = new URL(urlString);
193
+ } catch {
194
+ return { allowed: false };
195
+ }
196
+ const scheme = parsed.protocol.replace(/:$/, '').toLowerCase();
197
+ if (BLOCKED_ANDROID_INTENT_SCHEMES.has(scheme)) return { allowed: false };
198
+ if (ALLOWED_SCHEMES.has(scheme)) {
199
+ return validateCloudUrlWithDns(urlString, options);
200
+ }
201
+ return { allowed: /^[a-z][a-z0-9+.-]*$/.test(scheme) };
202
+ }
203
+
204
+ module.exports = {
205
+ validateAndroidIntentUrl,
206
+ validateCloudUrl,
207
+ validateCloudUrlWithDns,
208
+ isPrivateHost,
209
+ };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { randomUUID } = require('node:crypto');
6
+ const { createAbortError, throwIfAborted } = require('./abort');
7
+
8
+ async function writeBufferAtomic(destination, data, options = {}) {
9
+ const target = String(destination || '').trim();
10
+ if (!target) throw new Error('Atomic file destination is required.');
11
+ const resolved = path.resolve(target);
12
+ const temporary = `${resolved}.${process.pid}.${randomUUID()}.tmp`;
13
+ throwIfAborted(options.signal, 'File write was aborted.');
14
+ try {
15
+ await fs.promises.writeFile(temporary, data, {
16
+ flag: 'wx',
17
+ mode: options.mode ?? 0o600,
18
+ signal: options.signal,
19
+ });
20
+ throwIfAborted(options.signal, 'File write was aborted.');
21
+ await fs.promises.rename(temporary, resolved);
22
+ return resolved;
23
+ } catch (error) {
24
+ if (options.signal?.aborted) throw createAbortError(options.signal, 'File write was aborted.');
25
+ throw error;
26
+ } finally {
27
+ await fs.promises.rm(temporary, { force: true }).catch(() => {});
28
+ }
29
+ }
30
+
31
+ module.exports = { writeBufferAtomic };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const MAX_SCREENSHOT_BYTES = 20 * 1024 * 1024;
4
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
5
+
6
+ function imageError(message, code = 'INVALID_IMAGE_PAYLOAD') {
7
+ const error = new Error(message);
8
+ error.code = code;
9
+ return error;
10
+ }
11
+
12
+ function detectImage(buffer) {
13
+ if (
14
+ buffer.length >= 24
15
+ && buffer.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)
16
+ && buffer.subarray(12, 16).toString('ascii') === 'IHDR'
17
+ ) {
18
+ const width = buffer.readUInt32BE(16);
19
+ const height = buffer.readUInt32BE(20);
20
+ if (!width || !height || width > 32768 || height > 32768 || width * height > 150_000_000) {
21
+ throw imageError('Screenshot PNG has invalid or unsafe dimensions.');
22
+ }
23
+ return { contentType: 'image/png', extension: 'png', width, height };
24
+ }
25
+ if (
26
+ buffer.length >= 4
27
+ && buffer[0] === 0xff
28
+ && buffer[1] === 0xd8
29
+ && buffer[buffer.length - 2] === 0xff
30
+ && buffer[buffer.length - 1] === 0xd9
31
+ ) {
32
+ return { contentType: 'image/jpeg', extension: 'jpg', width: null, height: null };
33
+ }
34
+ throw imageError('Screenshot payload is not a valid PNG or JPEG image.');
35
+ }
36
+
37
+ function validateImageBuffer(value, options = {}) {
38
+ const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value || []);
39
+ const maxBytes = Math.max(1024, Number(options.maxBytes) || MAX_SCREENSHOT_BYTES);
40
+ if (!buffer.length) throw imageError('Screenshot payload is empty.');
41
+ if (buffer.length > maxBytes) {
42
+ throw imageError(
43
+ `Screenshot payload exceeds the ${maxBytes}-byte limit.`,
44
+ 'IMAGE_PAYLOAD_TOO_LARGE',
45
+ );
46
+ }
47
+ const detected = detectImage(buffer);
48
+ const allowedTypes = Array.isArray(options.allowedTypes) && options.allowedTypes.length
49
+ ? new Set(options.allowedTypes.map((type) => String(type).toLowerCase()))
50
+ : null;
51
+ if (allowedTypes && !allowedTypes.has(detected.contentType)) {
52
+ throw imageError(`Screenshot type ${detected.contentType} is not allowed.`);
53
+ }
54
+ return { buffer, ...detected };
55
+ }
56
+
57
+ function decodeBase64Image(value, options = {}) {
58
+ const text = String(value || '');
59
+ if (!text) throw imageError('Screenshot payload is empty.');
60
+ let declaredType = null;
61
+ let encoded = text;
62
+ if (text.startsWith('data:')) {
63
+ const match = text.match(/^data:image\/(png|jpeg|jpg);base64,([A-Za-z0-9+/]*={0,2})$/i);
64
+ if (!match) throw imageError('Screenshot data URL is malformed or uses an unsupported type.');
65
+ declaredType = match[1].toLowerCase() === 'png' ? 'image/png' : 'image/jpeg';
66
+ encoded = match[2];
67
+ }
68
+ if (!encoded || encoded.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded)) {
69
+ throw imageError('Screenshot base64 payload is malformed.');
70
+ }
71
+ const maxBytes = Math.max(1024, Number(options.maxBytes) || MAX_SCREENSHOT_BYTES);
72
+ const padding = encoded.endsWith('==') ? 2 : (encoded.endsWith('=') ? 1 : 0);
73
+ const estimatedBytes = Math.floor(encoded.length * 3 / 4) - padding;
74
+ if (estimatedBytes > maxBytes) {
75
+ throw imageError(
76
+ `Screenshot payload exceeds the ${maxBytes}-byte limit.`,
77
+ 'IMAGE_PAYLOAD_TOO_LARGE',
78
+ );
79
+ }
80
+ const buffer = Buffer.from(encoded, 'base64');
81
+ if (buffer.toString('base64').replace(/=+$/, '') !== encoded.replace(/=+$/, '')) {
82
+ throw imageError('Screenshot base64 payload is malformed.');
83
+ }
84
+ const validated = validateImageBuffer(buffer, { ...options, maxBytes });
85
+ if (declaredType && declaredType !== validated.contentType) {
86
+ throw imageError('Screenshot data URL type does not match its image bytes.');
87
+ }
88
+ return validated;
89
+ }
90
+
91
+ module.exports = {
92
+ MAX_SCREENSHOT_BYTES,
93
+ decodeBase64Image,
94
+ validateImageBuffer,
95
+ };
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ const { createAbortError, isAbortError } = require('./abort');
4
+
5
+ const RETRYABLE_HTTP_STATUS = new Set([
6
+ 408, 409, 425, 429, 500, 502, 503, 504, 520, 521, 522, 524, 529,
7
+ ]);
8
+ const RETRYABLE_NETWORK_CODES = new Set([
9
+ 'ECONNRESET',
10
+ 'ECONNREFUSED',
11
+ 'ETIMEDOUT',
12
+ 'EPIPE',
13
+ 'EAI_AGAIN',
14
+ 'ENOTFOUND',
15
+ 'ENETUNREACH',
16
+ 'EHOSTUNREACH',
17
+ 'EAGAIN',
18
+ 'UND_ERR_CONNECT_TIMEOUT',
19
+ 'UND_ERR_SOCKET',
20
+ 'UND_ERR_HEADERS_TIMEOUT',
21
+ 'INTEGRATION_HTTP_TIMEOUT',
22
+ ]);
23
+
24
+ function getHttpStatus(error) {
25
+ if (!error || typeof error !== 'object') return null;
26
+ const candidates = [
27
+ error.status,
28
+ error.statusCode,
29
+ error.response?.status,
30
+ error.cause?.status,
31
+ ];
32
+ for (const value of candidates) {
33
+ const status = Number(value);
34
+ if (Number.isFinite(status) && status >= 100 && status < 600) return status;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ function getErrorCode(error) {
40
+ if (!error || typeof error !== 'object') return null;
41
+ return error.code || error.errno || error.cause?.code || null;
42
+ }
43
+
44
+ function readHeader(headers, name) {
45
+ if (!headers) return undefined;
46
+ if (typeof headers.get === 'function') return headers.get(name);
47
+ return headers[name] ?? headers[name.toLowerCase()];
48
+ }
49
+
50
+ function retryAfterMilliseconds(headers, now = Date.now()) {
51
+ const milliseconds = readHeader(headers, 'retry-after-ms');
52
+ if (milliseconds !== undefined && milliseconds !== null && String(milliseconds).trim()) {
53
+ const parsed = Number(milliseconds);
54
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
55
+ }
56
+
57
+ const retryAfter = readHeader(headers, 'retry-after');
58
+ if (retryAfter === undefined || retryAfter === null || !String(retryAfter).trim()) {
59
+ return null;
60
+ }
61
+ const seconds = Number(retryAfter);
62
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
63
+ const date = Date.parse(String(retryAfter));
64
+ return Number.isFinite(date) ? Math.max(0, date - now) : null;
65
+ }
66
+
67
+ function computeBackoffMs(attempt, baseDelayMs, maxDelayMs) {
68
+ const exp = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
69
+ return Math.round(exp / 2 + Math.random() * (exp / 2));
70
+ }
71
+
72
+ function isTransientIoError(error) {
73
+ if (!error || isAbortError(error)) return false;
74
+ const status = getHttpStatus(error);
75
+ if (status !== null) return RETRYABLE_HTTP_STATUS.has(status);
76
+ const code = getErrorCode(error);
77
+ return Boolean(code && RETRYABLE_NETWORK_CODES.has(String(code)));
78
+ }
79
+
80
+ function abortableDelay(milliseconds, signal = null) {
81
+ if (signal?.aborted) return Promise.reject(createAbortError(signal));
82
+ return new Promise((resolve, reject) => {
83
+ let timer = null;
84
+ const onAbort = () => {
85
+ if (timer) clearTimeout(timer);
86
+ signal?.removeEventListener('abort', onAbort);
87
+ reject(createAbortError(signal));
88
+ };
89
+ timer = setTimeout(() => {
90
+ signal?.removeEventListener('abort', onAbort);
91
+ resolve();
92
+ }, Math.max(0, Number(milliseconds) || 0));
93
+ signal?.addEventListener('abort', onAbort, { once: true });
94
+ });
95
+ }
96
+
97
+ module.exports = {
98
+ RETRYABLE_HTTP_STATUS,
99
+ RETRYABLE_NETWORK_CODES,
100
+ abortableDelay,
101
+ computeBackoffMs,
102
+ getErrorCode,
103
+ getHttpStatus,
104
+ isTransientIoError,
105
+ readHeader,
106
+ retryAfterMilliseconds,
107
+ };