arthron 1.2.0 → 2.1.0

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.
@@ -0,0 +1,751 @@
1
+ //#region src/request/url.ts
2
+ const ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:\/\//i;
3
+ const HTTP_URL = /^https?:\/\//i;
4
+ function isAbsoluteUrl(path) {
5
+ return ABSOLUTE_URL.test(path);
6
+ }
7
+ /**
8
+ * Origin + pathname only — the query and userinfo tend to carry secrets.
9
+ * For anything printed: log lines, error messages, `ArthronError.toJSON`.
10
+ */
11
+ function redactUrl(url) {
12
+ try {
13
+ const parsed = new URL(url);
14
+ return `${parsed.origin}${parsed.pathname}`;
15
+ } catch {
16
+ return withoutSecrets(url);
17
+ }
18
+ }
19
+ /**
20
+ * `url` as written, without its query, fragment and userinfo — for a value
21
+ * that may not parse, or whose origin would print as "null" (ftp://…).
22
+ */
23
+ function withoutSecrets(url) {
24
+ return (url.split(/[?#]/)[0] ?? url).replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/\\]*@/i, "$1");
25
+ }
26
+ /**
27
+ * `text` with `url` redacted wherever it appears, as given or as the URL
28
+ * parser normalizes it — for a runtime's own message that repeats the URL.
29
+ */
30
+ function redactUrlIn(text, url) {
31
+ const shown = redactUrl(url);
32
+ let href;
33
+ try {
34
+ href = new URL(url).href;
35
+ } catch {}
36
+ let out = text.split(url).join(shown);
37
+ if (href && href !== url) out = out.split(href).join(shown);
38
+ return out;
39
+ }
40
+ /**
41
+ * `undefined` when `url` has no origin of its own. A relative baseURL takes
42
+ * the page's origin in a browser and has none elsewhere.
43
+ */
44
+ function originOf(url) {
45
+ const page = globalThis.location?.href;
46
+ try {
47
+ return new URL(url, page).origin;
48
+ } catch {
49
+ return;
50
+ }
51
+ }
52
+ /** The origin of an absolute http(s) URL; `undefined` for anything else. */
53
+ function httpOrigin(url) {
54
+ try {
55
+ const { protocol, origin } = new URL(url);
56
+ return protocol === "http:" || protocol === "https:" ? origin : void 0;
57
+ } catch {
58
+ return;
59
+ }
60
+ }
61
+ /**
62
+ * The origins of a list of http(s) URLs, for an option that names where
63
+ * requests may go. An empty list or another kind of entry is a TypeError in
64
+ * the option's own words.
65
+ */
66
+ function httpOrigins(urls, errors) {
67
+ if (urls.length === 0) throw new TypeError(errors.empty);
68
+ return new Set(urls.map((url) => {
69
+ const value = url;
70
+ if (typeof value !== "string") {
71
+ const kind = value === null ? "null" : typeof value;
72
+ const shown = kind === "null" ? kind : `${/^[aeiou]/.test(kind) ? "an" : "a"} ${kind}`;
73
+ throw new TypeError(errors.invalid(shown));
74
+ }
75
+ const origin = httpOrigin(value);
76
+ if (origin) return origin;
77
+ throw new TypeError(errors.invalid(JSON.stringify(withoutSecrets(value))));
78
+ }));
79
+ }
80
+ /** `allowedOrigins` as origins, checked once when the client is created. */
81
+ function originSet(urls) {
82
+ return httpOrigins(urls, {
83
+ empty: "arthron: allowedOrigins needs at least one URL — leave it out to stay on baseURL's origin",
84
+ invalid: (url) => `arthron: allowedOrigins needs http(s) URLs, got ${url} — e.g. allowedOrigins: ["https://cdn.example.com"]`
85
+ });
86
+ }
87
+ /** `path` is absolute and points away from `baseURL`'s origin. */
88
+ function leavesBase(baseURL, path) {
89
+ if (baseURL === void 0 || !isAbsoluteUrl(path)) return false;
90
+ const base = originOf(baseURL);
91
+ return base === void 0 || originOf(path) !== base;
92
+ }
93
+ function toQueryString(queryParams) {
94
+ if (!queryParams) return "";
95
+ if (Object.prototype.toString.call(queryParams) === "[object URLSearchParams]") return queryParams.toString();
96
+ const search = new URLSearchParams();
97
+ for (const [key, value] of Object.entries(queryParams)) for (const item of Array.isArray(value) ? value : [value]) {
98
+ if (item === null || item === void 0) continue;
99
+ if (typeof item === "object" || typeof item === "function") throw new TypeError(`arthron: queryParams.${key} is not a string, number or boolean — pass one of those or an array of them (dates: date.toISOString())`);
100
+ search.append(key, String(item));
101
+ }
102
+ return search.toString();
103
+ }
104
+ /**
105
+ * Fill `{name}` placeholders with encoded values, so each one stays inside
106
+ * its segment: "/", "?" and "#" are encoded, and the values WHATWG URL would
107
+ * turn into a different path — "", "." and ".." — are refused. A placeholder
108
+ * without a value, or a value without a placeholder, is a mistake too.
109
+ */
110
+ function fillPath(template, values) {
111
+ const used = /* @__PURE__ */ new Set();
112
+ const filled = template.replace(/\{([^{}/]+)\}/g, (_, name) => {
113
+ if (!Object.hasOwn(values, name)) throw new TypeError(`arthron: path "${template}" has {${name}} but no value for it — pass pathParams: { ${name}: … }`);
114
+ used.add(name);
115
+ const value = String(values[name]);
116
+ if (value === "" || value === "." || value === "..") throw new TypeError(`arthron: pathParams.${name}=${JSON.stringify(value)} would change the path, not fill a segment — pass a real id`);
117
+ return encodeURIComponent(value);
118
+ });
119
+ const unused = Object.keys(values).find((name) => !used.has(name));
120
+ if (unused !== void 0) throw new TypeError(`arthron: path "${template}" has no {${unused}} — add the placeholder or drop the value`);
121
+ return filled;
122
+ }
123
+ /**
124
+ * Join `baseURL` and `path` by plain string concatenation so a path prefix in
125
+ * the base (`/v1`) survives a leading slash in the path. WHATWG `new URL()`
126
+ * would drop it. An absolute path replaces the base only when it stays on the
127
+ * base's origin or goes to one in `allowed` (`allowedOrigins`) — a path built
128
+ * from user input or an API response must not carry the client's
129
+ * credentials somewhere else. Without a baseURL, `allowed` alone limits it.
130
+ */
131
+ function resolveUrl(baseURL, path, queryParams, allowed) {
132
+ let url;
133
+ if (isAbsoluteUrl(path)) {
134
+ if (!HTTP_URL.test(path)) {
135
+ const scheme = path.slice(0, path.indexOf(":") + 1);
136
+ throw new TypeError(`arthron: only http(s) URLs can be requested, got a "${scheme}" URL — use an http:// or https:// URL`);
137
+ }
138
+ const target = originOf(path);
139
+ if (target === void 0 && (baseURL !== void 0 || allowed)) throw new TypeError(`arthron: "${withoutSecrets(path)}" is not a valid URL — check it, or pass a path relative to baseURL`);
140
+ const listed = target !== void 0 && allowed?.has(target) === true;
141
+ if (!listed && leavesBase(baseURL, path)) {
142
+ const base = (baseURL && originOf(baseURL)) ?? withoutSecrets(baseURL ?? "");
143
+ throw new TypeError(`arthron: ${target} is outside baseURL ${base} — pass a path relative to baseURL, or add ${target} to arthron({ allowedOrigins }) (config headers are not sent there)`);
144
+ }
145
+ if (!listed && baseURL === void 0 && allowed) throw new TypeError(`arthron: ${target} is not in allowedOrigins — add it to arthron({ allowedOrigins }) or request a listed origin`);
146
+ url = path;
147
+ } else {
148
+ if (baseURL === void 0) throw new TypeError(`arthron: relative path "${redactUrl(path)}" needs a baseURL — pass baseURL in arthron({ baseURL }) or use an absolute URL`);
149
+ url = `${baseURL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
150
+ }
151
+ const query = toQueryString(queryParams);
152
+ if (!query) return url;
153
+ const hash = url.indexOf("#");
154
+ const [base, fragment] = hash === -1 ? [url, ""] : [url.slice(0, hash), url.slice(hash)];
155
+ return `${base}${base.includes("?") ? "&" : "?"}${query}${fragment}`;
156
+ }
157
+ //#endregion
158
+ //#region src/error/arthron-error.ts
159
+ /**
160
+ * Marks an ArthronError across copies of this package. The ESM and CJS
161
+ * builds can both end up loaded (a CJS dependency creates the client, ESM
162
+ * app code catches), each with its own class — `Symbol.for` is shared.
163
+ */
164
+ const BRAND = Symbol.for("arthron.ArthronError");
165
+ /**
166
+ * The only error arthron itself produces. Two sources: a response with
167
+ * `ok === false`, or fetch rejecting outright (network failure, `status` 0).
168
+ * Everything else — cancellation, interceptor bugs — is rethrown untouched.
169
+ */
170
+ var ArthronError = class ArthronError extends Error {
171
+ name = "ArthronError";
172
+ /** HTTP status of the failed response; `0` when fetch itself failed (network). */
173
+ status;
174
+ statusText;
175
+ /**
176
+ * The failed response's payload, parsed: JSON if it parses, else the text;
177
+ * `undefined` for a network failure, which has no response. At most 64 KiB
178
+ * is read (text if cut). `response.body`, by contrast, is the stream.
179
+ */
180
+ payload;
181
+ /**
182
+ * For people: the server's description of the failure, extracted from
183
+ * `payload` (by default its `message` → `error` → `error.message` →
184
+ * `detail`; see the `extractDescription` option). `undefined` when there was
185
+ * none, and for a network failure. Untrusted server text: show it as text,
186
+ * never as HTML. `message`, by contrast, is arthron's — method, URL
187
+ * without query, status — for logs, not for people.
188
+ */
189
+ description;
190
+ /** The underlying error for network failures, with the URL's query and userinfo taken out of its message, stack and causes. */
191
+ cause;
192
+ constructor(options) {
193
+ super(options.message);
194
+ this.status = options.status;
195
+ this.statusText = options.statusText;
196
+ this.payload = options.payload;
197
+ this.description = options.description;
198
+ this.cause = options.cause;
199
+ Object.defineProperties(this, {
200
+ headers: { value: options.headers },
201
+ requestContext: { value: options.requestContext },
202
+ response: { value: options.response },
203
+ [BRAND]: { value: true }
204
+ });
205
+ }
206
+ /** `instanceof ArthronError` holds for an error from any copy of the package. */
207
+ static [Symbol.hasInstance](value) {
208
+ return this === ArthronError ? isArthronError(value) : Function.prototype[Symbol.hasInstance].call(this, value);
209
+ }
210
+ /** What serializers see: no headers, no request body, no query or userinfo. */
211
+ toJSON() {
212
+ return {
213
+ name: this.name,
214
+ message: this.message,
215
+ description: this.description,
216
+ status: this.status,
217
+ statusText: this.statusText,
218
+ method: this.requestContext.init.method ?? "GET",
219
+ url: redactUrl(this.requestContext.url),
220
+ payload: this.payload
221
+ };
222
+ }
223
+ };
224
+ /** `true` for errors arthron produced: a `!ok` response or a network failure. Cancellations and plugin bugs are not ArthronErrors. */
225
+ function isArthronError(error) {
226
+ return typeof error === "object" && error !== null && error[BRAND] === true;
227
+ }
228
+ //#endregion
229
+ //#region src/request/body.ts
230
+ const RAW_TAGS = /* @__PURE__ */ new Set([
231
+ "FormData",
232
+ "Blob",
233
+ "File",
234
+ "ArrayBuffer",
235
+ "URLSearchParams",
236
+ "ReadableStream"
237
+ ]);
238
+ /**
239
+ * `Symbol.toStringTag`, not `instanceof`: a body from another realm or a
240
+ * polyfill (formdata-node, fetch-blob) must not be JSON-encoded into `{}`.
241
+ */
242
+ const tagOf = (value) => Object.prototype.toString.call(value).slice(8, -1);
243
+ function isReadableStream(value) {
244
+ return tagOf(value) === "ReadableStream";
245
+ }
246
+ /**
247
+ * Whether `RequestCtor` can send a ReadableStream body. Node and Chromium
248
+ * read `duplex` and leave the stream alone; Firefox ignores `duplex` and
249
+ * would send the text "[object ReadableStream]"; older Safari refuses it.
250
+ * The check from
251
+ * https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests
252
+ */
253
+ function canStreamRequestBody(RequestCtor) {
254
+ let duplexRead = false;
255
+ try {
256
+ const request = new RequestCtor("https://stream.test", {
257
+ method: "POST",
258
+ body: new ReadableStream(),
259
+ get duplex() {
260
+ duplexRead = true;
261
+ return "half";
262
+ }
263
+ });
264
+ return duplexRead && !request.headers.has("content-type");
265
+ } catch {
266
+ return false;
267
+ }
268
+ }
269
+ let runtimeStreams;
270
+ /** `canStreamRequestBody` for the global `Request`, checked once. */
271
+ function runtimeStreamsRequestBody() {
272
+ runtimeStreams ??= typeof Request !== "function" || canStreamRequestBody(Request);
273
+ return runtimeStreams;
274
+ }
275
+ /**
276
+ * What JSON.stringify would turn into `{}`, `undefined` or internal state
277
+ * instead of the data — with the fix, so it reads as an error message.
278
+ */
279
+ function mangledByJson(body) {
280
+ if (typeof body === "function" || typeof body === "symbol") return `a ${typeof body}, which JSON cannot carry — send its data instead`;
281
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return;
282
+ if (Symbol.asyncIterator in body) return "an async iterable (a Node stream?) — pass a ReadableStream (Readable.toWeb(stream)) or collect it first";
283
+ if (Symbol.iterator in body) return "an iterable such as a Map, Set or generator — convert it to an array or plain object first";
284
+ }
285
+ /** Bodies fetch already knows how to send. Content-Type is left to the caller / runtime. */
286
+ function isRawBody(body) {
287
+ if (typeof body === "string") return true;
288
+ if (ArrayBuffer.isView(body)) return true;
289
+ return RAW_TAGS.has(tagOf(body));
290
+ }
291
+ /**
292
+ * Turn a caller-supplied body into something fetch accepts. Raw bodies pass
293
+ * through untouched; anything else is JSON with `content-type` set — unless
294
+ * the caller already set one (headers arrive lowercased, so a plain lookup is
295
+ * enough).
296
+ */
297
+ function serializeBody(body, headers) {
298
+ if (body === void 0) return {
299
+ body: void 0,
300
+ headers
301
+ };
302
+ if (tagOf(body) === "FormData") {
303
+ const { "content-type": _dropped, ...rest } = headers;
304
+ return {
305
+ body,
306
+ headers: rest
307
+ };
308
+ }
309
+ if (isRawBody(body)) return {
310
+ body,
311
+ headers
312
+ };
313
+ const mangled = mangledByJson(body);
314
+ if (mangled) throw new TypeError(`arthron: the body is ${mangled}`);
315
+ const type = headers["content-type"];
316
+ if (type !== void 0 && !/\bjson\b/i.test(type)) throw new TypeError(`arthron: a ${typeof body} body is sent as JSON, but content-type is "${type}" — pass a string, URLSearchParams or FormData for that content type, or drop the header`);
317
+ const withType = "content-type" in headers ? headers : {
318
+ ...headers,
319
+ "content-type": "application/json"
320
+ };
321
+ return {
322
+ body: JSON.stringify(body),
323
+ headers: withType
324
+ };
325
+ }
326
+ //#endregion
327
+ //#region src/response/rebuild-response.ts
328
+ /**
329
+ * A new Response with `body` but everything else taken from `original`. The
330
+ * constructor resets `url`, `redirected` and `type`, so those are copied over
331
+ * as own properties — callers that resolve relative links or detect
332
+ * redirects keep working.
333
+ */
334
+ function rebuildResponse(original, body) {
335
+ const rebuilt = createResponse(body, {
336
+ status: original.status,
337
+ statusText: original.statusText,
338
+ headers: original.headers
339
+ });
340
+ const { url, redirected, type } = original;
341
+ return withFields(rebuilt, {
342
+ url,
343
+ redirected,
344
+ type
345
+ });
346
+ }
347
+ /** A status text `new Response` takes: tab, space, visible ASCII, Latin-1. */
348
+ const REASON_PHRASE = /^[\t\x20-\x7e\x80-\xff]*$/;
349
+ /**
350
+ * `new Response(body, init)` for anything fetch hands over. The constructor
351
+ * takes a status of 200–599 and a Latin-1 status text, but fetch passes on
352
+ * what a server sends (LinkedIn answers bots with 999, and Node's fetch reads
353
+ * a reason phrase as UTF-8). One it refuses is built as 599 or "" and set
354
+ * back as an own property; built as 599, an unusual status is never `ok`.
355
+ */
356
+ function createResponse(body, init) {
357
+ const { status, statusText = "" } = init;
358
+ const statusFits = status >= 200 && status <= 599;
359
+ const textFits = REASON_PHRASE.test(statusText);
360
+ if (statusFits && textFits) return new Response(body, init);
361
+ return withStatus(new Response(body, {
362
+ ...init,
363
+ status: statusFits ? status : 599,
364
+ statusText: textFits ? statusText : ""
365
+ }), status, statusText);
366
+ }
367
+ function withStatus(target, status, statusText) {
368
+ const clone = target.clone.bind(target);
369
+ return Object.defineProperties(target, {
370
+ status: { value: status },
371
+ statusText: { value: statusText },
372
+ clone: {
373
+ value: () => withStatus(clone(), status, statusText),
374
+ configurable: true
375
+ }
376
+ });
377
+ }
378
+ /** Set those fields on `target` — and on its `clone()`s as well. */
379
+ function withFields(target, fields) {
380
+ const clone = target.clone.bind(target);
381
+ return Object.defineProperties(target, {
382
+ url: { value: fields.url },
383
+ redirected: { value: fields.redirected },
384
+ type: { value: fields.type },
385
+ clone: { value: () => withFields(clone(), fields) }
386
+ });
387
+ }
388
+ //#endregion
389
+ //#region src/error/create-error.ts
390
+ /**
391
+ * Cancellation is not an HTTP failure. Matched by name so it works across
392
+ * realms — or by the request's own `signal`, since `abort(reason)` rejects
393
+ * with whatever reason the caller chose.
394
+ */
395
+ function isCancellation(error, signal) {
396
+ if (signal?.aborted) return true;
397
+ return error?.name === "AbortError" || isTimeout(error);
398
+ }
399
+ /** A deadline ran out: `timeout()`, or `AbortSignal.timeout()` as the signal. Matched by name, like isCancellation. */
400
+ function isTimeout(error) {
401
+ return error?.name === "TimeoutError";
402
+ }
403
+ /**
404
+ * What a cancelled call rejects with: its signal's reason. WebKit's fetch
405
+ * rejects, and errors a half-read body, with a generic AbortError instead,
406
+ * dropping a TimeoutError or the caller's own reason.
407
+ */
408
+ function cancellationError(error, signal) {
409
+ return signal?.aborted ? signal.reason : error;
410
+ }
411
+ const isRecord = (value) => typeof value === "object" && value !== null;
412
+ /** `message` → `error` (string) → `error.message` → `detail`. */
413
+ function defaultDescription(payload) {
414
+ if (!isRecord(payload)) return void 0;
415
+ if (typeof payload.message === "string") return payload.message;
416
+ if (typeof payload.error === "string") return payload.error;
417
+ if (isRecord(payload.error) && typeof payload.error.message === "string") return payload.error.message;
418
+ if (typeof payload.detail === "string") return payload.detail;
419
+ }
420
+ /** How much of an error body arthron reads. Error bodies are for diagnosis; a hostile or broken server must not be able to fill memory with one. */
421
+ const MAX_ERROR_BODY_BYTES = 65536;
422
+ /** Up to `limit` bytes of the body; the rest is cancelled unread. `undefined` when there is no body or reading it failed. */
423
+ async function readCapped(response, limit) {
424
+ if (!response.body) return void 0;
425
+ let reader;
426
+ const chunks = [];
427
+ let size = 0;
428
+ try {
429
+ reader = response.body.getReader();
430
+ while (size <= limit) {
431
+ const { value, done } = await reader.read();
432
+ if (done) break;
433
+ chunks.push(value.subarray(0, Math.max(0, limit - size)));
434
+ size += value.byteLength;
435
+ }
436
+ } catch {
437
+ return;
438
+ } finally {
439
+ reader?.cancel().catch(() => {});
440
+ reader?.releaseLock();
441
+ }
442
+ const bytes = new Uint8Array(Math.min(size, limit));
443
+ let offset = 0;
444
+ for (const chunk of chunks) {
445
+ bytes.set(chunk, offset);
446
+ offset += chunk.byteLength;
447
+ }
448
+ return size > limit ? {
449
+ bytes: bytes.subarray(0, limit),
450
+ truncated: true
451
+ } : {
452
+ bytes,
453
+ truncated: false
454
+ };
455
+ }
456
+ /** JSON if the whole body parses, else the text (cut at the limit), else `undefined` when empty. */
457
+ function parseBody(read) {
458
+ const text = new TextDecoder().decode(read.bytes);
459
+ if (text === "") return void 0;
460
+ if (read.truncated) return text;
461
+ try {
462
+ return JSON.parse(text);
463
+ } catch {
464
+ return text;
465
+ }
466
+ }
467
+ /** The request as error messages name it: method and URL, no query or userinfo. */
468
+ function describeRequest(requestContext) {
469
+ return `${requestContext.init.method ?? "GET"} ${redactUrl(requestContext.url)}`;
470
+ }
471
+ async function responseError(response, requestContext, extractDescription = defaultDescription) {
472
+ const read = await readCapped(response, MAX_ERROR_BODY_BYTES);
473
+ const payload = read && parseBody(read);
474
+ if (read) response = rebuildResponse(response, read.bytes);
475
+ const status = response.statusText ? `${response.status} ${response.statusText}` : String(response.status);
476
+ let description;
477
+ try {
478
+ description = extractDescription(payload, response) || void 0;
479
+ } catch {}
480
+ return new ArthronError({
481
+ message: `${describeRequest(requestContext)} failed with ${status}`,
482
+ description,
483
+ status: response.status,
484
+ statusText: response.statusText,
485
+ headers: response.headers,
486
+ payload,
487
+ requestContext,
488
+ response
489
+ });
490
+ }
491
+ /**
492
+ * Browsers that stream a request body (Chromium, current WebKit) do it only
493
+ * over HTTP/2, and fail one sent to an HTTP/1.1 server with the same bare
494
+ * "Failed to fetch" / "Load failed" as a dead server, so name the likely cause.
495
+ */
496
+ const STREAM_BODY_HINT = " — in a browser, a ReadableStream body streams only over HTTP/2; send a Blob or ArrayBuffer to an HTTP/1.1 server";
497
+ /** `error[key]`, or `undefined` if reading it throws. */
498
+ function readField(error, key) {
499
+ try {
500
+ return error[key];
501
+ } catch {
502
+ return;
503
+ }
504
+ }
505
+ /**
506
+ * An error that cannot be changed, copied with its prototype and own fields
507
+ * so the copy can be. Values are read through the original: V8's own
508
+ * `stack` accessor reads nothing on another object, and DOMException's
509
+ * `name`, `message` and `code` getters throw on one. Those become writable
510
+ * fields: stack rewriters assign to `stack`.
511
+ */
512
+ function copyError(error) {
513
+ const descriptors = Object.getOwnPropertyDescriptors(error);
514
+ for (const key of Reflect.ownKeys(descriptors)) {
515
+ const descriptor = descriptors[key];
516
+ descriptors[key] = descriptor.get || descriptor.set ? {
517
+ value: readField(error, key),
518
+ writable: true,
519
+ enumerable: descriptor.enumerable ?? false,
520
+ configurable: true
521
+ } : {
522
+ ...descriptor,
523
+ configurable: true
524
+ };
525
+ }
526
+ for (const key of [
527
+ "name",
528
+ "message",
529
+ "stack",
530
+ "code"
531
+ ]) {
532
+ if (Object.hasOwn(descriptors, key)) continue;
533
+ const value = readField(error, key);
534
+ if (value !== void 0) descriptors[key] = {
535
+ value,
536
+ writable: true,
537
+ configurable: true
538
+ };
539
+ }
540
+ return Object.create(Object.getPrototypeOf(error), descriptors);
541
+ }
542
+ /**
543
+ * Take `url`'s query and userinfo out of an error the runtime threw, in
544
+ * place when possible: its message and stack, string fields (Node's
545
+ * `input`), and its causes. An immutable error is copied with its prototype
546
+ * and fields so it cannot put the full URL back into logs via `cause`.
547
+ */
548
+ function redactErrorUrl(error, url, seen = /* @__PURE__ */ new Map()) {
549
+ if (typeof error === "string") return redactUrlIn(error, url);
550
+ if (typeof error !== "object" || error === null) return error;
551
+ if (seen.has(error)) return seen.get(error);
552
+ const fields = error;
553
+ let safe = error;
554
+ const changes = /* @__PURE__ */ new Map();
555
+ for (const key of /* @__PURE__ */ new Set([
556
+ "message",
557
+ "stack",
558
+ ...Object.keys(error)
559
+ ])) {
560
+ const value = fields[key];
561
+ if (typeof value !== "string") continue;
562
+ const redacted = redactUrlIn(value, url);
563
+ if (redacted === value) continue;
564
+ changes.set(key, redacted);
565
+ if (safe === error) try {
566
+ Object.defineProperty(error, key, { value: redacted });
567
+ } catch {
568
+ safe = copyError(error);
569
+ }
570
+ }
571
+ seen.set(error, safe);
572
+ if (safe !== error) for (const [key, value] of changes) Object.defineProperty(safe, key, {
573
+ value,
574
+ configurable: true
575
+ });
576
+ const cause = fields.cause;
577
+ const safeCause = redactErrorUrl(cause, url, seen);
578
+ if (safeCause !== cause) try {
579
+ Object.defineProperty(safe, "cause", { value: safeCause });
580
+ } catch {
581
+ safe = copyError(safe);
582
+ Object.defineProperty(safe, "cause", { value: safeCause });
583
+ seen.set(error, safe);
584
+ }
585
+ return safe;
586
+ }
587
+ function networkError(cause, requestContext) {
588
+ const safeCause = redactErrorUrl(cause, requestContext.url);
589
+ const reason = safeCause instanceof Error && safeCause.message ? safeCause.message : "network error";
590
+ const hint = isReadableStream(requestContext.init.body) ? STREAM_BODY_HINT : "";
591
+ return new ArthronError({
592
+ message: `${describeRequest(requestContext)} failed: ${reason}${hint}`,
593
+ status: 0,
594
+ statusText: "",
595
+ headers: new Headers(),
596
+ payload: void 0,
597
+ requestContext,
598
+ cause: safeCause
599
+ });
600
+ }
601
+ //#endregion
602
+ Object.defineProperty(exports, "ArthronError", {
603
+ enumerable: true,
604
+ get: function() {
605
+ return ArthronError;
606
+ }
607
+ });
608
+ Object.defineProperty(exports, "cancellationError", {
609
+ enumerable: true,
610
+ get: function() {
611
+ return cancellationError;
612
+ }
613
+ });
614
+ Object.defineProperty(exports, "createResponse", {
615
+ enumerable: true,
616
+ get: function() {
617
+ return createResponse;
618
+ }
619
+ });
620
+ Object.defineProperty(exports, "fillPath", {
621
+ enumerable: true,
622
+ get: function() {
623
+ return fillPath;
624
+ }
625
+ });
626
+ Object.defineProperty(exports, "httpOrigin", {
627
+ enumerable: true,
628
+ get: function() {
629
+ return httpOrigin;
630
+ }
631
+ });
632
+ Object.defineProperty(exports, "httpOrigins", {
633
+ enumerable: true,
634
+ get: function() {
635
+ return httpOrigins;
636
+ }
637
+ });
638
+ Object.defineProperty(exports, "isArthronError", {
639
+ enumerable: true,
640
+ get: function() {
641
+ return isArthronError;
642
+ }
643
+ });
644
+ Object.defineProperty(exports, "isCancellation", {
645
+ enumerable: true,
646
+ get: function() {
647
+ return isCancellation;
648
+ }
649
+ });
650
+ Object.defineProperty(exports, "isReadableStream", {
651
+ enumerable: true,
652
+ get: function() {
653
+ return isReadableStream;
654
+ }
655
+ });
656
+ Object.defineProperty(exports, "isTimeout", {
657
+ enumerable: true,
658
+ get: function() {
659
+ return isTimeout;
660
+ }
661
+ });
662
+ Object.defineProperty(exports, "leavesBase", {
663
+ enumerable: true,
664
+ get: function() {
665
+ return leavesBase;
666
+ }
667
+ });
668
+ Object.defineProperty(exports, "networkError", {
669
+ enumerable: true,
670
+ get: function() {
671
+ return networkError;
672
+ }
673
+ });
674
+ Object.defineProperty(exports, "originOf", {
675
+ enumerable: true,
676
+ get: function() {
677
+ return originOf;
678
+ }
679
+ });
680
+ Object.defineProperty(exports, "originSet", {
681
+ enumerable: true,
682
+ get: function() {
683
+ return originSet;
684
+ }
685
+ });
686
+ Object.defineProperty(exports, "rebuildResponse", {
687
+ enumerable: true,
688
+ get: function() {
689
+ return rebuildResponse;
690
+ }
691
+ });
692
+ Object.defineProperty(exports, "redactErrorUrl", {
693
+ enumerable: true,
694
+ get: function() {
695
+ return redactErrorUrl;
696
+ }
697
+ });
698
+ Object.defineProperty(exports, "redactUrl", {
699
+ enumerable: true,
700
+ get: function() {
701
+ return redactUrl;
702
+ }
703
+ });
704
+ Object.defineProperty(exports, "redactUrlIn", {
705
+ enumerable: true,
706
+ get: function() {
707
+ return redactUrlIn;
708
+ }
709
+ });
710
+ Object.defineProperty(exports, "resolveUrl", {
711
+ enumerable: true,
712
+ get: function() {
713
+ return resolveUrl;
714
+ }
715
+ });
716
+ Object.defineProperty(exports, "responseError", {
717
+ enumerable: true,
718
+ get: function() {
719
+ return responseError;
720
+ }
721
+ });
722
+ Object.defineProperty(exports, "runtimeStreamsRequestBody", {
723
+ enumerable: true,
724
+ get: function() {
725
+ return runtimeStreamsRequestBody;
726
+ }
727
+ });
728
+ Object.defineProperty(exports, "serializeBody", {
729
+ enumerable: true,
730
+ get: function() {
731
+ return serializeBody;
732
+ }
733
+ });
734
+ Object.defineProperty(exports, "tagOf", {
735
+ enumerable: true,
736
+ get: function() {
737
+ return tagOf;
738
+ }
739
+ });
740
+ Object.defineProperty(exports, "withFields", {
741
+ enumerable: true,
742
+ get: function() {
743
+ return withFields;
744
+ }
745
+ });
746
+ Object.defineProperty(exports, "withoutSecrets", {
747
+ enumerable: true,
748
+ get: function() {
749
+ return withoutSecrets;
750
+ }
751
+ });