qubesync 0.1.0 → 0.2.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 (258) hide show
  1. package/CHANGELOG.md +38 -2
  2. package/README.md +188 -5
  3. package/client.js +660 -0
  4. package/index.d.ts +211 -1
  5. package/index.js +5 -1
  6. package/index.mjs +2 -0
  7. package/operations/account.d.ts +263 -0
  8. package/operations/account.js +50 -0
  9. package/operations/account_tax_line_info.d.ts +31 -0
  10. package/operations/account_tax_line_info.js +26 -0
  11. package/operations/aging_report.d.ts +100 -0
  12. package/operations/aging_report.js +26 -0
  13. package/operations/alert.d.ts +118 -0
  14. package/operations/alert.js +50 -0
  15. package/operations/ar_refund_credit_card.d.ts +201 -0
  16. package/operations/ar_refund_credit_card.js +38 -0
  17. package/operations/bar_code.d.ts +40 -0
  18. package/operations/bar_code.js +26 -0
  19. package/operations/bill.d.ts +612 -0
  20. package/operations/bill.js +50 -0
  21. package/operations/bill_payment_check.d.ts +276 -0
  22. package/operations/bill_payment_check.js +50 -0
  23. package/operations/bill_payment_credit_card.d.ts +178 -0
  24. package/operations/bill_payment_credit_card.js +38 -0
  25. package/operations/bill_to_pay.d.ts +54 -0
  26. package/operations/bill_to_pay.js +26 -0
  27. package/operations/billing_rate.d.ts +159 -0
  28. package/operations/billing_rate.js +38 -0
  29. package/operations/budget_summary_report.d.ts +71 -0
  30. package/operations/budget_summary_report.js +26 -0
  31. package/operations/build_assembly.d.ts +264 -0
  32. package/operations/build_assembly.js +50 -0
  33. package/operations/charge.d.ts +330 -0
  34. package/operations/charge.js +50 -0
  35. package/operations/check.d.ts +394 -0
  36. package/operations/check.js +50 -0
  37. package/operations/class.d.ts +170 -0
  38. package/operations/class.js +50 -0
  39. package/operations/cleared_status.d.ts +43 -0
  40. package/operations/cleared_status.js +26 -0
  41. package/operations/client.d.ts +254 -0
  42. package/operations/company.d.ts +36 -0
  43. package/operations/company.js +26 -0
  44. package/operations/company_activity.d.ts +32 -0
  45. package/operations/company_activity.js +26 -0
  46. package/operations/credit_card_charge.d.ts +280 -0
  47. package/operations/credit_card_charge.js +50 -0
  48. package/operations/credit_card_credit.d.ts +280 -0
  49. package/operations/credit_card_credit.js +50 -0
  50. package/operations/credit_memo.d.ts +642 -0
  51. package/operations/credit_memo.js +50 -0
  52. package/operations/currency.d.ts +195 -0
  53. package/operations/currency.js +50 -0
  54. package/operations/custom_detail_report.d.ts +108 -0
  55. package/operations/custom_detail_report.js +26 -0
  56. package/operations/custom_summary_report.d.ts +119 -0
  57. package/operations/custom_summary_report.js +26 -0
  58. package/operations/customer.d.ts +639 -0
  59. package/operations/customer.js +50 -0
  60. package/operations/customer_msg.d.ts +121 -0
  61. package/operations/customer_msg.js +38 -0
  62. package/operations/customer_type.d.ts +127 -0
  63. package/operations/customer_type.js +38 -0
  64. package/operations/data_event_recovery_info.d.ts +61 -0
  65. package/operations/data_event_recovery_info.js +38 -0
  66. package/operations/data_ext.d.ts +197 -0
  67. package/operations/data_ext.js +50 -0
  68. package/operations/data_ext_def.d.ts +173 -0
  69. package/operations/data_ext_def.js +62 -0
  70. package/operations/date_driven_terms.d.ts +141 -0
  71. package/operations/date_driven_terms.js +38 -0
  72. package/operations/deposit.d.ts +402 -0
  73. package/operations/deposit.js +50 -0
  74. package/operations/employee.d.ts +683 -0
  75. package/operations/employee.js +50 -0
  76. package/operations/employee_default.d.ts +133 -0
  77. package/operations/employee_default.js +38 -0
  78. package/operations/entity.d.ts +96 -0
  79. package/operations/entity.js +26 -0
  80. package/operations/estimate.d.ts +590 -0
  81. package/operations/estimate.js +50 -0
  82. package/operations/form1099_category_account_mapping.d.ts +89 -0
  83. package/operations/form1099_category_account_mapping.js +38 -0
  84. package/operations/general_detail_report.d.ts +108 -0
  85. package/operations/general_detail_report.js +26 -0
  86. package/operations/general_summary_report.d.ts +115 -0
  87. package/operations/general_summary_report.js +26 -0
  88. package/operations/host.d.ts +43 -0
  89. package/operations/host.js +26 -0
  90. package/operations/index.js +397 -0
  91. package/operations/inventory_adjustment.d.ts +389 -0
  92. package/operations/inventory_adjustment.js +50 -0
  93. package/operations/inventory_site.d.ts +247 -0
  94. package/operations/inventory_site.js +50 -0
  95. package/operations/invoice.d.ts +682 -0
  96. package/operations/invoice.js +50 -0
  97. package/operations/item.d.ts +100 -0
  98. package/operations/item.js +26 -0
  99. package/operations/item_assemblies_can_build.d.ts +41 -0
  100. package/operations/item_assemblies_can_build.js +26 -0
  101. package/operations/item_discount.d.ts +256 -0
  102. package/operations/item_discount.js +50 -0
  103. package/operations/item_fixed_asset.d.ts +382 -0
  104. package/operations/item_fixed_asset.js +50 -0
  105. package/operations/item_group.d.ts +236 -0
  106. package/operations/item_group.js +50 -0
  107. package/operations/item_inventory.d.ts +350 -0
  108. package/operations/item_inventory.js +50 -0
  109. package/operations/item_inventory_assembly.d.ts +368 -0
  110. package/operations/item_inventory_assembly.js +50 -0
  111. package/operations/item_non_inventory.d.ts +264 -0
  112. package/operations/item_non_inventory.js +50 -0
  113. package/operations/item_other_charge.d.ts +244 -0
  114. package/operations/item_other_charge.js +50 -0
  115. package/operations/item_payment.d.ts +220 -0
  116. package/operations/item_payment.js +50 -0
  117. package/operations/item_receipt.d.ts +362 -0
  118. package/operations/item_receipt.js +50 -0
  119. package/operations/item_sales_tax.d.ts +228 -0
  120. package/operations/item_sales_tax.js +50 -0
  121. package/operations/item_sales_tax_group.d.ts +192 -0
  122. package/operations/item_sales_tax_group.js +50 -0
  123. package/operations/item_service.d.ts +256 -0
  124. package/operations/item_service.js +50 -0
  125. package/operations/item_sites.d.ts +81 -0
  126. package/operations/item_sites.js +26 -0
  127. package/operations/item_subtotal.d.ts +188 -0
  128. package/operations/item_subtotal.js +50 -0
  129. package/operations/job_report.d.ts +99 -0
  130. package/operations/job_report.js +26 -0
  131. package/operations/job_type.d.ts +127 -0
  132. package/operations/job_type.js +38 -0
  133. package/operations/journal_entry.d.ts +339 -0
  134. package/operations/journal_entry.js +50 -0
  135. package/operations/lead.d.ts +291 -0
  136. package/operations/lead.js +50 -0
  137. package/operations/list.d.ts +39 -0
  138. package/operations/list.js +26 -0
  139. package/operations/list_deleted.d.ts +44 -0
  140. package/operations/list_deleted.js +26 -0
  141. package/operations/list_display.d.ts +65 -0
  142. package/operations/list_display.js +38 -0
  143. package/operations/list_merge.d.ts +58 -0
  144. package/operations/list_merge.js +26 -0
  145. package/operations/other_name.d.ts +280 -0
  146. package/operations/other_name.js +50 -0
  147. package/operations/paycheck.d.ts +82 -0
  148. package/operations/paycheck.js +26 -0
  149. package/operations/payment_method.d.ts +131 -0
  150. package/operations/payment_method.js +38 -0
  151. package/operations/payroll_detail_report.d.ts +97 -0
  152. package/operations/payroll_detail_report.js +26 -0
  153. package/operations/payroll_item.d.ts +83 -0
  154. package/operations/payroll_item.js +26 -0
  155. package/operations/payroll_item_addition.d.ts +95 -0
  156. package/operations/payroll_item_addition.js +38 -0
  157. package/operations/payroll_item_company_contribution.d.ts +105 -0
  158. package/operations/payroll_item_company_contribution.js +38 -0
  159. package/operations/payroll_item_deduction.d.ts +101 -0
  160. package/operations/payroll_item_deduction.js +38 -0
  161. package/operations/payroll_item_direct_deposit.d.ts +65 -0
  162. package/operations/payroll_item_direct_deposit.js +38 -0
  163. package/operations/payroll_item_non_wage.d.ts +92 -0
  164. package/operations/payroll_item_non_wage.js +26 -0
  165. package/operations/payroll_item_tax.d.ts +108 -0
  166. package/operations/payroll_item_tax.js +38 -0
  167. package/operations/payroll_item_wage.d.ts +169 -0
  168. package/operations/payroll_item_wage.js +50 -0
  169. package/operations/payroll_item_workers_compensation.d.ts +77 -0
  170. package/operations/payroll_item_workers_compensation.js +38 -0
  171. package/operations/payroll_last_period.d.ts +31 -0
  172. package/operations/payroll_last_period.js +26 -0
  173. package/operations/payroll_liability_adjustment.d.ts +137 -0
  174. package/operations/payroll_liability_adjustment.js +38 -0
  175. package/operations/payroll_liability_check.d.ts +82 -0
  176. package/operations/payroll_liability_check.js +26 -0
  177. package/operations/payroll_prior_payment.d.ts +126 -0
  178. package/operations/payroll_prior_payment.js +38 -0
  179. package/operations/payroll_summary_report.d.ts +107 -0
  180. package/operations/payroll_summary_report.js +26 -0
  181. package/operations/payroll_year_to_date_adjustment.d.ts +156 -0
  182. package/operations/payroll_year_to_date_adjustment.js +38 -0
  183. package/operations/preferences.d.ts +64 -0
  184. package/operations/preferences.js +38 -0
  185. package/operations/price_level.d.ts +234 -0
  186. package/operations/price_level.js +50 -0
  187. package/operations/purchase_order.d.ts +620 -0
  188. package/operations/purchase_order.js +50 -0
  189. package/operations/qb_command_exec.d.ts +49 -0
  190. package/operations/qb_command_exec.js +26 -0
  191. package/operations/receive_payment.d.ts +450 -0
  192. package/operations/receive_payment.js +50 -0
  193. package/operations/receive_payment_to_deposit.d.ts +36 -0
  194. package/operations/receive_payment_to_deposit.js +26 -0
  195. package/operations/sales_order.d.ts +612 -0
  196. package/operations/sales_order.js +50 -0
  197. package/operations/sales_receipt.d.ts +650 -0
  198. package/operations/sales_receipt.js +50 -0
  199. package/operations/sales_rep.d.ts +170 -0
  200. package/operations/sales_rep.js +50 -0
  201. package/operations/sales_tax_code.d.ts +194 -0
  202. package/operations/sales_tax_code.js +50 -0
  203. package/operations/sales_tax_payable.d.ts +42 -0
  204. package/operations/sales_tax_payable.js +26 -0
  205. package/operations/sales_tax_payment_check.d.ts +254 -0
  206. package/operations/sales_tax_payment_check.js +50 -0
  207. package/operations/sales_tax_return.d.ts +35 -0
  208. package/operations/sales_tax_return.js +26 -0
  209. package/operations/sales_tax_return_line.d.ts +36 -0
  210. package/operations/sales_tax_return_line.js +26 -0
  211. package/operations/ship_method.d.ts +121 -0
  212. package/operations/ship_method.js +38 -0
  213. package/operations/special_account.d.ts +40 -0
  214. package/operations/special_account.js +26 -0
  215. package/operations/special_item.d.ts +44 -0
  216. package/operations/special_item.js +26 -0
  217. package/operations/standard_terms.d.ts +133 -0
  218. package/operations/standard_terms.js +38 -0
  219. package/operations/template.d.ts +36 -0
  220. package/operations/template.js +26 -0
  221. package/operations/terms.d.ts +90 -0
  222. package/operations/terms.js +26 -0
  223. package/operations/time_report.d.ts +84 -0
  224. package/operations/time_report.js +26 -0
  225. package/operations/time_tracking.d.ts +214 -0
  226. package/operations/time_tracking.js +50 -0
  227. package/operations/to_do.d.ts +254 -0
  228. package/operations/to_do.js +50 -0
  229. package/operations/transaction.d.ts +244 -0
  230. package/operations/transaction.js +26 -0
  231. package/operations/transfer.d.ts +178 -0
  232. package/operations/transfer.js +50 -0
  233. package/operations/transfer_inventory.d.ts +290 -0
  234. package/operations/transfer_inventory.js +50 -0
  235. package/operations/txn.d.ts +39 -0
  236. package/operations/txn.js +26 -0
  237. package/operations/txn_deleted.d.ts +44 -0
  238. package/operations/txn_deleted.js +26 -0
  239. package/operations/txn_display.d.ts +69 -0
  240. package/operations/txn_display.js +38 -0
  241. package/operations/txn_void.d.ts +39 -0
  242. package/operations/txn_void.js +26 -0
  243. package/operations/types.d.ts +1259 -0
  244. package/operations/unit_of_measure_set.d.ts +168 -0
  245. package/operations/unit_of_measure_set.js +38 -0
  246. package/operations/vehicle.d.ts +168 -0
  247. package/operations/vehicle.js +50 -0
  248. package/operations/vehicle_mileage.d.ts +147 -0
  249. package/operations/vehicle_mileage.js +38 -0
  250. package/operations/vendor.d.ts +541 -0
  251. package/operations/vendor.js +50 -0
  252. package/operations/vendor_credit.d.ts +365 -0
  253. package/operations/vendor_credit.js +50 -0
  254. package/operations/vendor_type.d.ts +127 -0
  255. package/operations/vendor_type.js +38 -0
  256. package/operations/workers_comp_code.d.ts +199 -0
  257. package/operations/workers_comp_code.js +50 -0
  258. package/package.json +14 -4
