@juspay/neurolink 12.17.1 → 12.17.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 (73) hide show
  1. package/CHANGELOG.md +6 -2
  2. package/dist/browser/neurolink.min.js +430 -421
  3. package/dist/cli/commands/proxy.js +455 -259
  4. package/dist/constants/contextWindows.d.ts +2 -0
  5. package/dist/constants/contextWindows.js +4 -0
  6. package/dist/neurolink.js +8 -3
  7. package/dist/providers/openaiChatCompletionsBase.js +60 -51
  8. package/dist/proxy/anthropicBudgetResponse.d.ts +3 -0
  9. package/dist/proxy/anthropicBudgetResponse.js +153 -0
  10. package/dist/proxy/bodyCaptureProcessing.d.ts +1 -0
  11. package/dist/proxy/bodyCaptureProcessing.js +23 -4
  12. package/dist/proxy/bodyCaptureWorker.js +20 -2
  13. package/dist/proxy/codexFallback.d.ts +10 -0
  14. package/dist/proxy/codexFallback.js +108 -24
  15. package/dist/proxy/codexUsage.js +2 -0
  16. package/dist/proxy/globalInstaller.d.ts +32 -1
  17. package/dist/proxy/globalInstaller.js +386 -3
  18. package/dist/proxy/otelLogSink.d.ts +3 -3
  19. package/dist/proxy/otelLogSink.js +137 -144
  20. package/dist/proxy/otlpLogTransport.d.ts +3 -0
  21. package/dist/proxy/otlpLogTransport.js +334 -0
  22. package/dist/proxy/proxyActivity.d.ts +22 -2
  23. package/dist/proxy/proxyActivity.js +114 -9
  24. package/dist/proxy/proxyAnalysis.js +82 -52
  25. package/dist/proxy/proxyClientCancellation.d.ts +7 -0
  26. package/dist/proxy/proxyClientCancellation.js +23 -0
  27. package/dist/proxy/proxyContextPreflight.d.ts +25 -0
  28. package/dist/proxy/proxyContextPreflight.js +337 -0
  29. package/dist/proxy/proxyFailureDetails.d.ts +19 -0
  30. package/dist/proxy/proxyFailureDetails.js +96 -0
  31. package/dist/proxy/proxyLifecycle.js +7 -0
  32. package/dist/proxy/proxyRouteBodyCapture.d.ts +13 -0
  33. package/dist/proxy/proxyRouteBodyCapture.js +112 -0
  34. package/dist/proxy/proxyTokenBudget.d.ts +40 -0
  35. package/dist/proxy/proxyTokenBudget.js +523 -0
  36. package/dist/proxy/proxyTracer.d.ts +3 -2
  37. package/dist/proxy/proxyTracer.js +46 -20
  38. package/dist/proxy/proxyTranslationEngine.d.ts +8 -10
  39. package/dist/proxy/proxyTranslationEngine.js +736 -376
  40. package/dist/proxy/requestLogger.d.ts +1 -0
  41. package/dist/proxy/requestLogger.js +107 -5
  42. package/dist/proxy/restartControl.js +9 -4
  43. package/dist/proxy/rollingWorkerProcess.js +65 -0
  44. package/dist/proxy/rollingWorkerSupervisor.js +7 -0
  45. package/dist/proxy/updateCoordinator.d.ts +14 -1
  46. package/dist/proxy/updateCoordinator.js +57 -2
  47. package/dist/server/routes/claudeProxyRoutes.d.ts +4 -5
  48. package/dist/server/routes/claudeProxyRoutes.js +809 -577
  49. package/dist/server/routes/codexProxyRoutes.js +191 -9
  50. package/dist/server/routes/openaiProxyRoutes.d.ts +1 -1
  51. package/dist/server/routes/openaiProxyRoutes.js +261 -69
  52. package/dist/types/cli.d.ts +3 -0
  53. package/dist/types/codex.d.ts +5 -1
  54. package/dist/types/index.d.ts +2 -0
  55. package/dist/types/index.js +2 -0
  56. package/dist/types/proxy.d.ts +148 -3
  57. package/dist/types/proxy.js +0 -15
  58. package/dist/types/proxyBudget.d.ts +74 -0
  59. package/dist/types/proxyBudget.js +1 -0
  60. package/dist/types/proxyContext.d.ts +35 -0
  61. package/dist/types/proxyContext.js +1 -0
  62. package/dist/utils/fileDetector.js +28 -7
  63. package/dist/utils/messageBuilder.js +51 -26
  64. package/dist/utils/pdfProcessor.js +26 -4
  65. package/docs/assets/dashboards/README.md +37 -0
  66. package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +197 -89
  67. package/docs-site/static/search-index.json +1 -1
  68. package/package.json +7 -2
  69. package/scripts/observability/NATIVE-DURABLE.md +63 -0
  70. package/scripts/observability/otel-collector.proxy-native-durable.yaml +93 -0
  71. package/scripts/observability/proxy-telemetry-backend.mjs +32 -5
  72. package/scripts/observability/proxy-telemetry-check.mjs +396 -61
  73. package/scripts/observability/query-proxy-history.mjs +34 -5
