nxus-qbd 0.8.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 (231) hide show
  1. package/README.md +269 -91
  2. package/dist/client.d.ts +81 -77
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +77 -71
  5. package/dist/client.js.map +1 -1
  6. package/dist/contracts.d.ts +1 -1
  7. package/dist/contracts.d.ts.map +1 -1
  8. package/dist/generated/count-capabilities.d.ts +64 -0
  9. package/dist/generated/count-capabilities.d.ts.map +1 -0
  10. package/dist/generated/count-capabilities.js +64 -0
  11. package/dist/generated/count-capabilities.js.map +1 -0
  12. package/dist/generated/index.d.ts +1 -1
  13. package/dist/generated/index.d.ts.map +1 -1
  14. package/dist/generated/index.js +1 -1
  15. package/dist/generated/index.js.map +1 -1
  16. package/dist/generated/resource-capabilities.d.ts +506 -0
  17. package/dist/generated/resource-capabilities.d.ts.map +1 -0
  18. package/dist/generated/resource-capabilities.js +506 -0
  19. package/dist/generated/resource-capabilities.js.map +1 -0
  20. package/dist/generated/types.gen.d.ts +29410 -20818
  21. package/dist/generated/types.gen.d.ts.map +1 -1
  22. package/dist/generated/types.gen.js +80 -0
  23. package/dist/generated/types.gen.js.map +1 -1
  24. package/dist/helpers/enums.d.ts +29 -0
  25. package/dist/helpers/enums.d.ts.map +1 -0
  26. package/dist/helpers/enums.js +38 -0
  27. package/dist/helpers/enums.js.map +1 -0
  28. package/dist/helpers/errors.d.ts +14 -3
  29. package/dist/helpers/errors.d.ts.map +1 -1
  30. package/dist/helpers/errors.js +274 -23
  31. package/dist/helpers/errors.js.map +1 -1
  32. package/dist/helpers/index.d.ts +4 -2
  33. package/dist/helpers/index.d.ts.map +1 -1
  34. package/dist/helpers/index.js +4 -2
  35. package/dist/helpers/index.js.map +1 -1
  36. package/dist/helpers/pagination.d.ts +11 -1
  37. package/dist/helpers/pagination.d.ts.map +1 -1
  38. package/dist/helpers/pagination.js.map +1 -1
  39. package/dist/helpers/response.d.ts +93 -0
  40. package/dist/helpers/response.d.ts.map +1 -0
  41. package/dist/helpers/response.js +84 -0
  42. package/dist/helpers/response.js.map +1 -0
  43. package/dist/index.d.ts +10 -10
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +17 -8
  46. package/dist/index.js.map +1 -1
  47. package/dist/models/core/auth_session.d.ts +1 -1
  48. package/dist/models/core/auth_session.d.ts.map +1 -1
  49. package/dist/models/core/connections.d.ts +1 -1
  50. package/dist/models/core/connections.d.ts.map +1 -1
  51. package/dist/models/core/index.d.ts +3 -3
  52. package/dist/models/core/index.d.ts.map +1 -1
  53. package/dist/models/core/index.js +3 -3
  54. package/dist/models/core/index.js.map +1 -1
  55. package/dist/models/core/qwc_auth_setup.d.ts +1 -1
  56. package/dist/models/core/qwc_auth_setup.d.ts.map +1 -1
  57. package/dist/models/index.d.ts +6 -6
  58. package/dist/models/index.d.ts.map +1 -1
  59. package/dist/models/index.js +5 -5
  60. package/dist/models/index.js.map +1 -1
  61. package/dist/models/qbd/account.d.ts +2 -2
  62. package/dist/models/qbd/account.d.ts.map +1 -1
  63. package/dist/models/qbd/account.js +1 -1
  64. package/dist/models/qbd/account.js.map +1 -1
  65. package/dist/models/qbd/account_tax_line_info.d.ts +1 -1
  66. package/dist/models/qbd/account_tax_line_info.d.ts.map +1 -1
  67. package/dist/models/qbd/ar_refund_credit_card.d.ts +1 -1
  68. package/dist/models/qbd/ar_refund_credit_card.d.ts.map +1 -1
  69. package/dist/models/qbd/bar_code.d.ts +1 -1
  70. package/dist/models/qbd/bar_code.d.ts.map +1 -1
  71. package/dist/models/qbd/bill.d.ts +1 -1
  72. package/dist/models/qbd/bill.d.ts.map +1 -1
  73. package/dist/models/qbd/bill_payment_or_credit.d.ts +1 -1
  74. package/dist/models/qbd/bill_payment_or_credit.d.ts.map +1 -1
  75. package/dist/models/qbd/billing_rate.d.ts +1 -1
  76. package/dist/models/qbd/billing_rate.d.ts.map +1 -1
  77. package/dist/models/qbd/build_assembly.d.ts +1 -1
  78. package/dist/models/qbd/build_assembly.d.ts.map +1 -1
  79. package/dist/models/qbd/charge.d.ts +1 -1
  80. package/dist/models/qbd/charge.d.ts.map +1 -1
  81. package/dist/models/qbd/check.d.ts +1 -1
  82. package/dist/models/qbd/check.d.ts.map +1 -1
  83. package/dist/models/qbd/check_bill_payment.d.ts +1 -1
  84. package/dist/models/qbd/check_bill_payment.d.ts.map +1 -1
  85. package/dist/models/qbd/class_.d.ts +1 -1
  86. package/dist/models/qbd/class_.d.ts.map +1 -1
  87. package/dist/models/qbd/credit_card_bill_payment.d.ts +1 -1
  88. package/dist/models/qbd/credit_card_bill_payment.d.ts.map +1 -1
  89. package/dist/models/qbd/credit_card_charge.d.ts +1 -1
  90. package/dist/models/qbd/credit_card_charge.d.ts.map +1 -1
  91. package/dist/models/qbd/credit_card_credit.d.ts +1 -1
  92. package/dist/models/qbd/credit_card_credit.d.ts.map +1 -1
  93. package/dist/models/qbd/credit_memo.d.ts +1 -1
  94. package/dist/models/qbd/credit_memo.d.ts.map +1 -1
  95. package/dist/models/qbd/currency.d.ts +2 -1
  96. package/dist/models/qbd/currency.d.ts.map +1 -1
  97. package/dist/models/qbd/currency.js +2 -1
  98. package/dist/models/qbd/currency.js.map +1 -1
  99. package/dist/models/qbd/custom_field_definitions.d.ts +1 -1
  100. package/dist/models/qbd/custom_field_definitions.d.ts.map +1 -1
  101. package/dist/models/qbd/custom_fields.d.ts +2 -2
  102. package/dist/models/qbd/custom_fields.d.ts.map +1 -1
  103. package/dist/models/qbd/custom_fields.js +1 -1
  104. package/dist/models/qbd/custom_fields.js.map +1 -1
  105. package/dist/models/qbd/customer.d.ts +2 -2
  106. package/dist/models/qbd/customer.d.ts.map +1 -1
  107. package/dist/models/qbd/customer.js +1 -1
  108. package/dist/models/qbd/customer.js.map +1 -1
  109. package/dist/models/qbd/customer_type.d.ts +1 -1
  110. package/dist/models/qbd/customer_type.d.ts.map +1 -1
  111. package/dist/models/qbd/date_driven_term.d.ts +1 -1
  112. package/dist/models/qbd/date_driven_term.d.ts.map +1 -1
  113. package/dist/models/qbd/deposit.d.ts +1 -1
  114. package/dist/models/qbd/deposit.d.ts.map +1 -1
  115. package/dist/models/qbd/employee.d.ts +2 -2
  116. package/dist/models/qbd/employee.d.ts.map +1 -1
  117. package/dist/models/qbd/employee.js +1 -1
  118. package/dist/models/qbd/employee.js.map +1 -1
  119. package/dist/models/qbd/estimate.d.ts +1 -1
  120. package/dist/models/qbd/estimate.d.ts.map +1 -1
  121. package/dist/models/qbd/index.d.ts +66 -66
  122. package/dist/models/qbd/index.d.ts.map +1 -1
  123. package/dist/models/qbd/index.js +66 -66
  124. package/dist/models/qbd/index.js.map +1 -1
  125. package/dist/models/qbd/inventory_adjustment.d.ts +1 -1
  126. package/dist/models/qbd/inventory_adjustment.d.ts.map +1 -1
  127. package/dist/models/qbd/inventory_site.d.ts +1 -1
  128. package/dist/models/qbd/inventory_site.d.ts.map +1 -1
  129. package/dist/models/qbd/invoice.d.ts +2 -1
  130. package/dist/models/qbd/invoice.d.ts.map +1 -1
  131. package/dist/models/qbd/invoice.js +2 -1
  132. package/dist/models/qbd/invoice.js.map +1 -1
  133. package/dist/models/qbd/item.d.ts +2 -2
  134. package/dist/models/qbd/item.d.ts.map +1 -1
  135. package/dist/models/qbd/item.js +1 -1
  136. package/dist/models/qbd/item.js.map +1 -1
  137. package/dist/models/qbd/item_discount.d.ts +1 -1
  138. package/dist/models/qbd/item_discount.d.ts.map +1 -1
  139. package/dist/models/qbd/item_fixed_asset.d.ts +1 -1
  140. package/dist/models/qbd/item_fixed_asset.d.ts.map +1 -1
  141. package/dist/models/qbd/item_group.d.ts +1 -1
  142. package/dist/models/qbd/item_group.d.ts.map +1 -1
  143. package/dist/models/qbd/item_inventory.d.ts +1 -1
  144. package/dist/models/qbd/item_inventory.d.ts.map +1 -1
  145. package/dist/models/qbd/item_inventory_assembly.d.ts +1 -1
  146. package/dist/models/qbd/item_inventory_assembly.d.ts.map +1 -1
  147. package/dist/models/qbd/item_non_inventory.d.ts +1 -1
  148. package/dist/models/qbd/item_non_inventory.d.ts.map +1 -1
  149. package/dist/models/qbd/item_other_charge.d.ts +1 -1
  150. package/dist/models/qbd/item_other_charge.d.ts.map +1 -1
  151. package/dist/models/qbd/item_payment.d.ts +1 -1
  152. package/dist/models/qbd/item_payment.d.ts.map +1 -1
  153. package/dist/models/qbd/item_receipt.d.ts +1 -1
  154. package/dist/models/qbd/item_receipt.d.ts.map +1 -1
  155. package/dist/models/qbd/item_sales_tax.d.ts +1 -1
  156. package/dist/models/qbd/item_sales_tax.d.ts.map +1 -1
  157. package/dist/models/qbd/item_sales_tax_group.d.ts +1 -1
  158. package/dist/models/qbd/item_sales_tax_group.d.ts.map +1 -1
  159. package/dist/models/qbd/item_service.d.ts +1 -1
  160. package/dist/models/qbd/item_service.d.ts.map +1 -1
  161. package/dist/models/qbd/item_subtotal.d.ts +1 -1
  162. package/dist/models/qbd/item_subtotal.d.ts.map +1 -1
  163. package/dist/models/qbd/journal_entry.d.ts +1 -1
  164. package/dist/models/qbd/journal_entry.d.ts.map +1 -1
  165. package/dist/models/qbd/other_name.d.ts +1 -1
  166. package/dist/models/qbd/other_name.d.ts.map +1 -1
  167. package/dist/models/qbd/payment_method.d.ts +1 -1
  168. package/dist/models/qbd/payment_method.d.ts.map +1 -1
  169. package/dist/models/qbd/payroll_item_non_wage.d.ts +1 -1
  170. package/dist/models/qbd/payroll_item_non_wage.d.ts.map +1 -1
  171. package/dist/models/qbd/payroll_item_wage.d.ts +1 -1
  172. package/dist/models/qbd/payroll_item_wage.d.ts.map +1 -1
  173. package/dist/models/qbd/price_level.d.ts +1 -1
  174. package/dist/models/qbd/price_level.d.ts.map +1 -1
  175. package/dist/models/qbd/purchase_order.d.ts +1 -1
  176. package/dist/models/qbd/purchase_order.d.ts.map +1 -1
  177. package/dist/models/qbd/receive_payment.d.ts +1 -1
  178. package/dist/models/qbd/receive_payment.d.ts.map +1 -1
  179. package/dist/models/qbd/report.d.ts +1 -1
  180. package/dist/models/qbd/report.d.ts.map +1 -1
  181. package/dist/models/qbd/sales_order.d.ts +1 -1
  182. package/dist/models/qbd/sales_order.d.ts.map +1 -1
  183. package/dist/models/qbd/sales_receipt.d.ts +1 -1
  184. package/dist/models/qbd/sales_receipt.d.ts.map +1 -1
  185. package/dist/models/qbd/sales_tax_code.d.ts +1 -1
  186. package/dist/models/qbd/sales_tax_code.d.ts.map +1 -1
  187. package/dist/models/qbd/sales_tax_payment_check.d.ts +1 -1
  188. package/dist/models/qbd/sales_tax_payment_check.d.ts.map +1 -1
  189. package/dist/models/qbd/ship_method.d.ts +1 -1
  190. package/dist/models/qbd/ship_method.d.ts.map +1 -1
  191. package/dist/models/qbd/special_item.d.ts +1 -1
  192. package/dist/models/qbd/special_item.d.ts.map +1 -1
  193. package/dist/models/qbd/tenant_me.d.ts +1 -1
  194. package/dist/models/qbd/tenant_me.d.ts.map +1 -1
  195. package/dist/models/qbd/term.d.ts +1 -1
  196. package/dist/models/qbd/term.d.ts.map +1 -1
  197. package/dist/models/qbd/time_tracking_activity.d.ts +1 -1
  198. package/dist/models/qbd/time_tracking_activity.d.ts.map +1 -1
  199. package/dist/models/qbd/transaction.d.ts +1 -1
  200. package/dist/models/qbd/transaction.d.ts.map +1 -1
  201. package/dist/models/qbd/unit_of_measure_set.d.ts +1 -1
  202. package/dist/models/qbd/unit_of_measure_set.d.ts.map +1 -1
  203. package/dist/models/qbd/vendor.d.ts +1 -1
  204. package/dist/models/qbd/vendor.d.ts.map +1 -1
  205. package/dist/models/qbd/vendor_credit.d.ts +1 -1
  206. package/dist/models/qbd/vendor_credit.d.ts.map +1 -1
  207. package/dist/models/qbd/vendor_type.d.ts +1 -1
  208. package/dist/models/qbd/vendor_type.d.ts.map +1 -1
  209. package/dist/models/qbd/workers_comp_code.d.ts +1 -1
  210. package/dist/models/qbd/workers_comp_code.d.ts.map +1 -1
  211. package/dist/resources/base.d.ts +159 -29
  212. package/dist/resources/base.d.ts.map +1 -1
  213. package/dist/resources/base.js +417 -151
  214. package/dist/resources/base.js.map +1 -1
  215. package/dist/resources/connections.d.ts +17 -4
  216. package/dist/resources/connections.d.ts.map +1 -1
  217. package/dist/resources/connections.js +39 -13
  218. package/dist/resources/connections.js.map +1 -1
  219. package/dist/resources/custom-fields.d.ts +35 -32
  220. package/dist/resources/custom-fields.d.ts.map +1 -1
  221. package/dist/resources/custom-fields.js +121 -119
  222. package/dist/resources/custom-fields.js.map +1 -1
  223. package/dist/resources/reports.d.ts +23 -10
  224. package/dist/resources/reports.d.ts.map +1 -1
  225. package/dist/resources/reports.js +40 -29
  226. package/dist/resources/reports.js.map +1 -1
  227. package/dist/transport.d.ts +61 -10
  228. package/dist/transport.d.ts.map +1 -1
  229. package/dist/transport.js +522 -113
  230. package/dist/transport.js.map +1 -1
  231. package/package.json +8 -4
