wontopos 2.2.40 → 2.2.42

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.
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WME = exports.Client = exports.ServerError = exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.PermissionDeniedError = exports.PaymentRequiredError = exports.AuthenticationError = exports.BadRequestError = exports.APIConnectionError = exports.WosError = exports.CONTEXT_LIMIT_MAX = exports.CONTEXT_LIMIT_MIN = exports.SEARCH_LIMIT_MAX = exports.SEARCH_LIMIT_MIN = void 0;
4
4
  exports._resetStoreIdWarnings = _resetStoreIdWarnings;
5
5
  exports._resetFilterWarnings = _resetFilterWarnings;
6
- const VERSION = "2.2.40";
6
+ const VERSION = "2.2.42";
7
7
  const RUNTIME = (() => {
8
8
  const p = globalThis?.process;
9
9
  return p?.version ? ` (node/${p.version}; ${p.platform}-${p.arch})` : "";
@@ -18,12 +18,29 @@ function logDebug(msg) {
18
18
  const DEFAULT_BASE_URL = "https://api.wontopos.com";
19
19
  const DEFAULT_MODEL = "tablet-2";
20
20
  const RETRY_ALWAYS = new Set([429]);
21
+ const MAX_RETRY_WAIT_MS = 30_000;
22
+ const MAX_TIMER_MS = 2_147_483_647;
21
23
  const RETRY_IF_IDEMPOTENT = new Set([408, 502, 503, 504]);
22
24
  const IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
25
+ const READ_POSTS = new Set([
26
+ "/api/v1/memory/search",
27
+ "/api/v1/memory/recall",
28
+ "/api/v1/memory/get",
29
+ "/api/v1/memory/list",
30
+ "/api/v1/memory/stats",
31
+ "/api/v1/memory/history",
32
+ "/api/v1/memory/lineage",
33
+ "/api/v1/memory/by-speaker",
34
+ "/api/v1/memory/images",
35
+ "/api/v1/memory/image",
36
+ "/api/v1/engram/run",
37
+ "/api/v1/won/revisions",
38
+ ]);
23
39
  const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
24
40
  const MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
25
41
  const MODEL_RE = /^[A-Za-z0-9._-]+$/;
26
42
  const MAX_ERR_MSG = 4096;
43
+ const MAX_ERR_DETAILS = 8192;
27
44
  const IDEMPOTENCY_KEY_RE = /^[A-Za-z0-9._:\-]{1,128}$/;
28
45
  function normalizeStoreId(id) {
29
46
  return id.toLowerCase().replace(/[^a-z0-9_]/g, "_");
@@ -78,6 +95,9 @@ function _resetStoreIdWarnings() {
78
95
  }
79
96
  const NO_BODY_STATUS = new Set([204, 205, 304]);
80
97
  const MAX_PAGES = 20_000;
98
+ function truncatedWalk(why) {
99
+ return new Error(`${why} This is a truncated answer, not the whole store.`);
100
+ }
81
101
  exports.SEARCH_LIMIT_MIN = 5;
82
102
  exports.SEARCH_LIMIT_MAX = 20;
83
103
  exports.CONTEXT_LIMIT_MIN = 0;
@@ -94,6 +114,26 @@ function checkContextLimit(n) {
94
114
  throw new Error(`context_limit must be an integer between ${exports.CONTEXT_LIMIT_MIN} and ${exports.CONTEXT_LIMIT_MAX}, got ${n}.`);
95
115
  }
96
116
  }
117
+ const PAGE_LIMIT_MIN = 5;
118
+ const PAGE_LIMIT_MAX = 20;
119
+ const LIST_LIMIT_MAX = 500;
120
+ const MAX_IMAGES_MAX = 5;
121
+ function checkRange(n, name, min, max) {
122
+ if (typeof n !== "number" || !Number.isInteger(n) || n < min || n > max) {
123
+ throw new Error(`${name} must be an integer between ${min} and ${max}, got ${String(n)}.`);
124
+ }
125
+ }
126
+ function optRange(n, name, min, max) {
127
+ if (n === undefined || n === null)
128
+ return undefined;
129
+ checkRange(n, name, min, max);
130
+ return n;
131
+ }
132
+ function checkMs(v) {
133
+ if (typeof v !== "number" || Number.isNaN(v) || v <= 0)
134
+ return undefined;
135
+ return Math.min(v, MAX_TIMER_MS);
136
+ }
97
137
  function normalizeImage(image) {
98
138
  if (!image || typeof image !== "object") {
99
139
  throw new Error("image must be an object — { data: '<base64>' }.");
@@ -123,14 +163,18 @@ const CONNECT_FAIL_CODES = new Set([
123
163
  "EADDRNOTAVAIL",
124
164
  "UND_ERR_CONNECT_TIMEOUT",
125
165
  ]);
166
+ const CONNECT_SYSCALLS = new Set(["connect", "getaddrinfo"]);
167
+ const CONNECT_ONLY_CODES = new Set(["ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"]);
126
168
  function isConnectFailure(e) {
127
169
  const stack = [e];
128
170
  for (let steps = 0; stack.length && steps < 24; steps++) {
129
171
  const cur = stack.pop();
130
172
  if (!cur || typeof cur !== "object")
131
173
  continue;
132
- if (typeof cur.code === "string" && CONNECT_FAIL_CODES.has(cur.code))
174
+ if (typeof cur.code === "string" &&
175
+ (CONNECT_ONLY_CODES.has(cur.code) || (CONNECT_FAIL_CODES.has(cur.code) && CONNECT_SYSCALLS.has(cur.syscall)))) {
133
176
  return true;
177
+ }
134
178
  if (cur.cause)
135
179
  stack.push(cur.cause);
136
180
  if (Array.isArray(cur.errors))
@@ -138,6 +182,191 @@ function isConnectFailure(e) {
138
182
  }
139
183
  return false;
140
184
  }
185
+ function causeCode(e) {
186
+ const queue = [e];
187
+ for (let steps = 0; queue.length && steps < 24; steps++) {
188
+ const cur = queue.shift();
189
+ if (!cur || typeof cur !== "object")
190
+ continue;
191
+ if (typeof cur.code === "string" && /^[A-Z0-9_]{1,64}$/.test(cur.code))
192
+ return cur.code;
193
+ if (cur.cause)
194
+ queue.push(cur.cause);
195
+ if (Array.isArray(cur.errors))
196
+ queue.push(...cur.errors.slice(0, 8));
197
+ }
198
+ return undefined;
199
+ }
200
+ function cleanServerText(s, max = MAX_ERR_MSG) {
201
+ const clean = s.replace(/[\u0000-\u001f\u007f]/g, "");
202
+ return clean.length > max ? clean.slice(0, max) + "…(truncated)" : clean;
203
+ }
204
+ function cleanDetails(d) {
205
+ if (!d)
206
+ return undefined;
207
+ const clean = (v) => {
208
+ if (typeof v === "string")
209
+ return cleanServerText(v);
210
+ if (Array.isArray(v))
211
+ return v.map(clean);
212
+ if (v && typeof v === "object") {
213
+ return Object.fromEntries(Object.entries(v).map(([k, x]) => [cleanServerText(k), clean(x)]));
214
+ }
215
+ return v;
216
+ };
217
+ let entries;
218
+ try {
219
+ JSON.stringify(d);
220
+ entries = Object.entries(clean(d)).map(([k, v]) => [
221
+ k,
222
+ v,
223
+ JSON.stringify(k).length + JSON.stringify(v).length + 2,
224
+ ]);
225
+ }
226
+ catch {
227
+ return undefined;
228
+ }
229
+ const keep = new Set();
230
+ let size = 2;
231
+ for (const [k, , n] of [...entries].sort((a, b) => a[2] - b[2])) {
232
+ if (size + n > MAX_ERR_DETAILS)
233
+ break;
234
+ keep.add(k);
235
+ size += n;
236
+ }
237
+ if (!keep.size)
238
+ return undefined;
239
+ return Object.fromEntries(entries.filter(([k]) => keep.has(k)).map(([k, v]) => [k, v]));
240
+ }
241
+ function parseErrorText(text) {
242
+ let data;
243
+ try {
244
+ data = JSON.parse(text);
245
+ }
246
+ catch {
247
+ return { message: text };
248
+ }
249
+ if (!data || typeof data !== "object" || Array.isArray(data))
250
+ return { message: text };
251
+ const err = data.error;
252
+ if (err && typeof err === "object" && !Array.isArray(err)) {
253
+ const { message, type, request_id, ...rest } = err;
254
+ return {
255
+ message: typeof message === "string" ? message : typeof type === "string" ? type : text,
256
+ requestId: typeof request_id === "string" ? request_id : undefined,
257
+ type: typeof type === "string" ? type : undefined,
258
+ details: Object.keys(rest).length ? rest : undefined,
259
+ };
260
+ }
261
+ if (typeof err === "string")
262
+ return { message: err };
263
+ if (typeof data.message === "string")
264
+ return { message: data.message };
265
+ return { message: text };
266
+ }
267
+ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
268
+ const IMF_FIXDATE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d{2}) ([A-Z][a-z]{2}) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$/;
269
+ const RFC850_DATE = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d{2})-([A-Z][a-z]{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$/;
270
+ const ASCTIME_DATE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) ([A-Z][a-z]{2}) ([ \d]\d) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/;
271
+ function retryAfterMs(v, now = Date.now()) {
272
+ if (v == null)
273
+ return undefined;
274
+ const s = v.trim();
275
+ if (/^\d+$/.test(s))
276
+ return Math.min(Number(s), Number.MAX_SAFE_INTEGER / 1000) * 1000;
277
+ let parts;
278
+ let m;
279
+ if ((m = IMF_FIXDATE.exec(s)))
280
+ parts = [m[1], m[2], Number(m[3]), m[4], m[5], m[6]];
281
+ else if ((m = RFC850_DATE.exec(s))) {
282
+ let year = 2000 + Number(m[3]);
283
+ if (year > new Date(now).getUTCFullYear() + 50)
284
+ year -= 100;
285
+ parts = [m[1], m[2], year, m[4], m[5], m[6]];
286
+ }
287
+ else if ((m = ASCTIME_DATE.exec(s)))
288
+ parts = [m[2].trim(), m[1], Number(m[6]), m[3], m[4], m[5]];
289
+ if (!parts)
290
+ return undefined;
291
+ const [day, mon, year, hh, mm, ss] = parts;
292
+ const month = MONTHS.indexOf(mon);
293
+ const d = Number(day), h = Number(hh), mi = Number(mm), se = Number(ss);
294
+ if (month < 0 || d < 1 || d > 31 || h > 23 || mi > 59 || se > 60)
295
+ return undefined;
296
+ return Math.max(0, Date.UTC(year, month, d, h, mi, se) - now);
297
+ }
298
+ const BRIEF_BODY_MS = 1_000;
299
+ class SlowBody extends Error {
300
+ }
301
+ const DEFAULT_TRANSPORTS = new WeakSet();
302
+ function defaultTransport() {
303
+ const g = globalThis.fetch;
304
+ if (typeof g !== "function")
305
+ return undefined;
306
+ const bound = g.bind(globalThis);
307
+ DEFAULT_TRANSPORTS.add(bound);
308
+ return bound;
309
+ }
310
+ function discard(res) {
311
+ res.body?.cancel().catch(() => { });
312
+ }
313
+ function tryUrl(s, base) {
314
+ try {
315
+ return base === undefined ? new URL(s) : new URL(s, base);
316
+ }
317
+ catch {
318
+ return undefined;
319
+ }
320
+ }
321
+ function pageHref() {
322
+ try {
323
+ const href = globalThis.location?.href;
324
+ return typeof href === "string" ? href : undefined;
325
+ }
326
+ catch {
327
+ return undefined;
328
+ }
329
+ }
330
+ function parseBase(base) {
331
+ let url = tryUrl(base);
332
+ if (!url) {
333
+ const page = pageHref();
334
+ if (page !== undefined)
335
+ url = tryUrl(base, page);
336
+ }
337
+ return url && (url.protocol === "http:" || url.protocol === "https:") ? url : undefined;
338
+ }
339
+ function asPageRelative(base) {
340
+ return tryUrl(base) ? undefined : tryUrl(base, "http://page.invalid/");
341
+ }
342
+ const BASE_URL_ENDS = /^\s+|\s+$/g;
343
+ function notAUrl(base) {
344
+ return `baseUrl is not a URL: ${JSON.stringify(maskBase(base))} (expected e.g. ${DEFAULT_BASE_URL})`;
345
+ }
346
+ function maskBase(raw) {
347
+ const at = raw.lastIndexOf("@");
348
+ if (at === -1)
349
+ return raw;
350
+ const from = /^(?:[a-z][a-z0-9+.-]*:)?[\\/]*/i.exec(raw)[0].length;
351
+ return `${raw.slice(0, from)}***@${raw.slice(at + 1)}`;
352
+ }
353
+ function maskUserinfo(text) {
354
+ let out = "";
355
+ let done = 0;
356
+ for (let at = text.indexOf("@"); at !== -1; at = text.indexOf("@", at + 1)) {
357
+ let start = at;
358
+ while (start > 0 && !/[\s\\/@]/.test(text[start - 1]))
359
+ start--;
360
+ const run = text.slice(start, at);
361
+ const afterSlash = start > 0 && (text[start - 1] === "/" || text[start - 1] === "\\");
362
+ if (!afterSlash && !run.includes(":"))
363
+ continue;
364
+ const scheme = afterSlash ? "" : (/^[a-z][a-z0-9+.-]*:/i.exec(run)?.[0] ?? "");
365
+ out += `${text.slice(done, start + scheme.length)}***`;
366
+ done = at;
367
+ }
368
+ return out + text.slice(done);
369
+ }
141
370
  function maskKey(key) {
142
371
  return key.length > 12 ? `${key.slice(0, 4)}...${key.slice(-4)}` : "***";
143
372
  }
@@ -188,11 +417,15 @@ function withForm(body, form, tz) {
188
417
  class WosError extends Error {
189
418
  status;
190
419
  requestId;
191
- constructor(status, message, requestId) {
192
- super(`[${status}] ${message}${requestId ? ` (request_id: ${requestId})` : ""}`);
420
+ type;
421
+ details;
422
+ constructor(status, message, requestId, init) {
423
+ super(`[${status}] ${message}${requestId ? ` (request_id: ${requestId})` : ""}`, init?.cause === undefined ? undefined : { cause: init.cause });
193
424
  this.name = new.target.name;
194
425
  this.status = status;
195
426
  this.requestId = requestId;
427
+ this.type = init?.type;
428
+ this.details = init?.details;
196
429
  }
197
430
  }
198
431
  exports.WosError = WosError;
@@ -215,9 +448,21 @@ class NotFoundError extends WosError {
215
448
  }
216
449
  exports.NotFoundError = NotFoundError;
217
450
  class ConflictError extends WosError {
451
+ conflictsWith;
452
+ constructor(status, message, requestId, init) {
453
+ super(status, message, requestId, init);
454
+ const c = init?.details?.conflicts_with;
455
+ this.conflictsWith = typeof c === "string" ? c : undefined;
456
+ }
218
457
  }
219
458
  exports.ConflictError = ConflictError;
220
459
  class RateLimitError extends WosError {
460
+ retryAfter;
461
+ constructor(status, message, requestId, init) {
462
+ super(status, message, requestId, init);
463
+ const r = init?.retryAfter;
464
+ this.retryAfter = typeof r === "number" && Number.isFinite(r) && r >= 0 ? r : undefined;
465
+ }
221
466
  }
222
467
  exports.RateLimitError = RateLimitError;
223
468
  class ServerError extends WosError {
@@ -230,13 +475,15 @@ const STATUS_ERRORS = {
230
475
  403: PermissionDeniedError,
231
476
  404: NotFoundError,
232
477
  409: ConflictError,
478
+ 413: BadRequestError,
479
+ 422: BadRequestError,
233
480
  429: RateLimitError,
234
481
  };
235
- function errorFor(status, message, requestId) {
482
+ function errorFor(status, message, requestId, init) {
236
483
  const Cls = status === 0
237
484
  ? APIConnectionError
238
485
  : STATUS_ERRORS[status] ?? (status >= 500 && status < 600 ? ServerError : WosError);
239
- return new Cls(status, message, requestId);
486
+ return new Cls(status, message, requestId, init);
240
487
  }
241
488
  const KNOWN_FILTER_KEYS = new Set([
242
489
  "categories",
@@ -255,6 +502,8 @@ const KNOWN_RECALL_KEYS = new Set(["form", "tz", "limit", "context_limit"]);
255
502
  const KNOWN_ENGRAM_KEYS = new Set(["form", "tz"]);
256
503
  function checkSearchOpts(opts) {
257
504
  checkOpts(opts, KNOWN_SEARCH_KEYS, "search");
505
+ const o = opts;
506
+ optRange(o.max_images !== undefined ? o.max_images : o.extra?.max_images, "max_images", 0, MAX_IMAGES_MAX);
258
507
  }
259
508
  function checkOpts(opts, known, what) {
260
509
  for (const k of Object.keys(opts)) {
@@ -288,24 +537,33 @@ function editWithin(a, b, max) {
288
537
  }
289
538
  return prev[b.length] <= max;
290
539
  }
291
- function warnOnUnknownFilters(filters) {
292
- if (!filters || typeof filters !== "object" || Array.isArray(filters))
540
+ function warnOnceUnknownKeys(obj, known, warned, message) {
541
+ if (!obj || typeof obj !== "object" || Array.isArray(obj))
293
542
  return;
294
- for (const k of Object.keys(filters)) {
295
- if (KNOWN_FILTER_KEYS.has(k) || warnedFilterKeys.has(k))
543
+ for (const k of Object.keys(obj)) {
544
+ if (known.has(k) || warned.has(k))
296
545
  continue;
297
- warnedFilterKeys.add(k);
298
- while (warnedFilterKeys.size > WARNED_FILTER_KEYS_MAX) {
299
- const oldest = warnedFilterKeys.values().next().value;
546
+ warned.add(k);
547
+ while (warned.size > WARNED_FILTER_KEYS_MAX) {
548
+ const oldest = warned.values().next().value;
300
549
  if (oldest === undefined)
301
550
  break;
302
- warnedFilterKeys.delete(oldest);
551
+ warned.delete(oldest);
303
552
  }
304
- console.warn(`wontopos: unknown search filter ${JSON.stringify(k)} — the API drops keys it does not know, ` +
305
- `so this filter has NO effect and the search is wider than you think. ` +
306
- `Known keys: ${[...KNOWN_FILTER_KEYS].join(", ")}.`);
553
+ console.warn(message(k));
307
554
  }
308
555
  }
556
+ function warnOnUnknownFilters(filters) {
557
+ warnOnceUnknownKeys(filters, KNOWN_FILTER_KEYS, warnedFilterKeys, (k) => `wontopos: unknown search filter ${JSON.stringify(k)} — the API drops keys it does not know, ` +
558
+ `so this filter has NO effect and the search is wider than you think. ` +
559
+ `Known keys: ${[...KNOWN_FILTER_KEYS].join(", ")}.`);
560
+ }
561
+ const KNOWN_METADATA_KEYS = new Set(["speaker", "event_date", "category", "conversation_id"]);
562
+ const warnedMetadataKeys = new Set();
563
+ function warnOnUnknownMetadata(metadata) {
564
+ warnOnceUnknownKeys(metadata, KNOWN_METADATA_KEYS, warnedMetadataKeys, (k) => `wontopos: metadata key ${JSON.stringify(k)} is not kept: the service keeps only ` +
565
+ `${[...KNOWN_METADATA_KEYS].join(", ")} and drops the rest, so this value is not stored.`);
566
+ }
309
567
  function _resetFilterWarnings() {
310
568
  warnedFilterKeys.clear();
311
569
  }
@@ -320,6 +578,9 @@ class Client {
320
578
  deadlineMs;
321
579
  signal;
322
580
  fetchImpl;
581
+ userinfo;
582
+ pageRelative;
583
+ customFetch;
323
584
  defaultUser;
324
585
  _rateLimit = null;
325
586
  get rateLimit() {
@@ -339,25 +600,41 @@ class Client {
339
600
  "en dash; copy the key from a plain-text field");
340
601
  }
341
602
  this.apiKey = key;
342
- this.base = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
343
- const tm = opts.timeoutMs;
344
- this.timeoutMs = typeof tm === "number" && Number.isFinite(tm) && tm > 0 ? tm : 30_000;
603
+ const rawBase = opts.baseUrl ?? DEFAULT_BASE_URL;
604
+ if (typeof rawBase !== "string")
605
+ throw new Error("baseUrl must be a string");
606
+ const trimmed = rawBase.replace(BASE_URL_ENDS, "");
607
+ if (/[\s\\\u0000-\u001f\u007f]/.test(trimmed)) {
608
+ throw new Error(`baseUrl contains whitespace, a backslash or a control character: ${JSON.stringify(maskBase(trimmed))}`);
609
+ }
610
+ this.base = trimmed.replace(/\/+$/, "");
611
+ const url = parseBase(this.base);
612
+ const parsed = url ?? (pageHref() === undefined ? asPageRelative(this.base) : undefined);
613
+ if (!parsed)
614
+ throw new Error(notAUrl(trimmed));
615
+ this.pageRelative = !url;
616
+ this.userinfo = Boolean(parsed.username || parsed.password);
617
+ this.timeoutMs = checkMs(opts.timeoutMs) ?? 30_000;
345
618
  this.model = opts.model ?? DEFAULT_MODEL;
346
619
  if (this.model && !MODEL_RE.test(this.model)) {
347
620
  throw new Error(`invalid model name: ${JSON.stringify(this.model)} (letters, digits, '.', '_', '-' only)`);
348
621
  }
622
+ if (opts && "userId" in opts && opts.userId === undefined) {
623
+ throw new Error("new Client() needs a store id in userId, or no userId option at all. It was given " +
624
+ "undefined. That is usually a lookup that found nothing. Leave userId out to use the default store.");
625
+ }
349
626
  assertUsableStoreId(opts.userId);
350
627
  this.defaultUser = opts.userId ?? DEFAULT_USER;
351
628
  const mr = opts.maxRetries ?? opts.retries;
352
629
  this.maxRetries = typeof mr === "number" && Number.isFinite(mr) ? Math.max(0, Math.floor(mr)) : 2;
353
- const dl = opts.deadlineMs;
354
- this.deadlineMs = typeof dl === "number" && Number.isFinite(dl) && dl > 0 ? dl : undefined;
630
+ this.deadlineMs = checkMs(opts.deadlineMs);
355
631
  this.signal = opts.signal;
356
632
  const f = opts.fetch;
357
633
  if (f !== undefined && typeof f !== "function") {
358
634
  throw new Error("fetch must be a function (a fetch-compatible transport)");
359
635
  }
360
- this.fetchImpl = f ?? (globalThis.fetch ? globalThis.fetch.bind(globalThis) : undefined);
636
+ this.customFetch = f !== undefined && !DEFAULT_TRANSPORTS.has(f);
637
+ this.fetchImpl = (f ?? defaultTransport());
361
638
  if (typeof this.fetchImpl !== "function") {
362
639
  throw new Error("no fetch available in this runtime — pass one as `fetch` (Node 18+ has it built in; older Node needs undici)");
363
640
  }
@@ -368,14 +645,8 @@ class Client {
368
645
  };
369
646
  if (this.model)
370
647
  this.headers["X-WOS-Model"] = this.model;
371
- let host;
372
- try {
373
- host = new URL(this.base).hostname.toLowerCase().replace(/^\[|\]$/g, "");
374
- }
375
- catch {
376
- host = "";
377
- }
378
- if (/^http:\/\//i.test(this.base) && !LOOPBACK_HOSTS.has(host)) {
648
+ const host = url?.hostname.toLowerCase().replace(/^\[|\]$/g, "") ?? "";
649
+ if (url?.protocol === "http:" && !LOOPBACK_HOSTS.has(host)) {
379
650
  console.warn("wontopos: baseUrl uses plain HTTP on a non-local host, so the API key travels unencrypted. Use https://.");
380
651
  }
381
652
  Object.defineProperty(this, "apiKey", { enumerable: false });
@@ -389,10 +660,10 @@ class Client {
389
660
  return new Client({ ...opts, apiKey: key });
390
661
  }
391
662
  toJSON() {
392
- return { baseUrl: this.base, model: this.model, userId: this.defaultUser, apiKey: maskKey(this.apiKey) };
663
+ return { baseUrl: maskBase(this.base), model: this.model, userId: this.defaultUser, apiKey: maskKey(this.apiKey) };
393
664
  }
394
665
  [Symbol.for("nodejs.util.inspect.custom")]() {
395
- return `Client(${this.base}, model=${this.model}, userId=${this.defaultUser}, apiKey=${maskKey(this.apiKey)})`;
666
+ return `Client(${maskBase(this.base)}, model=${this.model}, userId=${this.defaultUser}, apiKey=${maskKey(this.apiKey)})`;
396
667
  }
397
668
  uid(userId) {
398
669
  assertUsableStoreId(userId);
@@ -441,6 +712,7 @@ class Client {
441
712
  const body = { user_id: this.uid(userId), content, metadata };
442
713
  if (opts.image !== undefined)
443
714
  body.image = normalizeImage(opts.image);
715
+ warnOnUnknownMetadata(metadata);
444
716
  return this.post("/api/v1/memory/store", body, opts.idempotencyKey);
445
717
  }
446
718
  async store(content, userId, metadata = {}, opts = {}) {
@@ -461,6 +733,7 @@ class Client {
461
733
  async search(query, userId, limit = 10, opts = {}) {
462
734
  checkSearchOpts(opts);
463
735
  warnOnUnknownFilters(opts.filters);
736
+ limit ??= 10;
464
737
  checkCount(limit, "limit");
465
738
  const { extra, ...known } = opts;
466
739
  const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
@@ -469,6 +742,7 @@ class Client {
469
742
  async searchSelf(query, userId, limit = 10, opts = {}) {
470
743
  checkSearchOpts(opts);
471
744
  warnOnUnknownFilters(opts.filters);
745
+ limit ??= 10;
472
746
  checkCount(limit, "limit");
473
747
  const { extra, ...known } = opts;
474
748
  const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
@@ -477,6 +751,7 @@ class Client {
477
751
  async searchFull(query, userId, limit = 10, opts = {}) {
478
752
  checkSearchOpts(opts);
479
753
  warnOnUnknownFilters(opts.filters);
754
+ limit ??= 10;
480
755
  checkCount(limit, "limit");
481
756
  const { extra, ...known } = opts;
482
757
  const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
@@ -490,11 +765,11 @@ class Client {
490
765
  async recall(query, userId, opts = {}) {
491
766
  checkOpts(opts, KNOWN_RECALL_KEYS, "recall");
492
767
  const body = { user_id: this.uid(userId), query };
493
- if (opts.limit !== undefined) {
768
+ if (opts.limit != null) {
494
769
  checkCount(opts.limit, "limit");
495
770
  body.limit = opts.limit;
496
771
  }
497
- if (opts.context_limit !== undefined) {
772
+ if (opts.context_limit != null) {
498
773
  checkContextLimit(opts.context_limit);
499
774
  body.context_limit = opts.context_limit;
500
775
  }
@@ -524,7 +799,8 @@ class Client {
524
799
  return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId.trim() }).then(memoryFromGet);
525
800
  }
526
801
  async listMemories(userId, opts = {}) {
527
- const body = { user_id: this.uid(userId), limit: opts.limit ?? 100 };
802
+ const limit = optRange(opts.limit, "limit", 1, LIST_LIMIT_MAX);
803
+ const body = { user_id: this.uid(userId), limit: limit ?? 100 };
528
804
  if (opts.cursor)
529
805
  body.cursor = opts.cursor;
530
806
  return this.post("/api/v1/memory/list", body);
@@ -534,16 +810,21 @@ class Client {
534
810
  const seen = new Set();
535
811
  for (let page = 0; page < MAX_PAGES; page++) {
536
812
  const p = await this.listMemories(userId, { limit: opts.pageSize ?? 100, cursor });
537
- for (const m of asRecords(p.memories))
813
+ const rows = asRecords(p.memories);
814
+ for (const m of rows)
538
815
  yield m;
539
816
  const next = p.next_cursor ?? undefined;
540
- if (!next || seen.has(next))
817
+ if (!next)
818
+ return;
819
+ if (seen.has(next)) {
820
+ if (rows.length)
821
+ throw truncatedWalk(`stopped at page ${page + 1}: the service repeated a cursor, so the store did not end.`);
541
822
  return;
823
+ }
542
824
  seen.add(next);
543
825
  cursor = next;
544
826
  }
545
- throw new Error(`stopped after ${MAX_PAGES} pages — the store did not end. This is a truncated ` +
546
- `answer, not the whole store.`);
827
+ throw truncatedWalk(`stopped after ${MAX_PAGES} pages — the store did not end.`);
547
828
  }
548
829
  async exportMemories(userId) {
549
830
  const out = [];
@@ -568,14 +849,16 @@ class Client {
568
849
  throw new Error("memoryId is required — the id of the memory whose image you want removed.");
569
850
  }
570
851
  const body = { user_id: this.uid(userId), memory_id: memoryId.trim() };
571
- if (opts.preview)
572
- body.preview = true;
852
+ if (!opts.preview)
853
+ return this.remove("/api/v1/memory/image", body);
854
+ body.preview = true;
573
855
  return this.request("DELETE", "/api/v1/memory/image", body);
574
856
  }
575
857
  async listImages(userId, opts = {}) {
858
+ const limit = optRange(opts.limit, "limit", PAGE_LIMIT_MIN, PAGE_LIMIT_MAX);
576
859
  const body = { user_id: this.uid(userId) };
577
- if (opts.limit !== undefined)
578
- body.limit = opts.limit;
860
+ if (limit !== undefined)
861
+ body.limit = limit;
579
862
  if (opts.before !== undefined)
580
863
  body.before = opts.before;
581
864
  if (opts.skipIds !== undefined)
@@ -588,26 +871,30 @@ class Client {
588
871
  const seen = new Set();
589
872
  for (let page = 0; page < MAX_PAGES; page++) {
590
873
  const p = await this.listImages(userId, { limit: opts.pageSize, before, skipIds });
591
- for (const m of asRecords(p.images))
874
+ const rows = asRecords(p.images);
875
+ for (const m of rows)
592
876
  yield m;
593
877
  if (!p.has_more || !p.next_before)
594
878
  return;
595
879
  const key = `${p.next_before}|${(Array.isArray(p.next_skip_ids) ? p.next_skip_ids : []).join(",")}`;
596
- if (seen.has(key))
880
+ if (seen.has(key)) {
881
+ if (rows.length)
882
+ throw truncatedWalk(`stopped at page ${page + 1}: the service repeated a cursor, so the store did not end.`);
597
883
  return;
884
+ }
598
885
  seen.add(key);
599
886
  before = p.next_before;
600
887
  skipIds = Array.isArray(p.next_skip_ids) ? p.next_skip_ids : undefined;
601
888
  }
602
- throw new Error(`stopped after ${MAX_PAGES} pages — the store did not end. This is a truncated ` +
603
- `answer, not the whole store.`);
889
+ throw truncatedWalk(`stopped after ${MAX_PAGES} pages — the store did not end.`);
604
890
  }
605
891
  async revisions(userId, opts = {}) {
892
+ const limit = optRange(opts.limit, "limit", PAGE_LIMIT_MIN, PAGE_LIMIT_MAX);
606
893
  const body = {};
607
894
  if (opts.include !== undefined)
608
895
  body.include = opts.include;
609
- if (opts.limit !== undefined)
610
- body.limit = opts.limit;
896
+ if (limit !== undefined)
897
+ body.limit = limit;
611
898
  if (opts.before !== undefined)
612
899
  body.before = opts.before;
613
900
  if (opts.skipIds !== undefined)
@@ -625,9 +912,10 @@ class Client {
625
912
  if (!speaker || typeof speaker !== "string" || speaker.trim() === "") {
626
913
  throw new Error('speaker is required — "me" for the assistant, or a person\'s name.');
627
914
  }
915
+ const limit = optRange(opts.limit, "limit", PAGE_LIMIT_MIN, PAGE_LIMIT_MAX);
628
916
  const body = { user_id: this.uid(userId), speaker: speaker.trim() };
629
- if (opts.limit !== undefined)
630
- body.limit = opts.limit;
917
+ if (limit !== undefined)
918
+ body.limit = limit;
631
919
  if (opts.before !== undefined)
632
920
  body.before = opts.before;
633
921
  if (opts.skipIds !== undefined)
@@ -662,7 +950,7 @@ class Client {
662
950
  if (typeof userId !== "string" || !userId.trim())
663
951
  throw new Error("userId is required (a non-blank string) — deleteStore never falls back to the default store.");
664
952
  warnIfStoreIdCollapses(userId);
665
- return this.request("DELETE", "/api/v1/memory/collection", { user_id: userId });
953
+ return this.remove("/api/v1/memory/collection", { user_id: userId });
666
954
  }
667
955
  async addSpeaker(speaker, userId) {
668
956
  return this.post("/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
@@ -672,7 +960,7 @@ class Client {
672
960
  return { ...r, speakers: asRecords(r?.speakers) };
673
961
  }
674
962
  async removeSpeaker(speaker, userId) {
675
- return this.request("DELETE", "/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
963
+ return this.remove("/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
676
964
  }
677
965
  async delete(userId, memoryId) {
678
966
  if (typeof memoryId !== "string" || !memoryId.trim()) {
@@ -689,22 +977,26 @@ class Client {
689
977
  post(path, body, idempotencyKey) {
690
978
  return this.request("POST", path, body, idempotencyKey);
691
979
  }
980
+ remove(path, body) {
981
+ return this.request("DELETE", path, body, undefined, true);
982
+ }
692
983
  deadlineAt() {
693
984
  return this.deadlineMs === undefined ? undefined : Date.now() + this.deadlineMs;
694
985
  }
695
- beginAttempt(deadlineAt) {
986
+ beginAttempt(deadlineAt, answer) {
696
987
  let why = "timeout";
697
988
  let budget = this.timeoutMs;
698
989
  if (deadlineAt !== undefined) {
699
990
  const left = deadlineAt - Date.now();
700
991
  if (left <= 0)
701
- throw new APIConnectionError(0, this.abortMessage("deadline"));
992
+ throw answer ?? new APIConnectionError(0, this.abortMessage("deadline"));
702
993
  if (left < budget) {
703
994
  budget = left;
704
995
  why = "deadline";
705
996
  }
706
997
  }
707
998
  const ctrl = new AbortController();
999
+ const endsAt = Date.now() + budget;
708
1000
  const timer = setTimeout(() => ctrl.abort(), budget);
709
1001
  const outer = this.signal;
710
1002
  const onCaller = () => {
@@ -724,6 +1016,7 @@ class Client {
724
1016
  outer?.removeEventListener("abort", onCaller);
725
1017
  },
726
1018
  why: () => why,
1019
+ endsAt,
727
1020
  };
728
1021
  }
729
1022
  abortMessage(why) {
@@ -733,10 +1026,10 @@ class Client {
733
1026
  return `deadline of ${this.deadlineMs}ms exhausted`;
734
1027
  return `request timed out after ${this.timeoutMs}ms`;
735
1028
  }
736
- async backoffSleep(ms, deadlineAt) {
737
- if (deadlineAt !== undefined && ms > deadlineAt - Date.now()) {
738
- throw new APIConnectionError(0, this.abortMessage("deadline"));
739
- }
1029
+ fits(ms, deadlineAt) {
1030
+ return deadlineAt === undefined || ms <= deadlineAt - Date.now();
1031
+ }
1032
+ async pause(ms) {
740
1033
  const outer = this.signal;
741
1034
  if (!outer) {
742
1035
  await new Promise((r) => setTimeout(r, ms));
@@ -754,17 +1047,83 @@ class Client {
754
1047
  outer.addEventListener("abort", done);
755
1048
  });
756
1049
  }
757
- async requestBytes(path, body) {
1050
+ networkError(e, path) {
1051
+ let text = typeof e?.message === "string" && e.message ? e.message : String(e);
1052
+ const q = path.indexOf("?");
1053
+ if (q !== -1) {
1054
+ const query = path.slice(q);
1055
+ text = text.split(query).join("");
1056
+ try {
1057
+ text = text.split(decodeURIComponent(query)).join("");
1058
+ }
1059
+ catch {
1060
+ }
1061
+ }
1062
+ text = cleanServerText(maskUserinfo(text), 512);
1063
+ const code = causeCode(e);
1064
+ if (code && !text.includes(code))
1065
+ text += ` (${code})`;
1066
+ const quotable = this.userinfo || q !== -1;
1067
+ return new APIConnectionError(0, `network error: ${text}`, undefined, quotable ? undefined : { cause: e });
1068
+ }
1069
+ isRedirected(res) {
1070
+ return (res.status >= 300 && res.status < 400) || res.type === "opaqueredirect" || res.redirected === true;
1071
+ }
1072
+ errorFromAnswer(res, body, retryAfter, mayBeDeleted = false, unread) {
1073
+ const e = parseErrorText(new TextDecoder().decode(body));
1074
+ let msg = cleanServerText(e.message) || `HTTP ${res.status}`;
1075
+ if (unread)
1076
+ msg += ` (the error body could not be read: ${cleanServerText(unread, 512)})`;
1077
+ const requestId = e.requestId === undefined ? undefined : cleanServerText(e.requestId, 256) || undefined;
1078
+ const err = errorFor(res.status, msg, requestId, {
1079
+ type: e.type === undefined ? undefined : cleanServerText(e.type, 256) || undefined,
1080
+ details: cleanDetails(e.details),
1081
+ retryAfter: retryAfter === undefined ? undefined : Math.ceil(retryAfter / 1000),
1082
+ });
1083
+ if (mayBeDeleted && err instanceof NotFoundError) {
1084
+ err.message += " (an earlier attempt may already have deleted it)";
1085
+ }
1086
+ return err;
1087
+ }
1088
+ async send(method, path, payload, extraHeaders, removes = false, reads = false) {
1089
+ if (this.pageRelative && !this.customFetch && !parseBase(this.base)) {
1090
+ throw new Error(`${notAUrl(this.base)}. A relative baseUrl needs a page to resolve against, and this runtime has none.`);
1091
+ }
758
1092
  const attempts = this.maxRetries + 1;
1093
+ const upper = method.toUpperCase();
1094
+ const idempotent = IDEMPOTENT_METHODS.has(upper);
1095
+ const logPath = path.split("?")[0];
759
1096
  const deadlineAt = this.deadlineAt();
1097
+ const headers = extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers;
1098
+ let ambiguous = false;
1099
+ let refused;
760
1100
  for (let attempt = 0;; attempt++) {
761
- const att = this.beginAttempt(deadlineAt);
1101
+ const last = attempt + 1 >= attempts;
1102
+ const tag = `(attempt ${attempt + 1}/${attempts})`;
1103
+ const start = Date.now();
1104
+ const previous = refused;
1105
+ refused = undefined;
1106
+ const att = this.beginAttempt(deadlineAt, previous);
1107
+ const pauseWithoutAnswer = async (why) => {
1108
+ const delay = this.backoffMs(attempt);
1109
+ if (!this.fits(delay, deadlineAt))
1110
+ throw previous ?? new APIConnectionError(0, this.abortMessage("deadline"));
1111
+ logDebug(`${method} ${logPath}: ${why}. Retrying in ${Math.round(delay)}ms ${tag}`);
1112
+ await this.pause(delay);
1113
+ };
1114
+ const cutShort = () => {
1115
+ const why = att.why();
1116
+ if (why === "deadline" && (idempotent || reads || (upper === "POST" && READ_POSTS.has(logPath))) && previous) {
1117
+ return previous;
1118
+ }
1119
+ return new APIConnectionError(0, this.abortMessage(why));
1120
+ };
762
1121
  let res;
763
1122
  try {
764
1123
  res = await this.fetchImpl(`${this.base}${path}`, {
765
- method: "POST",
766
- headers: this.headers,
767
- body: JSON.stringify(body),
1124
+ method,
1125
+ headers,
1126
+ body: payload,
768
1127
  signal: att.signal,
769
1128
  redirect: "manual",
770
1129
  });
@@ -772,63 +1131,109 @@ class Client {
772
1131
  catch (e) {
773
1132
  att.clear();
774
1133
  const timedOut = e?.name === "AbortError" || att.signal.aborted;
775
- if (!timedOut && isConnectFailure(e) && attempt + 1 < attempts) {
776
- await this.backoffSleep(this.backoffMs(attempt), deadlineAt);
1134
+ if (timedOut)
1135
+ throw cutShort();
1136
+ const connect = isConnectFailure(e);
1137
+ if ((idempotent || connect) && !last) {
1138
+ if (!connect)
1139
+ ambiguous = true;
1140
+ await pauseWithoutAnswer(e?.code ?? e?.name ?? "network error");
777
1141
  continue;
778
1142
  }
779
- throw new APIConnectionError(0, timedOut ? this.abortMessage(att.why()) : `network error: ${e?.message ?? e}`);
1143
+ throw this.networkError(e, path);
780
1144
  }
781
- try {
782
- this._rateLimit = parseRateLimit(res.headers) ?? this._rateLimit;
783
- if ((res.status >= 300 && res.status < 400) || res.type === "opaqueredirect") {
784
- void res.body?.cancel();
785
- throw errorFor(res.status, "the API answered with a redirect; refusing to follow it");
1145
+ const readBody = async () => {
1146
+ try {
1147
+ return await this.readCappedBytes(res);
786
1148
  }
787
- if (!res.ok) {
788
- if (RETRY_ALWAYS.has(res.status) && attempt + 1 < attempts) {
789
- const retryAfter = res.headers.get("Retry-After");
790
- void res.body?.cancel();
791
- att.clear();
792
- await this.backoffSleep(this.backoffMs(attempt, retryAfter), deadlineAt);
793
- continue;
794
- }
795
- let msg = await this.readCapped(res).catch(() => "");
796
- try {
797
- const data = JSON.parse(msg);
798
- const err = data?.error;
799
- if (err && typeof err === "object")
800
- msg = err.message ?? err.type ?? msg;
801
- else if (typeof err === "string")
802
- msg = err;
803
- }
804
- catch {
805
- }
806
- if (msg.length > MAX_ERR_MSG)
807
- msg = msg.slice(0, MAX_ERR_MSG) + "…(truncated)";
808
- throw errorFor(res.status, msg || `HTTP ${res.status}`);
1149
+ catch (e) {
1150
+ if (e?.name === "AbortError" || att.signal.aborted)
1151
+ throw cutShort();
1152
+ if (e instanceof WosError)
1153
+ throw e;
1154
+ if (idempotent && !last)
1155
+ return null;
1156
+ throw this.networkError(e, path);
809
1157
  }
810
- let buf;
1158
+ };
1159
+ const readErrorBody = async (withinMs) => {
811
1160
  try {
812
- buf = await this.readCappedBytes(res);
1161
+ return { body: await this.readCappedBytes(res, withinMs) };
813
1162
  }
814
1163
  catch (e) {
815
- if (e?.name === "AbortError" || att.signal.aborted) {
816
- throw new APIConnectionError(0, this.abortMessage(att.why()));
1164
+ const aborted = e?.name === "AbortError" || att.signal.aborted;
1165
+ if (aborted && att.why() === "caller")
1166
+ throw new APIConnectionError(0, this.abortMessage("caller"));
1167
+ const unread = aborted
1168
+ ? this.abortMessage(att.why())
1169
+ : e instanceof SlowBody
1170
+ ? e.message
1171
+ : (e instanceof WosError ? e : this.networkError(e, path)).message.replace(/^\[\d+\] /, "");
1172
+ return { body: new Uint8Array(0), unread };
1173
+ }
1174
+ };
1175
+ try {
1176
+ if (this.isRedirected(res)) {
1177
+ discard(res);
1178
+ throw new WosError(res.status, "unexpected redirect — refused (the API key never follows a redirect). Check baseUrl: exact host, https://.");
1179
+ }
1180
+ this._rateLimit = parseRateLimit(res.headers) ?? this._rateLimit;
1181
+ if (!res.ok) {
1182
+ const ra = retryAfterMs(res.headers.get("Retry-After"));
1183
+ let wait = RETRY_ALWAYS.has(res.status) || (RETRY_IF_IDEMPOTENT.has(res.status) && idempotent)
1184
+ ? ra ?? this.backoffMs(attempt)
1185
+ : undefined;
1186
+ const retrying = wait !== undefined && !last && wait <= MAX_RETRY_WAIT_MS && this.fits(wait, deadlineAt);
1187
+ const { body, unread } = await readErrorBody(retrying ? Math.max(0, Math.min(BRIEF_BODY_MS, att.endsAt - Date.now())) : undefined);
1188
+ if (res.status === 409) {
1189
+ const d = parseErrorText(new TextDecoder().decode(body)).details;
1190
+ const ms = d?.retry_after_ms;
1191
+ if (typeof ms === "number" && Number.isFinite(ms) && ms >= 0 && d?.conflicts_with === undefined) {
1192
+ wait = Math.max(ms, this.backoffMs(attempt));
1193
+ }
817
1194
  }
818
- if (e instanceof WosError)
819
- throw e;
820
- throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
1195
+ if (wait !== undefined && !last) {
1196
+ if (wait > MAX_RETRY_WAIT_MS) {
1197
+ logDebug(`${method} ${logPath} -> ${res.status}: asked to wait ${Math.round(wait)}ms, over the cap. Not retrying ${tag}`);
1198
+ }
1199
+ else if (!this.fits(wait, deadlineAt)) {
1200
+ logDebug(`${method} ${logPath} -> ${res.status}: a ${Math.round(wait)}ms wait does not fit the deadline. Not retrying ${tag}`);
1201
+ }
1202
+ else {
1203
+ if (RETRY_IF_IDEMPOTENT.has(res.status))
1204
+ ambiguous = true;
1205
+ refused = this.errorFromAnswer(res, body, ra, ambiguous && removes, unread);
1206
+ att.clear();
1207
+ logDebug(`${method} ${logPath} -> ${res.status}. Retrying in ${Math.round(wait)}ms ${tag}`);
1208
+ await this.pause(wait);
1209
+ continue;
1210
+ }
1211
+ }
1212
+ logDebug(`${method} ${logPath} -> ${res.status} in ${Date.now() - start}ms ${tag}`);
1213
+ throw this.errorFromAnswer(res, body, ra, ambiguous && removes, unread);
821
1214
  }
822
- if (buf.byteLength === 0) {
823
- throw new WosError(res.status, "empty image body — the service returned no bytes");
1215
+ const body = await readBody();
1216
+ if (body === null) {
1217
+ ambiguous = true;
1218
+ att.clear();
1219
+ await pauseWithoutAnswer("body dropped mid-stream");
1220
+ continue;
824
1221
  }
825
- return { bytes: buf, contentType: res.headers.get("content-type") ?? "application/octet-stream" };
1222
+ logDebug(`${method} ${logPath} -> ${res.status} in ${Date.now() - start}ms ${tag}`);
1223
+ return { res, body };
826
1224
  }
827
1225
  finally {
828
1226
  att.clear();
829
1227
  }
830
1228
  }
831
1229
  }
1230
+ async requestBytes(path, body) {
1231
+ const { res, body: buf } = await this.send("POST", path, JSON.stringify(body), undefined, false, true);
1232
+ if (buf.byteLength === 0) {
1233
+ throw new WosError(res.status, "empty image body — the service returned no bytes");
1234
+ }
1235
+ return { bytes: buf, contentType: res.headers.get("content-type") ?? "application/octet-stream" };
1236
+ }
832
1237
  idemHeader(key) {
833
1238
  if (key === undefined || key === null)
834
1239
  return undefined;
@@ -838,58 +1243,72 @@ class Client {
838
1243
  return { "Idempotency-Key": key };
839
1244
  }
840
1245
  backoffMs(attempt, retryAfter) {
841
- if (retryAfter) {
842
- const s = Number(retryAfter);
843
- if (Number.isFinite(s) && s >= 0)
844
- return Math.min(30_000, s * 1000);
845
- const t = Date.parse(retryAfter);
846
- if (Number.isFinite(t))
847
- return Math.min(30_000, Math.max(0, t - Date.now()));
848
- }
1246
+ const ra = retryAfterMs(retryAfter ?? null);
1247
+ if (ra !== undefined)
1248
+ return ra;
849
1249
  return Math.min(8_000, 500 * 2 ** attempt) + Math.random() * 250;
850
1250
  }
851
- async readCapped(res) {
852
- return new TextDecoder().decode(await this.readCappedBytes(res));
853
- }
854
- async readCappedBytes(res) {
1251
+ async readCappedBytes(res, withinMs) {
855
1252
  const cl = res.headers.get("content-length");
856
1253
  if (cl && Number(cl) > MAX_RESPONSE_BYTES) {
1254
+ discard(res);
857
1255
  throw new WosError(res.status, `response too large (${cl} bytes) — refusing to buffer it`);
858
1256
  }
859
- if (!res.body) {
860
- const buf = new Uint8Array(await res.arrayBuffer());
861
- if (buf.byteLength > MAX_RESPONSE_BYTES) {
862
- throw new WosError(res.status, `response too large (${buf.byteLength} bytes) — refusing it`);
1257
+ const slow = () => new SlowBody(`not received within ${withinMs}ms`);
1258
+ let late = false;
1259
+ let reader;
1260
+ let stop = () => { };
1261
+ const stopped = new Promise((_, reject) => (stop = () => reject(slow())));
1262
+ stopped.catch(() => { });
1263
+ const timer = withinMs === undefined
1264
+ ? undefined
1265
+ : setTimeout(() => {
1266
+ late = true;
1267
+ stop();
1268
+ reader?.cancel().catch(() => { });
1269
+ }, withinMs);
1270
+ const within = (p) => (timer === undefined ? p : Promise.race([p, stopped]));
1271
+ try {
1272
+ if (!res.body) {
1273
+ const buf = new Uint8Array(await within(res.arrayBuffer()));
1274
+ if (buf.byteLength > MAX_RESPONSE_BYTES) {
1275
+ throw new WosError(res.status, `response too large (${buf.byteLength} bytes) — refusing it`);
1276
+ }
1277
+ return buf;
1278
+ }
1279
+ reader = res.body.getReader();
1280
+ const parts = [];
1281
+ let size = 0;
1282
+ for (;;) {
1283
+ const { done, value } = await within(reader.read());
1284
+ if (late)
1285
+ throw slow();
1286
+ if (done)
1287
+ break;
1288
+ size += value.byteLength;
1289
+ if (size > MAX_RESPONSE_BYTES) {
1290
+ reader.cancel().catch(() => { });
1291
+ throw new WosError(res.status, "response too large — refusing to buffer it");
1292
+ }
1293
+ parts.push(value);
1294
+ }
1295
+ const buf = new Uint8Array(size);
1296
+ let off = 0;
1297
+ for (const p of parts) {
1298
+ buf.set(p, off);
1299
+ off += p.byteLength;
863
1300
  }
864
1301
  return buf;
865
1302
  }
866
- const reader = res.body.getReader();
867
- const parts = [];
868
- let size = 0;
869
- for (;;) {
870
- const { done, value } = await reader.read();
871
- if (done)
872
- break;
873
- size += value.byteLength;
874
- if (size > MAX_RESPONSE_BYTES) {
875
- void reader.cancel();
876
- throw new WosError(res.status, "response too large — refusing to buffer it");
877
- }
878
- parts.push(value);
1303
+ catch (e) {
1304
+ throw late && !(e instanceof SlowBody) ? slow() : e;
879
1305
  }
880
- const buf = new Uint8Array(size);
881
- let off = 0;
882
- for (const p of parts) {
883
- buf.set(p, off);
884
- off += p.byteLength;
1306
+ finally {
1307
+ clearTimeout(timer);
885
1308
  }
886
- return buf;
887
1309
  }
888
- async request(method, path, body, idempotencyKey) {
1310
+ async request(method, path, body, idempotencyKey, removes = false) {
889
1311
  const extraHeaders = this.idemHeader(idempotencyKey);
890
- const attempts = this.maxRetries + 1;
891
- const logPath = path.split("?")[0];
892
- const deadlineAt = this.deadlineAt();
893
1312
  let payload;
894
1313
  try {
895
1314
  payload = body === undefined ? undefined : JSON.stringify(body);
@@ -898,117 +1317,28 @@ class Client {
898
1317
  throw new Error(`request body could not be serialized: ${e instanceof Error ? e.message : String(e)}. ` +
899
1318
  "Pass plain JSON values — an ORM entity, a BigInt or a circular reference cannot be sent.");
900
1319
  }
901
- for (let attempt = 0; attempt < attempts; attempt++) {
902
- const start = Date.now();
903
- const att = this.beginAttempt(deadlineAt);
904
- let res;
905
- try {
906
- res = await this.fetchImpl(`${this.base}${path}`, {
907
- method,
908
- headers: extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers,
909
- body: payload,
910
- signal: att.signal,
911
- redirect: "manual",
912
- });
913
- }
914
- catch (e) {
915
- att.clear();
916
- const timedOut = e?.name === "AbortError" || att.signal.aborted;
917
- const safe = IDEMPOTENT_METHODS.has(method.toUpperCase()) || isConnectFailure(e);
918
- if (!timedOut && safe && attempt + 1 < attempts) {
919
- const delay = this.backoffMs(attempt);
920
- logDebug(`${method} ${logPath}: ${e?.code ?? e?.name ?? "network error"} — retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
921
- await this.backoffSleep(delay, deadlineAt);
922
- continue;
923
- }
924
- throw new APIConnectionError(0, timedOut ? this.abortMessage(att.why()) : `network error: ${e?.message ?? e}`);
925
- }
926
- const retryable = RETRY_ALWAYS.has(res.status) ||
927
- (RETRY_IF_IDEMPOTENT.has(res.status) && IDEMPOTENT_METHODS.has(method.toUpperCase()));
928
- if (retryable && attempt + 1 < attempts) {
929
- att.clear();
930
- const retryAfter = res.headers.get("Retry-After");
931
- void res.body?.cancel();
932
- const delay = this.backoffMs(attempt, retryAfter);
933
- logDebug(`${method} ${logPath} -> ${res.status} — retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
934
- await this.backoffSleep(delay, deadlineAt);
935
- continue;
936
- }
937
- if ((res.status >= 300 && res.status < 400) || res.type === "opaqueredirect") {
938
- att.clear();
939
- void res.body?.cancel();
940
- throw new WosError(res.status, "unexpected redirect — refused (the API key never follows a redirect). Check baseUrl: exact host, https://.");
941
- }
942
- this._rateLimit = parseRateLimit(res.headers) ?? this._rateLimit;
943
- let text;
944
- try {
945
- text = await this.readCapped(res);
946
- }
947
- catch (e) {
948
- if (e?.name === "AbortError" || att.signal.aborted) {
949
- throw new APIConnectionError(0, this.abortMessage(att.why()));
950
- }
951
- if (e instanceof WosError)
952
- throw e;
953
- if (!(IDEMPOTENT_METHODS.has(method.toUpperCase()) && attempt + 1 < attempts)) {
954
- throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
955
- }
956
- att.clear();
957
- const delay = this.backoffMs(attempt);
958
- logDebug(`${method} ${logPath} — body dropped mid-stream, retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
959
- await this.backoffSleep(delay, deadlineAt);
960
- continue;
961
- }
962
- finally {
963
- att.clear();
964
- }
965
- logDebug(`${method} ${logPath} -> ${res.status} in ${Date.now() - start}ms (attempt ${attempt + 1}/${attempts})`);
966
- if (!res.ok) {
967
- let msg = text;
968
- let requestId;
969
- try {
970
- const data = JSON.parse(text);
971
- if (data && typeof data === "object") {
972
- const err = data.error;
973
- if (err && typeof err === "object") {
974
- msg = err.message ?? err.type ?? msg;
975
- if (typeof err.request_id === "string")
976
- requestId = err.request_id;
977
- }
978
- else if (typeof err === "string")
979
- msg = err;
980
- else if (typeof data.message === "string")
981
- msg = data.message;
982
- }
983
- }
984
- catch {
985
- }
986
- if (typeof msg === "string" && msg.length > MAX_ERR_MSG)
987
- msg = msg.slice(0, MAX_ERR_MSG) + "…(truncated)";
988
- throw errorFor(res.status, msg, requestId);
989
- }
990
- if (!text) {
991
- if (NO_BODY_STATUS.has(res.status))
992
- return {};
993
- throw new WosError(res.status, "empty response body — expected a JSON object");
994
- }
995
- let data;
996
- try {
997
- data = JSON.parse(text);
998
- }
999
- catch (e) {
1000
- throw new WosError(res.status, `invalid JSON in response: ${e?.message ?? e}`);
1001
- }
1002
- if (data === null || typeof data !== "object" || Array.isArray(data)) {
1003
- const got = data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
1004
- throw new WosError(res.status, `expected a JSON object in the response, got ${got}`);
1005
- }
1006
- if (res.headers.get("Idempotent-Replayed") === "true" && !("replayed" in data)) {
1007
- data.replayed = true;
1008
- }
1009
- return data;
1320
+ const { res, body: buf } = await this.send(method, path, payload, extraHeaders, removes);
1321
+ const text = new TextDecoder().decode(buf);
1322
+ if (!text) {
1323
+ if (NO_BODY_STATUS.has(res.status))
1324
+ return {};
1325
+ throw new WosError(res.status, "empty response body — expected a JSON object");
1326
+ }
1327
+ let data;
1328
+ try {
1329
+ data = JSON.parse(text);
1330
+ }
1331
+ catch (e) {
1332
+ throw new WosError(res.status, `invalid JSON in response: ${cleanServerText(String(e?.message ?? e), 512)}`);
1333
+ }
1334
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
1335
+ const got = data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
1336
+ throw new WosError(res.status, `expected a JSON object in the response, got ${got}`);
1337
+ }
1338
+ if (res.headers.get("Idempotent-Replayed") === "true" && !("replayed" in data)) {
1339
+ data.replayed = true;
1010
1340
  }
1011
- throw new Error("retries exhausted");
1341
+ return data;
1012
1342
  }
1013
1343
  }
1014
1344
  exports.Client = Client;