@@ -0,0 +1,334 @@
1
+ /**
2
+ * OTLP/HTTP JSON transport with response-aware acknowledgement. The SDK's HTTP
3
+ * exporter reports SUCCESS even for partialSuccess or malformed responses, so
4
+ * its callback cannot establish that every record in our batch was accepted.
5
+ * Serialization stays with the official OpenTelemetry implementation.
6
+ */
7
+ import { readFileSync } from "node:fs";
8
+ import { Agent as HttpAgent, request as httpRequest, validateHeaderName, validateHeaderValue, } from "node:http";
9
+ import { Agent as HttpsAgent, request as httpsRequest } from "node:https";
10
+ import { setTimeout as delay } from "node:timers/promises";
11
+ import { promisify } from "node:util";
12
+ import { gzip } from "node:zlib";
13
+ import { ExportResultCode, parseKeyPairsIntoRecord } from "@opentelemetry/core";
14
+ import { JsonLogsSerializer } from "@opentelemetry/otlp-transformer";
15
+ const gzipAsync = promisify(gzip);
16
+ const MAX_RESPONSE_BYTES = 64 * 1024;
17
+ const MAX_EXPORT_ATTEMPTS = 5;
18
+ const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]);
19
+ const RETRYABLE_NETWORK_CODES = new Set([
20
+ "ECONNRESET",
21
+ "ECONNREFUSED",
22
+ "EPIPE",
23
+ "ETIMEDOUT",
24
+ "EAI_AGAIN",
25
+ "ENOTFOUND",
26
+ "ENETUNREACH",
27
+ "EHOSTUNREACH",
28
+ ]);
29
+ class ProxyOtlpResponseError extends Error {
30
+ retryable;
31
+ retryAfterMs;
32
+ constructor(message, retryable = false, retryAfterMs) {
33
+ super(message);
34
+ this.retryable = retryable;
35
+ this.retryAfterMs = retryAfterMs;
36
+ this.name = "ProxyOtlpResponseError";
37
+ }
38
+ }
39
+ function signalSetting(suffix) {
40
+ return (process.env[`OTEL_EXPORTER_OTLP_LOGS_${suffix}`]?.trim() ||
41
+ process.env[`OTEL_EXPORTER_OTLP_${suffix}`]?.trim() ||
42
+ undefined);
43
+ }
44
+ function certificate(suffix) {
45
+ const path = signalSetting(suffix);
46
+ if (!path) {
47
+ return undefined;
48
+ }
49
+ try {
50
+ return readFileSync(path);
51
+ }
52
+ catch {
53
+ // File paths, headers, backend bodies and native TLS errors can contain
54
+ // credentials. Export diagnostics intentionally expose only fixed reasons.
55
+ throw new Error(`Proxy OTLP ${suffix} file could not be read`);
56
+ }
57
+ }
58
+ function retryAfter(value) {
59
+ if (!value) {
60
+ return undefined;
61
+ }
62
+ if (/^\d+$/.test(value.trim())) {
63
+ return Number(value) * 1000;
64
+ }
65
+ const at = Date.parse(value);
66
+ return Number.isFinite(at) ? Math.max(0, at - Date.now()) : undefined;
67
+ }
68
+ function validateAcknowledgement(bytes) {
69
+ let payload;
70
+ try {
71
+ payload = JSON.parse(bytes.toString("utf8"));
72
+ }
73
+ catch {
74
+ // Empty JSON is not a serialized ExportLogsServiceResponse. Its valid
75
+ // empty message is {}, unlike the zero-byte protobuf representation.
76
+ throw new ProxyOtlpResponseError("OTLP response JSON is empty or malformed");
77
+ }
78
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
79
+ throw new ProxyOtlpResponseError("OTLP response must be a JSON object");
80
+ }
81
+ if (Object.keys(payload).some((key) => key !== "partialSuccess" && key !== "partial_success")) {
82
+ throw new ProxyOtlpResponseError("OTLP response has unexpected fields");
83
+ }
84
+ if ("partialSuccess" in payload && "partial_success" in payload) {
85
+ throw new ProxyOtlpResponseError("OTLP response has ambiguous field aliases");
86
+ }
87
+ const partial = "partialSuccess" in payload
88
+ ? payload.partialSuccess
89
+ : "partial_success" in payload
90
+ ? payload.partial_success
91
+ : undefined;
92
+ if (partial === null || partial === undefined) {
93
+ return;
94
+ }
95
+ if (typeof partial !== "object" || Array.isArray(partial)) {
96
+ throw new ProxyOtlpResponseError("OTLP partial success response is malformed");
97
+ }
98
+ const allowedFields = new Set([
99
+ "rejectedLogRecords",
100
+ "rejected_log_records",
101
+ "errorMessage",
102
+ "error_message",
103
+ ]);
104
+ if (Object.keys(partial).some((key) => !allowedFields.has(key))) {
105
+ throw new ProxyOtlpResponseError("OTLP partial success response has unexpected fields");
106
+ }
107
+ if (("rejectedLogRecords" in partial && "rejected_log_records" in partial) ||
108
+ ("errorMessage" in partial && "error_message" in partial)) {
109
+ throw new ProxyOtlpResponseError("OTLP partial success has ambiguous field aliases");
110
+ }
111
+ const message = "errorMessage" in partial
112
+ ? partial.errorMessage
113
+ : "error_message" in partial
114
+ ? partial.error_message
115
+ : undefined;
116
+ if (message !== null &&
117
+ message !== undefined &&
118
+ typeof message !== "string") {
119
+ throw new ProxyOtlpResponseError("OTLP partial success response is malformed");
120
+ }
121
+ const rejected = "rejectedLogRecords" in partial
122
+ ? partial.rejectedLogRecords
123
+ : "rejected_log_records" in partial
124
+ ? partial.rejected_log_records
125
+ : 0;
126
+ // ProtoJSON accepts integer strings or numbers; null is the absent default.
127
+ if (rejected === null || rejected === 0 || rejected === "0") {
128
+ return;
129
+ }
130
+ const integer = typeof rejected === "string" && /^\d+$/.test(rejected)
131
+ ? BigInt(rejected)
132
+ : typeof rejected === "number" &&
133
+ Number.isSafeInteger(rejected) &&
134
+ rejected >= 0
135
+ ? BigInt(rejected)
136
+ : undefined;
137
+ if (integer === undefined || integer > 9223372036854775807n) {
138
+ throw new ProxyOtlpResponseError("OTLP rejected record count is malformed");
139
+ }
140
+ if (integer > 0n) {
141
+ // We cannot identify the rejected subset. Mark the entire batch uncertain;
142
+ // replaying a known partial response would duplicate its accepted subset.
143
+ throw new ProxyOtlpResponseError("OTLP partial success rejected log records");
144
+ }
145
+ }
146
+ /** Both proxy queues use this transport; success is not backend persistence. */
147
+ export function createProxyOtlpLogTransport(endpoint, timeoutMillis) {
148
+ let url;
149
+ try {
150
+ url = new URL(endpoint);
151
+ }
152
+ catch {
153
+ throw new Error("Proxy OTLP endpoint is invalid");
154
+ }
155
+ if (!new Set(["http:", "https:"]).has(url.protocol)) {
156
+ throw new Error("Proxy OTLP endpoint must use HTTP or HTTPS");
157
+ }
158
+ if (url.username || url.password) {
159
+ throw new Error("Proxy OTLP endpoint credentials must use OTLP headers");
160
+ }
161
+ const compression = signalSetting("COMPRESSION") ?? "none";
162
+ if (compression !== "none" && compression !== "gzip") {
163
+ throw new Error("Proxy OTLP compression must be none or gzip");
164
+ }
165
+ const headers = {};
166
+ try {
167
+ for (const raw of [
168
+ process.env.OTEL_EXPORTER_OTLP_HEADERS,
169
+ process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS,
170
+ ]) {
171
+ for (const [key, value] of Object.entries(parseKeyPairsIntoRecord(raw))) {
172
+ validateHeaderName(key);
173
+ validateHeaderValue(key, value);
174
+ headers[key.toLowerCase()] = value;
175
+ }
176
+ }
177
+ }
178
+ catch {
179
+ throw new Error("Proxy OTLP headers are invalid");
180
+ }
181
+ // These describe the serialized wire payload, irrespective of user headers.
182
+ headers["content-type"] = "application/json";
183
+ headers["accept"] = "application/json";
184
+ headers["accept-encoding"] = "identity";
185
+ delete headers["content-encoding"];
186
+ if (compression === "gzip") {
187
+ headers["content-encoding"] = "gzip";
188
+ }
189
+ const tls = {
190
+ ca: certificate("CERTIFICATE"),
191
+ cert: certificate("CLIENT_CERTIFICATE"),
192
+ key: certificate("CLIENT_KEY"),
193
+ };
194
+ if (Boolean(tls.cert) !== Boolean(tls.key)) {
195
+ throw new Error("Proxy OTLP client certificate and key must be set together");
196
+ }
197
+ const agent = url.protocol === "https:"
198
+ ? new HttpsAgent({ keepAlive: true, maxSockets: 30, ...tls })
199
+ : new HttpAgent({ keepAlive: true, maxSockets: 30 });
200
+ const request = url.protocol === "https:" ? httpsRequest : httpRequest;
201
+ let closed = false;
202
+ const active = new Set();
203
+ const sendOnce = (data, signal) => new Promise((resolve, reject) => {
204
+ let receivedResponse = false;
205
+ const req = request(url, {
206
+ method: "POST",
207
+ agent,
208
+ signal,
209
+ headers: { ...headers, "content-length": String(data.byteLength) },
210
+ }, (response) => {
211
+ receivedResponse = true;
212
+ const status = response.statusCode ?? 0;
213
+ const fail = (error) => {
214
+ reject(error);
215
+ response.destroy();
216
+ };
217
+ // An HTTP status, a partial response or a broken success response must
218
+ // never be mistaken for an acknowledgement of individual log records.
219
+ if (status !== 200) {
220
+ fail(new ProxyOtlpResponseError(`OTLP HTTP ${status}`, RETRYABLE_STATUSES.has(status), retryAfter(response.headers["retry-after"])));
221
+ return;
222
+ }
223
+ const encoding = response.headers["content-encoding"];
224
+ if (encoding && encoding !== "identity") {
225
+ fail(new ProxyOtlpResponseError("OTLP response encoding is unsupported"));
226
+ return;
227
+ }
228
+ const contentType = response.headers["content-type"];
229
+ if (contentType &&
230
+ contentType.split(";", 1)[0].trim().toLowerCase() !==
231
+ "application/json") {
232
+ fail(new ProxyOtlpResponseError("OTLP response content type is not JSON"));
233
+ return;
234
+ }
235
+ const chunks = [];
236
+ let size = 0;
237
+ response.on("data", (chunk) => {
238
+ size += chunk.length;
239
+ if (size > MAX_RESPONSE_BYTES) {
240
+ fail(new ProxyOtlpResponseError("OTLP response exceeds 64 KiB limit"));
241
+ return;
242
+ }
243
+ chunks.push(chunk);
244
+ });
245
+ response.on("aborted", () => reject(new ProxyOtlpResponseError("OTLP success response was interrupted")));
246
+ response.on("error", () => reject(new ProxyOtlpResponseError("OTLP success response could not be read")));
247
+ response.on("end", () => {
248
+ try {
249
+ validateAcknowledgement(Buffer.concat(chunks));
250
+ resolve();
251
+ }
252
+ catch (error) {
253
+ reject(error);
254
+ }
255
+ });
256
+ });
257
+ req.on("error", (error) => reject(new ProxyOtlpResponseError(signal.aborted
258
+ ? "OTLP export deadline exceeded"
259
+ : "OTLP transport failed", !signal.aborted &&
260
+ !receivedResponse &&
261
+ RETRYABLE_NETWORK_CODES.has(error.code ?? ""))));
262
+ req.end(data);
263
+ });
264
+ const send = async (records) => {
265
+ const controller = new AbortController();
266
+ const deadlineAt = Date.now() + timeoutMillis;
267
+ const deadline = setTimeout(() => controller.abort(), timeoutMillis);
268
+ deadline.unref();
269
+ try {
270
+ const serialized = JsonLogsSerializer.serializeRequest(records);
271
+ if (!serialized) {
272
+ throw new ProxyOtlpResponseError("OTLP serialization produced no payload");
273
+ }
274
+ // Serialize and compress once, so ambiguous retries retain byte-for-byte
275
+ // event identities and payloads for query-side deduplication.
276
+ const bytes = compression === "gzip" ? await gzipAsync(serialized) : serialized;
277
+ let attempt = 0;
278
+ while (!controller.signal.aborted && Date.now() < deadlineAt) {
279
+ attempt++;
280
+ try {
281
+ await sendOnce(bytes, controller.signal);
282
+ return;
283
+ }
284
+ catch (error) {
285
+ if (!(error instanceof ProxyOtlpResponseError) || !error.retryable) {
286
+ throw error;
287
+ }
288
+ if (attempt >= MAX_EXPORT_ATTEMPTS) {
289
+ throw new ProxyOtlpResponseError("OTLP retry attempt limit reached");
290
+ }
291
+ const exponential = Math.min(5000, 1000 * 2 ** Math.min(attempt - 1, 3));
292
+ // Retry-After is a lower bound, never permission to hammer a busy
293
+ // collector. Even Retry-After: 0 retains exponential backoff+jitter.
294
+ const wait = Math.max(error.retryAfterMs ?? 0, exponential * (0.8 + Math.random() * 0.4));
295
+ if (wait >= deadlineAt - Date.now()) {
296
+ throw new ProxyOtlpResponseError("OTLP retry exceeds export deadline");
297
+ }
298
+ await delay(wait, undefined, { signal: controller.signal });
299
+ }
300
+ }
301
+ throw new ProxyOtlpResponseError("OTLP export deadline exceeded");
302
+ }
303
+ catch (error) {
304
+ if (error instanceof ProxyOtlpResponseError) {
305
+ throw error;
306
+ }
307
+ throw new ProxyOtlpResponseError(controller.signal.aborted
308
+ ? "OTLP export deadline exceeded"
309
+ : "OTLP export failed");
310
+ }
311
+ finally {
312
+ clearTimeout(deadline);
313
+ }
314
+ };
315
+ return {
316
+ export(records, callback) {
317
+ if (closed) {
318
+ callback({
319
+ code: ExportResultCode.FAILED,
320
+ error: new Error("OTLP transport is shut down"),
321
+ });
322
+ return;
323
+ }
324
+ const operation = send(records).then(() => callback({ code: ExportResultCode.SUCCESS }), (error) => callback({ code: ExportResultCode.FAILED, error }));
325
+ active.add(operation);
326
+ void operation.finally(() => active.delete(operation)).catch(() => { });
327
+ },
328
+ async shutdown() {
329
+ closed = true;
330
+ await Promise.allSettled(active);
331
+ agent.destroy();
332
+ },
333
+ };
334
+ }
@@ -1,7 +1,27 @@
1
1
  import type { ProxyActivitySnapshot, ProxyResponseTrackingObserver, RequestLogEntry, RequestAttemptLogEntry } from "../types/index.js";
