@danceroutine/tango-core 0.1.0 → 1.0.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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -0
  3. package/dist/{TangoError-NPkVPfuH.js → TangoError-DdQVQNZU.js} +5 -1
  4. package/dist/TangoError-DdQVQNZU.js.map +1 -0
  5. package/dist/errors/AuthenticationError.d.ts +6 -1
  6. package/dist/errors/ConflictError.d.ts +6 -1
  7. package/dist/errors/NotFoundError.d.ts +6 -1
  8. package/dist/errors/PermissionDenied.d.ts +6 -1
  9. package/dist/errors/TangoError.d.ts +13 -1
  10. package/dist/errors/ValidationError.d.ts +5 -1
  11. package/dist/errors/factories/HttpErrorFactory.d.ts +20 -11
  12. package/dist/errors/factories/index.d.ts +1 -1
  13. package/dist/errors/index.d.ts +2 -2
  14. package/dist/errors/index.js +4 -4
  15. package/dist/{errors-CzSQXdgI.js → errors-_tWsmNyZ.js} +81 -34
  16. package/dist/errors-_tWsmNyZ.js.map +1 -0
  17. package/dist/http/TangoBody.d.ts +43 -18
  18. package/dist/http/TangoHeaders.d.ts +10 -5
  19. package/dist/http/TangoQueryParams.d.ts +69 -0
  20. package/dist/http/TangoRequest.d.ts +82 -0
  21. package/dist/http/TangoResponse.d.ts +184 -49
  22. package/dist/http/index.d.ts +2 -1
  23. package/dist/http/index.js +4 -4
  24. package/dist/{http-DOLwwAYt.js → http-D20MQa6p.js} +675 -295
  25. package/dist/http-D20MQa6p.js.map +1 -0
  26. package/dist/index.d.ts +9 -7
  27. package/dist/index.js +7 -6
  28. package/dist/logging/ConsoleLogger.d.ts +15 -0
  29. package/dist/logging/Logger.d.ts +13 -0
  30. package/dist/logging/getLogger.d.ts +23 -0
  31. package/dist/logging/index.d.ts +3 -0
  32. package/dist/logging/index.js +3 -0
  33. package/dist/logging-BWeD4HOO.js +48 -0
  34. package/dist/logging-BWeD4HOO.js.map +1 -0
  35. package/dist/runtime/binary/isArrayBuffer.d.ts +3 -0
  36. package/dist/runtime/binary/isBlob.d.ts +3 -0
  37. package/dist/runtime/binary/isUint8Array.d.ts +3 -0
  38. package/dist/runtime/date/isDate.d.ts +3 -0
  39. package/dist/runtime/error/isError.d.ts +3 -0
  40. package/dist/runtime/index.d.ts +1 -1
  41. package/dist/runtime/index.js +2 -2
  42. package/dist/runtime/object/index.d.ts +1 -0
  43. package/dist/runtime/object/isNil.d.ts +4 -0
  44. package/dist/runtime/object/isObject.d.ts +3 -0
  45. package/dist/runtime/web/isFile.d.ts +3 -0
  46. package/dist/runtime/web/isFormData.d.ts +3 -0
  47. package/dist/runtime/web/isReadableStream.d.ts +3 -0
  48. package/dist/runtime/web/isURLSearchParams.d.ts +3 -0
  49. package/dist/{runtime-DPpCYEe_.js → runtime-B8KkgD3R.js} +14 -4
  50. package/dist/runtime-B8KkgD3R.js.map +1 -0
  51. package/dist/sql/SqlDialect.d.ts +5 -0
  52. package/dist/sql/SqlIdentifierRole.d.ts +13 -0
  53. package/dist/sql/SqlSafetyEngine.d.ts +50 -0
  54. package/dist/sql/TrustedSqlFragment.d.ts +5 -0
  55. package/dist/sql/ValidatedSqlIdentifier.d.ts +7 -0
  56. package/dist/sql/index.d.ts +13 -0
  57. package/dist/sql/index.js +3 -0
  58. package/dist/sql/isTrustedSqlFragment.d.ts +5 -0
  59. package/dist/sql/quoteSqlIdentifier.d.ts +6 -0
  60. package/dist/sql/trustedSql.d.ts +5 -0
  61. package/dist/sql/validateSqlIdentifier.d.ts +6 -0
  62. package/dist/sql-D3frkfy-.js +116 -0
  63. package/dist/sql-D3frkfy-.js.map +1 -0
  64. package/package.json +65 -54
  65. package/dist/TangoError-NPkVPfuH.js.map +0 -1
  66. package/dist/errors/factories/HttpErrorFactory.js +0 -91
  67. package/dist/errors-CzSQXdgI.js.map +0 -1
  68. package/dist/http/TangoHeaders.js +0 -396
  69. package/dist/http/TangoResponse.js +0 -556
  70. package/dist/http-DOLwwAYt.js.map +0 -1
  71. package/dist/result/Err.d.ts +0 -21
  72. package/dist/result/Ok.d.ts +0 -21
  73. package/dist/result/Result.d.ts +0 -33
  74. package/dist/result/index.d.ts +0 -8
  75. package/dist/result/index.js +0 -3
  76. package/dist/result-CBqw9Hlg.js +0 -82
  77. package/dist/result-CBqw9Hlg.js.map +0 -1
  78. package/dist/runtime-DPpCYEe_.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { __export } from "./chunk-BkvOhyD0.js";
2
- import { TangoError } from "./TangoError-NPkVPfuH.js";
3
- import { isArrayBuffer, isBlob, isFile, isFormData, isReadableStream, isURLSearchParams, isUint8Array } from "./runtime-DPpCYEe_.js";
2
+ import { TangoError } from "./TangoError-DdQVQNZU.js";
3
+ import { isArrayBuffer, isBlob, isFile, isFormData, isNil, isReadableStream, isURLSearchParams, isUint8Array } from "./runtime-B8KkgD3R.js";
4
4
 
5
5
  //#region src/http/TangoBody.ts
