wontopos 2.2.26 → 2.2.28
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.d.ts +8 -1
- package/dist/wontopos.js +80 -6
- package/package.json +1 -1
package/dist/wontopos.d.ts
CHANGED
|
@@ -418,7 +418,14 @@ export declare class Client {
|
|
|
418
418
|
addBulk(content: string, userId?: string, category?: string, timestamp?: string, opts?: WriteOptions): Promise<StatusResult>;
|
|
419
419
|
/** Supersede an old memory with new content. Payload first, userId last — same shape as add/search. */
|
|
420
420
|
update(oldMemoryId: string, newContent: string, userId?: string, opts?: WriteOptions): Promise<UpdateResult>;
|
|
421
|
-
/** Semantic search over a store's memories. Returns memories, most relevant first.
|
|
421
|
+
/** Semantic search over a store's memories. Returns memories, most relevant first.
|
|
422
|
+
*
|
|
423
|
+
* `limit` bounds the MAIN lane, not the returned array. On a self-lane model
|
|
424
|
+
* (Scroll 1.2+) the service answers in two disjoint lanes — the assistant's own
|
|
425
|
+
* words come back separately — and both are returned here, so the array can hold
|
|
426
|
+
* more than `limit`. They were retrieved and billed either way; dropping them would
|
|
427
|
+
* only hide what you already paid for. Size a prompt window on the array you get
|
|
428
|
+
* back, not on `limit`. `searchSelf()` hands the lanes back apart. */
|
|
422
429
|
search(query: string, userId?: string, limit?: number, opts?: SearchOptions): Promise<Memory[]>;
|
|
423
430
|
/** Search a self-memory model (Scroll 1.2+): both lanes from ONE call. Returns
|
|
424
431
|
* `{ memories, self_memories }` — `memories` is the main lane (what others said /
|
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.28";
|
|
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). */
|
|
@@ -91,6 +91,19 @@ const IDEMPOTENCY_KEY_RE = /^[A-Za-z0-9._:\-]{1,128}$/;
|
|
|
91
91
|
function normalizeStoreId(id) {
|
|
92
92
|
return id.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
93
93
|
}
|
|
94
|
+
/** Ids already warned about, so each is reported once.
|
|
95
|
+
*
|
|
96
|
+
* Bounded on purpose. The warning fires on ids that fold, and the shape it exists
|
|
97
|
+
* to catch is an email — which this SDK's own documented pattern ("one store per
|
|
98
|
+
* end user") turns into one entry PER USER, held for the life of the process and
|
|
99
|
+
* never released, even when the client is. A server passing 50,000 user ids grew
|
|
100
|
+
* this to 50,000 entries, a leak in exactly the case being warned about. A `Set`
|
|
101
|
+
* keeps insertion order, so the oldest entry is the one evicted; dropping the
|
|
102
|
+
* oldest rather than refusing new ones means a collision that first shows up late
|
|
103
|
+
* still gets its one warning. */
|
|
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;
|
|
94
107
|
const warnedStoreIds = new Set();
|
|
95
108
|
function warnIfStoreIdCollapses(id) {
|
|
96
109
|
if (!id || warnedStoreIds.has(id))
|
|
@@ -99,6 +112,12 @@ function warnIfStoreIdCollapses(id) {
|
|
|
99
112
|
if (normalized === id)
|
|
100
113
|
return;
|
|
101
114
|
warnedStoreIds.add(id);
|
|
115
|
+
while (warnedStoreIds.size > WARNED_STORE_IDS_MAX) {
|
|
116
|
+
const oldest = warnedStoreIds.values().next().value;
|
|
117
|
+
if (oldest === undefined)
|
|
118
|
+
break;
|
|
119
|
+
warnedStoreIds.delete(oldest);
|
|
120
|
+
}
|
|
102
121
|
console.warn(`wontopos: store id ${JSON.stringify(id)} is stored as ${JSON.stringify(normalized)} ` +
|
|
103
122
|
`(lowercased, and anything outside [a-z0-9_] becomes "_"). Ids that differ only by case or ` +
|
|
104
123
|
`punctuation share ONE store and therefore one set of memories — if these ids come from your ` +
|
|
@@ -292,6 +311,14 @@ function warnOnUnknownFilters(filters) {
|
|
|
292
311
|
if (KNOWN_FILTER_KEYS.has(k) || warnedFilterKeys.has(k))
|
|
293
312
|
continue;
|
|
294
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
|
+
}
|
|
295
322
|
console.warn(`wontopos: unknown search filter ${JSON.stringify(k)} — the API drops keys it does not know, ` +
|
|
296
323
|
`so this filter has NO effect and the search is wider than you think. ` +
|
|
297
324
|
`Known keys: ${[...KNOWN_FILTER_KEYS].join(", ")}.`);
|
|
@@ -369,11 +396,30 @@ export class Client {
|
|
|
369
396
|
// An API key on plain HTTP travels readable by anyone on the path. Loopback
|
|
370
397
|
// is fine (local dev / self-hosted on the same box); anything else gets a
|
|
371
398
|
// warning, not an error, so private-network gateways keep working.
|
|
372
|
-
|
|
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
|
+
}
|
|
373
410
|
// Scheme compare is case-insensitive — `HTTP://` connects in plaintext too.
|
|
374
411
|
if (/^http:\/\//i.test(this.base) && !LOOPBACK_HOSTS.has(host)) {
|
|
375
412
|
console.warn("wontopos: baseUrl uses plain HTTP on a non-local host, so the API key travels unencrypted. Use https://.");
|
|
376
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 });
|
|
377
423
|
}
|
|
378
424
|
/** A client whose key comes from `WONTOPOS_API_KEY` (or `WOS_API_KEY`) — keeps
|
|
379
425
|
* keys out of source code. Node/Bun/Deno-with-env only. */
|
|
@@ -397,6 +443,15 @@ export class Client {
|
|
|
397
443
|
}
|
|
398
444
|
/** Resolve a call's store: the explicit userId, else the client default. */
|
|
399
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
|
+
if (userId !== undefined && !String(userId).trim()) {
|
|
452
|
+
throw new Error("userId was passed but is blank. Omit it to use the client's default store, or pass a real store id — " +
|
|
453
|
+
"a blank id would silently write into the default store.");
|
|
454
|
+
}
|
|
400
455
|
const id = userId || this.defaultUser;
|
|
401
456
|
warnIfStoreIdCollapses(id);
|
|
402
457
|
return id;
|
|
@@ -473,7 +528,14 @@ export class Client {
|
|
|
473
528
|
return this.post("/api/v1/memory/supersede", { user_id: this.uid(userId), old_memory_id: oldMemoryId, new_content: newContent }, opts.idempotencyKey);
|
|
474
529
|
}
|
|
475
530
|
// ----- read -----
|
|
476
|
-
/** Semantic search over a store's memories. Returns memories, most relevant first.
|
|
531
|
+
/** Semantic search over a store's memories. Returns memories, most relevant first.
|
|
532
|
+
*
|
|
533
|
+
* `limit` bounds the MAIN lane, not the returned array. On a self-lane model
|
|
534
|
+
* (Scroll 1.2+) the service answers in two disjoint lanes — the assistant's own
|
|
535
|
+
* words come back separately — and both are returned here, so the array can hold
|
|
536
|
+
* more than `limit`. They were retrieved and billed either way; dropping them would
|
|
537
|
+
* only hide what you already paid for. Size a prompt window on the array you get
|
|
538
|
+
* back, not on `limit`. `searchSelf()` hands the lanes back apart. */
|
|
477
539
|
async search(query, userId, limit = 10, opts = {}) {
|
|
478
540
|
// Reserved fields win over ...opts: an app that forwards untrusted input as
|
|
479
541
|
// opts must not be able to override the store (user_id), query, or limit.
|
|
@@ -620,6 +682,7 @@ export class Client {
|
|
|
620
682
|
}
|
|
621
683
|
/** Delete a store and ALL its memories. Returns `{ user_id, status }`. */
|
|
622
684
|
deleteStore(userId) {
|
|
685
|
+
warnIfStoreIdCollapses(userId); // destructive: the collision warning belongs here too
|
|
623
686
|
if (!userId || !userId.trim())
|
|
624
687
|
throw new Error("userId is required (non-blank) — deleteStore never falls back to the default store.");
|
|
625
688
|
return this.request("DELETE", "/api/v1/memory/collection", { user_id: userId });
|
|
@@ -645,16 +708,27 @@ export class Client {
|
|
|
645
708
|
delete(userId, memoryId) {
|
|
646
709
|
// Guard: without a memory_id the API's forget endpoint means "delete the
|
|
647
710
|
// whole store". An undefined/"" slipping in here must never become a wipe.
|
|
648
|
-
|
|
649
|
-
|
|
711
|
+
//
|
|
712
|
+
// `.trim()` matters as much as the emptiness check: " " is truthy, so it used to
|
|
713
|
+
// pass this guard and travel as memory_id. A server that trims it back to nothing
|
|
714
|
+
// reads the request as the whole-store form — the single most destructive way for
|
|
715
|
+
// this SDK to be wrong. `deleteAll` below already trimmed; the more dangerous path
|
|
716
|
+
// was the one that did not.
|
|
717
|
+
if (typeof memoryId !== "string" || !memoryId.trim()) {
|
|
718
|
+
throw new Error("memoryId is required (non-blank). To delete every memory in a store, call deleteAll(userId) explicitly.");
|
|
650
719
|
}
|
|
651
|
-
return this.post("/api/v1/memory/forget", { user_id: this.uid(userId), memory_id: memoryId });
|
|
720
|
+
return this.post("/api/v1/memory/forget", { user_id: this.uid(userId), memory_id: memoryId.trim() });
|
|
652
721
|
}
|
|
653
722
|
/** Delete ALL memories for a store (GDPR erase). `userId` is required on purpose -
|
|
654
723
|
* this is destructive, so it never falls back to the default store. */
|
|
655
724
|
deleteAll(userId) {
|
|
656
725
|
if (!userId || !userId.trim())
|
|
657
726
|
throw new Error("userId is required (non-blank) for deleteAll — a blank/whitespace id would wipe the default store.");
|
|
727
|
+
// These two take the store id directly instead of going through uid(), so the
|
|
728
|
+
// collision warning — the one that says `Alice.Smith` and `alice_smith` are ONE
|
|
729
|
+
// store — never fired on the two calls that DESTROY data. Erasing the wrong
|
|
730
|
+
// tenant's memories is exactly the outcome that warning exists to prevent.
|
|
731
|
+
warnIfStoreIdCollapses(userId);
|
|
658
732
|
return this.post("/api/v1/memory/forget", { user_id: userId });
|
|
659
733
|
}
|
|
660
734
|
// ----- internal -----
|
package/package.json
CHANGED