package/dist/transport.js CHANGED
@@ -4,11 +4,25 @@
4
4
  * Handles authentication, base URL resolution, JSON serialization,
5
5
  * and error mapping for all SDK requests.
6
6
  */
7
- import { NxusApiError } from './helpers/errors';
7
+ import { NxusApiError } from "./helpers/errors.js";
8
8
  export const DEFAULT_TIMEOUT_MS = 100_000;
9
9
  export const DEFAULT_MAX_RETRIES = 2;
10
10
  const RETRY_BASE_DELAY_MS = 500;
11
11
  const RETRY_MAX_DELAY_MS = 8_000;
12
+ /**
13
+ * Ceiling on how long a server-supplied `Retry-After` will be honoured.
14
+ *
15
+ * Separate from RETRY_MAX_DELAY_MS, which bounds our own exponential backoff.
16
+ * Clamping the server's value to the backoff cap was actively harmful: on
17
+ * `Retry-After: 60` the SDK slept 8s, retried into a window still in force,
18
+ * and repeated until the budget was gone — every attempt guaranteed to fail.
19
+ *
20
+ * Beyond this ceiling the SDK does not sleep and does not spend an attempt; it
21
+ * returns the error with `retryAfter` intact so the caller can schedule the
22
+ * wait itself. Blocking a process for an unbounded server value is not the
23
+ * SDK's decision to make.
24
+ */
25
+ const RETRY_AFTER_MAX_MS = 60_000;
12
26
  // 409 is intentionally omitted: the backend overloads it for both retryable
