@openparachute/vault 0.7.3-rc.9 → 0.7.3
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/core/src/attachment/bytes-provider.ts +65 -0
- package/core/src/content-range-constants.ts +19 -0
- package/core/src/content-range.test.ts +127 -0
- package/core/src/content-range.ts +105 -8
- package/core/src/core.test.ts +66 -4
- package/core/src/expand.ts +11 -3
- package/core/src/lede.test.ts +96 -0
- package/core/src/mcp-manifest.test.ts +200 -0
- package/core/src/mcp-manifest.ts +736 -0
- package/core/src/mcp.ts +357 -607
- package/core/src/notes.ts +69 -10
- package/core/src/vault-projection.ts +17 -10
- package/package.json +1 -1
- package/src/attachment-bytes.ts +68 -0
- package/src/attachment-tickets.test.ts +126 -1
- package/src/attachment-tickets.ts +77 -1
- package/src/auth-hub-jwt.test.ts +118 -1
- package/src/auth.ts +64 -0
- package/src/config.test.ts +16 -0
- package/src/config.ts +17 -0
- package/src/embedding/select.test.ts +58 -30
- package/src/embedding/select.ts +62 -21
- package/src/live-frame-parity.test.ts +21 -0
- package/src/mcp-http.ts +20 -3
- package/src/mcp-tools.ts +15 -3
- package/src/oauth-discovery.ts +31 -0
- package/src/read-attachment.test.ts +436 -0
- package/src/routes.ts +80 -4
- package/src/routing.test.ts +229 -4
- package/src/routing.ts +135 -23
- package/src/scopes.ts +22 -0
- package/src/server.ts +17 -8
- package/src/storage.test.ts +200 -1
- package/src/subscriptions.ts +13 -1
- package/src/transcription-worker.test.ts +151 -0
- package/src/transcription-worker.ts +113 -52
- package/src/vault-embeddings-capability.test.ts +28 -6
- package/src/vault-store-embedding-wiring.test.ts +25 -16
- package/src/vault-store.ts +32 -16
- package/src/vault.test.ts +26 -13
- package/src/ws-server.ts +9 -1
- package/src/ws-subscribe.test.ts +87 -0
- package/src/ws-subscribe.ts +25 -6
package/src/ws-subscribe.test.ts
CHANGED
|
@@ -486,3 +486,90 @@ describe("WS live-query — bad query + cap", () => {
|
|
|
486
486
|
expect(ok.kind).toBe("upgraded");
|
|
487
487
|
});
|
|
488
488
|
});
|
|
489
|
+
|
|
490
|
+
describe("WS live-query — lean list subscriptions (include_content=false)", () => {
|
|
491
|
+
it("ships the lean NoteIndex snapshot shape — no bodies, has preview/displayTitle/byteSize", async () => {
|
|
492
|
+
await store.createNote("# Title line\n\nbody paragraph here", { tags: ["chat"] });
|
|
493
|
+
const { server } = makeServer();
|
|
494
|
+
|
|
495
|
+
const h = connect(server.port, `/vault/${VAULT}/api/subscribe?tag=chat&include_content=false`);
|
|
496
|
+
await h.ready();
|
|
497
|
+
h.send({ type: "auth", token: "good" });
|
|
498
|
+
|
|
499
|
+
const snap = await h.readSnapshot();
|
|
500
|
+
expect(snap.notes.length).toBe(1);
|
|
501
|
+
const n = snap.notes[0];
|
|
502
|
+
// Lean shape = what REST lists return: no `content`, but the index fields.
|
|
503
|
+
expect(n.content).toBeUndefined();
|
|
504
|
+
expect(typeof n.byteSize).toBe("number");
|
|
505
|
+
expect(typeof n.preview).toBe("string");
|
|
506
|
+
expect(n.displayTitle).toBe("Title line");
|
|
507
|
+
expect(n.tags).toEqual(["chat"]);
|
|
508
|
+
h.close();
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("a default subscription still ships FULL notes (regression: note-view path unchanged)", async () => {
|
|
512
|
+
await store.createNote("full body content", { tags: ["chat"] });
|
|
513
|
+
const { server } = makeServer();
|
|
514
|
+
|
|
515
|
+
// No include_content param → full content (the byte-unchanged default).
|
|
516
|
+
const h = connect(server.port, `/vault/${VAULT}/api/subscribe?tag=chat`);
|
|
517
|
+
await h.ready();
|
|
518
|
+
h.send({ type: "auth", token: "good" });
|
|
519
|
+
|
|
520
|
+
const snap = await h.readSnapshot();
|
|
521
|
+
expect(snap.notes[0].content).toBe("full body content");
|
|
522
|
+
// Full Note carries no lean-only fields.
|
|
523
|
+
expect(snap.notes[0].byteSize).toBeUndefined();
|
|
524
|
+
expect(snap.notes[0].preview).toBeUndefined();
|
|
525
|
+
h.close();
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("emits a LEAN upsert on a matching insert", async () => {
|
|
529
|
+
const { server } = makeServer();
|
|
530
|
+
const h = connect(server.port, `/vault/${VAULT}/api/subscribe?tag=chat&include_content=false`);
|
|
531
|
+
await h.ready();
|
|
532
|
+
h.send({ type: "auth", token: "good" });
|
|
533
|
+
await h.readSnapshot();
|
|
534
|
+
|
|
535
|
+
await store.createNote("live lean note", { tags: ["chat"] });
|
|
536
|
+
const m = await h.nextMessage();
|
|
537
|
+
expect(m.type).toBe("upsert");
|
|
538
|
+
expect(m.note.content).toBeUndefined();
|
|
539
|
+
expect(m.note.displayTitle).toBe("live lean note");
|
|
540
|
+
expect(typeof m.note.byteSize).toBe("number");
|
|
541
|
+
h.close();
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("emits a LEAN upsert when a note in the set CHANGES (live-update correctness)", async () => {
|
|
545
|
+
const note = await store.createNote("before", { tags: ["chat"] });
|
|
546
|
+
const { server } = makeServer();
|
|
547
|
+
const h = connect(server.port, `/vault/${VAULT}/api/subscribe?tag=chat&include_content=false`);
|
|
548
|
+
await h.ready();
|
|
549
|
+
h.send({ type: "auth", token: "good" });
|
|
550
|
+
await h.readSnapshot();
|
|
551
|
+
|
|
552
|
+
await store.updateNote(note.id, { content: "after edit" });
|
|
553
|
+
const m = await h.nextMessage();
|
|
554
|
+
expect(m.type).toBe("upsert");
|
|
555
|
+
expect(m.note.id).toBe(note.id);
|
|
556
|
+
expect(m.note.content).toBeUndefined();
|
|
557
|
+
expect(m.note.displayTitle).toBe("after edit");
|
|
558
|
+
h.close();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("still emits a thin remove{id} under a lean subscription", async () => {
|
|
562
|
+
const note = await store.createNote("doomed lean", { tags: ["chat"] });
|
|
563
|
+
const { server } = makeServer();
|
|
564
|
+
const h = connect(server.port, `/vault/${VAULT}/api/subscribe?tag=chat&include_content=false`);
|
|
565
|
+
await h.ready();
|
|
566
|
+
h.send({ type: "auth", token: "good" });
|
|
567
|
+
await h.readSnapshot();
|
|
568
|
+
|
|
569
|
+
await store.deleteNote(note.id);
|
|
570
|
+
const m = await h.nextMessage();
|
|
571
|
+
expect(m.type).toBe("remove");
|
|
572
|
+
expect(m.id).toBe(note.id);
|
|
573
|
+
h.close();
|
|
574
|
+
});
|
|
575
|
+
});
|
package/src/ws-subscribe.ts
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* visible to browser JS: 4400 protocol, 4401 unauthorized/expired/revoked,
|
|
31
31
|
* 4403 scope, 4408 auth-timeout.
|
|
32
32
|
*/
|
|
33
|
-
import type { Note, QueryOpts } from "../core/src/types.ts";
|
|
33
|
+
import type { Note, NoteIndex, QueryOpts } from "../core/src/types.ts";
|
|
34
34
|
import { parseNotesQueryOpts } from "./routes.ts";
|
|
35
35
|
import { unsupportedSubscriptionReason } from "./live-match.ts";
|
|
36
36
|
import { hasScopeForVault, type VaultVerb } from "./scopes.ts";
|
|
@@ -82,9 +82,20 @@ function json(data: unknown, status = 200): Response {
|
|
|
82
82
|
* filters can't be evaluated against a single changed note. The 400 bodies +
|
|
83
83
|
* `UNSUPPORTED_SUBSCRIPTION_QUERY` code are byte-identical to the SSE route (and
|
|
84
84
|
* to the cloud door) so all three agree. Returns a ready 400 Response on
|
|
85
|
-
* rejection, else the parsed `QueryOpts
|
|
85
|
+
* rejection, else the parsed `QueryOpts` plus the resolved `includeContent`
|
|
86
|
+
* intent.
|
|
87
|
+
*
|
|
88
|
+
* `include_content` — the lean-snapshot knob. Unlike the REST list
|
|
89
|
+
* route (which defaults `include_content=false`, i.e. lean), a live subscription
|
|
90
|
+
* defaults to `true` (FULL notes) so every already-deployed subscriber — cached
|
|
91
|
+
* notes-ui bundles, surface-client — keeps receiving byte-identical full-note
|
|
92
|
+
* snapshots/upserts. A subscriber opts INTO the lean `NoteIndex` wire shape (the
|
|
93
|
+
* same projection REST lists return) by passing `include_content=false`; list
|
|
94
|
+
* views do this, the single-note view leaves it default → full. Truthiness
|
|
95
|
+
* mirrors the REST route's `parseBool` (`true`/`1` → full; anything else → lean)
|
|
96
|
+
* so the two doors read the flag identically.
|
|
86
97
|
*/
|
|
87
|
-
export function validateWsSubscribeQuery(url: URL): { error: Response } | { queryOpts: QueryOpts } {
|
|
98
|
+
export function validateWsSubscribeQuery(url: URL): { error: Response } | { queryOpts: QueryOpts; includeContent: boolean } {
|
|
88
99
|
if (url.searchParams.get("search")) {
|
|
89
100
|
return {
|
|
90
101
|
error: json(
|
|
@@ -118,7 +129,11 @@ export function validateWsSubscribeQuery(url: URL): { error: Response } | { quer
|
|
|
118
129
|
if (unsupported) {
|
|
119
130
|
return { error: json({ error: unsupported, code: "UNSUPPORTED_SUBSCRIPTION_QUERY" }, 400) };
|
|
120
131
|
}
|
|
121
|
-
|
|
132
|
+
// Default TRUE (full) — the live protocol has always shipped full notes;
|
|
133
|
+
// a subscriber opts into the lean shape with `include_content=false`.
|
|
134
|
+
const rawIncludeContent = url.searchParams.get("include_content");
|
|
135
|
+
const includeContent = rawIncludeContent === null ? true : rawIncludeContent === "true" || rawIncludeContent === "1";
|
|
136
|
+
return { queryOpts, includeContent };
|
|
122
137
|
}
|
|
123
138
|
|
|
124
139
|
/** 503 body for the per-vault WS-subscription cap (mirrors the SSE 503). */
|
|
@@ -149,11 +164,15 @@ export function urlFromQuery(q: string): URL {
|
|
|
149
164
|
* exactly once per (re)connect. The consumer concatenates `notes` across frames
|
|
150
165
|
* until `done:true`, then replaces its set (the SSE self-correcting-reconnect
|
|
151
166
|
* semantics). Byte-shaped identically to the cloud door's `buildSnapshotFrames`.
|
|
167
|
+
*
|
|
168
|
+
* Shape-agnostic: the framing only `JSON.stringify`s each entry, so
|
|
169
|
+
* it carries whichever note shape the caller projected — full `Note` (the
|
|
170
|
+
* default) or the lean `NoteIndex` for an `include_content=false` subscription.
|
|
152
171
|
*/
|
|
153
|
-
export function buildSnapshotFrames(notes: Note
|
|
172
|
+
export function buildSnapshotFrames(notes: Array<Note | NoteIndex>): string[] {
|
|
154
173
|
const frames: string[] = [];
|
|
155
174
|
const enc = new TextEncoder();
|
|
156
|
-
let batch: Note
|
|
175
|
+
let batch: Array<Note | NoteIndex> = [];
|
|
157
176
|
let batchBytes = 0;
|
|
158
177
|
for (const note of notes) {
|
|
159
178
|
const noteBytes = enc.encode(JSON.stringify(note)).byteLength + 1; // +1 ~ comma
|