2
+ export declare function getProxyBridgeResult(requestId: string): RequestLogEntry | undefined;
3
+ export declare function registerInternalProxyRequest(parentRequestId: string): {
4
+ token: `${string}-${string}-${string}-${string}-${string}`;
5
+ requestId: `${string}-${string}-${string}-${string}-${string}`;
6
+ dispose(): void;
7
+ };
8
+ export declare function consumeInternalProxyRequest(token: string | null): {
9
+ parentRequestId: string;
10
+ requestId: string;
11
+ } | undefined;
12
+ export declare function getProxyRequestAccounting(requestId: string): {
13
+ parentRequestId?: string;
14
+ accountingScope?: "client" | "internal";
15
+ usageOwnerRequestId?: string;
16
+ } | undefined;
17
+ export declare function releaseProxyRequestAccounting(requestId: string): void;
18
+ /** Deferred native callbacks retain this owner even after transport cleanup. */
19
+ export declare function withProxyFinalLogOwnership<T>(requestId: string, run: () => T): T;
2
20
  /** Join route accounting to the HTTP lifecycle without relying on write order. */
3
21
  export declare function observeProxyFinalLog(requestId: string, observer: (entry: RequestLogEntry) => void, attemptObserver?: (entry: RequestAttemptLogEntry) => void): () => void;
