wontopos 2.2.27 → 2.2.29
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.
- package/dist/wontopos.js +71 -9
- package/package.json +1 -1
package/dist/wontopos.js
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* runtime's (never disabled by this SDK; Node 18+ floors TLS at 1.2). Prefer
|
|
38
38
|
* `Client.fromEnv()` over keys in source code.
|
|
39
39
|
*/
|
|
40
|
-
const VERSION = "2.2.
|
|
40
|
+
const VERSION = "2.2.29";
|
|
41
41
|
/** Runtime info helps support debug a report ("node 18 on Windows...") —
|
|
42
42
|
* platform only, never anything identifying. Browsers have no `process` (and
|
|
43
43
|
* silently drop the UA header anyway). */
|
|
@@ -102,6 +102,8 @@ function normalizeStoreId(id) {
|
|
|
102
102
|
* oldest rather than refusing new ones means a collision that first shows up late
|
|
103
103
|
* still gets its one warning. */
|
|
104
104
|
const WARNED_STORE_IDS_MAX = 1024;
|
|
105
|
+
/** Same cap, same reason, for the unknown-filter warn set. */
|
|
106
|
+
const WARNED_FILTER_KEYS_MAX = 1024;
|
|
105
107
|
const warnedStoreIds = new Set();
|
|
106
108
|
function warnIfStoreIdCollapses(id) {
|
|
107
109
|
if (!id || warnedStoreIds.has(id))
|
|
@@ -309,6 +311,14 @@ function warnOnUnknownFilters(filters) {
|
|
|
309
311
|
if (KNOWN_FILTER_KEYS.has(k) || warnedFilterKeys.has(k))
|
|
310
312
|
continue;
|
|
311
313
|
warnedFilterKeys.add(k);
|
|
314
|
+
// 2.2.27 capped the store-id warn set and left this sibling unbounded. An app that
|
|
315
|
+
// forwards user-supplied filter keys grows it forever, one entry per distinct typo.
|
|
316
|
+
while (warnedFilterKeys.size > WARNED_FILTER_KEYS_MAX) {
|
|
317
|
+
const oldest = warnedFilterKeys.values().next().value;
|
|
318
|
+
if (oldest === undefined)
|
|
319
|
+
break;
|
|
320
|
+
warnedFilterKeys.delete(oldest);
|
|
321
|
+
}
|
|
312
322
|
console.warn(`wontopos: unknown search filter ${JSON.stringify(k)} — the API drops keys it does not know, ` +
|
|
313
323
|
`so this filter has NO effect and the search is wider than you think. ` +
|
|
314
324
|
`Known keys: ${[...KNOWN_FILTER_KEYS].join(", ")}.`);
|
|
@@ -386,11 +396,30 @@ export class Client {
|
|
|
386
396
|
// An API key on plain HTTP travels readable by anyone on the path. Loopback
|
|
387
397
|
// is fine (local dev / self-hosted on the same box); anything else gets a
|
|
388
398
|
// warning, not an error, so private-network gateways keep working.
|
|
389
|
-
|
|
399
|
+
// Parse, don't split. `http://127.0.0.1:9@evil.example` splits to "127.0.0.1" — the
|
|
400
|
+
// USERINFO, not the host — so this check called it loopback and stayed quiet while
|
|
401
|
+
// the key travelled in cleartext to evil.example. `new URL()` knows the difference.
|
|
402
|
+
// (Python already used urlsplit here; this was the copy that did not.)
|
|
403
|
+
let host;
|
|
404
|
+
try {
|
|
405
|
+
host = new URL(this.base).hostname.toLowerCase();
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
host = ""; // unparseable → not loopback → warn, which is the safe direction
|
|
409
|
+
}
|
|
390
410
|
// Scheme compare is case-insensitive — `HTTP://` connects in plaintext too.
|
|
391
411
|
if (/^http:\/\//i.test(this.base) && !LOOPBACK_HOSTS.has(host)) {
|
|
392
412
|
console.warn("wontopos: baseUrl uses plain HTTP on a non-local host, so the API key travels unencrypted. Use https://.");
|
|
393
413
|
}
|
|
414
|
+
// TypeScript `private` is erased at runtime: `apiKey` is an ordinary enumerable
|
|
415
|
+
// property, so `{...client}` copies it and `JSON.stringify({...client})` prints the
|
|
416
|
+
// key in full. The existing masking covers `JSON.stringify(client)` and
|
|
417
|
+
// `util.inspect(client)` — but spreading an object into a log record is the common
|
|
418
|
+
// shape in structured logging, and it went straight past both. Make the key and the
|
|
419
|
+
// prepared auth header non-enumerable so a spread cannot pick them up; they stay
|
|
420
|
+
// readable inside the class, which is all the code needs.
|
|
421
|
+
Object.defineProperty(this, "apiKey", { enumerable: false });
|
|
422
|
+
Object.defineProperty(this, "headers", { enumerable: false });
|
|
394
423
|
}
|
|
395
424
|
/** A client whose key comes from `WONTOPOS_API_KEY` (or `WOS_API_KEY`) — keeps
|
|
396
425
|
* keys out of source code. Node/Bun/Deno-with-env only. */
|
|
@@ -414,6 +443,24 @@ export class Client {
|
|
|
414
443
|
}
|
|
415
444
|
/** Resolve a call's store: the explicit userId, else the client default. */
|
|
416
445
|
uid(userId) {
|
|
446
|
+
// An OMITTED id means "use the client's default" — that is the documented shortcut.
|
|
447
|
+
// An id that was PASSED but is blank is a different thing: the caller computed a
|
|
448
|
+
// tenant id and got nothing. Falling back there writes one customer's memories into
|
|
449
|
+
// whatever store this client defaults to, silently. Omission is a choice; a blank
|
|
450
|
+
// string is a bug, and it should say so where it happened.
|
|
451
|
+
//
|
|
452
|
+
// The check is `typeof !== "string"`, not `!String(userId).trim()`. The old form
|
|
453
|
+
// only caught a blank STRING, and the value a failed lookup actually produces in
|
|
454
|
+
// JavaScript is `null` — `String(null)` is the truthy "null", so it sailed through
|
|
455
|
+
// the guard and then `null || default` sent the write to the client's default
|
|
456
|
+
// store. `0` (an integer primary key) did the same. Both are the exact outcome the
|
|
457
|
+
// guard was written to stop, and both were still silent.
|
|
458
|
+
//
|
|
459
|
+
// `undefined` alone means "omitted", because that is what an absent argument is.
|
|
460
|
+
if (userId !== undefined && (typeof userId !== "string" || !userId.trim())) {
|
|
461
|
+
throw new Error(`userId must be a non-blank string; got ${JSON.stringify(userId)}. Omit it to use the client's ` +
|
|
462
|
+
"default store, or pass a real store id — anything else would silently write into the default store.");
|
|
463
|
+
}
|
|
417
464
|
const id = userId || this.defaultUser;
|
|
418
465
|
warnIfStoreIdCollapses(id);
|
|
419
466
|
return id;
|
|
@@ -644,8 +691,12 @@ export class Client {
|
|
|
644
691
|
}
|
|
645
692
|
/** Delete a store and ALL its memories. Returns `{ user_id, status }`. */
|
|
646
693
|
deleteStore(userId) {
|
|
647
|
-
|
|
648
|
-
|
|
694
|
+
// Validate BEFORE warning. The warning helper lowercases the id, so a non-string
|
|
695
|
+
// used to die there as "id.toLowerCase is not a function" — the caller got a
|
|
696
|
+
// TypeError from our internals instead of being told what was wrong with the id.
|
|
697
|
+
if (typeof userId !== "string" || !userId.trim())
|
|
698
|
+
throw new Error("userId is required (a non-blank string) — deleteStore never falls back to the default store.");
|
|
699
|
+
warnIfStoreIdCollapses(userId); // destructive: the collision warning belongs here too
|
|
649
700
|
return this.request("DELETE", "/api/v1/memory/collection", { user_id: userId });
|
|
650
701
|
}
|
|
651
702
|
// ----- speakers (who said it) -----
|
|
@@ -669,16 +720,27 @@ export class Client {
|
|
|
669
720
|
delete(userId, memoryId) {
|
|
670
721
|
// Guard: without a memory_id the API's forget endpoint means "delete the
|
|
671
722
|
// whole store". An undefined/"" slipping in here must never become a wipe.
|
|
672
|
-
|
|
673
|
-
|
|
723
|
+
//
|
|
724
|
+
// `.trim()` matters as much as the emptiness check: " " is truthy, so it used to
|
|
725
|
+
// pass this guard and travel as memory_id. A server that trims it back to nothing
|
|
726
|
+
// reads the request as the whole-store form — the single most destructive way for
|
|
727
|
+
// this SDK to be wrong. `deleteAll` below already trimmed; the more dangerous path
|
|
728
|
+
// was the one that did not.
|
|
729
|
+
if (typeof memoryId !== "string" || !memoryId.trim()) {
|
|
730
|
+
throw new Error("memoryId is required (non-blank). To delete every memory in a store, call deleteAll(userId) explicitly.");
|
|
674
731
|
}
|
|
675
|
-
return this.post("/api/v1/memory/forget", { user_id: this.uid(userId), memory_id: memoryId });
|
|
732
|
+
return this.post("/api/v1/memory/forget", { user_id: this.uid(userId), memory_id: memoryId.trim() });
|
|
676
733
|
}
|
|
677
734
|
/** Delete ALL memories for a store (GDPR erase). `userId` is required on purpose -
|
|
678
735
|
* this is destructive, so it never falls back to the default store. */
|
|
679
736
|
deleteAll(userId) {
|
|
680
|
-
if (
|
|
681
|
-
throw new Error("userId is required (non-blank) for deleteAll —
|
|
737
|
+
if (typeof userId !== "string" || !userId.trim())
|
|
738
|
+
throw new Error("userId is required (a non-blank string) for deleteAll — anything else would wipe the default store.");
|
|
739
|
+
// These two take the store id directly instead of going through uid(), so the
|
|
740
|
+
// collision warning — the one that says `Alice.Smith` and `alice_smith` are ONE
|
|
741
|
+
// store — never fired on the two calls that DESTROY data. Erasing the wrong
|
|
742
|
+
// tenant's memories is exactly the outcome that warning exists to prevent.
|
|
743
|
+
warnIfStoreIdCollapses(userId);
|
|
682
744
|
return this.post("/api/v1/memory/forget", { user_id: userId });
|
|
683
745
|
}
|
|
684
746
|
// ----- internal -----
|
package/package.json
CHANGED