@openparachute/vault 0.6.5-rc.9 → 0.6.5

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 (52) hide show
  1. package/.parachute/module.json +1 -0
  2. package/core/src/core.test.ts +7 -7
  3. package/core/src/mcp.ts +1 -1
  4. package/core/src/schema.ts +5 -5
  5. package/core/src/seed-packs.test.ts +222 -56
  6. package/core/src/seed-packs.ts +334 -70
  7. package/core/src/tag-hierarchy.ts +1 -1
  8. package/core/src/tag-schemas.ts +2 -2
  9. package/core/src/types.ts +1 -1
  10. package/package.json +1 -1
  11. package/src/admin-spa.ts +2 -1
  12. package/src/auth.ts +1 -1
  13. package/src/cli.ts +317 -53
  14. package/src/export-watch.ts +1 -1
  15. package/src/live-frame-parity.test.ts +201 -0
  16. package/src/module-manifest.ts +8 -0
  17. package/src/onboarding-seed.test.ts +82 -20
  18. package/src/onboarding-seed.ts +2 -2
  19. package/src/routes.ts +3 -3
  20. package/src/routing.test.ts +2 -2
  21. package/src/routing.ts +18 -6
  22. package/src/self-register.test.ts +19 -0
  23. package/src/self-register.ts +6 -1
  24. package/src/server.ts +56 -0
  25. package/src/services-manifest.ts +8 -0
  26. package/src/subscriptions.ts +100 -42
  27. package/src/tag-scope.ts +3 -3
  28. package/src/test-support/live-frame-corpus.ts +72 -0
  29. package/src/token-store.ts +2 -2
  30. package/src/transcription/build.test.ts +86 -5
  31. package/src/transcription/build.ts +87 -7
  32. package/src/transcription/capability.test.ts +25 -0
  33. package/src/transcription/capability.ts +27 -2
  34. package/src/transcription/download.test.ts +101 -0
  35. package/src/transcription/download.ts +71 -0
  36. package/src/transcription/install-python.test.ts +366 -0
  37. package/src/transcription/install-python.ts +471 -0
  38. package/src/transcription/providers/onnx-asr.test.ts +229 -0
  39. package/src/transcription/providers/onnx-asr.ts +239 -0
  40. package/src/transcription/providers/parakeet-mlx.test.ts +290 -0
  41. package/src/transcription/providers/parakeet-mlx.ts +242 -0
  42. package/src/transcription/select.test.ts +166 -1
  43. package/src/transcription/select.ts +234 -1
  44. package/src/transcription/tiers.test.ts +197 -0
  45. package/src/transcription/tiers.ts +184 -0
  46. package/src/vault-create.test.ts +19 -14
  47. package/src/vault.test.ts +1 -1
  48. package/src/ws-server.ts +408 -0
  49. package/src/ws-subscribe.test.ts +474 -0
  50. package/src/ws-subscribe.ts +242 -0
  51. package/src/subscribe.test.ts +0 -609
  52. package/src/subscribe.ts +0 -248