13
27
  // lock contention (`ObjectInUse`, `LockFailed`) and terminal business-rule
14
28
  // violations (`OutdatedEditSequence`, `NameNotUnique`, `TimeCreationMismatch`).
@@ -16,13 +30,13 @@ const RETRY_MAX_DELAY_MS = 8_000;
16
30
  // attempts on errors that need client-side action. Servers that emit the
17
31
  // header (`x-should-retry: true`) override this fallback and opt 409s in.
18
32
  const RETRYABLE_STATUSES = new Set([408, 429]);
19
- const REDACTED = '[REDACTED]';
33
+ const REDACTED = "[REDACTED]";
20
34
  const SENSITIVE_HEADER_NAMES = new Set([
21
- 'authorization',
22
- 'proxy-authorization',
23
- 'x-api-key',
24
- 'cookie',
25
- 'set-cookie',
35
+ "authorization",
36
+ "proxy-authorization",
37
+ "x-api-key",
38
+ "cookie",
39
+ "set-cookie",
26
40
  ]);
27
41
  function redactHeaders(headers) {
28
42
  const out = {};
@@ -34,12 +48,43 @@ function redactHeaders(headers) {
34
48
  function defaultLogger() {
35
49
  // Bind to console so callers can swap the logger without losing context.
36
50
  return {
37
- debug: (m, c) => console.debug(`[nxus-qbd] ${m}`, c ?? ''),
38
- info: (m, c) => console.info(`[nxus-qbd] ${m}`, c ?? ''),
39
- warn: (m, c) => console.warn(`[nxus-qbd] ${m}`, c ?? ''),
40
- error: (m, c) => console.error(`[nxus-qbd] ${m}`, c ?? ''),
51
+ debug: (m, c) => console.debug(`[nxus-qbd] ${m}`, c ?? ""),
52
+ info: (m, c) => console.info(`[nxus-qbd] ${m}`, c ?? ""),
53
+ warn: (m, c) => console.warn(`[nxus-qbd] ${m}`, c ?? ""),
54
+ error: (m, c) => console.error(`[nxus-qbd] ${m}`, c ?? ""),
41
55
  };
42
56
  }
57
+ function hasHeader(headers, name) {
58
+ return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
59
+ }
60
+ function setHeader(headers, name, value) {
61
+ for (const key of Object.keys(headers)) {
62
+ if (key.toLowerCase() === name.toLowerCase() && key !== name) {
63
+ delete headers[key];
64
+ }
65
+ }
66
+ headers[name] = value;
67
+ }
68
+ /** Resolve case-insensitive duplicates after all header sources are merged. */
69
+ function canonicalizeHeader(headers, canonicalName) {
70
+ const matching = Object.entries(headers).filter(([name]) => name.toLowerCase() === canonicalName.toLowerCase());
71
+ const value = matching.at(-1)?.[1];
72
+ if (value !== undefined) {
73
+ setHeader(headers, canonicalName, value);
74
+ }
75
+ }
76
+ const RETRY_IDEMPOTENT_CREATE_TIMEOUT = Symbol("retryIdempotentCreateTimeout");
77
+ /** @internal Marks only SDK-owned generic Create options as timeout-retryable. */
78
+ export function enableIdempotentCreateTimeoutRetry(options) {
79
+ Object.defineProperty(options, RETRY_IDEMPOTENT_CREATE_TIMEOUT, {
80
+ value: true,
81
+ });
82
+ return options;
83
+ }
84
+ function retriesIdempotentCreateTimeout(options) {
85
+ return Boolean(options &&
86
+ options[RETRY_IDEMPOTENT_CREATE_TIMEOUT]);
87
+ }
43
88
  function normalizeErrorPayload(errorBody, response) {
44
89
  if (errorBody == null) {
45
90
  return {
@@ -47,28 +92,108 @@ function normalizeErrorPayload(errorBody, response) {
47
92
  message: response.statusText,
48
93
  };
49
94
  }
50
- if (typeof errorBody !== 'object') {
95
+ if (typeof errorBody !== "object") {
51
96
  return errorBody;
52
97
  }
53
- const normalized = { ...errorBody };
98
+ const source = errorBody;
99
+ const nestedEnvelope = asRecord(source.nxusApiError) ?? asRecord(source.qbdApiError);
100
+ const normalized = {
101
+ ...(nestedEnvelope ?? source),
102
+ };
103
+ if (nestedEnvelope) {
104
+ if (normalized.requestId == null && source.requestId != null) {
105
+ normalized.requestId = source.requestId;
106
+ }
107
+ if (normalized.status == null && source.status != null) {
108
+ normalized.status = source.status;
109
+ }
110
+ if (normalized.statusCode == null && source.statusCode != null) {
111
+ normalized.statusCode = source.statusCode;
112
+ }
113
+ if (normalized.retryAfter == null && source.retryAfter != null) {
114
+ normalized.retryAfter = source.retryAfter;
115
+ }
116
+ if (normalized.restriction == null && source.restriction != null) {
117
+ normalized.restriction = source.restriction;
118
+ }
119
+ if (normalized.billing == null && source.billing != null) {
120
+ normalized.billing = source.billing;
121
+ }
122
+ if (normalized.lifecycleState == null && source.lifecycleState != null) {
123
+ normalized.lifecycleState = source.lifecycleState;
124
+ }
125
+ if (normalized.restrictionReason == null &&
126
+ source.restrictionReason != null) {
127
+ normalized.restrictionReason = source.restrictionReason;
128
+ }
129
+ if (normalized.restrictionCode == null && source.restrictionCode != null) {
130
+ normalized.restrictionCode = source.restrictionCode;
131
+ }
132
+ if (normalized.requiresPayment == null && source.requiresPayment != null) {
133
+ normalized.requiresPayment = source.requiresPayment;
134
+ }
135
+ if (normalized.checkoutUrl == null && source.checkoutUrl != null) {
136
+ normalized.checkoutUrl = source.checkoutUrl;
137
+ }
138
+ }
54
139
  if (normalized.status == null) {
55
140
  normalized.status = response.status;
56
141
  }
57
- const nestedError = normalized.error;
58
- if (nestedError && typeof nestedError === 'object') {
142
+ if (normalized.httpStatusCode == null) {
143
+ normalized.httpStatusCode = response.status;
144
+ }
145
+ if (normalized.retryAfter == null) {
146
+ const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
147
+ if (retryAfterMs != null) {
148
+ normalized.retryAfter = retryAfterMs / 1_000;
149
+ }
150
+ }
151
+ // Bodies that carry no request id still get one from the header, so
152
+ // `err.requestId` is usable for support regardless of the error shape.
153
+ if (normalized.requestId == null) {
154
+ const headerRequestId = response.headers.get("x-request-id");
155
+ if (headerRequestId) {
156
+ normalized.requestId = headerRequestId;
157
+ }
158
+ }
159
+ const normalizedError = normalized.error;
160
+ if (normalizedError && typeof normalizedError === "object") {
59
161
  normalized.error = {
60
- ...nestedError,
61
- httpStatusCode: nestedError.httpStatusCode ?? response.status,
162
+ ...normalizedError,
163
+ httpStatusCode: normalizedError.httpStatusCode ??
164
+ response.status,
62
165
  };
63
166
  }
64
167
  return normalized;
65
168
  }
169
+ function asRecord(value) {
170
+ return typeof value === "object" && value !== null
171
+ ? value
172
+ : undefined;
173
+ }
174
+ function extractLogicalErrorPayload(body) {
175
+ const record = asRecord(body);
176
+ if (!record) {
177
+ return undefined;
178
+ }
179
+ if (record.success === false) {
180
+ return body;
181
+ }
182
+ if (asRecord(record.nxusApiError) || asRecord(record.qbdApiError)) {
183
+ return body;
184
+ }
185
+ if (String(record.status).toLowerCase() === "failed") {
186
+ return body;
187
+ }
188
+ return undefined;
189
+ }
66
190
  // ---------------------------------------------------------------------------
67
191
  // Transport
68
192
  // ---------------------------------------------------------------------------
69
193
  export class NxusHttpTransport {
70
194
  baseUrl;
71
195
  apiKey;
196
+ defaultConnectionId;
72
197
  defaultHeaders;
73
198
  defaultTimeout;
74
199
  defaultServerTimeoutSeconds;
@@ -81,8 +206,9 @@ export class NxusHttpTransport {
81
206
  dispatcherPromise;
82
207
  constructor(options) {
83
208
  // Ensure trailing slash for consistent URL joining
84
- this.baseUrl = options.baseUrl.replace(/\/+$/, '');
209
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
85
210
  this.apiKey = options.apiKey;
211
+ this.defaultConnectionId = options.connectionId;
86
212
  this.defaultHeaders = options.headers ?? {};
87
213
  this.defaultTimeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
88
214
  this.defaultServerTimeoutSeconds = options.serverTimeoutSeconds;
@@ -93,35 +219,76 @@ export class NxusHttpTransport {
93
219
  this.fetchOptions = options.fetchOptions ?? {};
94
220
  }
95
221
  async get(path, query, options) {
96
- const url = this.buildUrl(path, query);
97
- return this.request(url, { method: 'GET' }, options);
222
+ return (await this.sendGet(path, query, options)).body;
98
223
  }
99
224
  async post(path, body, options) {
225
+ return (await this.sendPost(path, body, options)).body;
226
+ }
227
+ async delete(path, options) {
228
+ return (await this.sendDelete(path, options)).body;
229
+ }
230
+ /** @internal */
231
+ async deleteWithBody(path, body, options) {
232
+ return (await this.sendDeleteWithBody(path, body, options)).body;
233
+ }
234
+ /**
235
+ * Snapshot-returning counterparts of `get`/`post`/`delete`.
236
+ *
237
+ * These are the real implementations; the three above are thin wrappers that
238
+ * discard the metadata. Both paths therefore share one retry loop, one error
239
+ * translation, and one parse — metadata-aware calls cannot drift from plain
240
+ * ones.
241
+ *
242
+ * @internal
243
+ */
244
+ async sendGet(path, query, options) {
245
+ const url = this.buildUrl(path, query);
246
+ return this.request(url, { method: "GET" }, options);
247
+ }
248
+ /** @internal */
249
+ async sendPost(path, body, options) {
100
250
  const url = this.buildUrl(path);
101
251
  return this.request(url, {
102
- method: 'POST',
252
+ method: "POST",
103
253
  body: body != null ? JSON.stringify(body) : undefined,
104
254
  }, options);
105
255
  }
106
- async delete(path, options) {
256
+ /** @internal */
257
+ async sendDelete(path, options) {
107
258
  const url = this.buildUrl(path);
108
- return this.request(url, { method: 'DELETE' }, options);
259
+ return this.request(url, { method: "DELETE" }, options);
260
+ }
261
+ /** @internal */
262
+ async sendDeleteWithBody(path, body, options) {
263
+ const url = this.buildUrl(path);
264
+ return this.request(url, {
265
+ method: "DELETE",
266
+ body: JSON.stringify(body),
267
+ }, options);
109
268
  }
110
269
  /**
111
270
  * Issue a raw HTTP request and return the unparsed `Response`.
112
271
  *
113
- * Use this when you need direct access to status, headers, or the response
114
- * body as a stream/blob/text. Bypasses JSON parsing and the typed error
115
- * mapping, but still applies authentication, the default headers, the
116
- * timeout, and retries. Non-2xx responses are returned, not thrown — the
117
- * caller is responsible for `response.ok` handling.
272
+ * Gives direct access to status, headers, and the response body as a
273
+ * stream/blob/text. Bypasses JSON parsing and the typed error mapping, but
274
+ * still applies authentication, the default headers, the timeout, and
275
+ * retries. Non-2xx responses are returned, not thrown — the caller is
276
+ * responsible for `response.ok` handling.
277
+ *
278
+ * **SDK-internal.** `NxusClient.transport` is private, so this is not
279
+ * reachable from consumer code. The previous `@example` here showed
280
+ * `client.transport.raw(...)`, which does not compile. Consumers needing
281
+ * status/headers/request-id should not reach for the transport — that is
282
+ * what the planned public response wrapper is for.
118
283
  *
119
284
  * @example
120
285
  * ```ts
121
- * const res = await client.transport.raw('/api/v1/vendors', { method: 'GET' });
122
- * console.log(res.status, res.headers.get('x-request-id'));
123
- * const blob = await res.blob();
286
+ * // Internal use only — see resources/custom-fields.ts `deleteWithBody`,
287
+ * // which needs a JSON body on DELETE and maps non-2xx onto NxusApiError.
288
+ * const res = await this.transport.raw(path, { method: 'DELETE', body });
124
289
  * ```
290
+ *
291
+ * @internal
125
292
  */
126
293
  async raw(path, init = {}, options) {
127
294
  const { query, ...rest } = init;
@@ -132,10 +299,10 @@ export class NxusHttpTransport {
132
299
  // Internal
133
300
  // -------------------------------------------------------------------------
134
301
  buildUrl(path, query) {
135
- const url = new URL(path, this.baseUrl + '/');
302
+ const url = new URL(path, this.baseUrl + "/");
136
303
  // The URL constructor resolves relative to base — if path starts with /
137
304
  // we need to set it directly
138
- if (path.startsWith('/')) {
305
+ if (path.startsWith("/")) {
139
306
  url.pathname = path;
140
307
  }
141
308
  if (query) {
@@ -164,11 +331,17 @@ export class NxusHttpTransport {
164
331
  const timeout = options?.timeout ?? this.defaultTimeout;
165
332
  const maxRetries = Math.max(0, options?.maxRetries ?? this.defaultMaxRetries);
166
333
  const verbose = options?.verbose ?? this.verbose;
167
- const extraFetchOptions = { ...this.fetchOptions, ...(options?.fetchOptions ?? {}) };
334
+ const extraFetchOptions = {
335
+ ...this.fetchOptions,
336
+ ...(options?.fetchOptions ?? {}),
337
+ };
338
+ // Read here as well as inside the attempt, so a cancellation lands during
339
+ // the backoff wait rather than only at the next dispatch.
340
+ const { callerSignal } = extractCallerSignal(extraFetchOptions);
168
341
  let attempt = 0;
169
342
  while (true) {
170
343
  if (verbose) {
171
- this.logger.debug('request', {
344
+ this.logger.debug("request", {
172
345
  method: init.method,
173
346
  url,
174
347
  headers: redactHeaders(headers),
@@ -176,20 +349,26 @@ export class NxusHttpTransport {
176
349
  maxRetries,
177
350
  });
178
351
  }
179
- const outcome = await this.attempt(url, init, headers, timeout, extraFetchOptions);
352
+ const outcome = await this.attempt(url, init, headers, timeout, extraFetchOptions, options?.includeRawBody ?? false);
180
353
  if (verbose) {
181
354
  this.logOutcome(outcome, { method: init.method, url, attempt });
182
355
  }
183
- if (outcome.kind === 'success') {
356
+ if (outcome.kind === "success") {
184
357
  return outcome.value;
185
358
  }
359
+ // Rethrow the caller's own abort reason rather than wrapping it. A
360
+ // consumer racing an AbortController checks `err.name === "AbortError"`;
361
+ // an NxusApiError here would read as a request failure instead.
362
+ if (outcome.kind === "canceled") {
363
+ throw outcome.reason;
364
+ }
186
365
  if (attempt >= maxRetries ||
187
- !shouldRetry(outcome)) {
366
+ !shouldRetry(outcome, retriesIdempotentCreateTimeout(options))) {
188
367
  throw outcome.error;
189
368
  }
190
369
  const delayMs = computeRetryDelay(attempt, outcome);
191
370
  if (verbose) {
192
- this.logger.debug('retry-scheduled', {
371
+ this.logger.debug("retry-scheduled", {
193
372
  url,
194
373
  attempt: attempt + 1,
195
374
  delayMs,
@@ -197,7 +376,7 @@ export class NxusHttpTransport {
197
376
  }
198
377
  attempt += 1;
199
378
  if (delayMs > 0) {
200
- await sleep(delayMs);
379
+ await sleep(delayMs, callerSignal);
201
380
  }
202
381
  }
203
382
  }
@@ -206,12 +385,18 @@ export class NxusHttpTransport {
206
385
  const timeout = options?.timeout ?? this.defaultTimeout;
207
386
  const maxRetries = Math.max(0, options?.maxRetries ?? this.defaultMaxRetries);
208
387
  const verbose = options?.verbose ?? this.verbose;
209
- const extraFetchOptions = { ...this.fetchOptions, ...(options?.fetchOptions ?? {}) };
388
+ const extraFetchOptions = {
389
+ ...this.fetchOptions,
390
+ ...(options?.fetchOptions ?? {}),
391
+ };
392
+ // Read here as well as inside the attempt, so a cancellation lands during
393
+ // the backoff wait rather than only at the next dispatch.
394
+ const { callerSignal } = extractCallerSignal(extraFetchOptions);
210
395
  let attempt = 0;
211
396
  while (true) {
212
397
  if (verbose) {
213
- this.logger.debug('request', {
214
- method: init.method ?? 'GET',
398
+ this.logger.debug("request", {
399
+ method: init.method ?? "GET",
215
400
  url,
216
401
  headers: redactHeaders(headers),
217
402
  attempt,
@@ -221,22 +406,30 @@ export class NxusHttpTransport {
221
406
  }
222
407
  const outcome = await this.attemptRaw(url, init, headers, timeout, extraFetchOptions);
223
408
  if (verbose) {
224
- this.logRawOutcome(outcome, { method: init.method ?? 'GET', url, attempt });
409
+ this.logRawOutcome(outcome, {
410
+ method: init.method ?? "GET",
411
+ url,
412
+ attempt,
413
+ });
225
414
  }
226
- if (outcome.kind === 'response') {
415
+ if (outcome.kind === "response") {
227
416
  return outcome.response;
228
417
  }
229
- if (attempt >= maxRetries || !shouldRetry(outcome)) {
418
+ if (outcome.kind === "canceled") {
419
+ throw outcome.reason;
420
+ }
421
+ if (attempt >= maxRetries ||
422
+ !shouldRetry(outcome, retriesIdempotentCreateTimeout(options))) {
230
423
  // Non-2xx that we won't retry: return the Response so the caller can
231
424
  // inspect status/headers/body, matching the documented contract.
232
- if (outcome.kind === 'http-error') {
425
+ if (outcome.kind === "http-error") {
233
426
  return outcome.response;
234
427
  }
235
428
  throw outcome.error;
236
429
  }
237
430
  const delayMs = computeRetryDelay(attempt, outcome);
238
431
  if (verbose) {
239
- this.logger.debug('retry-scheduled', {
432
+ this.logger.debug("retry-scheduled", {
240
433
  url,
241
434
  attempt: attempt + 1,
242
435
  delayMs,
@@ -244,24 +437,32 @@ export class NxusHttpTransport {
244
437
  }
245
438
  attempt += 1;
246
439
  if (delayMs > 0) {
247
- await sleep(delayMs);
440
+ await sleep(delayMs, callerSignal);
248
441
  }
249
442
  }
250
443
  }
251
444
  buildHeaders(options) {
252
445
  const headers = {
253
- 'Content-Type': 'application/json',
446
+ "Content-Type": "application/json",
254
447
  Authorization: `Bearer ${this.apiKey}`,
255
448
  ...this.defaultHeaders,
256
449
  ...options?.headers,
257
450
  };
258
- if (options?.connectionId) {
259
- headers['X-Connection-Id'] = options.connectionId;
451
+ if (options?.connectionId !== undefined) {
452
+ setHeader(headers, "X-Connection-Id", options.connectionId);
260
453
  }
261
- const serverTimeoutSeconds = options?.serverTimeoutSeconds ?? this.defaultServerTimeoutSeconds;
262
- if (serverTimeoutSeconds != null && !('X-Nxus-Timeout-Seconds' in headers)) {
263
- headers['X-Nxus-Timeout-Seconds'] = String(serverTimeoutSeconds);
454
+ else if (this.defaultConnectionId != null &&
455
+ !hasHeader(headers, "X-Connection-Id")) {
456
+ setHeader(headers, "X-Connection-Id", this.defaultConnectionId);
264
457
  }
458
+ if (options?.serverTimeoutSeconds !== undefined) {
459
+ setHeader(headers, "X-Nxus-Timeout-Seconds", String(options.serverTimeoutSeconds));
460
+ }
461
+ else if (this.defaultServerTimeoutSeconds != null &&
462
+ !hasHeader(headers, "X-Nxus-Timeout-Seconds")) {
463
+ setHeader(headers, "X-Nxus-Timeout-Seconds", String(this.defaultServerTimeoutSeconds));
464
+ }
465
+ canonicalizeHeader(headers, "Idempotency-Key");
265
466
  return headers;
266
467
  }
267
468
  async resolveDispatcher() {
@@ -274,15 +475,16 @@ export class NxusHttpTransport {
274
475
  // handling without throwing.
275
476
  try {
276
477
  // @ts-expect-error - undici is an optional runtime peer
277
- const mod = await import('undici');
278
- const Agent = mod.ProxyAgent;
478
+ const mod = await import("undici");
479
+ const Agent = mod
480
+ .ProxyAgent;
279
481
  if (!Agent)
280
482
  return undefined;
281
483
  return new Agent(this.proxyUrl);
282
484
  }
283
485
  catch (err) {
284
486
  if (this.verbose) {
285
- this.logger.warn('proxy-agent-unavailable', {
487
+ this.logger.warn("proxy-agent-unavailable", {
286
488
  proxy: this.proxyUrl,
287
489
  reason: err instanceof Error ? err.message : String(err),
288
490
  });
@@ -294,6 +496,11 @@ export class NxusHttpTransport {
294
496
  return this.dispatcherPromise;
295
497
  }
296
498
  async resolveFetchInit(init, headers, signal, extraFetchOptions) {
499
+ // `signal` is placed last on purpose, and `extraFetchOptions` must already
500
+ // have had the caller's own signal removed by extractCallerSignal — the
501
+ // combined signal passed in here is what represents both. Leaving a raw
502
+ // caller signal in extraFetchOptions would let this spread drop the
503
+ // timeout instead, which is the same bug in the other direction.
297
504
  const merged = {
298
505
  ...extraFetchOptions,
299
506
  ...init,
@@ -315,22 +522,25 @@ export class NxusHttpTransport {
315
522
  }
316
523
  logOutcome(outcome, ctx) {
317
524
  switch (outcome.kind) {
318
- case 'success':
319
- this.logger.debug('response', { ...ctx, ok: true });
525
+ case "success":
526
+ this.logger.debug("response", { ...ctx, ok: true });
320
527
  return;
321
- case 'http-error':
322
- this.logger.warn('response', {
528
+ case "http-error":
529
+ this.logger.warn("response", {
323
530
  ...ctx,
324
531
  status: outcome.status,
325
532
  retryAfter: outcome.retryAfter,
326
533
  shouldRetry: outcome.shouldRetry,
327
534
  });
328
535
  return;
329
- case 'timeout':
330
- this.logger.warn('timeout', ctx);
536
+ case "timeout":
537
+ this.logger.warn("timeout", ctx);
331
538
  return;
332
- case 'network-error':
333
- this.logger.warn('network-error', {
539
+ case "canceled":
540
+ this.logger.debug("canceled", ctx);
541
+ return;
542
+ case "network-error":
543
+ this.logger.warn("network-error", {
334
544
  ...ctx,
335
545
  reason: outcome.error.message,
336
546
  });
@@ -339,15 +549,15 @@ export class NxusHttpTransport {
339
549
  }
340
550
  logRawOutcome(outcome, ctx) {
341
551
  switch (outcome.kind) {
342
- case 'response':
343
- this.logger.debug('response', {
552
+ case "response":
553
+ this.logger.debug("response", {
344
554
  ...ctx,
345
555
  status: outcome.response.status,
346
556
  raw: true,
347
557
  });
348
558
  return;
349
- case 'http-error':
350
- this.logger.warn('response', {
559
+ case "http-error":
560
+ this.logger.warn("response", {
351
561
  ...ctx,
352
562
  status: outcome.status,
353
563
  retryAfter: outcome.retryAfter,
@@ -355,22 +565,33 @@ export class NxusHttpTransport {
355
565
  raw: true,
356
566
  });
357
567
  return;
358
- case 'timeout':
359
- this.logger.warn('timeout', ctx);
568
+ case "timeout":
569
+ this.logger.warn("timeout", ctx);
570
+ return;
571
+ case "canceled":
572
+ this.logger.debug("canceled", ctx);
360
573
  return;
361
- case 'network-error':
362
- this.logger.warn('network-error', {
574
+ case "network-error":
575
+ this.logger.warn("network-error", {
363
576
  ...ctx,
364
577
  reason: outcome.error.message,
365
578
  });
366
579
  return;
367
580
  }
368
581
  }
369
- async attempt(url, init, headers, timeout, extraFetchOptions) {
582
+ async attempt(url, init, headers, timeout, extraFetchOptions, includeRawBody = false) {
583
+ const { callerSignal, rest } = extractCallerSignal(extraFetchOptions);
584
+ // Bail before dispatching. A caller who aborted before the call was made
585
+ // still had the request sent and the response returned, because the
586
+ // caller's signal never reached fetch at all.
587
+ if (callerSignal?.aborted) {
588
+ return { kind: "canceled", reason: callerSignal.reason };
589
+ }
370
590
  const controller = new AbortController();
371
591
  const timer = setTimeout(() => controller.abort(), timeout);
592
+ const composed = combineAbortSignals(callerSignal, controller.signal);
372
593
  try {
373
- const fetchInit = await this.resolveFetchInit(init, headers, controller.signal, extraFetchOptions);
594
+ const fetchInit = await this.resolveFetchInit(init, headers, composed.signal, rest);
374
595
  const response = await fetch(url, fetchInit);
375
596
  if (!response.ok) {
376
597
  let errorBody;
@@ -385,42 +606,78 @@ export class NxusHttpTransport {
385
606
  // body's `error.retryAfter` (seconds, integer) when the header is
386
607
  // missing. This keeps behavior correct against proxies that strip
387
608
  // hop-by-hop headers but preserve the body.
388
- const retryAfter = parseRetryAfter(response.headers.get('retry-after')) ??
609
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after")) ??
389
610
  parseBodyRetryAfter(errorBody);
390
611
  return {
391
- kind: 'http-error',
612
+ kind: "http-error",
392
613
  status: response.status,
393
614
  retryAfter,
394
- shouldRetry: parseShouldRetry(response.headers.get('x-should-retry')),
615
+ shouldRetry: parseShouldRetry(response.headers.get("x-should-retry")),
395
616
  error,
396
617
  };
397
618
  }
398
619
  // 204 No Content
399
620
  if (response.status === 204) {
400
- return { kind: 'success', value: undefined };
621
+ return {
622
+ kind: "success",
623
+ value: snapshot(response, undefined, undefined, includeRawBody),
624
+ };
401
625
  }
402
626
  const text = await response.text();
403
627
  if (!text) {
404
- return { kind: 'success', value: undefined };
628
+ return {
629
+ kind: "success",
630
+ value: snapshot(response, undefined, "", includeRawBody),
631
+ };
632
+ }
633
+ const parsed = JSON.parse(text);
634
+ const logicalErrorPayload = extractLogicalErrorPayload(parsed);
635
+ if (logicalErrorPayload !== undefined) {
636
+ const normalizedError = normalizeErrorPayload(logicalErrorPayload, response);
637
+ return {
638
+ kind: "http-error",
639
+ status: typeof asRecord(normalizedError)?.status === "number"
640
+ ? asRecord(normalizedError)?.status
641
+ : response.status,
642
+ retryAfter: parseBodyRetryAfter(normalizedError),
643
+ shouldRetry: parseShouldRetry(response.headers.get("x-should-retry")),
644
+ error: NxusApiError.from(normalizedError),
645
+ };
405
646
  }
406
- return { kind: 'success', value: JSON.parse(text) };
647
+ return {
648
+ kind: "success",
649
+ value: snapshot(response, parsed, text, includeRawBody),
650
+ };
407
651
  }
408
652
  catch (error) {
409
- if (error instanceof DOMException && error.name === 'AbortError') {
653
+ // The caller's signal is checked FIRST, and independently of what was
654
+ // thrown. fetch rejects with the signal's abort reason verbatim, and
655
+ // `controller.abort(new Error("deployment shutdown"))` produces a plain
656
+ // Error whose name is "Error" — so keying off the thrown value's name
657
+ // classified a real cancellation as a network failure and retried it.
658
+ // Whether the caller aborted is a fact about the signal, not about the
659
+ // shape of the rejection.
660
+ if (callerSignal?.aborted) {
661
+ return { kind: "canceled", reason: callerSignal.reason };
662
+ }
663
+ if (isAbortError(error)) {
664
+ // Not the caller, so this is our own timeout controller firing. The
665
+ // two mean opposite things: a timeout leaves the outcome unknown and
666
+ // may deserve a retry, a cancellation is an instruction to stop.
410
667
  return {
411
- kind: 'timeout',
668
+ kind: "timeout",
412
669
  error: new NxusApiError({
413
670
  message: `Request timed out after ${timeout}ms`,
414
- userMessage: 'The request timed out. Please try again.',
671
+ userMessage: "The request timed out. Please try again.",
415
672
  status: 0,
416
673
  }),
417
674
  };
418
675
  }
419
676
  return {
420
- kind: 'network-error',
677
+ kind: "network-error",
421
678
  error: new NxusApiError({
422
- message: error instanceof Error ? error.message : 'Network request failed',
423
- userMessage: 'A network error occurred. Please check your connection and try again.',
679
+ message: error instanceof Error ? error.message : "Network request failed",
680
+ userMessage: "A network error occurred. Please check your connection and try again.",
424
681
  status: 0,
425
682
  raw: error,
426
683
  }),
@@ -428,44 +685,54 @@ export class NxusHttpTransport {
428
685
  }
429
686
  finally {
430
687
  clearTimeout(timer);
688
+ composed.dispose();
431
689
  }
432
690
  }
433
691
  async attemptRaw(url, init, headers, timeout, extraFetchOptions) {
692
+ const { callerSignal, rest } = extractCallerSignal(extraFetchOptions);
693
+ if (callerSignal?.aborted) {
694
+ return { kind: "canceled", reason: callerSignal.reason };
695
+ }
434
696
  const controller = new AbortController();
435
697
  const timer = setTimeout(() => controller.abort(), timeout);
698
+ const composed = combineAbortSignals(callerSignal, controller.signal);
436
699
  try {
437
- const fetchInit = await this.resolveFetchInit(init, headers, controller.signal, extraFetchOptions);
700
+ const fetchInit = await this.resolveFetchInit(init, headers, composed.signal, rest);
438
701
  const response = await fetch(url, fetchInit);
439
702
  if (!response.ok) {
440
703
  // Classify retryable failures by status + headers only — never read
441
704
  // the body, since the caller owns the stream.
442
- const retryAfter = parseRetryAfter(response.headers.get('retry-after')) ?? undefined;
705
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after")) ?? undefined;
443
706
  return {
444
- kind: 'http-error',
707
+ kind: "http-error",
445
708
  status: response.status,
446
709
  retryAfter,
447
- shouldRetry: parseShouldRetry(response.headers.get('x-should-retry')),
710
+ shouldRetry: parseShouldRetry(response.headers.get("x-should-retry")),
448
711
  response,
449
712
  };
450
713
  }
451
- return { kind: 'response', response };
714
+ return { kind: "response", response };
452
715
  }
453
716
  catch (error) {
454
- if (error instanceof DOMException && error.name === 'AbortError') {
717
+ // Checked first and independently of the thrown value — see attempt().
718
+ if (callerSignal?.aborted) {
719
+ return { kind: "canceled", reason: callerSignal.reason };
720
+ }
721
+ if (isAbortError(error)) {
455
722
  return {
456
- kind: 'timeout',
723
+ kind: "timeout",
457
724
  error: new NxusApiError({
458
725
  message: `Request timed out after ${timeout}ms`,
459
- userMessage: 'The request timed out. Please try again.',
726
+ userMessage: "The request timed out. Please try again.",
460
727
  status: 0,
461
728
  }),
462
729
  };
463
730
  }
464
731
  return {
465
- kind: 'network-error',
732
+ kind: "network-error",
466
733
  error: new NxusApiError({
467
- message: error instanceof Error ? error.message : 'Network request failed',
468
- userMessage: 'A network error occurred. Please check your connection and try again.',
734
+ message: error instanceof Error ? error.message : "Network request failed",
735
+ userMessage: "A network error occurred. Please check your connection and try again.",
469
736
  status: 0,
470
737
  raw: error,
471
738
  }),
@@ -473,15 +740,59 @@ export class NxusHttpTransport {
473
740
  }
474
741
  finally {
475
742
  clearTimeout(timer);
743
+ composed.dispose();
476
744
  }
477
745
  }
478
746
  }
479
- function shouldRetry(outcome) {
480
- if (outcome.kind === 'network-error')
481
- return true;
482
- if (outcome.kind === 'timeout')
747
+ /**
748
+ * Copy everything worth keeping out of a response before it is released.
749
+ *
750
+ * The body is already read by the time this is called, so nothing downstream
751
+ * can touch a consumed stream. `rawBody` is retained only on request — holding
752
+ * the undecoded text of every response would keep a second full copy of every
753
+ * payload alive.
754
+ */
755
+ function snapshot(response, body, text, includeRawBody) {
756
+ const headers = {};
757
+ response.headers.forEach((value, key) => {
758
+ headers[key.toLowerCase()] = value;
759
+ });
760
+ const bodyRequestId = body != null && typeof body === "object"
761
+ ? body.requestId
762
+ : undefined;
763
+ return {
764
+ body,
765
+ status: response.status,
766
+ headers: Object.freeze(headers),
767
+ requestId: (typeof bodyRequestId === "string" ? bodyRequestId : undefined) ??
768
+ headers["x-request-id"],
769
+ rawBody: includeRawBody ? text : undefined,
770
+ };
771
+ }
772
+ function shouldRetry(outcome, retryTimeout = false) {
773
+ // The caller asked us to stop. Retrying would be the opposite of that, and
774
+ // for a Create it would resubmit work the caller is trying to abandon.
775
+ if (outcome.kind === "canceled")
483
776
  return false;
484
- if (outcome.kind === 'http-error') {
777
+ if (outcome.kind === "network-error")
778
+ return true;
779
+ if (outcome.kind === "timeout")
780
+ return retryTimeout;
781
+ if (outcome.kind === "http-error") {
782
+ // A wait longer than we are willing to block for. Returning false here
783
+ // rather than retrying early is the point: an early retry cannot succeed
784
+ // inside a window the server just told us is still open, and it consumes
785
+ // an attempt to learn that. The caller gets the error with `retryAfter`
786
+ // on it and can schedule the real wait.
787
+ //
788
+ // Checked before `x-should-retry` on purpose. The two answer different
789
+ // questions — whether to retry, and when — and a server sending
790
+ // `x-should-retry: true` with `Retry-After: 300` is asking for a retry in
791
+ // five minutes, not for this process to block for five minutes. Nothing
792
+ // is lost: both facts reach the caller on the error.
793
+ if (outcome.retryAfter != null && outcome.retryAfter > RETRY_AFTER_MAX_MS) {
794
+ return false;
795
+ }
485
796
  if (outcome.shouldRetry != null)
486
797
  return outcome.shouldRetry;
487
798
  if (RETRYABLE_STATUSES.has(outcome.status))
@@ -493,8 +804,10 @@ function shouldRetry(outcome) {
493
804
  return false;
494
805
  }
495
806
  function computeRetryDelay(attempt, outcome) {
496
- if (outcome.kind === 'http-error' && outcome.retryAfter != null) {
497
- return Math.min(outcome.retryAfter, RETRY_MAX_DELAY_MS);
807
+ if (outcome.kind === "http-error" && outcome.retryAfter != null) {
808
+ // Honoured in full. shouldRetry() has already refused anything above
809
+ // RETRY_AFTER_MAX_MS, so this cannot exceed the ceiling.
810
+ return outcome.retryAfter;
498
811
  }
499
812
  const exponential = Math.min(RETRY_BASE_DELAY_MS * 2 ** attempt, RETRY_MAX_DELAY_MS);
500
813
  const jitter = Math.random() * exponential * 0.5;
@@ -515,16 +828,18 @@ function parseRetryAfter(value) {
515
828
  return undefined;
516
829
  }
517
830
  function parseBodyRetryAfter(body) {
518
- if (body == null || typeof body !== 'object')
831
+ if (body == null || typeof body !== "object")
519
832
  return undefined;
520
833
  const root = body;
521
834
  // Shape per backend contract: `{ error: { retryAfter: <seconds> } }`.
522
835
  // Tolerate top-level `retryAfter` for older payloads / future flattening.
523
- const errorObj = root.error && typeof root.error === 'object'
836
+ const errorObj = root.error && typeof root.error === "object"
524
837
  ? root.error
525
838
  : undefined;
526
839
  const candidate = errorObj?.retryAfter ?? root.retryAfter;
527
- if (typeof candidate !== 'number' || !Number.isFinite(candidate) || candidate < 0) {
840
+ if (typeof candidate !== "number" ||
841
+ !Number.isFinite(candidate) ||
842
+ candidate < 0) {
528
843
  return undefined;
529
844
  }
530
845
  return candidate * 1000;
@@ -533,13 +848,107 @@ function parseShouldRetry(value) {
533
848
  if (!value)
534
849
  return undefined;
535
850
  const normalized = value.trim().toLowerCase();
536
- if (['1', 'true', 'yes'].includes(normalized))
851
+ if (["1", "true", "yes"].includes(normalized))
537
852
  return true;
538
- if (['0', 'false', 'no'].includes(normalized))
853
+ if (["0", "false", "no"].includes(normalized))
539
854
  return false;
540
855
  return undefined;
541
856
  }
542
- function sleep(ms) {
543
- return new Promise((resolve) => setTimeout(resolve, ms));
857
+ /**
858
+ * Sleep that a caller can interrupt.
859
+ *
860
+ * Retry backoff can be seconds long. Without the signal, a caller cancelling
861
+ * mid-backoff would still wait out the delay and then issue another request.
862
+ */
863
+ /**
864
+ * Abort detection that survives a cross-realm boundary.
865
+ *
866
+ * `instanceof DOMException` is false for an abort raised in another realm
867
+ * (undici's fetch, a worker, a polyfilled test double), and a miss there gets
868
+ * reported as a network error and retried — the opposite of what a cancelling
869
+ * caller asked for.
870
+ */
871
+ function isAbortError(error) {
872
+ if (error instanceof DOMException && error.name === "AbortError")
873
+ return true;
874
+ return (typeof error === "object" &&
875
+ error !== null &&
876
+ error.name === "AbortError");
877
+ }
878
+ function sleep(ms, signal) {
879
+ if (!signal) {
880
+ return new Promise((resolve) => setTimeout(resolve, ms));
881
+ }
882
+ if (signal.aborted) {
883
+ return Promise.reject(signal.reason);
884
+ }
885
+ return new Promise((resolve, reject) => {
886
+ const timer = setTimeout(() => {
887
+ signal.removeEventListener("abort", onAbort);
888
+ resolve();
889
+ }, ms);
890
+ const onAbort = () => {
891
+ clearTimeout(timer);
892
+ reject(signal.reason);
893
+ };
894
+ signal.addEventListener("abort", onAbort, { once: true });
895
+ });
896
+ }
897
+ /**
898
+ * Read a caller-supplied `signal` out of per-request `fetchOptions`.
899
+ *
900
+ * Returned separately from the rest of the options so the SDK's timeout signal
901
+ * cannot silently overwrite it, which is exactly what used to happen.
902
+ */
903
+ function extractCallerSignal(extraFetchOptions) {
904
+ const { signal, ...rest } = extraFetchOptions;
905
+ return {
906
+ callerSignal: signal instanceof AbortSignal ? signal : undefined,
907
+ rest,
908
+ };
909
+ }
910
+ /**
911
+ * One signal that aborts when either the caller or the timeout does, plus the
912
+ * cleanup that detaches whatever it attached.
913
+ *
914
+ * `AbortSignal.any` landed in Node 20.3; the package supports Node 18, so the
915
+ * manual path is not dead code.
916
+ *
917
+ * The caller's signal usually outlives the request — one controller cancels a
918
+ * whole batch — so the fallback's listeners MUST be removed when the request
919
+ * finishes, not only when an abort fires. `{ once: true }` covers the abort
920
+ * case and nothing else: twelve successful requests sharing one caller signal
921
+ * left twelve listeners attached, each retaining a controller and its closure.
922
+ * Hence the returned `dispose`, which every attempt calls from its finally.
923
+ */
924
+ function combineAbortSignals(callerSignal, timeoutSignal) {
925
+ if (!callerSignal)
926
+ return { signal: timeoutSignal, dispose: () => { } };
927
+ const anyFn = AbortSignal.any;
928
+ if (typeof anyFn === "function") {
929
+ // Native composition detaches itself once the composed signal is
930
+ // unreachable; there is nothing for us to clean up.
931
+ return { signal: anyFn([callerSignal, timeoutSignal]), dispose: () => { } };
932
+ }
933
+ const controller = new AbortController();
934
+ const attached = [];
935
+ for (const source of [callerSignal, timeoutSignal]) {
936
+ if (source.aborted) {
937
+ controller.abort(source.reason);
938
+ break;
939
+ }
940
+ const forward = () => controller.abort(source.reason);
941
+ source.addEventListener("abort", forward, { once: true });
942
+ attached.push([source, forward]);
943
+ }
944
+ return {
945
+ signal: controller.signal,
946
+ dispose: () => {
947
+ for (const [source, forward] of attached) {
948
+ source.removeEventListener("abort", forward);
949
+ }
950
+ attached.length = 0;
951
+ },
952
+ };
544
953
  }
545
954
  //# sourceMappingURL=transport.js.map