4
- export declare function notifyProxyFinalLog(entry: RequestLogEntry): void;
22
+ /** Check without claiming: aggregate accounting must not revive a cancelled request. */
23
+ export declare function isProxyRequestFinalized(requestId: string): boolean;
24
+ export declare function notifyProxyFinalLog(entry: RequestLogEntry): boolean;
5
25
  /** Retain only the last attempt for a currently observed HTTP request. */
6
26
  export declare function notifyProxyAttemptLog(entry: RequestAttemptLogEntry): void;
7
27
  export declare function registerProxyResponseObserver(metadata: object, observer: ProxyResponseTrackingObserver): void;
@@ -11,5 +31,5 @@ export declare function beginProxyRequest(): () => void;
11
31
  export declare function getProxyActivitySnapshot(): ProxyActivitySnapshot;
12
32
  export declare function isProxyActivityQuiet(snapshot: ProxyActivitySnapshot, quietThresholdMs: number, nowMs?: number): boolean;
13
33
  /** Keep activity open until the response body completes, errors, or is cancelled. */
14
- export declare function trackProxyResponse(response: Response, finishRequest: () => void, observer?: ProxyResponseTrackingObserver): Response;
34
+ export declare function trackProxyResponse(response: Response, finishRequest: () => void, observer?: ProxyResponseTrackingObserver, abortSignal?: AbortSignal): Response;
15
35
  export declare function resetProxyActivityForTests(): void;
