@agent-device/proxy 0.0.0-stage → 0.21.23

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Callstack
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,108 @@
1
- # Temporary Holding Version
1
+ # @agent-device/proxy
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The proxy behind `agent-device proxy`, as a library. Embed it in your own gateway to give remote
4
+ `agent-device` clients access to a daemon on another machine, over whichever transport you choose.
5
+
6
+ The proxy:
7
+
8
+ - authenticates clients with a token you choose, and never exposes the daemon's own token;
9
+ - forwards only the routes a remote client needs: `/health`, `/rpc`, uploads, artifacts, and
10
+ request diagnostics;
11
+ - refuses install sources that name a file on the daemon host;
12
+ - rewrites upload tickets so clients upload through the proxy, not to the daemon directly.
13
+
14
+ Requires Node.js 22.12 or later.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @agent-device/proxy
20
+ ```
21
+
22
+ Keep its version close to the daemon behind it. Clients refuse a proxy whose daemon speaks a
23
+ different RPC protocol version, before any command runs.
24
+
25
+ ## Serve the proxy over HTTP
26
+
27
+ ```ts
28
+ import { createDaemonProxyServer } from '@agent-device/proxy';
29
+
30
+ const server = createDaemonProxyServer({
31
+ upstreamBaseUrl: 'http://127.0.0.1:4310',
32
+ upstreamToken: process.env.AGENT_DEVICE_DAEMON_TOKEN!,
33
+ clientToken: process.env.GATEWAY_CLIENT_TOKEN!,
34
+ });
35
+ server.listen(8080);
36
+ ```
37
+
38
+ Clients connect with `agent-device connect proxy --daemon-base-url https://gateway.example.com/agent-device`
39
+ and `AGENT_DEVICE_DAEMON_AUTH_TOKEN` set to `clientToken`.
40
+
41
+ To mount the proxy in an existing `node:http` or `node:https` server, use
42
+ `createDaemonProxyRequestListener(createDaemonProxy(options))`.
43
+
44
+ ## Bring your own transport
45
+
46
+ `createDaemonProxy` has no server of its own. It answers standard `Request` objects with standard
47
+ `Response` objects, so any Node.js-compatible runtime or framework that speaks the Fetch API can
48
+ host it:
49
+
50
+ ```ts
51
+ import { createDaemonProxy } from '@agent-device/proxy';
52
+
53
+ const proxy = createDaemonProxy({
54
+ upstreamBaseUrl: 'http://127.0.0.1:4310',
55
+ upstreamToken: daemonToken,
56
+ clientToken,
57
+ });
58
+
59
+ // A fetch-style server entry, as used by Bun and by Hono on Node.js
60
+ export default { fetch: proxy.handle };
61
+ ```
62
+
63
+ For a WebSocket or other message transport, turn each incoming message into a `Request` and send
64
+ the `Response` back. Three things the proxy relies on:
65
+
66
+ - `request.url` is the URL the client used. Upload tickets point clients at its origin. An
67
+ `x-forwarded-proto` header overrides the scheme.
68
+ - `request.signal` aborts when the client goes away. The proxy then cancels the daemon request, so
69
+ work the client stopped waiting for does not keep running.
70
+ - Response bodies stream. Artifact downloads can be large, so forward the body as it arrives.
71
+
72
+ `handle` never rejects. Errors come back as JSON responses with a 4xx or 5xx status, except a route
73
+ the proxy does not serve, which gets a plain `Not found` 404.
74
+
75
+ ### Reach the daemon over your own transport
76
+
77
+ By default the proxy calls the daemon with the global `fetch`. Pass `upstreamFetch` when the daemon
78
+ is only reachable some other way, such as a tunnel the device host opened to your gateway:
79
+
80
+ ```ts
81
+ const proxy = createDaemonProxy({
82
+ upstreamBaseUrl: 'http://device-host-17.internal',
83
+ upstreamToken: daemonToken,
84
+ clientToken,
85
+ upstreamFetch: (request) => deviceHostTunnel.send(request),
86
+ });
87
+ ```
88
+
89
+ `upstreamFetch` receives a `Request` addressed to `upstreamBaseUrl` and must resolve with the
90
+ daemon's `Response`. Abort the exchange when `request.signal` aborts.
91
+
92
+ ## Options
93
+
94
+ | Option | Default | Meaning |
95
+ | --- | --- | --- |
96
+ | `upstreamBaseUrl` | required | Base URL of the daemon's HTTP server. |
97
+ | `upstreamToken` | required | The daemon's auth token. |
98
+ | `clientToken` | required | Token clients must send as their daemon auth token. |
99
+ | `upstreamFetch` | global `fetch` | Sends one request to the daemon. |
100
+ | `maxRpcBodyBytes` | 1 MiB | Larger `/rpc` bodies get a 400 response. |
101
+ | `upstreamTimeoutMs` | 5 minutes | Longest time one daemon request may take, including its body. |
102
+
103
+ ## Health and restarts
104
+
105
+ `GET /health` (also served as `/agent-device/health`) needs no token. It reports the proxy's version and `instanceId`, and
106
+ includes the daemon's own health under `upstream`. A new `createDaemonProxy` call gets a new
107
+ `instanceId`, so connected clients notice the restart and re-check the connection before sending
108
+ more commands.
@@ -0,0 +1,304 @@
1
+ //#region ../kernel/src/redaction.ts
2
+ const SENSITIVE_KEY_RE = /(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key|user[_-]?code|device[_-]?code|refresh[_-]?credential)/i;
3
+ const SECRET_TOKEN_RE = /\b(?:bearer\s+[a-z0-9._-]+|adc_(?:agent|live|refresh|cli)_[a-z0-9._-]+)\b/gi;
4
+ const SENSITIVE_ASSIGNMENT_RE = /\b([a-z0-9_-]*(?:api[_-]?key|token|secret|password|user[_-]?code|device[_-]?code|refresh[_-]?credential)[a-z0-9_-]*)(\s*[=:]\s*)("[^"]*"|'[^']*'|\S+)/gi;
5
+ const URL_RE = /https?:\/\/[^\s"'<>]+/gi;
6
+ const REDACTED_STRING_MAX_LENGTH = 400;
7
+ const REDACTED_STDERR_MAX_LENGTH = 8192;
8
+ const TRUNCATION_SUFFIX = "...<truncated>";
9
+ function redactDiagnosticData(input) {
10
+ return redactValue(input, /* @__PURE__ */ new WeakSet());
11
+ }
12
+ /** Sanitizes an untrusted structured cause before it crosses a process or client boundary. */
13
+ function sanitizeErrorCause(cause) {
14
+ if (!cause || typeof cause !== "object") return void 0;
15
+ const candidate = cause;
16
+ if (typeof candidate.message !== "string" || candidate.message.length === 0) return void 0;
17
+ return redactDiagnosticData({
18
+ message: candidate.message,
19
+ ...typeof candidate.code === "string" && candidate.code.length > 0 ? { code: candidate.code } : {}
20
+ });
21
+ }
22
+ function redactValue(value, seen, keyHint) {
23
+ if (value === null || value === void 0) return value;
24
+ if (typeof value === "string") return redactString(value, keyHint);
25
+ if (typeof value !== "object") return value;
26
+ if (seen.has(value)) return "[Circular]";
27
+ seen.add(value);
28
+ if (Array.isArray(value)) return value.map((entry) => redactValue(entry, seen));
29
+ const output = {};
30
+ for (const [key, entry] of Object.entries(value)) {
31
+ if (SENSITIVE_KEY_RE.test(key)) {
32
+ output[key] = "[REDACTED]";
33
+ continue;
34
+ }
35
+ output[key] = redactValue(entry, seen, key);
36
+ }
37
+ return output;
38
+ }
39
+ function redactString(value, keyHint) {
40
+ const trimmed = value.trim();
41
+ if (!trimmed) return boundRedactedString(value, keyHint);
42
+ if (keyHint && SENSITIVE_KEY_RE.test(keyHint)) return "[REDACTED]";
43
+ let output = redactUrls(trimmed);
44
+ output = output.replace(SECRET_TOKEN_RE, "[REDACTED]");
45
+ output = output.replace(SENSITIVE_ASSIGNMENT_RE, (match, key, separator, rawValue, offset, input) => {
46
+ if (isSafeSetupUrlAssignment({
47
+ key,
48
+ separator,
49
+ rawValue,
50
+ offset,
51
+ input
52
+ })) return match;
53
+ if (isDocumentedTokenPlaceholder(rawValue)) return match;
54
+ return `${key}${separator}[REDACTED]`;
55
+ });
56
+ return boundRedactedString(output, keyHint);
57
+ }
58
+ function boundRedactedString(value, keyHint) {
59
+ if (keyHint === "stderr") {
60
+ if (value.length <= REDACTED_STDERR_MAX_LENGTH) return value;
61
+ const marker = `\n${TRUNCATION_SUFFIX}\n`;
62
+ const retainedLength = REDACTED_STDERR_MAX_LENGTH - marker.length;
63
+ const headLength = Math.ceil(retainedLength / 2);
64
+ return `${value.slice(0, headLength)}${marker}${value.slice(-(retainedLength - headLength))}`;
65
+ }
66
+ if (value.length <= REDACTED_STRING_MAX_LENGTH) return value;
67
+ return `${value.slice(0, 386)}${TRUNCATION_SUFFIX}`;
68
+ }
69
+ function redactUrls(value) {
70
+ return value.replace(URL_RE, (url) => redactUrl(url) ?? url);
71
+ }
72
+ function isDocumentedTokenPlaceholder(value) {
73
+ return /^adc_(?:agent|live|refresh|cli)_\.\.\.$/i.test(value);
74
+ }
75
+ function isSafeSetupUrlAssignment(options) {
76
+ if (options.key.toLowerCase() !== "token") return false;
77
+ if (!options.separator.includes(":")) return false;
78
+ try {
79
+ if (new URL(options.rawValue).pathname.replace(/\/+$/, "") !== "/api-keys") return false;
80
+ return /(?:^|\b)(?:service\/)?api\s+$/i.test(options.input.slice(0, options.offset));
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+ function redactUrl(value) {
86
+ try {
87
+ const parsed = new URL(value);
88
+ if (parsed.search) parsed.search = "?REDACTED";
89
+ if (parsed.username || parsed.password) {
90
+ parsed.username = "REDACTED";
91
+ parsed.password = "REDACTED";
92
+ }
93
+ return parsed.toString();
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+ //#endregion
99
+ //#region ../kernel/src/errors.ts
100
+ /**
101
+ * The known error codes as a value, so gates can enumerate them: every code
102
+ * here must resolve a hint through `defaultHintForCode`, and every code
103
+ * `retriableForErrorCode` classifies must have a recovery quiz in the help
104
+ * benchmark (scripts/__tests__/help-conformance-error-recovery-coverage.test.ts).
105
+ * `KnownAppErrorCode` is derived from this array, so a new code cannot be added
106
+ * to the type without entering the enumeration.
107
+ */
108
+ const KNOWN_APP_ERROR_CODES = [
109
+ "INVALID_ARGS",
110
+ "DEVICE_NOT_FOUND",
111
+ "DEVICE_IN_USE",
112
+ "TOOL_MISSING",
113
+ "APP_NOT_INSTALLED",
114
+ "UNSUPPORTED_PLATFORM",
115
+ "UNSUPPORTED_OPERATION",
116
+ "NOT_IMPLEMENTED",
117
+ "COMMAND_FAILED",
118
+ "SESSION_NOT_FOUND",
119
+ "UNAUTHORIZED",
120
+ "AMBIGUOUS_MATCH",
121
+ "REPLAY_DIVERGENCE",
122
+ "REPAIR_SESSION_EXPIRED",
123
+ "REPAIR_COMMIT_FAILED",
124
+ "UNKNOWN"
125
+ ];
126
+ var AppError = class extends Error {
127
+ code;
128
+ details;
129
+ cause;
130
+ constructor(code, message, details, cause) {
131
+ super(message);
132
+ this.code = code;
133
+ this.details = details;
134
+ this.cause = cause;
135
+ }
136
+ };
137
+ /**
138
+ * `details.reason` of a request its requester abandoned — an explicit cancel or
139
+ * a client disconnect. One definition, so every layer that must let a
140
+ * cancellation through untouched (retry loops, provider adapters, runner
141
+ * transports) dispatches on the same typed reason.
142
+ */
143
+ const REQUEST_CANCELED_REASON = "request_canceled";
144
+ const REQUEST_CANCELED_MESSAGE = "request canceled";
145
+ const REQUEST_CANCELED_HINT = "The request was canceled intentionally (explicit cancel or client disconnect) — no retry is needed unless the cancellation was unintended.";
146
+ /**
147
+ * The canceled-request error. `details` may add evidence (what was released,
148
+ * which command was interrupted) or override the hint; the reason itself is
149
+ * not overridable, so a caller cannot build one this predicate misses.
150
+ */
151
+ function createRequestCanceledError(details, cause) {
152
+ return new AppError("COMMAND_FAILED", REQUEST_CANCELED_MESSAGE, {
153
+ hint: REQUEST_CANCELED_HINT,
154
+ ...details,
155
+ reason: REQUEST_CANCELED_REASON
156
+ }, cause);
157
+ }
158
+ function asAppError(err, fallbackCode = "UNKNOWN") {
159
+ if (err instanceof AppError) return err;
160
+ if (err instanceof Error) return new AppError(fallbackCode, err.message, void 0, err);
161
+ return new AppError(fallbackCode, "Unknown error", { err });
162
+ }
163
+ function normalizeError(err, context = {}) {
164
+ const appErr = asAppError(err);
165
+ const details = appErr.details ? redactDiagnosticData(appErr.details) : void 0;
166
+ const diagnosticId = stringDetail(details, "diagnosticId") ?? context.diagnosticId;
167
+ const logPath = stringDetail(details, "logPath") ?? context.logPath;
168
+ const logPathUnavailable = stringDetail(details, "logPathUnavailable");
169
+ const diagnosticsRecord = readDiagnosticsRecordRef(details?.diagnosticsRecord) ?? context.diagnosticsRecord;
170
+ const hint = stringDetail(details, "hint") ?? defaultHintForCode(appErr.code);
171
+ const retriable = booleanDetail(details, "retriable") ?? retriableForErrorCode(appErr.code);
172
+ const supportedOn = stringDetail(details, "supportedOn");
173
+ const cleanDetails = stripDiagnosticMeta(details);
174
+ const message = maybeEnrichCommandFailedMessage(appErr.code, appErr.message, details);
175
+ const cause = sanitizeErrorCause(appErr.cause);
176
+ return {
177
+ code: appErr.code,
178
+ message,
179
+ ...cause !== void 0 ? { cause } : {},
180
+ hint,
181
+ diagnosticId,
182
+ logPath,
183
+ ...logPathUnavailable !== void 0 ? { logPathUnavailable } : {},
184
+ ...diagnosticsRecord !== void 0 ? { diagnosticsRecord } : {},
185
+ ...retriable !== void 0 ? { retriable } : {},
186
+ ...supportedOn !== void 0 ? { supportedOn } : {},
187
+ details: cleanDetails
188
+ };
189
+ }
190
+ const GENERIC_EXIT_MESSAGE = /^\S+ exited with code -?\d+$/;
191
+ function maybeEnrichCommandFailedMessage(code, message, details) {
192
+ if (code !== "COMMAND_FAILED") return message;
193
+ if (details?.processExitError !== true) return message;
194
+ const excerpt = firstStderrLine(typeof details?.stderr === "string" ? details.stderr : "");
195
+ if (!excerpt) return message;
196
+ if (GENERIC_EXIT_MESSAGE.test(message)) return excerpt;
197
+ if (message.includes(excerpt)) return message;
198
+ return `${message}: ${excerpt}`;
199
+ }
200
+ const STDERR_SKIP_PATTERNS = [
201
+ /^an error was encountered processing the command/i,
202
+ /^underlying error\b/i,
203
+ /^simulator device failed to complete the requested operation/i
204
+ ];
205
+ const STDERR_NOISE_PREFIX = /^(?:(?:adb|xcrun|simctl):\s*)?(?:error:\s*)?/i;
206
+ function firstStderrLine(stderr) {
207
+ for (const rawLine of stderr.split("\n")) {
208
+ const line = rawLine.trim();
209
+ if (!line) continue;
210
+ if (STDERR_SKIP_PATTERNS.some((pattern) => pattern.test(line))) continue;
211
+ const excerpt = line.replace(STDERR_NOISE_PREFIX, "").trim();
212
+ if (!excerpt) continue;
213
+ return excerpt.length > 200 ? `${excerpt.slice(0, 200)}...` : excerpt;
214
+ }
215
+ return null;
216
+ }
217
+ function stringDetail(details, key) {
218
+ const value = details?.[key];
219
+ return typeof value === "string" ? value : void 0;
220
+ }
221
+ /**
222
+ * Narrows an untrusted `diagnosticsRecord` — off the wire or out of a details
223
+ * bag — to the locator type, or `undefined`. One reader, so a daemon payload
224
+ * and a details bag can never be accepted on different terms.
225
+ */
226
+ function readDiagnosticsRecordRef(value) {
227
+ if (!value || typeof value !== "object") return void 0;
228
+ const { session, requestId } = value;
229
+ if (typeof session !== "string" || typeof requestId !== "string") return void 0;
230
+ if (session.length === 0 || requestId.length === 0) return void 0;
231
+ return {
232
+ session,
233
+ requestId
234
+ };
235
+ }
236
+ function booleanDetail(details, key) {
237
+ const value = details?.[key];
238
+ return typeof value === "boolean" ? value : void 0;
239
+ }
240
+ /**
241
+ * Facts a publisher leaves for a later catch in the same process, never for a caller: whether a rule
242
+ * row named this failure, and whether the host's own deadline ended the command behind it. Both
243
+ * describe our machinery rather than the caller's problem, and the caller was already handed the
244
+ * verdict those facts produced as `reason` and `hint` (#2690 review).
245
+ */
246
+ const INTERNAL_PLUMBING_DETAIL_KEYS = ["startupRuleMatched", "startupHostDeadlineHit"];
247
+ function stripDiagnosticMeta(details) {
248
+ if (!details) return void 0;
249
+ const output = { ...details };
250
+ for (const key of INTERNAL_PLUMBING_DETAIL_KEYS) delete output[key];
251
+ delete output.hint;
252
+ delete output.diagnosticId;
253
+ delete output.logPath;
254
+ delete output.logPathUnavailable;
255
+ delete output.diagnosticsRecord;
256
+ delete output.retriable;
257
+ delete output.supportedOn;
258
+ return Object.keys(output).length > 0 ? output : void 0;
259
+ }
260
+ /**
261
+ * Conservative retriability policy for the Phase 2 typed-error graft. A code is
262
+ * listed only when the verdict is clear (a retry can succeed without the caller
263
+ * changing anything, or it definitely cannot); unlisted codes stay `undefined`
264
+ * so the error wire shape is unchanged unless we have a confident answer.
265
+ *
266
+ * Keyed by `KnownAppErrorCode`, so a verdict cannot be given to a code the
267
+ * registry does not enumerate — that would classify a code as retriable while
268
+ * escaping the gate that requires a recovery quiz for it.
269
+ */
270
+ const RETRIABILITY_BY_CODE = { DEVICE_IN_USE: true };
271
+ function isKnownAppErrorCode(code) {
272
+ return KNOWN_APP_ERROR_CODES.includes(code);
273
+ }
274
+ /**
275
+ * The retriability verdict for a code, or `undefined` when there is none —
276
+ * including for every daemon/runner-originated code outside
277
+ * `KNOWN_APP_ERROR_CODES`, which cannot carry a verdict at all.
278
+ */
279
+ function retriableForErrorCode(code) {
280
+ return isKnownAppErrorCode(code) ? RETRIABILITY_BY_CODE[code] : void 0;
281
+ }
282
+ function defaultHintForCode(code) {
283
+ switch (code) {
284
+ case "INVALID_ARGS": return "Check command arguments and run --help for usage examples.";
285
+ case "SESSION_NOT_FOUND": return "Run open first or pass an explicit device selector.";
286
+ case "TOOL_MISSING": return "Install required platform tooling and ensure it is available in PATH.";
287
+ case "DEVICE_NOT_FOUND": return "Verify the target device is booted/connected and selectors match.";
288
+ case "APP_NOT_INSTALLED": return "Run apps to discover the exact installed package or bundle id, or install the app before open.";
289
+ case "UNSUPPORTED_OPERATION": return "This command is not available for the selected platform/device.";
290
+ case "UNSUPPORTED_PLATFORM": return "This platform is not supported for the requested operation; run devices to inspect available targets.";
291
+ case "AMBIGUOUS_MATCH": return "Multiple candidates matched. Narrow the query or pass an exact identifier.";
292
+ case "DEVICE_IN_USE": return "The device is busy with another agent-device request; retry once it frees up.";
293
+ case "REPLAY_DIVERGENCE": return "Read details.divergence (screen/suggestions) for repair context, or rerun with --json for the full report.";
294
+ case "REPAIR_SESSION_EXPIRED": return "The --save-script repair session was reaped before it was finalized; re-run replay <script> --save-script from the start.";
295
+ case "REPAIR_COMMIT_FAILED": return "The repair transaction completed, but committing its healed script failed at teardown (no-clobber refusal or a filesystem error); inspect the target path/permissions, then re-run replay <script> --save-script to retry.";
296
+ case "NOT_IMPLEMENTED": return "This command is part of the planned API but is not implemented yet.";
297
+ case "COMMAND_FAILED": return "Retry with --debug and inspect diagnostics log for details.";
298
+ case "UNAUTHORIZED": return "Refresh daemon metadata and retry the command.";
299
+ case "UNKNOWN": return "Unexpected internal error. Retry with --debug and report the diagnostics log if it persists.";
300
+ default: return "Retry with --debug and inspect diagnostics log for details.";
301
+ }
302
+ }
303
+ //#endregion
304
+ export { redactDiagnosticData as i, createRequestCanceledError as n, normalizeError as r, AppError as t };
@@ -0,0 +1,36 @@
1
+ import http from "node:http";
2
+ //#region src/daemon-proxy.d.ts
3
+ /**
4
+ * Carries one request to the upstream daemon and resolves with its response. The default is the
5
+ * global `fetch`; supply your own to reach the daemon over another transport, such as a
6
+ * WebSocket tunnel. Abort the exchange when `request.signal` aborts.
7
+ */
8
+ type DaemonProxyUpstreamFetch = (request: Request) => Promise<Response>;
9
+ type DaemonProxyOptions = {
10
+ /** Base URL of the upstream agent-device daemon HTTP server. */
11
+ upstreamBaseUrl: string;
12
+ /** Auth token of the upstream daemon. Never leaves the proxy. */
13
+ upstreamToken: string;
14
+ /** Token proxy clients must present as their daemon auth token. */
15
+ clientToken: string;
16
+ maxRpcBodyBytes?: number;
17
+ upstreamTimeoutMs?: number;
18
+ upstreamFetch?: DaemonProxyUpstreamFetch;
19
+ };
20
+ type DaemonProxy = {
21
+ /** Identifies this proxy instance in its health payload, so clients notice a restart. */
22
+ readonly instanceId: string;
23
+ /**
24
+ * Answers one client request. `request.url` must be the URL the client used, because upload
25
+ * tickets are rewritten to its origin; an `x-forwarded-proto` header overrides its scheme.
26
+ * Abort `request.signal` when the client goes away so in-flight daemon work is cancelled.
27
+ * Resolves with an error response rather than rejecting.
28
+ */
29
+ handle(request: Request): Promise<Response>;
30
+ };
31
+ declare function createDaemonProxy(options: DaemonProxyOptions): DaemonProxy;
32
+ declare function createDaemonProxyServer(options: DaemonProxyOptions): http.Server;
33
+ /** Serves a proxy from any `node:http` or `node:https` server; upload tickets follow its scheme. */
34
+ declare function createDaemonProxyRequestListener(proxy: DaemonProxy): http.RequestListener;
35
+ //#endregion
36
+ export { type DaemonProxy, type DaemonProxyOptions, type DaemonProxyUpstreamFetch, createDaemonProxy, createDaemonProxyRequestListener, createDaemonProxyServer };