package/src/subscribe.ts DELETED
@@ -1,248 +0,0 @@
1
- /**
2
- * Live-query SSE subscribe route (live-query SSE — design
3
- * `design/2026-06-08-live-query-sse.md`).
4
- *
5
- * GET /vault/<name>/api/subscribe?<query params>
6
- *
7
- * Sends an `event: snapshot` of the currently-matching (scoped) notes, then
8
- * live `upsert`/`remove` events as notes change. Auth + tag-scope are already
9
- * resolved by the caller (routing.ts) and threaded in — the same `?key=` /
10
- * header credential and the same `tagScope` the notes path uses. This route
11
- * adds NO new auth plumbing.
12
- *
13
- * The query string is parsed by the SAME `parseNotesQueryOpts` the structured
14
- * notes-query branch uses, so the snapshot predicate and the live matcher
15
- * evaluate an identical `QueryOpts`. `search` (FTS) and `near` (graph BFS) are
16
- * not evaluable against a single in-memory note, so a subscribe request using
17
- * them is rejected with 400 BEFORE any stream opens.
18
- */
19
-
20
- import type { Store, QueryOpts } from "../core/src/types.ts";
21
- import { parseNotesQueryOpts, type TagScopeCtx } from "./routes.ts";
22
- import { filterNotesByTagScope } from "./tag-scope.ts";
23
- import { buildLiveMatcher, unsupportedSubscriptionReason } from "./live-match.ts";
24
- import {
25
- snapshotFrame,
26
- subscriptionManager,
27
- type SubscriptionManager,
28
- type SubscriptionSink,
29
- } from "./subscriptions.ts";
30
-
31
- /** Keepalive interval — `:` comment every ~25s to defeat idle-proxy timeouts. */
32
- const KEEPALIVE_MS = 25_000;
33
-
34
- function json(data: unknown, status = 200): Response {
35
- return Response.json(data, { status });
36
- }
37
-
38
- /**
39
- * Handle `GET /api/subscribe`. `vaultName` + `tagScope` come from routing.ts
40
- * (auth already validated; tag-scope already expanded). `manager` is injectable
41
- * for tests; defaults to the process-wide singleton.
42
- */
43
- export async function handleSubscribe(
44
- req: Request,
45
- store: Store,
46
- vaultName: string,
47
- tagScope: TagScopeCtx,
48
- manager: SubscriptionManager = subscriptionManager,
49
- ): Promise<Response> {
50
- if (req.method !== "GET") {
51
- return json({ error: "Method not allowed", message: "subscribe is GET-only" }, 405);
52
- }
53
-
54
- const url = new URL(req.url);
55
-
56
- // Reject the un-live-evaluable query shapes up front (before any stream).
57
- if (url.searchParams.get("search")) {
58
- return json(
59
- {
60
- error: "search (full-text) is not supported for live subscriptions — FTS can't be evaluated against a single changed note. Drop `search` or poll GET /notes?search=.",
61
- code: "UNSUPPORTED_SUBSCRIPTION_QUERY",
62
- },
63
- 400,
64
- );
65
- }
66
- if (url.searchParams.get("near[note_id]")) {
67
- return json(
68
- {
69
- error: "near (graph neighborhood) is not supported for live subscriptions — BFS can't be evaluated against a single changed note. Drop `near` or poll GET /notes?near[note_id]=.",
70
- code: "UNSUPPORTED_SUBSCRIPTION_QUERY",
71
- },
72
- 400,
73
- );
74
- }
75
-
76
- const parsed = parseNotesQueryOpts(url);
77
- if (parsed.error) return parsed.error;
78
- const queryOpts = parsed.queryOpts!;
79
-
80
- // Belt-and-suspenders: reject cursor (paging) too — meaningless for a live set.
81
- const unsupported = unsupportedSubscriptionReason(queryOpts);
82
- if (unsupported) {
83
- return json({ error: unsupported, code: "UNSUPPORTED_SUBSCRIPTION_QUERY" }, 400);
84
- }
85
-
86
- // Build the in-process matcher (resolves tag descendants once, same
87
- // hierarchy the snapshot query uses) — may throw QueryError on a malformed
88
- // metadata filter shape; surface as 400, same as the notes path.
89
- let matcher;
90
- try {
91
- matcher = await buildLiveMatcher(store, queryOpts);
92
- } catch (e: any) {
93
- if (e && e.name === "QueryError") {
94
- return json({ error: e.message, code: e.code ?? "INVALID_QUERY" }, 400);
95
- }
96
- throw e;
97
- }
98
-
99
- // Snapshot: the scoped query result. queryNotes throws QueryError on e.g. a
100
- // non-indexed metadata operator field — surface as 400 (no stream opened).
101
- //
102
- // Strip paging (M3): the live matcher ignores limit/offset, so a default
103
- // limit:50 would truncate the snapshot while live events deliver the full
104
- // set — snapshot ⊊ live. The snapshot must be the COMPLETE matching set.
105
- // queryNotes defaults an ABSENT limit to 100 (not unlimited), so pass a
106
- // large sentinel to fetch every matching row.
107
- const SNAPSHOT_UNBOUNDED = Number.MAX_SAFE_INTEGER;
108
- const snapshotOpts: QueryOpts = { ...queryOpts, limit: SNAPSHOT_UNBOUNDED, offset: undefined };
109
- let snapshotNotes;
110
- try {
111
- const raw = await store.queryNotes(snapshotOpts);
112
- snapshotNotes = filterNotesByTagScope(raw, tagScope.allowed, tagScope.raw);
113
- } catch (e: any) {
114
- if (e && e.name === "QueryError") {
115
- return json({ error: e.message, code: e.code ?? "INVALID_QUERY" }, 400);
116
- }
117
- throw e;
118
- }
119
-
120
- // Cap check BEFORE opening a stream so we can return a real 503. (The
121
- // in-`start` re-check below only guards the rare interleave race where two
122
- // subscribes pass this check before either registers.)
123
- if (!manager.hasCapacity(vaultName)) {
124
- return json(
125
- {
126
- error: "subscription cap reached for this vault — too many concurrent live subscriptions. Retry shortly or close idle streams.",
127
- code: "SUBSCRIPTION_CAP_REACHED",
128
- },
129
- 503,
130
- );
131
- }
132
-
133
- // ---- Build the SSE stream ----
134
- //
135
- // A pull ReadableStream with an internal frame queue. The manager's sink
136
- // writes frames into the queue; `pull` drains them to the controller and
137
- // notifies the manager (so the backpressure counter decrements). When the
138
- // client disconnects, `cancel` fires → we unregister the subscription and
139
- // clear the keepalive timer.
140
- let handle: { flushed: () => void; close: () => void } | null = null;
141
- let keepalive: ReturnType<typeof setInterval> | null = null;
142
-
143
- const queue: string[] = [];
144
- let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
145
- let cancelled = false;
146
- const encoder = new TextEncoder();
147
-
148
- const flushQueue = () => {
149
- if (!controllerRef || cancelled) return;
150
- while (queue.length > 0) {
151
- const frame = queue.shift()!;
152
- try {
153
- controllerRef.enqueue(encoder.encode(frame));
154
- } catch {
155
- // Controller closed underneath us — stop.
156
- cancelled = true;
157
- return;
158
- }
159
- handle?.flushed();
160
- }
161
- };
162
-
163
- const sink: SubscriptionSink = {
164
- write(frame: string): boolean {
165
- if (cancelled) return false;
166
- queue.push(frame);
167
- flushQueue();
168
- return true;
169
- },
170
- close(): void {
171
- if (cancelled) return;
172
- cancelled = true;
173
- if (keepalive) {
174
- clearInterval(keepalive);
175
- keepalive = null;
176
- }
177
- try {
178
- controllerRef?.close();
179
- } catch {
180
- /* already closed */
181
- }
182
- },
183
- };
184
-
185
- const stream = new ReadableStream<Uint8Array>({
186
- start(controller) {
187
- controllerRef = controller;
188
- // 1. Snapshot first — written into the queue and flushed on this tick.
189
- queue.push(snapshotFrame(snapshotNotes));
190
-
191
- // 2. Register the live subscription. Hook dispatch is deferred to a
192
- // microtask, and we register synchronously here within start(), so no
193
- // live event can slip in front of the snapshot. Over-cap → 503; we
194
- // can't return a Response from inside the stream, so the cap is also
195
- // checked below before constructing the Response. (Re-check here for
196
- // the race where two subscribes interleave.)
197
- handle = manager.register({
198
- vaultName,
199
- matcher,
200
- tagScopeAllowed: tagScope.allowed,
201
- tagScopeRaw: tagScope.raw,
202
- sink,
203
- });
204
- if (!handle) {
205
- // Lost the cap race. Emit nothing further and close; the pre-check
206
- // below normally catches this, so this path is rare.
207
- flushQueue();
208
- try {
209
- controller.close();
210
- } catch {
211
- /* noop */
212
- }
213
- cancelled = true;
214
- return;
215
- }
216
-
217
- flushQueue();
218
-
219
- // 3. Keepalive comments.
220
- keepalive = setInterval(() => {
221
- if (cancelled) return;
222
- sink.write(":\n\n");
223
- }, KEEPALIVE_MS);
224
- },
225
- pull() {
226
- flushQueue();
227
- },
228
- cancel() {
229
- cancelled = true;
230
- if (keepalive) {
231
- clearInterval(keepalive);
232
- keepalive = null;
233
- }
234
- handle?.close();
235
- },
236
- });
237
-
238
- return new Response(stream, {
239
- status: 200,
240
- headers: {
241
- "Content-Type": "text/event-stream",
242
- "Cache-Control": "no-cache, no-transform",
243
- Connection: "keep-alive",
244
- // Defeat nginx-style proxy buffering of the event stream.
245
- "X-Accel-Buffering": "no",
246
- },
247
- });
248
- }