@@ -1,6 +1,68 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { withTimeout } from "../utils/async/withTimeout.js";
2
3
  import { logger } from "../utils/logger.js";
3
4
  const PROXY_RESPONSE_CANCEL_TIMEOUT_MS = 1_000;
5
+ // Bridge identity is registered by this process, never trusted from an inbound
6
+ // parent-id header. One-use capabilities expire even if loopback dispatch fails.
7
+ const internalRequests = new Map();
8
+ const bridgeResults = new Map();
9
+ export function getProxyBridgeResult(requestId) {
10
+ return bridgeResults.get(requestId);
11
+ }
12
+ const requestAccounting = new Map();
13
+ export function registerInternalProxyRequest(parentRequestId) {
14
+ if (internalRequests.size >= 5000) {
15
+ throw Object.assign(new Error("Internal proxy dispatch capacity is full"), {
16
+ status: 503,
17
+ code: "PROXY_INTERNAL_CAPACITY",
18
+ retryable: false,
19
+ });
20
+ }
21
+ const token = crypto.randomUUID();
22
+ const requestId = crypto.randomUUID();
23
+ internalRequests.set(token, { parentRequestId, requestId });
24
+ requestAccounting.set(parentRequestId, {
25
+ accountingScope: "client",
26
+ usageOwnerRequestId: requestId,
27
+ });
28
+ const timer = setTimeout(() => {
29
+ if (internalRequests.delete(token)) {
30
+ requestAccounting.delete(parentRequestId);
31
+ bridgeResults.delete(parentRequestId);
32
+ }
33
+ }, 300_000);
34
+ timer.unref?.();
35
+ return {
36
+ token,
37
+ requestId,
38
+ dispose() {
39
+ clearTimeout(timer);
40
+ internalRequests.delete(token);
41
+ },
42
+ };
43
+ }
44
+ export function consumeInternalProxyRequest(token) {
45
+ if (!token) {
46
+ return undefined;
47
+ }
48
+ const result = internalRequests.get(token);
49
+ internalRequests.delete(token);
50
+ if (result) {
51
+ requestAccounting.set(result.requestId, {
52
+ parentRequestId: result.parentRequestId,
53
+ accountingScope: "internal",
54
+ usageOwnerRequestId: result.requestId,
55
+ });
56
+ }
57
+ return result;
58
+ }
59
+ export function getProxyRequestAccounting(requestId) {
60
+ return requestAccounting.get(requestId);
61
+ }
62
+ export function releaseProxyRequestAccounting(requestId) {
63
+ requestAccounting.delete(requestId);
64
+ bridgeResults.delete(requestId);
65
+ }
4
66
  let activeRequests = 0;