6
6
  var TangoBody = class TangoBody {
@@ -14,18 +14,30 @@ var TangoBody = class TangoBody {
14
14
  this.bodyUsedInternal = false;
15
15
  this.headers = headers;
16
16
  }
17
+ /**
18
+ * Narrow an unknown value to `TangoBody`.
19
+ */
17
20
  static isTangoBody(value) {
18
21
  return typeof value === "object" && value !== null && value.__tangoBrand === TangoBody.BRAND;
19
22
  }
23
+ /**
24
+ * Expose the original body source for cloning and adapter integration.
25
+ */
20
26
  get bodySource() {
21
27
  return this.bodySourceInternal;
22
28
  }
29
+ /**
30
+ * Report whether a reader method has already consumed this body.
31
+ */
23
32
  get bodyUsed() {
24
33
  return this.bodyUsedInternal;
25
34
  }
35
+ /**
36
+ * Describe the current body shape in a way that is useful for diagnostics.
37
+ */
26
38
  get bodyType() {
27
39
  const body = this.bodySourceInternal;
28
- if (body == null) return "null";
40
+ if (isNil(body)) return "null";
29
41
  if (typeof body === "string") return "string";
30
42
  if (isArrayBuffer(body)) return "ArrayBuffer";
31
43
  if (isUint8Array(body)) return "Uint8Array";
@@ -46,6 +58,97 @@ var TangoBody = class TangoBody {
46
58
  return false;
47
59
  }
48
60
  /**
61
+ * Deep clone utility for body values. Preserves types and values as in the legacy implementation.
62
+ * If the source is a stream, the stream will be cloned if readable, otherwise throws.
63
+ */
64
+ static deepCloneBody(body) {
65
+ if (isNil(body)) return null;
66
+ if (typeof body === "string") return body;
67
+ if (isArrayBuffer(body)) return body.slice(0);
68
+ if (isUint8Array(body)) return new Uint8Array(body);
69
+ if (isBlob(body)) return body.slice(0, body.size, body.type);
70
+ if (isFormData(body)) {
71
+ const cloned = new FormData();
72
+ for (const [k, v] of body) if (isFile(v)) {
73
+ const file = new File([v], v.name, {
74
+ type: v.type,
75
+ lastModified: v.lastModified
76
+ });
77
+ cloned.append(k, file);
78
+ } else cloned.append(k, v);
79
+ return cloned;
80
+ }
81
+ if (isReadableStream(body)) throw new TypeError("Cannot deep clone a ReadableStream directly; use TangoBody.clone()");
82
+ if (TangoBody.isJsonValue(body)) return JSON.parse(JSON.stringify(body));
83
+ return null;
84
+ }
85
+ /**
86
+ * Read a `ReadableStream` into an `ArrayBuffer`.
87
+ */
88
+ static async readStreamToArrayBuffer(stream) {
89
+ const chunks = [];
90
+ const reader = stream.getReader();
91
+ let total = 0;
92
+ while (true) {
93
+ const { done, value } = await reader.read();
94
+ if (done) break;
95
+ chunks.push(value);
96
+ total += value.length;
97
+ }
98
+ const joined = new Uint8Array(total);
99
+ let off = 0;
100
+ for (const chunk of chunks) {
101
+ joined.set(chunk, off);
102
+ off += chunk.length;
103
+ }
104
+ return joined.buffer.slice(0, joined.byteLength);
105
+ }
106
+ /**
107
+ * Read a `ReadableStream` into a `Uint8Array`.
108
+ */
109
+ static async readStreamToUint8Array(stream) {
110
+ const buf = await TangoBody.readStreamToArrayBuffer(stream);
111
+ return new Uint8Array(buf);
112
+ }
113
+ /**
114
+ * Read a `ReadableStream` into UTF-8 text.
115
+ */
116
+ static async readStreamToText(stream) {
117
+ const arr = await TangoBody.readStreamToUint8Array(stream);
118
+ return new TextDecoder().decode(arr);
119
+ }
120
+ /**
121
+ * Determine the content type for this body, if possible.
122
+ * Respects an explicitly passed header, otherwise infers from value type.
123
+ */
124
+ static detectContentType(body, providedType) {
125
+ if (providedType) return providedType;
126
+ if (isNil(body)) return undefined;
127
+ if (typeof body === "string") return "text/plain; charset=utf-8";
128
+ if (isArrayBuffer(body) || isUint8Array(body)) return "application/octet-stream";
129
+ if (isBlob(body)) {
130
+ if (body.type) return body.type;
131
+ return "application/octet-stream";
132
+ }
133
+ if (isFormData(body)) return undefined;
134
+ if (typeof body === "object" && TangoBody.isJsonValue(body)) return "application/json; charset=utf-8";
135
+ if (isReadableStream(body)) return undefined;
136
+ return undefined;
137
+ }
138
+ /**
139
+ * Attempt to determine the content length, in bytes, for this body.
140
+ * Only available for certain body types, otherwise returns undefined.
141
+ */
142
+ static async getContentLength(body) {
143
+ if (isNil(body)) return 0;
144
+ if (typeof body === "string") return new TextEncoder().encode(body).length;
145
+ if (isUint8Array(body)) return body.byteLength;
146
+ if (isArrayBuffer(body)) return body.byteLength;
147
+ if (isBlob(body)) return body.size;
148
+ if (TangoBody.isJsonValue(body)) return new TextEncoder().encode(JSON.stringify(body)).length;
149
+ return undefined;
150
+ }
151
+ /**
49
152
  * Reads the body as an ArrayBuffer.
50
153
  */
51
154
  async arrayBuffer() {
@@ -126,13 +229,12 @@ else throw new TypeError("Body is not valid for FormData");
126
229
  for (const part of parts) {
127
230
  const trimmed = part.trim();
128
231
  if (!trimmed || trimmed === "--" || trimmed === "") continue;
129
- const [rawHeaders, ...rawBodyParts] = trimmed.split(/\r?\n\r?\n/);
232
+ const [rawHeaders = "", ...rawBodyParts] = trimmed.split(/\r?\n\r?\n/);
130
233
  const body = rawBodyParts.join("\n\n").replace(/\r?\n$/, "");
131
- if (!rawHeaders) continue;
132
234
  const dispositionMatch = /Content-Disposition:\s*form-data;\s*name="([^"]+)"/i.exec(rawHeaders);
133
235
  if (!dispositionMatch) continue;
134
236
  const name = dispositionMatch[1];
135
- if (typeof name === "string") form.append(name, body);
237
+ form.append(name, body);
136
238
  }
137
239
  return form;
138
240
  }
@@ -178,31 +280,6 @@ else throw new TypeError("Body is not valid for FormData");
178
280
  return this.bodySourceInternal;
179
281
  }
180
282
  /**
181
- * Deep clone utility for body values. Preserves types and values as in the legacy implementation.
182
- * If the source is a stream, the stream will be cloned if readable, otherwise throws.
183
- */
184
- static deepCloneBody(body) {
185
- if (body == null || typeof body === "undefined") return null;
186
- if (typeof body === "string") return body;
187
- if (isArrayBuffer(body)) return body.slice(0);
188
- if (isUint8Array(body)) return new Uint8Array(body);
189
- if (isBlob(body)) return body.slice(0, body.size, body.type);
190
- if (isFormData(body)) {
191
- const cloned = new FormData();
192
- for (const [k, v] of body) if (isFile(v)) {
193
- const file = new File([v], v.name, {
194
- type: v.type,
195
- lastModified: v.lastModified
196
- });
197
- cloned.append(k, file);
198
- } else cloned.append(k, v);
199
- return cloned;
200
- }
201
- if (isReadableStream(body)) throw new TypeError("Cannot deep clone a ReadableStream directly; use TangoBody.clone()");
202
- if (TangoBody.isJsonValue(body)) return JSON.parse(JSON.stringify(body));
203
- return null;
204
- }
205
- /**
206
283
  * Clone the body instance and stream if possible.
207
284
  * If the source is a stream, the stream will be cloned if readable, otherwise throws.
208
285
  */
@@ -221,69 +298,10 @@ else throw new TypeError("Body is not valid for FormData");
221
298
  * Helper for all readers. Only allows reading once.
222
299
  */
223
300
  async consumeBody(parser) {
224
- if (this.bodyUsedInternal) return Promise.reject(new TypeError("Body has already been consumed."));
301
+ if (this.bodyUsedInternal) throw new TypeError("Body has already been consumed.");
225
302
  this.bodyUsedInternal = true;
226
303
  return parser(this.bodySourceInternal);
227
304
  }
228
- static async readStreamToArrayBuffer(stream) {
229
- const chunks = [];
230
- const reader = stream.getReader();
231
- let total = 0;
232
- while (true) {
233
- const { done, value } = await reader.read();
234
- if (done) break;
235
- if (value) {
236
- chunks.push(value);
237
- total += value.length;
238
- }
239
- }
240
- const joined = new Uint8Array(total);
241
- let off = 0;
242
- for (const chunk of chunks) {
243
- joined.set(chunk, off);
244
- off += chunk.length;
245
- }
246
- return joined.buffer.slice(0, joined.byteLength);
247
- }
248
- static async readStreamToUint8Array(stream) {
249
- const buf = await TangoBody.readStreamToArrayBuffer(stream);
250
- return new Uint8Array(buf);
251
- }
252
- static async readStreamToText(stream) {
253
- const arr = await TangoBody.readStreamToUint8Array(stream);
254
- return new TextDecoder().decode(arr);
255
- }
256
- /**
257
- * Determine the content type for this body, if possible.
258
- * Respects an explicitly passed header, otherwise infers from value type.
259
- */
260
- static detectContentType(body, providedType) {
261
- if (providedType) return providedType;
262
- if (body == null) return undefined;
263
- if (typeof body === "string") return "text/plain; charset=utf-8";
264
- if (isArrayBuffer(body) || isUint8Array(body)) return "application/octet-stream";
265
- if (isBlob(body)) {
266
- if (body.type) return body.type;
267
- return "application/octet-stream";
268
- }
269
- if (isFormData(body)) return undefined;
270
- if (typeof body === "object" && TangoBody.isJsonValue(body)) return "application/json; charset=utf-8";
271
- if (isReadableStream(body)) return undefined;
272
- return undefined;
273
- }
274
- /**
275
- * Attempt to determine the content length, in bytes, for this body.
276
- * Only available for certain body types, otherwise returns undefined.
277
- */
278
- static async getContentLength(body) {
279
- if (body == null) return 0;
280
- if (typeof body === "string") return new TextEncoder().encode(body).length;
281
- if (isUint8Array(body)) return body.byteLength;
282
- if (isArrayBuffer(body)) return body.byteLength;
283
- if (isBlob(body)) return body.size;
284
- if (TangoBody.isJsonValue(body)) return new TextEncoder().encode(JSON.stringify(body)).length;
285
- return undefined;
286
- }
287
305
  };
