@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.
Files changed (43) hide show
  1. package/core/src/attachment/bytes-provider.ts +65 -0
  2. package/core/src/content-range-constants.ts +19 -0
  3. package/core/src/content-range.test.ts +127 -0
  4. package/core/src/content-range.ts +105 -8
  5. package/core/src/core.test.ts +66 -4
  6. package/core/src/expand.ts +11 -3
  7. package/core/src/lede.test.ts +96 -0
  8. package/core/src/mcp-manifest.test.ts +200 -0
  9. package/core/src/mcp-manifest.ts +736 -0
  10. package/core/src/mcp.ts +357 -607
  11. package/core/src/notes.ts +69 -10
  12. package/core/src/vault-projection.ts +17 -10
  13. package/package.json +1 -1
  14. package/src/attachment-bytes.ts +68 -0
  15. package/src/attachment-tickets.test.ts +126 -1
  16. package/src/attachment-tickets.ts +77 -1
  17. package/src/auth-hub-jwt.test.ts +118 -1
  18. package/src/auth.ts +64 -0
  19. package/src/config.test.ts +16 -0
  20. package/src/config.ts +17 -0
  21. package/src/embedding/select.test.ts +58 -30
  22. package/src/embedding/select.ts +62 -21
  23. package/src/live-frame-parity.test.ts +21 -0
  24. package/src/mcp-http.ts +20 -3
  25. package/src/mcp-tools.ts +15 -3
  26. package/src/oauth-discovery.ts +31 -0
  27. package/src/read-attachment.test.ts +436 -0
  28. package/src/routes.ts +80 -4
  29. package/src/routing.test.ts +229 -4
  30. package/src/routing.ts +135 -23
  31. package/src/scopes.ts +22 -0
  32. package/src/server.ts +17 -8
  33. package/src/storage.test.ts +200 -1
  34. package/src/subscriptions.ts +13 -1
  35. package/src/transcription-worker.test.ts +151 -0
  36. package/src/transcription-worker.ts +113 -52
  37. package/src/vault-embeddings-capability.test.ts +28 -6
  38. package/src/vault-store-embedding-wiring.test.ts +25 -16
  39. package/src/vault-store.ts +32 -16
  40. package/src/vault.test.ts +26 -13
  41. package/src/ws-server.ts +9 -1
  42. package/src/ws-subscribe.test.ts +87 -0
  43. package/src/ws-subscribe.ts +25 -6
@@ -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
+ });
@@ -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
- return { queryOpts };
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[]): string[] {
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