package/client.js ADDED
@@ -0,0 +1,660 @@
1
+ "use strict";
2
+
3
+ // The QuBe Sync v2 API client: connections, workflow runs, questions and queued
4
+ // requests. Zero dependencies: Node's own http and https modules.
5
+
6
+ const http = require("http");
7
+ const https = require("https");
8
+ const crypto = require("crypto");
9
+ const util = require("util");
10
+ const { performance } = require("perf_hooks");
11
+
12
+ const DEFAULT_BASE_URL = "https://qubesync.com";
13
+ const API_PATH = "/api/v2";
14
+ const RETRY_STATUSES = new Set([429, 502, 503, 504]);
15
+ const RETRY_AFTER_STATUSES = new Set([429, 503]);
16
+ const RETRY_AFTER_CAP_MS = 30000;
17
+ const RETRY_ERROR_CODES = new Set([
18
+ "ECONNREFUSED",
19
+ "ECONNRESET",
20
+ "ECONNABORTED",
21
+ "EPIPE",
22
+ "ETIMEDOUT",
23
+ "EHOSTUNREACH",
24
+ "ENETUNREACH",
25
+ "EAI_AGAIN",
26
+ ]);
27
+ const RETRY_SAFE_METHODS = new Set(["GET", "PUT", "DELETE"]);
28
+ const QUEUE_METHODS = new Set(["GET", "POST", "PUT", "DELETE"]);
29
+ const MESSAGE_BODY_LIMIT = 500;
30
+ // A retry is made only when at least this much of the call's total time is left after its wait.
31
+ const MIN_TIME_LEFT_MS = 1000;
32
+ // Statuses that mean the request may have reached QuBe Sync and been acted on.
33
+ const MAY_HAVE_ARRIVED_STATUSES = new Set([502, 504]);
34
+ // Network errors raised after the connection opened: the request may have arrived.
35
+ const mayHaveArrived = new WeakSet();
36
+
37
+ function userAgent() {
38
+ // Required lazily: index.js requires this file, and owns VERSION.
39
+ return `qubesync-node/${require("./index").VERSION}`;
40
+ }
41
+
42
+ /** Any answer outside 2xx. `message` is one line, safe to log: it never holds the API key. */
43
+ class APIError extends Error {
44
+ constructor(status, body, requestId) {
45
+ const text = typeof body === "string" ? body : "";
46
+ let errors = null;
47
+ try {
48
+ const parsed = JSON.parse(text);
49
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && "errors" in parsed) {
50
+ errors = parsed.errors === undefined ? null : parsed.errors;
51
+ }
52
+ } catch (_error) {
53
+ errors = null;
54
+ }
55
+ super(`QuBe Sync answered ${status}: ${describe(errors, text, status)}`);
56
+ this.name = "APIError";
57
+ this.status = status;
58
+ this.errors = errors;
59
+ this.code = isPlainObject(errors) && typeof errors.code === "string" ? errors.code : null;
60
+ this.body = text;
61
+ this.requestId = requestId || null;
62
+ }
63
+ }
64
+
65
+ function isPlainObject(value) {
66
+ return value !== null && typeof value === "object" && !Array.isArray(value);
67
+ }
68
+
69
+ function oneLine(text) {
70
+ return String(text).replace(/\s+/g, " ").trim();
71
+ }
72
+
73
+ function describe(errors, body, status) {
74
+ if (typeof errors === "string" && errors.trim() !== "") return oneLine(errors);
75
+ if (isPlainObject(errors)) {
76
+ if (typeof errors.detail === "string") return oneLine(errors.detail);
77
+ const fields = Object.entries(errors).map(([field, messages]) => {
78
+ const list = Array.isArray(messages) ? messages.map(String).join(", ") : String(messages);
79
+ return `${field}: ${list}`;
80
+ });
81
+ if (fields.length > 0) return oneLine(fields.join("; "));
82
+ }
83
+ if (Array.isArray(errors) && errors.length > 0 && errors.every((e) => isPlainObject(e) && "message" in e)) {
84
+ return oneLine(errors.map((e) => (e.path == null ? String(e.message) : `${e.path}: ${e.message}`)).join("; "));
85
+ }
86
+ const raw = oneLine(body);
87
+ if (raw !== "") return raw.slice(0, MESSAGE_BODY_LIMIT);
88
+ return http.STATUS_CODES[status] || "no body";
89
+ }
90
+
91
+ function requireString(value, name) {
92
+ if (typeof value !== "string" || value === "") throw new TypeError(`${name} is required (a non-empty string)`);
93
+ return value;
94
+ }
95
+
96
+ function requireId(value, name) {
97
+ if ((typeof value !== "string" && typeof value !== "number") || value === "") {
98
+ throw new TypeError(`${name} is required`);
99
+ }
100
+ return encodeURIComponent(String(value));
101
+ }
102
+
103
+ function requireQuery(q) {
104
+ if (q === undefined || q === null || typeof q === "object") throw new TypeError("q is required (a string)");
105
+ return String(q);
106
+ }
107
+
108
+ function nonNegative(value, name, fallback) {
109
+ if (value === undefined || value === null) return fallback;
110
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
111
+ throw new TypeError(`${name} must be a non-negative number`);
112
+ }
113
+ return value;
114
+ }
115
+
116
+ function compact(object) {
117
+ const out = {};
118
+ for (const [key, value] of Object.entries(object)) if (value !== undefined && value !== null) out[key] = value;
119
+ return out;
120
+ }
121
+
122
+ // Query strings as the v2 API reads them: an array of scalars repeats its bare key
123
+ // (`list_id=a&list_id=b`); a nested object brackets its keys (`name_range[from]=A`), and an array
124
+ // inside one brackets too (`filter[list_id][]=a`), as Plug decodes them.
125
+ function appendQuery(url, query) {
126
+ if (query === undefined || query === null) return;
127
+ if (!isPlainObject(query)) throw new TypeError("query must be an object");
128
+ for (const [key, value] of Object.entries(query)) appendValue(url, key, value, true);
129
+ }
130
+
131
+ function appendValue(url, key, value, top) {
132
+ if (value === undefined || value === null) return;
133
+ if (Array.isArray(value)) {
134
+ for (const item of value) {
135
+ if (item === undefined || item === null) continue;
136
+ if (Array.isArray(item)) throw new TypeError(`query parameter ${key} can't hold an array of arrays`);
137
+ if (isPlainObject(item)) appendValue(url, `${key}[]`, item, false);
138
+ else url.searchParams.append(top ? key : `${key}[]`, scalar(key, item));
139
+ }
140
+ return;
141
+ }
142
+ if (isPlainObject(value)) {
143
+ for (const [sub, inner] of Object.entries(value)) appendValue(url, `${key}[${sub}]`, inner, false);
144
+ return;
145
+ }
146
+ url.searchParams.append(key, scalar(key, value));
147
+ }
148
+
149
+ function scalar(key, value) {
150
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
151
+ if (value instanceof Date) return value.toISOString();
152
+ throw new TypeError(`query parameter ${key} must be a string, number, boolean, array or plain object`);
153
+ }
154
+
155
+ let operationIndex = null;
156
+
157
+ function operations() {
158
+ if (!operationIndex) operationIndex = require("./operations/index.js");
159
+ return operationIndex;
160
+ }
161
+
162
+ function defaultSleep(ms) {
163
+ return new Promise((resolve) => setTimeout(resolve, ms));
164
+ }
165
+
166
+ function defaultClock() {
167
+ return performance.now();
168
+ }
169
+
170
+ function hasPromptToken(pathOrUrl) {
171
+ try {
172
+ const token = new URL(pathOrUrl, "http://localhost").searchParams.get("prompt_token");
173
+ return typeof token === "string" && token !== "";
174
+ } catch (_error) {
175
+ return false;
176
+ }
177
+ }
178
+
179
+ function timeoutError(message) {
180
+ const error = new Error(message);
181
+ error.code = "ETIMEDOUT";
182
+ return error;
183
+ }
184
+
185
+ function retryAfterMs(response) {
186
+ if (!RETRY_AFTER_STATUSES.has(response.status)) return null;
187
+ const header = response.headers["retry-after"];
188
+ if (typeof header !== "string" || !/^\s*\d+(\.\d+)?\s*$/.test(header)) return null;
189
+ return Math.min(Number(header) * 1000, RETRY_AFTER_CAP_MS);
190
+ }
191
+
192
+ class Client {
193
+ #apiKey;
194
+ #apiRoot;
195
+ #origin;
196
+ #sleep;
197
+ #clock;
198
+ #onRetry;
199
+
200
+ /**
201
+ * @param {object} options
202
+ * @param {string} options.apiKey your app's API key (required; never read from the environment)
203
+ * @param {string} [options.baseUrl="https://qubesync.com"]
204
+ * @param {number} [options.connectTimeout=5000] ms to open the connection
205
+ * @param {number} [options.timeout=30000] ms the socket may sit idle waiting for the answer
206
+ * @param {number} [options.maxRetries=2]
207
+ * @param {number} [options.totalTimeout=60000] ms a call may take, retries and their waits included
208
+ * @param {(info: object) => void} [options.onRetry] called before each retry's wait; a throw stops the call
209
+ * @param {(ms: number) => Promise<void>} [options.sleep] for tests
210
+ * @param {() => number} [options.clock] a monotonic clock in ms, for tests
211
+ */
212
+ constructor(options) {
213
+ if (!isPlainObject(options)) throw new TypeError("new Client({ apiKey }) takes an options object");
214
+ const {
215
+ apiKey,
216
+ baseUrl = DEFAULT_BASE_URL,
217
+ connectTimeout,
218
+ timeout,
219
+ maxRetries,
220
+ totalTimeout,
221
+ onRetry,
222
+ sleep,
223
+ clock,
224
+ } = options;
225
+ if (typeof apiKey !== "string" || apiKey === "") throw new TypeError("apiKey is required (a non-empty string)");
226
+ if (typeof baseUrl !== "string" || baseUrl === "") throw new TypeError("baseUrl must be a non-empty string");
227
+ const trimmed = baseUrl.replace(/\/+$/, "");
228
+ let parsed;
229
+ try {
230
+ parsed = new URL(trimmed);
231
+ } catch (_error) {
232
+ throw new TypeError(`baseUrl isn't a URL: ${trimmed}`);
233
+ }
234
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
235
+ throw new TypeError(`baseUrl must be http or https: ${trimmed}`);
236
+ }
237
+ if (sleep !== undefined && typeof sleep !== "function") throw new TypeError("sleep must be a function");
238
+ if (clock !== undefined && typeof clock !== "function") throw new TypeError("clock must be a function");
239
+ if (onRetry !== undefined && onRetry !== null && typeof onRetry !== "function") {
240
+ throw new TypeError("onRetry must be a function");
241
+ }
242
+ const retries = nonNegative(maxRetries, "maxRetries", 2);
243
+ const total = nonNegative(totalTimeout, "totalTimeout", 60000);
244
+ if (total === 0) throw new TypeError("totalTimeout must be more than 0");
245
+ if (!Number.isInteger(retries)) throw new TypeError("maxRetries must be a whole number");
246
+
247
+ this.#apiKey = apiKey;
248
+ this.#origin = parsed.origin;
249
+ this.#apiRoot = trimmed + API_PATH;
250
+ this.#sleep = sleep || defaultSleep;
251
+ this.#clock = clock || defaultClock;
252
+ this.#onRetry = onRetry || null;
253
+ Object.defineProperties(this, {
254
+ baseUrl: { value: trimmed, enumerable: true },
255
+ connectTimeout: { value: nonNegative(connectTimeout, "connectTimeout", 5000), enumerable: true },
256
+ timeout: { value: nonNegative(timeout, "timeout", 30000), enumerable: true },
257
+ maxRetries: { value: retries, enumerable: true },
258
+ totalTimeout: { value: total, enumerable: true },
259
+ });
260
+ }
261
+
262
+ [util.inspect.custom]() {
263
+ return `Client { baseUrl: ${util.inspect(this.baseUrl)} }`;
264
+ }
265
+
266
+ toJSON() {
267
+ return { baseUrl: this.baseUrl };
268
+ }
269
+
270
+ // -------------------------------------------------------------------------
271
+ // Connections
272
+
273
+ async createConnection({ webhookUrl, name } = {}) {
274
+ return this.#data("POST", "/connections", compact({ webhook_url: requireString(webhookUrl, "webhookUrl"), name }));
275
+ }
276
+
277
+ async getConnection(id) {
278
+ return this.#data("GET", `/connections/${requireId(id, "id")}`);
279
+ }
280
+
281
+ async listConnections() {
282
+ return this.#data("GET", "/connections");
283
+ }
284
+
285
+ async updateConnection(id, { webhookUrl, name } = {}) {
286
+ return this.#data("PUT", `/connections/${requireId(id, "id")}`, compact({ webhook_url: webhookUrl, name }));
287
+ }
288
+
289
+ async deleteConnection(id) {
290
+ return this.#data("DELETE", `/connections/${requireId(id, "id")}`);
291
+ }
292
+
293
+ async regenerateOnboarding(id) {
294
+ return this.#data("POST", `/connections/${requireId(id, "id")}/onboarding-url`);
295
+ }
296
+
297
+ async setPassword(id, { password } = {}) {
298
+ const body = password === undefined || password === null ? undefined : { password };
299
+ return this.#data("POST", `/connections/${requireId(id, "id")}/password`, body);
300
+ }
301
+
302
+ async getQwc(id) {
303
+ const body = await this.request("POST", `/connections/${requireId(id, "id")}/qwc`);
304
+ return isPlainObject(body) && typeof body.qwc === "string" ? body.qwc : null;
305
+ }
306
+
307
+ // -------------------------------------------------------------------------
308
+ // Workflow runs
309
+
310
+ async startRun(connectionId, workflow, input, { reference, webhookUrl, version } = {}) {
311
+ const path = `/connections/${requireId(connectionId, "connectionId")}/workflow-runs`;
312
+ const body = compact({
313
+ workflow: requireString(workflow, "workflow"),
314
+ reference: requireString(reference, "reference"),
315
+ input: input === undefined ? {} : input,
316
+ webhook_url: requireString(webhookUrl, "webhookUrl"),
317
+ version,
318
+ });
319
+ return this.#data("POST", path, body, { headers: { "Idempotency-Key": reference } });
320
+ }
321
+
322
+ async getRun(runUrlOrConnectionId, runId) {
323
+ if (runId === undefined || runId === null) return this.#data("GET", requireString(runUrlOrConnectionId, "runUrl"));
324
+ const path = `/connections/${requireId(runUrlOrConnectionId, "connectionId")}/workflow-runs/${requireId(runId, "runId")}`;
325
+ return this.#data("GET", path);
326
+ }
327
+
328
+ async listRuns(connectionId, { reference, state, outcome } = {}) {
329
+ return this.#data("GET", `/connections/${requireId(connectionId, "connectionId")}/workflow-runs`, undefined, {
330
+ query: { reference, state, outcome },
331
+ });
332
+ }
333
+
334
+ async cancelRun(cancelUrl, { reason } = {}) {
335
+ return this.#data("POST", requireString(cancelUrl, "cancelUrl"), compact({ reason }));
336
+ }
337
+
338
+ /** The whole body: `{ data: [...events], next_after }`. */
339
+ async runEvents(connectionId, runId, { after, limit } = {}) {
340
+ const path = `/connections/${requireId(connectionId, "connectionId")}/workflow-runs/${requireId(runId, "runId")}/events`;
341
+ return this.request("GET", path, undefined, { query: { after, limit } });
342
+ }
343
+
344
+ // -------------------------------------------------------------------------
345
+ // Questions
346
+
347
+ async answer(decisionsUrl, { choice, data } = {}) {
348
+ if (choice === undefined || choice === null) throw new TypeError("choice is required");
349
+ const body = data === undefined || data === null ? { choice } : { choice, data };
350
+ // Safe to send again only while it names the question it answers: without a prompt_token a
351
+ // repeat could be taken for the run's next question.
352
+ return this.#data("POST", requireString(decisionsUrl, "decisionsUrl"), body, { retrySafe: hasPromptToken(decisionsUrl) });
353
+ }
354
+
355
+ async search(searchUrl, { field, q } = {}) {
356
+ return this.#data("GET", requireString(searchUrl, "searchUrl"), undefined, {
357
+ query: { field: requireString(field, "field"), q: requireQuery(q) },
358
+ });
359
+ }
360
+
361
+ // -------------------------------------------------------------------------
362
+ // Queued requests
363
+
364
+ /**
365
+ * Queues any operation by its id (`"customer.create"`), as `qube.customer.create(...)` does.
366
+ * An id this version doesn't know (an operation newer than it) needs `method` and `path`
367
+ * (relative to the connection: `"customers"`).
368
+ */
369
+ async queue(operationId, connectionId, params, { webhookUrl, idempotencyKey, method, path } = {}) {
370
+ const id = requireString(operationId, "operationId");
371
+ const known = Object.prototype.hasOwnProperty.call(operations().OPERATIONS, id) ? operations().OPERATIONS[id] : null;
372
+ if (!known && (typeof method !== "string" || method === "" || typeof path !== "string" || path === "")) {
373
+ throw new TypeError(
374
+ `operation ${JSON.stringify(id)} isn't one this version of qubesync knows: pass method and path (relative to the connection, such as "customers")`
375
+ );
376
+ }
377
+ const verb = (typeof method === "string" && method !== "" ? method : known.method).toUpperCase();
378
+ if (!QUEUE_METHODS.has(verb)) throw new TypeError(`method must be GET, POST, PUT or DELETE, not ${method}`);
379
+ const relative = (typeof path === "string" && path !== "" ? path : known.path).replace(/^\/+/, "");
380
+ if (relative === "") throw new TypeError("path must name the operation, such as \"customers\"");
381
+ const paramsIn = known && !method ? known.paramsIn : verb === "GET" ? "query" : "body";
382
+ const values = params === undefined || params === null ? {} : params;
383
+ if (!isPlainObject(values)) throw new TypeError("params must be an object");
384
+ const query = { webhook_url: requireString(webhookUrl, "webhookUrl") };
385
+ let body;
386
+ if (paramsIn === "query") {
387
+ for (const [key, value] of Object.entries(values)) if (key !== "webhook_url") query[key] = value;
388
+ } else {
389
+ body = values;
390
+ }
391
+ const url = `/connections/${requireId(connectionId, "connectionId")}/${relative}`;
392
+ return this.#data(verb, url, body, { query, headers: { "Idempotency-Key": idempotencyKey || crypto.randomUUID() } });
393
+ }
394
+
395
+ async queueQbxml(connectionId, requestXml, { webhookUrl, idempotencyKey } = {}) {
396
+ const path = `/connections/${requireId(connectionId, "connectionId")}/queued-requests`;
397
+ return this.#data("POST", path, { request_xml: requireString(requestXml, "requestXml") }, {
398
+ query: { webhook_url: requireString(webhookUrl, "webhookUrl") },
399
+ headers: { "Idempotency-Key": idempotencyKey || crypto.randomUUID() },
400
+ });
401
+ }
402
+
403
+ async getRequest(connectionId, id) {
404
+ return this.#data("GET", `/connections/${requireId(connectionId, "connectionId")}/queued-requests/${requireId(id, "id")}`);
405
+ }
406
+
407
+ async requestPages(connectionId, id) {
408
+ return this.#data(
409
+ "GET",
410
+ `/connections/${requireId(connectionId, "connectionId")}/queued-requests/${requireId(id, "id")}/pages`
411
+ );
412
+ }
413
+
414
+ /** The whole body: `{ data: [...requests], meta: { page, total_pages } }`. */
415
+ async listRequests(connectionId, { page, pageSize, state, webhookState, sort, sortDirection, searchPhrase } = {}) {
416
+ return this.request("GET", `/connections/${requireId(connectionId, "connectionId")}/queued-requests`, undefined, {
417
+ query: {
418
+ page,
419
+ page_size: pageSize,
420
+ state,
421
+ webhook_state: webhookState,
422
+ sort,
423
+ sort_direction: sortDirection,
424
+ search_phrase: searchPhrase,
425
+ },
426
+ });
427
+ }
428
+
429
+ /** Every request on every page, from page 1 until `meta.total_pages` (or an empty page). */
430
+ async *eachRequest(connectionId, filters = {}) {
431
+ let page = 1;
432
+ for (;;) {
433
+ const body = await this.listRequests(connectionId, { ...filters, page });
434
+ const data = isPlainObject(body) && Array.isArray(body.data) ? body.data : [];
435
+ for (const request of data) yield request;
436
+ const totalPages = isPlainObject(body) && isPlainObject(body.meta) ? Number(body.meta.total_pages) : NaN;
437
+ if (data.length === 0 || !Number.isFinite(totalPages) || page >= totalPages) return;
438
+ page += 1;
439
+ }
440
+ }
441
+
442
+ async discardRequest(connectionId, id) {
443
+ return this.#data(
444
+ "POST",
445
+ `/connections/${requireId(connectionId, "connectionId")}/queued-requests/${requireId(id, "id")}/discard`
446
+ );
447
+ }
448
+
449
+ // -------------------------------------------------------------------------
450
+ // Anything else
451
+
452
+ /**
453
+ * Sends any v2 call and returns the parsed JSON body whole (null when empty).
454
+ * `pathOrUrl` is a path under /api/v2 ("/connections") or an absolute URL on the
455
+ * client's own origin (a run's or webhook's `links`).
456
+ */
457
+ async request(method, pathOrUrl, body, { query, headers } = {}) {
458
+ return this.#request(method, pathOrUrl, body, { query, headers });
459
+ }
460
+
461
+ async #request(method, pathOrUrl, body, { query, headers, retrySafe = false } = {}) {
462
+ if (typeof method !== "string" || method === "") throw new TypeError("method is required");
463
+ const verb = method.toUpperCase();
464
+ const url = this.#resolve(pathOrUrl);
465
+ appendQuery(url, query);
466
+ const response = await this.#send(verb, url, body, headers || {}, retrySafe);
467
+ if (response.text === "") return null;
468
+ return JSON.parse(response.text);
469
+ }
470
+
471
+ async #data(method, pathOrUrl, body, options) {
472
+ const parsed = await this.#request(method, pathOrUrl, body, options);
473
+ if (isPlainObject(parsed) && Object.prototype.hasOwnProperty.call(parsed, "data")) return parsed.data;
474
+ return parsed;
475
+ }
476
+
477
+ #resolve(pathOrUrl) {
478
+ if (typeof pathOrUrl !== "string" || pathOrUrl === "") throw new TypeError("a path or URL is required");
479
+ if (pathOrUrl.startsWith("/")) return new URL(this.#apiRoot + pathOrUrl);
480
+ let url;
481
+ try {
482
+ url = new URL(pathOrUrl);
483
+ } catch (_error) {
484
+ throw new TypeError(`expected a path starting with / or an absolute URL, got ${JSON.stringify(pathOrUrl)}`);
485
+ }
486
+ if (url.origin !== this.#origin) {
487
+ throw new TypeError(`refusing to send a request to ${url.origin}: this client only calls ${this.#origin}`);
488
+ }
489
+ return url;
490
+ }
491
+
492
+ async #send(method, url, body, extraHeaders, forceRetrySafe) {
493
+ const headers = {
494
+ Accept: "application/json",
495
+ "User-Agent": userAgent(),
496
+ };
497
+ let payload;
498
+ if (body !== undefined && body !== null) {
499
+ payload = Buffer.from(JSON.stringify(body), "utf8");
500
+ headers["Content-Type"] = "application/json";
501
+ headers["Content-Length"] = String(payload.length);
502
+ }
503
+ for (const [name, value] of Object.entries(extraHeaders)) {
504
+ if (value !== undefined && value !== null) headers[name] = String(value);
505
+ }
506
+ headers.Authorization = `Basic ${Buffer.from(`${this.#apiKey}:`, "utf8").toString("base64")}`;
507
+
508
+ const retrySafe =
509
+ forceRetrySafe ||
510
+ RETRY_SAFE_METHODS.has(method) ||
511
+ Object.keys(headers).some((name) => name.toLowerCase() === "idempotency-key");
512
+ const started = this.#clock();
513
+ // A DELETE whose earlier attempt may have deleted the thing: a 404 on its retry is success.
514
+ let deletedEarlier = false;
515
+ const elapsed = () => this.#clock() - started;
516
+
517
+ for (let attempt = 0; ; attempt += 1) {
518
+ const canRetry = retrySafe && attempt < this.maxRetries;
519
+ const left = Math.max(0, this.totalTimeout - elapsed());
520
+ let response;
521
+ try {
522
+ response = await this.#once(method, url, headers, payload, {
523
+ connectTimeout: Math.min(this.connectTimeout, left),
524
+ timeout: Math.min(this.timeout, left),
525
+ });
526
+ } catch (error) {
527
+ if (canRetry && error && RETRY_ERROR_CODES.has(error.code)) {
528
+ if (mayHaveArrived.has(error)) deletedEarlier = true;
529
+ if (await this.#retry(attempt, this.#backoff(attempt), elapsed, method, url, null, error)) continue;
530
+ }
531
+ throw error;
532
+ }
533
+ if (response.status >= 200 && response.status < 300) return response;
534
+ if (response.status === 404 && method === "DELETE" && deletedEarlier) {
535
+ return { status: 204, headers: response.headers, text: "" };
536
+ }
537
+ if (canRetry && RETRY_STATUSES.has(response.status)) {
538
+ const wait = retryAfterMs(response);
539
+ const delay = wait === null ? this.#backoff(attempt) : wait;
540
+ if (MAY_HAVE_ARRIVED_STATUSES.has(response.status)) deletedEarlier = true;
541
+ if (await this.#retry(attempt, delay, elapsed, method, url, response.status, null)) continue;
542
+ }
543
+ const requestId = response.headers["x-request-id"];
544
+ throw new APIError(response.status, response.text, typeof requestId === "string" ? requestId : null);
545
+ }
546
+ }
547
+
548
+ // Waits and answers true when the retry fits in the call's total time; calls onRetry first.
549
+ async #retry(attempt, delayMs, elapsed, method, url, status, error) {
550
+ const elapsedMs = elapsed();
551
+ if (this.totalTimeout - elapsedMs - delayMs < MIN_TIME_LEFT_MS) return false;
552
+ if (this.#onRetry) {
553
+ this.#onRetry(
554
+ Object.freeze({
555
+ attempt: attempt + 1,
556
+ maxRetries: this.maxRetries,
557
+ delayMs,
558
+ elapsedMs,
559
+ method,
560
+ // Never the query: it can hold a search term or a prompt_token.
561
+ url: url.origin + url.pathname,
562
+ status,
563
+ error,
564
+ })
565
+ );
566
+ }
567
+ await this.#sleep(delayMs);
568
+ return true;
569
+ }
570
+
571
+ #backoff(attempt) {
572
+ return 500 * 2 ** attempt * (1 + Math.random() * 0.25);
573
+ }
574
+
575
+ #once(method, url, headers, payload, { connectTimeout, timeout }) {
576
+ const transport = url.protocol === "https:" ? https : http;
577
+ return new Promise((resolve, reject) => {
578
+ let connectTimer = null;
579
+ let failure = null;
580
+ let connected = false;
581
+ const req = transport.request(url, { method, headers });
582
+ const settleError = (error) => {
583
+ if (connected && error && typeof error === "object") mayHaveArrived.add(error);
584
+ reject(error);
585
+ };
586
+ const fail = (error) => {
587
+ if (!failure) failure = error;
588
+ req.destroy(failure);
589
+ };
590
+ const clear = () => {
591
+ if (connectTimer) clearTimeout(connectTimer);
592
+ connectTimer = null;
593
+ };
594
+ const startReadTimer = () => {
595
+ req.setTimeout(timeout, () => fail(timeoutError(`QuBe Sync didn't answer within ${timeout} ms`)));
596
+ };
597
+ req.on("socket", (socket) => {
598
+ if (socket.connecting) {
599
+ connectTimer = setTimeout(
600
+ () => fail(timeoutError(`couldn't connect to ${url.origin} within ${connectTimeout} ms`)),
601
+ connectTimeout
602
+ );
603
+ // Over TLS the request can only arrive once the handshake is done.
604
+ socket.once(url.protocol === "https:" ? "secureConnect" : "connect", () => {
605
+ connected = true;
606
+ });
607
+ socket.once("connect", () => {
608
+ clear();
609
+ startReadTimer();
610
+ });
611
+ } else {
612
+ connected = true;
613
+ startReadTimer();
614
+ }
615
+ });
616
+ req.on("error", (error) => {
617
+ clear();
618
+ settleError(failure || error);
619
+ });
620
+ req.on("close", clear);
621
+ req.on("response", (res) => {
622
+ const chunks = [];
623
+ const cutOff = (error) => {
624
+ clear();
625
+ settleError(failure || error || Object.assign(new Error("the answer was cut off"), { code: "ECONNRESET" }));
626
+ };
627
+ res.on("data", (chunk) => chunks.push(chunk));
628
+ res.on("error", cutOff);
629
+ res.on("aborted", () => cutOff(null));
630
+ res.on("end", () => {
631
+ clear();
632
+ if (failure) return settleError(failure);
633
+ resolve({ status: res.statusCode, headers: res.headers, text: Buffer.concat(chunks).toString("utf8") });
634
+ });
635
+ });
636
+ req.end(payload);
637
+ });
638
+ }
639
+ }
640
+
641
+ // The entity accessors, `qube.customer` and the rest: each loads its operations/<entity>.js on
642
+ // first use, and keeps one instance per client.
643
+ const entityInstances = new WeakMap();
644
+ for (const [name, load] of Object.entries(operations().ENTITIES)) {
645
+ Object.defineProperty(Client.prototype, name, {
646
+ configurable: true,
647
+ enumerable: false,
648
+ get() {
649
+ let mine = entityInstances.get(this);
650
+ if (!mine) entityInstances.set(this, (mine = Object.create(null)));
651
+ if (!mine[name]) {
652
+ const EntityClass = load();
653
+ mine[name] = new EntityClass(this);
654
+ }
655
+ return mine[name];
656
+ },
657
+ });
658
+ }
659
+
660
+ module.exports = { Client, APIError };