288
306
 
289
307
  //#endregion
@@ -291,13 +309,40 @@ else throw new TypeError("Body is not valid for FormData");
291
309
  var TangoHeaders = class TangoHeaders extends Headers {
292
310
  static BRAND = "tango.http.headers";
293
311
  __tangoBrand = TangoHeaders.BRAND;
294
- constructor(headers) {
295
- super(headers);
296
- }
312
+ /**
313
+ * Narrow an unknown value to `TangoHeaders`.
314
+ */
297
315
  static isTangoHeaders(value) {
298
316
  return typeof value === "object" && value !== null && value.__tangoBrand === TangoHeaders.BRAND;
299
317
  }
300
318
  /**
319
+ * Serialize a cookie for the Set-Cookie header line.
320
+ */
321
+ static serializeCookie(name, value, options = {}) {
322
+ let cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value ?? "");
323
+ if (options.domain) cookie += `; Domain=${options.domain}`;
324
+ if (options.path) cookie += `; Path=${options.path}`;
325
+ else cookie += "; Path=/";
326
+ if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
327
+ if (typeof options.maxAge === "number") cookie += `; Max-Age=${options.maxAge}`;
328
+ if (options.secure) cookie += "; Secure";
329
+ if (options.httpOnly) cookie += "; HttpOnly";
330
+ if (options.sameSite) cookie += `; SameSite=${options.sameSite}`;
331
+ if (options.priority) cookie += `; Priority=${options.priority}`;
332
+ if (options.partitioned) cookie += "; Partitioned";
333
+ return cookie;
334
+ }
335
+ static hasNumberSize(value) {
336
+ return typeof value === "object" && value !== null && typeof value.size === "number";
337
+ }
338
+ static hasNumberLength(value) {
339
+ return typeof value === "object" && value !== null && typeof value.length === "number";
340
+ }
341
+ static isNodeBuffer(value) {
342
+ const maybeBuffer = Buffer;
343
+ return typeof Buffer !== "undefined" && typeof maybeBuffer.isBuffer === "function" && maybeBuffer.isBuffer(value);
344
+ }
345
+ /**
301
346
  * Sets the Content-Disposition header with type "inline" and the specified filename.
302
347
  * This is useful to indicate that the content should be displayed inline in the browser,
303
348
  * but with a suggested filename for saving.
@@ -318,19 +363,22 @@ var TangoHeaders = class TangoHeaders extends Headers {
318
363
  const encoded = encodeURIComponent(filename);
319
364
  this.set("Content-Disposition", `attachment; filename="${encoded}"; filename*=UTF-8''${encoded}`);
320
365
  }
366
+ /**
367
+ * Create a copy that preserves all header names and values.
368
+ */
321
369
  clone() {
322
370
  const copy = new TangoHeaders();
323
371
  for (const [name, value] of this.entries()) copy.append(name, value);
324
372
  return copy;
325
373
  }
326
374
  /**
327
- * Set a header, replacing any existing value.
375
+ * Set a header, replacing the existing value.
328
376
  */
329
377
  setHeader(name, value) {
330
378
  this.set(name, value);
331
379
  }
332
380
  /**
333
- * Append a header, adding to any existing value(s) for the name.
381
+ * Append a header, adding to existing value(s) for the name.
334
382
  */
335
383
  appendHeader(name, value) {
336
384
  this.append(name, value);
@@ -400,23 +448,6 @@ var TangoHeaders = class TangoHeaders extends Headers {
400
448
  });
401
449
  }
402
450
  /**
403
- * Serialize a cookie for the Set-Cookie header line.
404
- */
405
- static serializeCookie(name, value, options = {}) {
406
- let cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value ?? "");
407
- if (options.domain) cookie += `; Domain=${options.domain}`;
408
- if (options.path) cookie += `; Path=${options.path}`;
409
- else cookie += "; Path=/";
410
- if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
411
- if (typeof options.maxAge === "number") cookie += `; Max-Age=${options.maxAge}`;
412
- if (options.secure) cookie += "; Secure";
413
- if (options.httpOnly) cookie += "; HttpOnly";
414
- if (options.sameSite) cookie += `; SameSite=${options.sameSite}`;
415
- if (options.priority) cookie += `; Priority=${options.priority}`;
416
- if (options.partitioned) cookie += "; Partitioned";
417
- return cookie;
418
- }
419
- /**
420
451
  * Add or override Cache-Control header with helpers for common policies.
421
452
  */