5
67
  let lastActivityAtMs = null;
6
68
  // Route handlers can attach terminal observers to their request context without
@@ -8,11 +70,19 @@ let lastActivityAtMs = null;
8
70
  // response through one tracker, which fans these observers out at the point
9
71
  // where bytes actually leave the proxy.
10
72
  const responseObserversByMetadata = new WeakMap();
73
+ const finalOwnership = new Map();
74
+ const finalOwnershipContext = new AsyncLocalStorage();
75
+ /** Deferred native callbacks retain this owner even after transport cleanup. */
76
+ export function withProxyFinalLogOwnership(requestId, run) {
77
+ const owner = finalOwnership.get(requestId);
78
+ return owner ? finalOwnershipContext.run(owner, run) : run();
79
+ }
11
80
  const finalLogObservers = new Map();
12
81
  const attemptLogObservers = new Map();
13
82
  /** Join route accounting to the HTTP lifecycle without relying on write order. */
14
83
  export function observeProxyFinalLog(requestId, observer, attemptObserver) {
15
84
  finalLogObservers.set(requestId, observer);
85
+ finalOwnership.set(requestId, { requestId, finalized: false });
16
86
  if (attemptObserver) {
17
87
  attemptLogObservers.set(requestId, attemptObserver);
18
88
  }
@@ -22,12 +92,35 @@ export function observeProxyFinalLog(requestId, observer, attemptObserver) {
22
92
  return () => {
23
93
  if (finalLogObservers.get(requestId) === observer) {
24
94
  finalLogObservers.delete(requestId);
95
+ finalOwnership.delete(requestId);
25
96
  attemptLogObservers.delete(requestId);
26
97
  }
27
98
  };
28
99
  }
100
+ /** Check without claiming: aggregate accounting must not revive a cancelled request. */
101
+ export function isProxyRequestFinalized(requestId) {
102
+ const contextual = finalOwnershipContext.getStore();
103
+ return ((contextual?.requestId === requestId
104
+ ? contextual
105
+ : finalOwnership.get(requestId))?.finalized ?? false);
106
+ }
29
107
  export function notifyProxyFinalLog(entry) {
108
+ const contextual = finalOwnershipContext.getStore();
109
+ const owner = contextual?.requestId === entry.requestId
110
+ ? contextual
111
+ : finalOwnership.get(entry.requestId);
112
+ if (owner) {
113
+ if (owner.finalized) {
114
+ return false;
115
+ }
116
+ owner.finalized = true;
117
+ }
118
+ const parent = requestAccounting.get(entry.requestId)?.parentRequestId;
119
+ if (parent && requestAccounting.has(parent)) {
120
+ bridgeResults.set(parent, entry);
121
+ }
30
122
  finalLogObservers.get(entry.requestId)?.(entry);
123
+ return true;
31
124
  }
32
125
  /** Retain only the last attempt for a currently observed HTTP request. */
33
126
  export function notifyProxyAttemptLog(entry) {
@@ -99,7 +192,7 @@ export function isProxyActivityQuiet(snapshot, quietThresholdMs, nowMs = Date.no
99
192
  return nowMs - snapshot.lastActivityAt.getTime() >= quietThresholdMs;
100
193
  }
101
194
  /** Keep activity open until the response body completes, errors, or is cancelled. */
102
- export function trackProxyResponse(response, finishRequest, observer) {
195
+ export function trackProxyResponse(response, finishRequest, observer, abortSignal) {
103
196
  if (!response.body) {
104
197
  try {
105
198
  const notified = observer?.onTerminal?.({
@@ -107,7 +200,7 @@ export function trackProxyResponse(response, finishRequest, observer) {
107
200
  observedBodyBytes: 0,
108
201
  responseChunks: 0,
109
202
  });
110
- void Promise.resolve(notified).then(finishRequest, finishRequest);
203
+ void withTimeout(Promise.resolve(notified), 4000, "Timed out finalizing proxy response accounting").then(finishRequest, finishRequest);
111
204
  }
112
205
  catch {
113
206
  finishRequest();
@@ -119,17 +212,12 @@ export function trackProxyResponse(response, finishRequest, observer) {
119
212
  let responseChunks = 0;
120
213
  let settled = false;
121
214
  let sourceClosed = false;
122
- // A framework can cancel its adapter after the upstream stream has already
123
- // closed. Snapshot reader.closed before cancel() so that normal cleanup is
124
- // not recorded as a client-aborted request.
125
- void reader.closed.then(() => {
126
- sourceClosed = true;
127
- }, () => undefined);
128
215
  const settle = (outcome, error) => {
129
216
  if (settled) {
130
217
  return;
131
218
  }
132
219
  settled = true;
220
+ abortSignal?.removeEventListener("abort", onAbort);
133
221
  // Keep drain accounting open through bounded terminal bookkeeping, but
134
222
  // never hold back the client's response body while telemetry is written.
135
223
  try {
@@ -139,12 +227,29 @@ export function trackProxyResponse(response, finishRequest, observer) {
139
227
  observedBodyBytes,
140
228
  responseChunks,
141
229
  });
142
- void Promise.resolve(notified).then(finishRequest, finishRequest);
230
+ void withTimeout(Promise.resolve(notified), 4000, "Timed out finalizing proxy response accounting").then(finishRequest, finishRequest);
143
231
  }
144
232
  catch {
145
233
  finishRequest();
146
234
  }
147
235
  };
236
+ const onAbort = () => {
237
+ settle(abortSignal?.reason?.name === "TimeoutError"
238
+ ? "stream_error"
239
+ : "client_cancelled", abortSignal?.reason);
240
+ void withTimeout(reader.cancel(abortSignal?.reason), PROXY_RESPONSE_CANCEL_TIMEOUT_MS, "Timed out cancelling the upstream proxy response").catch(() => undefined);
241
+ };
242
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
243
+ if (abortSignal?.aborted) {
244
+ onAbort();
245
+ }
246
+ // A failed source cannot be observed only on a future downstream pull: a
247
+ // stalled reader might never pull again, leaving admission/drain open forever.
248
+ void reader.closed.then(() => {
249
+ sourceClosed = true;
250
+ }, (error) => {
251
+ settle("stream_error", error);
252
+ });
148
253
  const trackedBody = new ReadableStream({
149
254
  async pull(controller) {
150
255
  try {