422
453
  cacheControl(control) {
@@ -451,7 +482,8 @@ else cookie += "; Path=/";
451
482
  setContentTypeByFile(file, filename) {
452
483
  if (this.has("Content-Type")) return;
453
484
  if (typeof file === "string" && filename) {
454
- const ext = filename.split(".").pop()?.toLowerCase() ?? "";
485
+ const dotIndex = filename.lastIndexOf(".");
486
+ const ext = dotIndex >= 0 ? filename.slice(dotIndex + 1).toLowerCase() : "";
455
487
  const map = {
456
488
  txt: "text/plain",
457
489
  text: "text/plain",
@@ -492,11 +524,11 @@ else this.set("Content-Type", "application/octet-stream");
492
524
  let len;
493
525
  if (typeof body === "string") len = new TextEncoder().encode(body).length;
494
526
  else if (isArrayBuffer(body)) len = body.byteLength;
527
+ else if (TangoHeaders.isNodeBuffer(body)) len = body.length;
495
528
  else if (isUint8Array(body)) len = body.byteLength;
496
529
  else if (isBlob(body)) len = body.size;
497
- else if (typeof Buffer !== "undefined" && typeof Buffer.isBuffer === "function" && Buffer.isBuffer(body)) len = body.length;
498
- else if (typeof body?.size === "number") len = body.size;
499
- else if (typeof body?.length === "number" && typeof body !== "string" && !isArrayBuffer(body) && !isUint8Array(body)) len = body.length;
530
+ else if (TangoHeaders.hasNumberSize(body)) len = body.size;
531
+ else if (TangoHeaders.hasNumberLength(body) && typeof body !== "string" && !isArrayBuffer(body) && !isUint8Array(body)) len = body.length;
500
532
  if (typeof len === "number") this.set("Content-Length", len.toString());
501
533
  }
502
534
  /**
@@ -579,112 +611,314 @@ else this.set("Server-Timing", value);
579
611
  };
580
612
 
581
613
  //#endregion
582
- //#region src/http/TangoRequest.ts
583
- var TangoRequest = class TangoRequest {
584
- static BRAND = "tango.http.request";
585
- __tangoBrand = TangoRequest.BRAND;
586
- request;
587
- bodySourceValue;
588
- constructor(input, init = {}) {
589
- const sourceRequest = typeof input === "string" ? undefined : input;
590
- const method = (init.method ?? sourceRequest?.method ?? "GET").toUpperCase();
591
- const headers = new Headers(init.headers ?? sourceRequest?.headers);
592
- const normalizedBody = this.normalizeBody(init.body, headers, method);
593
- const requestInit = {
594
- method,
595
- headers,
596
- redirect: init.redirect ?? sourceRequest?.redirect,
597
- cache: init.cache ?? sourceRequest?.cache,
598
- credentials: init.credentials ?? sourceRequest?.credentials,
599
- integrity: init.integrity ?? sourceRequest?.integrity,
600
- keepalive: init.keepalive ?? sourceRequest?.keepalive,
601
- mode: init.mode ?? sourceRequest?.mode,
602
- referrer: init.referrer ?? sourceRequest?.referrer,
603
- referrerPolicy: init.referrerPolicy ?? sourceRequest?.referrerPolicy,
604
- signal: init.signal ?? sourceRequest?.signal
605
- };
606
- if (normalizedBody !== undefined) requestInit.body = normalizedBody;
607
- this.request = new Request(input, requestInit);
608
- this.bodySourceValue = normalizedBody ?? null;
609
- }
610
- static isTangoRequest(value) {
611
- return typeof value === "object" && value !== null && value.__tangoBrand === TangoRequest.BRAND;
614
+ //#region src/http/TangoQueryParams.ts
615
+ var TangoQueryParams = class TangoQueryParams {
616
+ static BRAND = "tango.http.query_params";
617
+ __tangoBrand = TangoQueryParams.BRAND;
618
+ values;
619
+ constructor(values) {
620
+ this.values = values;
612
621
  }
613
- get cache() {
614
- return this.request.cache;
622
+ /**
623
+ * Narrow an unknown value to `TangoQueryParams`.
624
+ */
625
+ static isTangoQueryParams(value) {
626
+ return typeof value === "object" && value !== null && value.__tangoBrand === TangoQueryParams.BRAND;
615
627
  }
616
- get credentials() {
617
- return this.request.credentials;
628
+ /**
629
+ * Build query params from a `URLSearchParams` instance.
630
+ */
631
+ static fromURLSearchParams(params) {
632
+ const values = new Map();
633
+ for (const [key, value] of params.entries()) {
634
+ const current = values.get(key);
635
+ if (current) {
636
+ current.push(value);
637
+ continue;
638
+ }
639
+ values.set(key, [value]);
640
+ }
641
+ return new TangoQueryParams(values);
618
642
  }
619
- get destination() {
620
- return this.request.destination;
643
+ /**
644
+ * Build query params from framework record-style search params.
645
+ */
646
+ static fromRecord(params) {
647
+ const values = new Map();
648
+ for (const [key, value] of Object.entries(params)) {
649
+ if (Array.isArray(value)) {
650
+ const normalized = value.filter((entry) => typeof entry === "string");
651
+ if (normalized.length > 0) values.set(key, normalized);
652
+ continue;
653
+ }
654
+ if (typeof value === "string") values.set(key, [value]);
655
+ }
656
+ return new TangoQueryParams(values);
621
657
  }
658
+ /**
659
+ * Build query params from a full URL string or URL object.
660
+ */
661
+ static fromURL(input) {
662
+ const url = typeof input === "string" ? new URL(input) : input;
663
+ return TangoQueryParams.fromURLSearchParams(url.searchParams);
664
+ }
665
+ /**
666
+ * Build query params from a request-like object with a URL.
667
+ */
668
+ static fromRequest(request) {
669
+ return TangoQueryParams.fromURL(request.url);
670
+ }
671
+ /**
672
+ * Get the first value for a query param.
673
+ */
674
+ get(name) {
675
+ return this.values.get(name)?.[0];
676
+ }
677
+ /**
678
+ * Get all values for a query param.
679
+ */
680
+ getAll(name) {
681
+ return [...this.values.get(name) ?? []];
682
+ }
683
+ /**
684
+ * Check whether a query param exists.
685
+ */
686
+ has(name) {
687
+ return (this.values.get(name)?.length ?? 0) > 0;
688
+ }
689
+ /**
690
+ * Iterate key -> values entries.
691
+ */
692
+ *entries() {
693
+ for (const [key, values] of this.values.entries()) yield [key, [...values]];
694
+ }
695
+ /**
696
+ * Iterate keys present in the query params.
697
+ */
698
+ *keys() {
699
+ yield* this.values.keys();
700
+ }
701
+ /**
702
+ * Convert back to a native `URLSearchParams` object.
703
+ */
704
+ toURLSearchParams() {
705
+ const params = new URLSearchParams();
706
+ for (const [key, values] of this.values.entries()) for (const value of values) params.append(key, value);
707
+ return params;
708
+ }
709
+ /**
710
+ * Get a trimmed value, omitting blank strings.
711
+ */
712
+ getTrimmed(name) {
713
+ const value = this.get(name)?.trim();
714
+ return value ? value : undefined;
715
+ }
716
+ /**
717
+ * Get the free-text search param using Tango's default key.
718
+ */
719
+ getSearch(key = "search") {
720
+ return this.getTrimmed(key);
721
+ }
722
+ /**
723
+ * Get the ordering param as trimmed field tokens.
724
+ */
725
+ getOrdering(key = "ordering") {
726
+ const value = this.get(key);
727
+ if (!value) return [];
728
+ return value.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
729
+ }
730
+ };
731
+
732
+ //#endregion
733
+ //#region src/http/TangoRequest.ts
734
+ var TangoRequest = class TangoRequest {
735
+ static BRAND = "tango.http.request";
736
+ __tangoBrand = TangoRequest.BRAND;
737
+ request;
738
+ bodySourceValue;
739
+ queryParamsValue;
740
+ constructor(input, init = {}) {
741
+ const sourceRequest = typeof input === "string" ? undefined : input;
742
+ const method = (init.method ?? sourceRequest?.method ?? "GET").toUpperCase();
743
+ const headers = new Headers(init.headers ?? sourceRequest?.headers);
744
+ const normalizedBody = this.normalizeBody(init.body, headers, method);
745
+ const requestInit = {
746
+ method,
747
+ headers,
748
+ redirect: init.redirect ?? sourceRequest?.redirect,
749
+ cache: init.cache ?? sourceRequest?.cache,
750
+ credentials: init.credentials ?? sourceRequest?.credentials,
751
+ integrity: init.integrity ?? sourceRequest?.integrity,
752
+ keepalive: init.keepalive ?? sourceRequest?.keepalive,
753
+ mode: init.mode ?? sourceRequest?.mode,
754
+ referrer: init.referrer ?? sourceRequest?.referrer,
755
+ referrerPolicy: init.referrerPolicy ?? sourceRequest?.referrerPolicy,
756
+ signal: init.signal ?? sourceRequest?.signal
757
+ };
758
+ if (normalizedBody !== undefined) {
759
+ requestInit.body = normalizedBody;
760
+ if (isReadableStream(normalizedBody)) requestInit.duplex = "half";
761
+ }
762
+ this.request = new Request(input, requestInit);
763
+ this.bodySourceValue = normalizedBody ?? null;
764
+ }
765
+ /**
766
+ * Narrow an unknown value to `TangoRequest`.
767
+ */
768
+ static isTangoRequest(value) {
769
+ return typeof value === "object" && value !== null && value.__tangoBrand === TangoRequest.BRAND;
770
+ }
771
+ /**
772
+ * Expose the request cache mode from the underlying fetch request.
773
+ */
774
+ get cache() {
775
+ return this.request.cache;
776
+ }
777
+ /**
778
+ * Expose the request credentials mode from the underlying fetch request.
779
+ */
780
+ get credentials() {
781
+ return this.request.credentials;
782
+ }
783
+ /**
784
+ * Expose the request destination from the underlying fetch request.
785
+ */
786
+ get destination() {
787
+ return this.request.destination;
788
+ }
789
+ /**
790
+ * Expose the request headers from the underlying fetch request.
791
+ */
622
792
  get headers() {
623
793
  return this.request.headers;
624
794
  }
795
+ /**
796
+ * Expose the request integrity value from the underlying fetch request.
797
+ */
625
798
  get integrity() {
626
799
  return this.request.integrity;
627
800
  }
801
+ /**
802
+ * Expose the request keepalive flag from the underlying fetch request.
803
+ */
628
804
  get keepalive() {
629
805
  return this.request.keepalive;
630
806
  }
807
+ /**
808
+ * Expose the normalized HTTP method.
809
+ */
631
810
  get method() {
632
811
  return this.request.method;
633
812
  }
813
+ /**
814
+ * Expose the request mode from the underlying fetch request.
815
+ */
634
816
  get mode() {
635
817
  return this.request.mode;
636
818
  }
819
+ /**
820
+ * Expose the redirect policy from the underlying fetch request.
821
+ */
637
822
  get redirect() {
638
823
  return this.request.redirect;
639
824
  }
825
+ /**
826
+ * Expose the referrer from the underlying fetch request.
827
+ */
640
828
  get referrer() {
641
829
  return this.request.referrer;
642
830
  }
831
+ /**
832
+ * Expose the referrer policy from the underlying fetch request.
833
+ */
643
834
  get referrerPolicy() {
644
835
  return this.request.referrerPolicy;
645
836
  }
837
+ /**
838
+ * Expose the abort signal from the underlying fetch request.
839
+ */
646
840
  get signal() {
647
841
  return this.request.signal;
648
842
  }
843
+ /**
844
+ * Expose the absolute request URL.
845
+ */
649
846
  get url() {
650
847
  return this.request.url;
651
848
  }
849
+ /**
850
+ * Expose the readable request body stream when one exists.
851
+ */
652
852
  get body() {
653
853
  return this.request.body;
654
854
  }
855
+ /**
856
+ * Report whether the body has been consumed.
857
+ */
655
858
  get bodyUsed() {
656
859
  return this.request.bodyUsed;
657
860
  }
861
+ /**
862
+ * Expose the pre-normalized body value used to build the request.
863
+ */
658
864
  get bodySource() {
659
865
  return this.bodySourceValue;
660
866
  }
867
+ /**
868
+ * Expose normalized query parameters derived from the request URL.
869
+ */
870
+ get queryParams() {
871
+ this.queryParamsValue ??= TangoQueryParams.fromURL(this.request.url);
872
+ return this.queryParamsValue;
873
+ }
874
+ /**
875
+ * Read the request body as an array buffer.
876
+ */
661
877
  async arrayBuffer() {
662
878
  return this.request.arrayBuffer();
663
879
  }
880
+ /**
881
+ * Read the request body as a blob.
882
+ */
664
883
  async blob() {
665
884
  return this.request.blob();
666
885
  }
886
+ /**
887
+ * Read the request body as bytes, including runtimes without `Request.bytes()`.
888
+ */
667
889
  async bytes() {
668
890
  const requestWithBytes = this.request;
669
891
  if (typeof requestWithBytes.bytes === "function") return requestWithBytes.bytes();
670
892
  const buffer = await this.request.arrayBuffer();
671
893
  return new Uint8Array(buffer);
672
894
  }
895
+ /**
896
+ * Read the request body as form data.
897
+ */
673
898
  async formData() {
674
899
  return this.request.formData();
675
900
  }
901
+ /**
902
+ * Parse the request body as JSON.
903
+ */
676
904
  async json() {
677
905
  return this.request.json();
678
906
  }
907
+ /**
908
+ * Read the request body as text.
909
+ */
679
910
  async text() {
680
911
  return this.request.text();
681
912
  }
913
+ /**
914
+ * Clone the request so downstream code can consume it independently.
915
+ */
682
916
  clone() {
683
917
  return new TangoRequest(this.request.clone());
684
918
  }
685
919
  normalizeBody(body, headers, method) {
686
920
  if (method === "GET" || method === "HEAD") return undefined;
687
- if (body == null) return undefined;
921
+ if (isNil(body)) return undefined;
688
922
  if (typeof body === "string") return body;
689
923
  if (isArrayBuffer(body) || isUint8Array(body) || isBlob(body)) return body;
690
924
  if (isURLSearchParams(body) || isFormData(body) || isReadableStream(body)) return body;
@@ -699,8 +933,6 @@ var TangoRequest = class TangoRequest {
699
933
  var TangoResponse = class TangoResponse {
700
934
  static BRAND = "tango.http.response";
701
935
  __tangoBrand = TangoResponse.BRAND;
702
- tangoBody;
703
- okValue;
704
936
  headers;
705
937
  redirected;
706
938
  status;
@@ -708,12 +940,8 @@ var TangoResponse = class TangoResponse {
708
940
  type;
709
941
  url;
710
942
  body;
711
- static isTangoResponse(value) {
712
- return typeof value === "object" && value !== null && value.__tangoBrand === TangoResponse.BRAND;
713
- }
714
- get bodySource() {
715
- return this.tangoBody.bodySource;
716
- }
943
+ tangoBody;
944
+ okValue;
717
945
  constructor(init = {}) {
718
946
  this.headers = new TangoHeaders(init.headers);
719
947
  this.redirected = Boolean(init.redirected);
@@ -725,103 +953,15 @@ var TangoResponse = class TangoResponse {
725
953
  this.tangoBody = new TangoBody(init.body ?? null, this.headers);
726
954
  this.body = isReadableStream(this.tangoBody.bodySource) ? this.tangoBody.bodySource : null;
727
955
  }
728
- get ok() {
729
- if (typeof this.okValue === "boolean") return this.okValue;
730
- return this.status >= 200 && this.status < 300;
731
- }
732
- get bodyUsed() {
733
- return this.tangoBody.bodyUsed;
734
- }
735
- setHeader(name, value) {
736
- this.headers.set(name, value);
737
- }
738
- appendHeader(name, value) {
739
- this.headers.append(name, value);
740
- }
741
- getHeader(name) {
742
- return this.headers.get(name);
743
- }
744
- hasHeader(name) {
745
- return this.headers.has(name);
746
- }
747
- deleteHeader(name) {
748
- this.headers.delete(name);
749
- }
750
- vary(...fields) {
751
- this.headers.vary(...fields);
752
- }
753
- setCookie(name, value, options) {
754
- this.headers.setCookie(name, value, options);
755
- }
756
- appendCookie(name, value, options) {
757
- this.headers.appendCookie(name, value, options);
758
- }
759
- deleteCookie(name, options) {
760
- this.headers.deleteCookie(name, options);
761
- }
762
- cacheControl(control) {
763
- this.headers.cacheControl(control);
764
- }
765
- location(url) {
766
- this.headers.location(url);
767
- }
768
- contentType(mime) {
769
- this.headers.contentType(mime);
770
- }
771
- /**
772
- * Set the X-Request-Id header (request correlation).
773
- * Returns this for fluent chaining.
774
- */
775
- withRequestId(requestId) {
776
- if (requestId != null && typeof requestId === "string" && requestId !== "") this.headers.set("X-Request-Id", requestId);
777
- return this;
778
- }
779
- /**
780
- * Set the traceparent header (W3C Trace Context propagation).
781
- * Returns this for fluent chaining.
782
- */
783
- withTraceparent(traceparent) {
784
- if (traceparent != null && typeof traceparent === "string" && traceparent !== "") this.headers.set("traceparent", traceparent);
785
- return this;
786
- }
787
956
  /**
788
- * Set the Server-Timing header.
789
- * Accepts a string or array of timing metrics.
790
- * Returns this for fluent chaining.
957
+ * Narrow an unknown value to `TangoResponse`.
791
958
  */
792
- withServerTiming(timing) {
793
- if (Array.isArray(timing)) this.headers.set("Server-Timing", timing.join(", "));
794
- else if (typeof timing === "string") this.headers.set("Server-Timing", timing);
795
- return this;
796
- }
797
- /**
798
- * Set the X-Response-Time header (in ms).
799
- * Numeric or formatted string (e.g. "76ms").
800
- * Returns this for fluent chaining.
801
- */
802
- withResponseTime(time) {
803
- if (typeof time === "number") this.headers.set("X-Response-Time", `${time}ms`);
804
- else if (typeof time === "string") this.headers.set("X-Response-Time", time);
805
- return this;
959
+ static isTangoResponse(value) {
960
+ return typeof value === "object" && value !== null && value.__tangoBrand === TangoResponse.BRAND;
806
961
  }
807
962
  /**
808
- * Propagate common tracing/correlation headers from provided Headers, TangoHeaders, or plain object.
809
- * Known headers: x-request-id, traceparent, server-timing
810
- * Returns this for fluent chaining.
963
+ * Create a JSON response with sensible content headers.
811
964
  */
812
- propagateTraceHeaders(input) {
813
- const incoming = new TangoHeaders(input);
814
- const traceHeaderNames = [
815
- "x-request-id",
816
- "traceparent",
817
- "server-timing"
818
- ];
819
- for (const name of traceHeaderNames) {
820
- const value = incoming.get(name);
821
- if (value != null) this.headers.set(name, value);
822
- }
823
- return this;
824
- }
825
965
  static json(data, init) {
826
966
  const headers = new TangoHeaders(init?.headers);
827
967
  if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json; charset=utf-8");
@@ -833,6 +973,9 @@ else if (typeof time === "string") this.headers.set("X-Response-Time", time);
833
973
  headers
834
974
  });
835
975
  }
976
+ /**
977
+ * Create a plain-text response with sensible content headers.
978
+ */
836
979
  static text(text, init) {
837
980
  const headers = new TangoHeaders(init?.headers);
838
981
  if (!headers.has("Content-Type")) headers.set("Content-Type", "text/plain; charset=utf-8");
@@ -843,6 +986,9 @@ else if (typeof time === "string") this.headers.set("X-Response-Time", time);
843
986
  headers
844
987
  });
845
988
  }
989
+ /**
990
+ * Create an HTML response with sensible content headers.
991
+ */
846
992
  static html(html, init) {
847
993
  const headers = new TangoHeaders(init?.headers);
848
994
  if (!headers.has("Content-Type")) headers.set("Content-Type", "text/html; charset=utf-8");
@@ -853,6 +999,9 @@ else if (typeof time === "string") this.headers.set("X-Response-Time", time);
853
999
  headers
854
1000
  });
855
1001
  }
1002
+ /**
1003
+ * Create a streaming response without buffering the payload in memory.
1004
+ */
856
1005
  static stream(stream, init) {
857
1006
  return new TangoResponse({
858
1007
  ...init,
@@ -860,6 +1009,9 @@ else if (typeof time === "string") this.headers.set("X-Response-Time", time);
860
1009
  headers: new TangoHeaders(init?.headers)
861
1010
  });
862
1011
  }
1012
+ /**
1013
+ * Create a redirect response and set the `Location` header.
1014
+ */
863
1015
  static redirect(url, status = 302, init) {
864
1016
  const headers = new TangoHeaders(init?.headers);
865
1017
  headers.set("Location", url);
@@ -872,6 +1024,9 @@ else if (typeof time === "string") this.headers.set("X-Response-Time", time);
872
1024
  url
873
1025
  });
874
1026
  }
1027
+ /**
1028
+ * Create an empty `204 No Content` response.
1029
+ */
875
1030
  static noContent(init) {
876
1031
  const headers = new TangoHeaders(init?.headers);
877
1032
  return new TangoResponse({
@@ -881,6 +1036,9 @@ else if (typeof time === "string") this.headers.set("X-Response-Time", time);
881
1036
  headers
882
1037
  });
883
1038
  }
1039
+ /**
1040
+ * Create a `201 Created` response and optionally attach a location or body.
1041
+ */
884
1042
  static created(location, body, init) {
885
1043
  const headers = new TangoHeaders(init?.headers);
886
1044
  if (location) headers.set("Location", location);
@@ -890,8 +1048,6 @@ else if (typeof time === "string") this.headers.set("X-Response-Time", time);
890
1048
  if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json; charset=utf-8");
891
1049
  }
892
1050
  if (typeof respBody === "string" && !headers.has("Content-Length")) headers.set("Content-Length", new TextEncoder().encode(respBody).length.toString());
893
- else if (isArrayBuffer(respBody) && !headers.has("Content-Length")) headers.set("Content-Length", respBody.byteLength.toString());
894
- else if (isBlob(respBody) && !headers.has("Content-Length")) headers.set("Content-Length", respBody.size.toString());
895
1051
  return new TangoResponse({
896
1052
  ...init,
897
1053
  body: respBody,
@@ -899,11 +1055,26 @@ else if (isBlob(respBody) && !headers.has("Content-Length")) headers.set("Conten
899
1055
  headers
900
1056
  });
901
1057
  }
1058
+ /**
1059
+ * Create a `405 Method Not Allowed` response and optionally populate `Allow`.
1060
+ */
1061
+ static methodNotAllowed(allow, detail = "Method not allowed.", init) {
1062
+ const headers = new TangoHeaders(init?.headers);
1063
+ if (allow && allow.length > 0) headers.set("Allow", allow.join(", "));
1064
+ return TangoResponse.json({ error: detail }, {
1065
+ ...init,
1066
+ status: 405,
1067
+ headers
1068
+ });
1069
+ }
1070
+ /**
1071
+ * Normalize a Tango error or problem-details object into an error response.
1072
+ */
902
1073
  static error(error, init) {
903
1074
  let code;
904
1075
  let message;
905
- let details = undefined;
906
- let fields = undefined;
1076
+ let details;
1077
+ let fields;
907
1078
  let status = init?.status ?? 500;
908
1079
  if (TangoError.isTangoError(error)) {
909
1080
  const envelope = error.toErrorEnvelope();
@@ -928,6 +1099,9 @@ else if (isBlob(respBody) && !headers.has("Content-Length")) headers.set("Conten
928
1099
  status
929
1100
  });
930
1101
  }
1102
+ /**
1103
+ * Create a `400 Bad Request` response from a string or structured error.
1104
+ */
931
1105
  static badRequest(detail, init) {
932
1106
  if (TangoError.isTangoError(detail) || TangoError.isProblemDetails(detail)) return TangoResponse.error(detail, {
933
1107
  ...init,
@@ -948,6 +1122,9 @@ else if (isBlob(respBody) && !headers.has("Content-Length")) headers.set("Conten
948
1122
  status: 400
949
1123
  });
950
1124
  }
1125
+ /**
1126
+ * Create a `401 Unauthorized` response from a string or structured error.
1127
+ */
951
1128
  static unauthorized(detail, init) {
952
1129
  if (TangoError.isTangoError(detail) || TangoError.isProblemDetails(detail)) return TangoResponse.error(detail, {
953
1130
  ...init,
@@ -968,6 +1145,9 @@ else if (isBlob(respBody) && !headers.has("Content-Length")) headers.set("Conten
968
1145
  status: 401
969
1146
  });
970
1147
  }
1148
+ /**
1149
+ * Create a `403 Forbidden` response from a string or structured error.
1150
+ */
971
1151
  static forbidden(detail, init) {
972
1152
  if (TangoError.isTangoError(detail) || TangoError.isProblemDetails(detail)) return TangoResponse.error(detail, {
973
1153
  ...init,
@@ -988,6 +1168,9 @@ else if (isBlob(respBody) && !headers.has("Content-Length")) headers.set("Conten
988
1168
  status: 403
989
1169
  });
990
1170
  }
1171
+ /**
1172
+ * Create a `404 Not Found` response from a string or structured error.
1173
+ */
991
1174
  static notFound(detail, init) {
992
1175
  if (TangoError.isTangoError(detail) || TangoError.isProblemDetails(detail)) return TangoResponse.error(detail, {
993
1176
  ...init,
@@ -1008,6 +1191,9 @@ else if (isBlob(respBody) && !headers.has("Content-Length")) headers.set("Conten
1008
1191
  status: 404
1009
1192
  });
1010
1193
  }
1194
+ /**
1195
+ * Create a `409 Conflict` response from a string or structured error.
1196
+ */
1011
1197
  static conflict(detail, init) {
1012
1198
  if (TangoError.isTangoError(detail) || TangoError.isProblemDetails(detail)) return TangoResponse.error(detail, {
1013
1199
  ...init,
@@ -1028,6 +1214,9 @@ else if (isBlob(respBody) && !headers.has("Content-Length")) headers.set("Conten
1028
1214
  status: 409
1029
1215
  });
1030
1216
  }
1217
+ /**
1218
+ * Create a `422 Unprocessable Entity` response from a string or structured error.
1219
+ */
1031
1220
  static unprocessableEntity(detail, init) {
1032
1221
  if (TangoError.isTangoError(detail) || TangoError.isProblemDetails(detail)) return TangoResponse.error(detail, {
1033
1222
  ...init,
@@ -1048,6 +1237,9 @@ else if (isBlob(respBody) && !headers.has("Content-Length")) headers.set("Conten
1048
1237
  status: 422
1049
1238
  });
1050
1239
  }
1240
+ /**
1241
+ * Create a `429 Too Many Requests` response from a string or structured error.
1242
+ */
1051
1243
  static tooManyRequests(detail, init) {
1052
1244
  if (TangoError.isTangoError(detail) || TangoError.isProblemDetails(detail)) return TangoResponse.error(detail, {
1053
1245
  ...init,
@@ -1068,6 +1260,9 @@ else if (isBlob(respBody) && !headers.has("Content-Length")) headers.set("Conten
1068
1260
  status: 429
1069
1261
  });
1070
1262
  }
1263
+ /**
1264
+ * Create a problem-details style error response with Tango's envelope shape.
1265
+ */
1071
1266
  static problem(problem, init) {
1072
1267
  let status = init?.status ?? 500;
1073
1268
  const headers = new TangoHeaders(init?.headers);
@@ -1091,15 +1286,13 @@ else if (isBlob(respBody) && !headers.has("Content-Length")) headers.set("Conten
1091
1286
  } else if (typeof problem === "string") message = problem;
1092
1287
  else if (problem && typeof problem === "object") {
1093
1288
  const extracted = problem;
1094
- if (typeof extracted.code === "string") code = extracted.code;
1095
- if (typeof extracted.message === "string") message = extracted.message;
1096
1289
  details = extracted.details;
1097
1290
  fields = extracted.fields;
1098
1291
  }
1099
1292
  const envelope = { error: {
1100
1293
  code,
1101
1294
  message,
1102
- ...details !== undefined ? { details } : {},
1295
+ ...details === undefined ? {} : { details },
1103
1296
  ...fields ? { fields } : {}
1104
1297
  } };
1105
1298
  const body = JSON.stringify(envelope);
@@ -1111,6 +1304,189 @@ else if (problem && typeof problem === "object") {
1111
1304
  body
1112
1305
  });
1113
1306
  }
1307
+ /**
1308
+ * Returns a response for serving a file.
1309
+ */
1310
+ static file(file, opts) {
1311
+ const headers = new TangoHeaders(opts?.init?.headers ?? {});
1312
+ if (opts?.filename) headers.setContentDispositionInline(opts.filename);
1313
+ if (opts?.contentType && !headers.has("Content-Type")) headers.set("Content-Type", opts.contentType);
1314
+ else if (!headers.has("Content-Type")) headers.setContentTypeByFile(file, opts?.filename);
1315
+ if (!headers.has("Content-Length")) headers.setContentLengthFromBody(file);
1316
+ return new TangoResponse({
1317
+ ...opts?.init,
1318
+ body: file,
1319
+ headers
1320
+ });
1321
+ }
1322
+ /**
1323
+ * Returns a response that prompts the user to download the file.
1324
+ */
1325
+ static download(file, opts) {
1326
+ const headers = new TangoHeaders(opts?.init?.headers ?? {});
1327
+ if (opts?.filename) headers.setContentDispositionAttachment(opts.filename);
1328
+ else headers.set("Content-Disposition", "attachment");
1329
+ if (opts?.contentType && !headers.has("Content-Type")) headers.set("Content-Type", opts.contentType);
1330
+ else if (!headers.has("Content-Type")) headers.setContentTypeByFile(file, opts?.filename);
1331
+ if (!headers.has("Content-Length")) headers.setContentLengthFromBody(file);
1332
+ return new TangoResponse({
1333
+ ...opts?.init,
1334
+ body: file,
1335
+ headers
1336
+ });
1337
+ }
1338
+ static normalizeWebBody(body) {
1339
+ if (isNil(body) || typeof body === "string" || isBlob(body) || isFormData(body) || isArrayBuffer(body) || isUint8Array(body) || isURLSearchParams(body) || isReadableStream(body)) return body;
1340
+ return JSON.stringify(body);
1341
+ }
1342
+ /**
1343
+ * Expose the original body source for cloning and adapter integration.
1344
+ */
1345
+ get bodySource() {
1346
+ return this.tangoBody.bodySource;
1347
+ }
1348
+ /**
1349
+ * Report whether the status code falls inside the 2xx range.
1350
+ */
1351
+ get ok() {
1352
+ if (typeof this.okValue === "boolean") return this.okValue;
1353
+ return this.status >= 200 && this.status < 300;
1354
+ }
1355
+ /**
1356
+ * Report whether the body has been consumed.
1357
+ */
1358
+ get bodyUsed() {
1359
+ return this.tangoBody.bodyUsed;
1360
+ }
1361
+ /**
1362
+ * Replace a header value on the response.
1363
+ */
1364
+ setHeader(name, value) {
1365
+ this.headers.set(name, value);
1366
+ }
1367
+ /**
1368
+ * Append another value for a repeated response header.
1369
+ */
1370
+ appendHeader(name, value) {
1371
+ this.headers.append(name, value);
1372
+ }
1373
+ /**
1374
+ * Read a response header value.
1375
+ */
1376
+ getHeader(name) {
1377
+ return this.headers.get(name);
1378
+ }
1379
+ /**
1380
+ * Check whether a response header is present.
1381
+ */
1382
+ hasHeader(name) {
1383
+ return this.headers.has(name);
1384
+ }
1385
+ /**
1386
+ * Remove a response header.
1387
+ */
1388
+ deleteHeader(name) {
1389
+ this.headers.delete(name);
1390
+ }
1391
+ /**
1392
+ * Merge one or more values into the `Vary` header.
1393
+ */
1394
+ vary(...fields) {
1395
+ this.headers.vary(...fields);
1396
+ }
1397
+ /**
1398
+ * Add a `Set-Cookie` header that replaces prior application intent.
1399
+ */
1400
+ setCookie(name, value, options) {
1401
+ this.headers.setCookie(name, value, options);
1402
+ }
1403
+ /**
1404
+ * Append another `Set-Cookie` header.
1405
+ */
1406
+ appendCookie(name, value, options) {
1407
+ this.headers.appendCookie(name, value, options);
1408
+ }
1409
+ /**
1410
+ * Expire a cookie by issuing a matching deletion cookie header.
1411
+ */
1412
+ deleteCookie(name, options) {
1413
+ this.headers.deleteCookie(name, options);
1414
+ }
1415
+ /**
1416
+ * Set the `Cache-Control` header through Tango's higher-level helper.
1417
+ */
1418
+ cacheControl(control) {
1419
+ this.headers.cacheControl(control);
1420
+ }
1421
+ /**
1422
+ * Set the `Location` header on the response.
1423
+ */
1424
+ location(url) {
1425
+ this.headers.location(url);
1426
+ }
1427
+ /**
1428
+ * Set the response content type.
1429
+ */
1430
+ contentType(mime) {
1431
+ this.headers.contentType(mime);
1432
+ }
1433
+ /**
1434
+ * Set the X-Request-Id header (request correlation).
1435
+ * Returns this for fluent chaining.
1436
+ */
1437
+ withRequestId(requestId) {
1438
+ if (!isNil(requestId) && typeof requestId === "string" && requestId !== "") this.headers.set("X-Request-Id", requestId);
1439
+ return this;
1440
+ }
1441
+ /**
1442
+ * Set the traceparent header (W3C Trace Context propagation).
1443
+ * Returns this for fluent chaining.
1444
+ */
1445
+ withTraceparent(traceparent) {
1446
+ if (!isNil(traceparent) && typeof traceparent === "string" && traceparent !== "") this.headers.set("traceparent", traceparent);
1447
+ return this;
1448
+ }
1449
+ /**
1450
+ * Set the Server-Timing header.
1451
+ * Accepts a string or array of timing metrics.
1452
+ * Returns this for fluent chaining.
1453
+ */
1454
+ withServerTiming(timing) {
1455
+ if (Array.isArray(timing)) this.headers.set("Server-Timing", timing.join(", "));
1456
+ else if (typeof timing === "string") this.headers.set("Server-Timing", timing);
1457
+ return this;
1458
+ }
1459
+ /**
1460
+ * Set the X-Response-Time header (in ms).
1461
+ * Numeric or formatted string (e.g. "76ms").
1462
+ * Returns this for fluent chaining.
1463
+ */
1464
+ withResponseTime(time) {
1465
+ if (typeof time === "number") this.headers.set("X-Response-Time", `${time}ms`);
1466
+ else if (typeof time === "string") this.headers.set("X-Response-Time", time);
1467
+ return this;
1468
+ }
1469
+ /**
1470
+ * Propagate common tracing/correlation headers from provided Headers, TangoHeaders, or plain object.
1471
+ * Known headers: x-request-id, traceparent, server-timing
1472
+ * Returns this for fluent chaining.
1473
+ */
1474
+ propagateTraceHeaders(input) {
1475
+ const incoming = new TangoHeaders(input);
1476
+ const traceHeaderNames = [
1477
+ "x-request-id",
1478
+ "traceparent",
1479
+ "server-timing"
1480
+ ];
1481
+ for (const name of traceHeaderNames) {
1482
+ const value = incoming.get(name);
1483
+ if (!isNil(value)) this.headers.set(name, value);
1484
+ }
1485
+ return this;
1486
+ }
1487
+ /**
1488
+ * Clone the response so its body can be consumed independently.
1489
+ */
1114
1490
  clone() {
1115
1491
  if (this.bodyUsed) throw new TypeError("Body has already been used");
1116
1492
  const clonedBody = this.tangoBody.clone();
@@ -1125,54 +1501,57 @@ else if (problem && typeof problem === "object") {
1125
1501
  url: this.url
1126
1502
  });
1127
1503
  }
1504
+ /**
1505
+ * Convert this Tango-owned response into a native web `Response`.
1506
+ *
1507
+ * Adapters use this at the host-framework boundary so Tango can standardize
1508
+ * on `TangoResponse` internally while Next.js and other hosts still receive
1509
+ * a platform-native response object.
1510
+ */
1511
+ toWebResponse() {
1512
+ const responseForTransfer = !this.bodyUsed && isReadableStream(this.bodySource) ? this.clone() : this;
1513
+ const body = TangoResponse.normalizeWebBody(responseForTransfer.bodySource);
1514
+ return new Response(body, {
1515
+ headers: new Headers(responseForTransfer.headers),
1516
+ status: responseForTransfer.status,
1517
+ statusText: responseForTransfer.statusText
1518
+ });
1519
+ }
1520
+ /**
1521
+ * Read the response body as an array buffer.
1522
+ */
1128
1523
  async arrayBuffer() {
1129
1524
  return this.tangoBody.arrayBuffer();
1130
1525
  }
1526
+ /**
1527
+ * Read the response body as a blob.
1528
+ */
1131
1529
  async blob() {
1132
1530
  return this.tangoBody.blob();
1133
1531
  }
1532
+ /**
1533
+ * Read the response body as bytes.
1534
+ */
1134
1535
  async bytes() {
1135
1536
  return this.tangoBody.bytes();
1136
1537
  }
1538
+ /**
1539
+ * Read the response body as form data.
1540
+ */
1137
1541
  async formData() {
1138
1542
  return this.tangoBody.formData();
1139
1543
  }
1140
- async json() {
1141
- return this.tangoBody.json();
1142
- }
1143
- async text() {
1144
- return this.tangoBody.text();
1145
- }
1146
1544
  /**
1147
- * Returns a response for serving a file.
1545
+ * Parse the response body as JSON.
1148
1546
  */
1149
- static file(file, opts) {
1150
- const headers = new TangoHeaders(opts?.init?.headers ?? {});
1151
- if (opts?.filename) headers.setContentDispositionInline(opts.filename);
1152
- if (opts?.contentType && !headers.has("Content-Type")) headers.set("Content-Type", opts.contentType);
1153
- else if (!headers.has("Content-Type")) headers.setContentTypeByFile(file, opts?.filename);
1154
- if (!headers.has("Content-Length")) headers.setContentLengthFromBody(file);
1155
- return new TangoResponse({
1156
- ...opts?.init,
1157
- body: file,
1158
- headers
1159
- });
1547
+ async json() {
1548
+ return this.tangoBody.json();
1160
1549
  }
1161
1550
  /**
1162
- * Returns a response that prompts the user to download the file.
1551
+ * Read the response body as text.
1163
1552
  */
1164
- static download(file, opts) {
1165
- const headers = new TangoHeaders(opts?.init?.headers ?? {});
1166
- if (opts?.filename) headers.setContentDispositionAttachment(opts.filename);
1167
- else headers.set("Content-Disposition", "attachment");
1168
- if (opts?.contentType && !headers.has("Content-Type")) headers.set("Content-Type", opts.contentType);
1169
- else if (!headers.has("Content-Type")) headers.setContentTypeByFile(file, opts?.filename);
1170
- if (!headers.has("Content-Length")) headers.setContentLengthFromBody(file);
1171
- return new TangoResponse({
1172
- ...opts?.init,
1173
- body: file,
1174
- headers
1175
- });
1553
+ async text() {
1554
+ return this.tangoBody.text();
1176
1555
  }
1177
1556
  /**
1178
1557
  * Returns a plain object debug representation of this response: { status, headers: { ... }, bodyType: ... }.
@@ -1182,7 +1561,7 @@ else if (!headers.has("Content-Type")) headers.setContentTypeByFile(file, opts?.
1182
1561
  return {
1183
1562
  status: this.status,
1184
1563
  headers: Object.fromEntries(this.headers.entries()),
1185
- bodyType: this.tangoBody ? this.tangoBody.bodyType : typeof this.bodySource
1564
+ bodyType: this.tangoBody.bodyType
1186
1565
  };
1187
1566
  }
1188
1567
  /**
@@ -1209,10 +1588,11 @@ var http_exports = {};
1209
1588
  __export(http_exports, {
1210
1589
  TangoBody: () => TangoBody,
1211
1590
  TangoHeaders: () => TangoHeaders,
1591
+ TangoQueryParams: () => TangoQueryParams,
1212
1592
  TangoRequest: () => TangoRequest,
1213
1593
  TangoResponse: () => TangoResponse
1214
1594
  });
1215
1595
 
1216
1596
  //#endregion
1217
- export { TangoBody, TangoHeaders, TangoRequest, TangoResponse, http_exports };
1218
- //# sourceMappingURL=http-DOLwwAYt.js.map
1597
+ export { TangoBody, TangoHeaders, TangoQueryParams, TangoRequest, TangoResponse, http_exports };
1598
+ //# sourceMappingURL=http-D20MQa6p.js.map