@openparachute/vault 0.3.3 → 0.4.0

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 (79) hide show
  1. package/.parachute/module.json +15 -0
  2. package/core/src/core.test.ts +2252 -7
  3. package/core/src/links.ts +1 -1
  4. package/core/src/mcp.ts +801 -67
  5. package/core/src/note-schemas.ts +232 -0
  6. package/core/src/notes.ts +313 -35
  7. package/core/src/obsidian.ts +3 -3
  8. package/core/src/paths.ts +1 -1
  9. package/core/src/query-operators.ts +23 -7
  10. package/core/src/schema-defaults.ts +287 -0
  11. package/core/src/schema.ts +393 -9
  12. package/core/src/store.ts +248 -6
  13. package/core/src/tag-hierarchy.ts +137 -0
  14. package/core/src/tag-schemas.ts +242 -42
  15. package/core/src/types.ts +100 -6
  16. package/core/src/wikilinks.ts +3 -3
  17. package/package.json +13 -3
  18. package/src/admin-spa.test.ts +161 -0
  19. package/src/admin-spa.ts +161 -0
  20. package/src/auth-hub-jwt.test.ts +231 -0
  21. package/src/auth-status.ts +84 -0
  22. package/src/auth.test.ts +135 -23
  23. package/src/auth.ts +144 -15
  24. package/src/backup.ts +4 -7
  25. package/src/cli.ts +322 -57
  26. package/src/config.test.ts +44 -0
  27. package/src/config.ts +68 -40
  28. package/src/hub-jwt.test.ts +296 -0
  29. package/src/hub-jwt.ts +79 -0
  30. package/src/init.test.ts +216 -0
  31. package/src/mcp-http.ts +30 -28
  32. package/src/mcp-install.ts +1 -1
  33. package/src/mcp-tools.ts +294 -6
  34. package/src/module-config.ts +1 -1
  35. package/src/oauth.test.ts +345 -0
  36. package/src/oauth.ts +85 -14
  37. package/src/owner-auth.ts +57 -1
  38. package/src/prompt.ts +6 -5
  39. package/src/routes.ts +686 -58
  40. package/src/routing.test.ts +466 -1
  41. package/src/routing.ts +108 -24
  42. package/src/scopes.test.ts +66 -8
  43. package/src/scopes.ts +163 -37
  44. package/src/server.ts +24 -2
  45. package/src/services-manifest.test.ts +20 -0
  46. package/src/services-manifest.ts +9 -2
  47. package/src/stop-signal.test.ts +85 -0
  48. package/src/storage.test.ts +92 -0
  49. package/src/tag-scope.ts +118 -0
  50. package/src/token-store.test.ts +47 -0
  51. package/src/token-store.ts +128 -13
  52. package/src/tokens-routes.test.ts +720 -0
  53. package/src/tokens-routes.ts +392 -0
  54. package/src/transcription-worker.test.ts +5 -0
  55. package/src/triggers.ts +1 -1
  56. package/src/two-factor.ts +2 -2
  57. package/src/vault-create.test.ts +193 -0
  58. package/src/vault-name.test.ts +123 -0
  59. package/src/vault-name.ts +80 -0
  60. package/src/vault.test.ts +868 -3
  61. package/tsconfig.json +8 -1
  62. package/.claude/settings.local.json +0 -8
  63. package/.dockerignore +0 -8
  64. package/.env.example +0 -9
  65. package/CHANGELOG.md +0 -175
  66. package/CLAUDE.md +0 -125
  67. package/Caddyfile +0 -3
  68. package/Dockerfile +0 -22
  69. package/bun.lock +0 -219
  70. package/bunfig.toml +0 -2
  71. package/deploy/parachute-vault.service +0 -20
  72. package/docker-compose.yml +0 -50
  73. package/docs/HTTP_API.md +0 -434
  74. package/docs/auth-model.md +0 -340
  75. package/fly.toml +0 -24
  76. package/package/package.json +0 -32
  77. package/railway.json +0 -14
  78. package/scripts/migrate-audio-to-opus.test.ts +0 -237
  79. package/scripts/migrate-audio-to-opus.ts +0 -499
package/docs/HTTP_API.md DELETED
@@ -1,434 +0,0 @@
1
- # Parachute Vault HTTP API
2
-
3
- A flat reference for the Parachute Vault HTTP surface. Intended for humans *and*
4
- agents building tools that read or write a vault over HTTP.
5
-
6
- All endpoints serve JSON. The same vault is reachable at two roots:
7
-
8
- - `/api/...` — the server's default vault
9
- - `/vaults/{name}/api/...` — any named vault on this server
10
-
11
- Use whichever is convenient. Examples below use the default `/api` root.
12
-
13
- ## Quick start — render a graph in 5 lines
14
-
15
- ```js
16
- const res = await fetch("http://localhost:1940/api/graph", {
17
- headers: { Authorization: `Bearer ${apiKey}` },
18
- });
19
- const { notes, links, tags, meta } = await res.json();
20
- // notes: lightweight NoteIndex[] — id, path, tags, createdAt, byteSize, preview
21
- // links: Link[] — sourceId, targetId, relationship, metadata
22
- // Hand this to d3-force, cytoscape, sigma.js, etc.
23
- ```
24
-
25
- That's the whole happy path. Everything else in this doc is detail.
26
-
27
- ## Conventions
28
-
29
- - **Response payloads are camelCase**: `createdAt`, `sourceId`, `mimeType`,
30
- `totalNotes`.
31
- - **Request payloads are camelCase**: you `POST {sourceId, targetId, ...}` and
32
- get the same shape back.
33
- - **Query params are snake_case**: `?include_content=true`, `?tag_match=any`,
34
- `?date_from=2025-01-01`. This matches the MCP tool-arg convention, so one
35
- concept ports cleanly between HTTP and MCP.
36
- - **Timestamps are ISO-8601** UTC strings (e.g. `2026-04-07T15:30:00.000Z`).
37
- - **No envelope**. Responses are the data itself (`{...}` or `[...]`), not
38
- wrapped in `{data: ...}`. Error responses are `{error: "...", message?: "..."}`.
39
-
40
- ## Authentication
41
-
42
- Pass your API key as either:
43
-
44
- ```
45
- Authorization: Bearer <key>
46
- X-API-Key: <key>
47
- ```
48
-
49
- Every request is authenticated — localhost and remote traffic go through the
50
- same path, there is no bypass. Local dev feels friction-free because you can
51
- hand the CLI-generated API key to your script without exposing it to the
52
- network, not because the auth check is skipped.
53
-
54
- Keys have a **scope**:
55
-
56
- - `write` — full access
57
- - `read` — `GET`/`HEAD`/`OPTIONS` only; writes return `403 Forbidden`
58
-
59
- A read-only key is the right thing to hand to a visualizer or static-site
60
- generator.
61
-
62
- ## The shapes
63
-
64
- ### `Note`
65
-
66
- ```ts
67
- {
68
- id: string;
69
- content: string;
70
- path?: string;
71
- metadata?: Record<string, unknown>;
72
- createdAt: string;
73
- updatedAt?: string;
74
- tags?: string[];
75
- }
76
- ```
77
-
78
- ### `NoteIndex` (lean shape)
79
-
80
- Returned by list endpoints by default. Same as `Note` minus `content`, plus
81
- `byteSize` and a one-line `preview` (120 code points, whitespace collapsed).
82
-
83
- ```ts
84
- {
85
- id: string;
86
- path?: string;
87
- createdAt: string;
88
- updatedAt?: string;
89
- tags?: string[];
90
- metadata?: Record<string, unknown>;
91
- byteSize: number; // UTF-8 bytes of the full content
92
- preview: string; // first ~120 chars, single line
93
- }
94
- ```
95
-
96
- ### `Link`
97
-
98
- ```ts
99
- {
100
- sourceId: string;
101
- targetId: string;
102
- relationship: string;
103
- metadata?: Record<string, unknown>;
104
- createdAt: string;
105
- }
106
- ```
107
-
108
- ### `VaultStats`
109
-
110
- ```ts
111
- {
112
- totalNotes: number;
113
- earliestNote: { id: string; createdAt: string } | null;
114
- latestNote: { id: string; createdAt: string } | null;
115
- notesByMonth: { month: string; count: number }[]; // e.g. "2026-04"
116
- topTags: { tag: string; count: number }[];
117
- tagCount: number;
118
- }
119
- ```
120
-
121
- ## Defaults: lean lists, fat point reads
122
-
123
- - **List endpoints** (`GET /notes`, `GET /graph`) default to `NoteIndex`. The
124
- common case is viz/listing, which doesn't need the full body of every note.
125
- - **Point reads** (`GET /notes/:id`) default to the full `Note`. If you asked
126
- for one specific thing by ID, you probably want its content.
127
-
128
- Both shapes can be forced either way with `?include_content=true|false`.
129
-
130
- ## Endpoints
131
-
132
- ### Server-level
133
-
134
- #### `GET /health`
135
- Returns `{status: "ok", vaults: string[]}`. No auth required.
136
-
137
- #### `GET /vaults`
138
- List all vaults on the server.
139
- ```json
140
- {
141
- "vaults": [
142
- { "name": "default", "description": "...", "created_at": "..." }
143
- ]
144
- }
145
- ```
146
-
147
- #### `GET /vaults/{name}`
148
- Single-vault landing payload — name, description, createdAt, and stats in one
149
- round trip. Useful for a viz site's home page.
150
- ```json
151
- {
152
- "name": "default",
153
- "description": "My knowledge graph",
154
- "createdAt": "2026-01-01T00:00:00.000Z",
155
- "stats": { "totalNotes": 617, "topTags": [...], "notesByMonth": [...], ... }
156
- }
157
- ```
158
-
159
- ### Notes
160
-
161
- #### `GET /notes`
162
- Query notes. Returns `NoteIndex[]` by default.
163
-
164
- Query params:
165
- - `include_content=true` — return full `Note[]` instead.
166
- - `ids=a,b,c` — fetch specific notes by ID. Practical limit ~50 IDs due to
167
- URL length; for larger batches call multiple times.
168
- - `tag=foo&tag=bar` — filter by tags (repeat param to pass multiple).
169
- - `tag_match=all|any` — default `all`.
170
- - `exclude_tag=foo` — exclude notes with this tag.
171
- - `date_from=ISO` — inclusive lower bound on `createdAt`.
172
- - `date_to=ISO` — exclusive upper bound.
173
- - `sort=asc|desc` — by `createdAt`. Default `asc`.
174
- - `limit=N` — default 100.
175
- - `offset=N` — default 0.
176
-
177
- #### `POST /notes`
178
- Create a note. Body:
179
- ```json
180
- {
181
- "content": "...", // required
182
- "id": "optional-client-id",
183
- "path": "Projects/Foo",
184
- "tags": ["a", "b"],
185
- "metadata": { "status": "draft" },
186
- "createdAt": "2026-04-07T..."
187
- }
188
- ```
189
- Returns the created `Note`, `201 Created`.
190
-
191
- #### `GET /notes/{id}`
192
- Returns the full `Note`. `?include_content=false` returns a `NoteIndex`.
193
-
194
- #### `PATCH /notes/{id}`
195
- Update content, path, or metadata. Body:
196
- ```json
197
- { "content": "new body", "path": "new/path", "metadata": {...} }
198
- ```
199
-
200
- #### `DELETE /notes/{id}`
201
- Returns `{deleted: true}`.
202
-
203
- #### `POST /notes/{id}/tags`, `DELETE /notes/{id}/tags`
204
- Body: `{"tags": ["a", "b"]}`.
205
-
206
- #### `POST /notes/{id}/attachments`
207
- Body: `{"path": "files/a.png", "mimeType": "image/png", "transcribe"?: boolean}`.
208
-
209
- When `transcribe: true` and the file is audio, the server queues a
210
- transcription job: `attachment.metadata.transcribe_status = "pending"` is
211
- set, and `note.metadata.transcribe_stub = true` is written as the opt-in to
212
- overwrite content when the transcript lands. A background worker (enabled
213
- by setting `SCRIBE_URL` on the server) drains the queue FIFO, one at a
214
- time, calling `${SCRIBE_URL}/v1/audio/transcriptions` with the audio as
215
- multipart `file` and expecting `{ text: string }` back.
216
-
217
- On success:
218
- - If `note.metadata.transcribe_stub === true`, the worker replaces the
219
- literal `_Transcript pending._` placeholder in the note body with the
220
- transcript, or the whole body if the placeholder is absent. The stub
221
- marker is cleared. A user edit clearing `transcribe_stub` before the
222
- transcript arrives opts out of the overwrite.
223
- - `attachment.metadata.transcribe_status` becomes `"done"` and
224
- `transcript` + `transcribe_done_at` are recorded on the attachment even
225
- when the note opted out, so the transcript is always addressable.
226
-
227
- On failure, the worker retries with exponential backoff up to three
228
- attempts before setting `transcribe_status = "failed"` and capturing
229
- `transcribe_error`.
230
-
231
- The queue lives in the DB (`attachments` table), so a server restart
232
- resumes pending work without replay.
233
-
234
- #### `GET /notes/{id}/attachments`
235
- Returns `Attachment[]`.
236
-
237
- #### `DELETE /notes/{id}/attachments/{attId}`
238
- Returns `204 No Content`. The attachment record is removed and the underlying
239
- storage file is unlinked when no other attachment still references the same
240
- path (orphan-check). Returns `404` if the attachment doesn't exist or belongs
241
- to a different note. Idempotent: a second delete of the same id returns `404`.
242
-
243
- ### Links
244
-
245
- #### `GET /links`
246
- List edges. Polymorphic — filters compose freely.
247
-
248
- Query params:
249
- - `note_id=abc` — only edges touching this note.
250
- - `direction=outbound|inbound|both` — only meaningful with `note_id`.
251
- Default `both`.
252
- - `relationship=cites` — only edges of this type.
253
-
254
- Returns bare `Link[]` — no hydration. If you need the connected notes'
255
- details, pair the result with `GET /notes?ids=...`.
256
-
257
- Examples:
258
- ```
259
- GET /links # everything
260
- GET /links?note_id=abc # all edges touching note abc
261
- GET /links?note_id=abc&direction=outbound
262
- GET /links?relationship=cites # vault-wide, by type
263
- ```
264
-
265
- #### `POST /links`
266
- Body:
267
- ```json
268
- { "sourceId": "a", "targetId": "b", "relationship": "cites", "metadata": {...} }
269
- ```
270
-
271
- #### `DELETE /links`
272
- Body:
273
- ```json
274
- { "sourceId": "a", "targetId": "b", "relationship": "cites" }
275
- ```
276
-
277
- ### Graph
278
-
279
- #### `GET /graph`
280
- One-shot knowledge graph payload for visualization.
281
-
282
- ```json
283
- {
284
- "notes": [ /* NoteIndex[] by default, Note[] if include_content=true */ ],
285
- "links": [ /* Link[] */ ],
286
- "tags": [ { "name": "...", "count": 12 } ],
287
- "meta": {
288
- "totalNotes": 617,
289
- "totalLinks": 1234,
290
- "filteredNotes": 617,
291
- "filteredLinks": 1234,
292
- "includeContent": false
293
- }
294
- }
295
- ```
296
-
297
- Query params:
298
- - `include_content=true` — fatten each note to include full content.
299
- - `tag=foo&tag=bar` — filter to a subgraph (only notes with these tags, and
300
- only links where **both** endpoints are in the subset).
301
- - `tag_match=all|any` — default `all`.
302
- - `exclude_tag=foo`.
303
-
304
- `meta.totalNotes` / `meta.totalLinks` always reflect the full vault;
305
- `filteredNotes` / `filteredLinks` reflect the response.
306
-
307
- ### Search
308
-
309
- #### `GET /search?q=query`
310
- Full-text search. Returns `Note[]` (full shape).
311
-
312
- Query params:
313
- - `q=...` — required.
314
- - `tag=foo` — optional tag filter (repeatable).
315
- - `limit=N` — default 50.
316
-
317
- ### Tags
318
-
319
- #### `GET /tags`
320
- Returns `[{name, count}]`.
321
-
322
- #### `POST /tags/{name}/rename`
323
- Body: `{ "new_name": string }`. Atomically renames the tag across `tags`,
324
- `note_tags`, and `tag_schemas` in a single transaction.
325
-
326
- Returns `{ "renamed": number }` on success — the number of note-tag rows
327
- rewritten.
328
-
329
- Errors:
330
- - `404 { "error": "not_found" }` — source tag does not exist.
331
- - `409 { "error": "target_exists", "target": string, "message": "..." }` —
332
- `new_name` is already a tag. The client should call `POST /tags/merge`
333
- instead if combining the two tags is the intent.
334
-
335
- #### `POST /tags/merge`
336
- Body: `{ "sources": string[], "target": string }`. Retags every note carrying
337
- any of the `sources` tags with `target`, then drops the source tags (and
338
- their schemas) in a single transaction. `target`'s own schema is preserved.
339
-
340
- `target` is created if it doesn't exist yet. Sources that don't exist are
341
- recorded with count `0`. Duplicate sources are deduped; `target` appearing
342
- in `sources` is a no-op for that entry.
343
-
344
- Returns `{ "merged": { [source]: count }, "target": string }`.
345
-
346
- ### Vault stats
347
-
348
- You usually want `GET /vaults/{name}` which bundles stats with vault metadata.
349
- If you only need the stats, call `GET /vaults/{name}` and read `.stats`.
350
-
351
- ### Vault config
352
-
353
- #### `GET /api/vault`
354
- Returns the vault's identity plus a nested `config` block for mutable
355
- settings.
356
-
357
- ```json
358
- {
359
- "name": "default",
360
- "description": "My knowledge graph",
361
- "config": {
362
- "audio_retention": "keep"
363
- }
364
- }
365
- ```
366
-
367
- `?include_stats=true` folds the same `VaultStats` shape into the response
368
- under `stats`.
369
-
370
- #### `PATCH /api/vault`
371
- Update the description and/or nested `config` fields. Only the fields you
372
- pass are changed; omitted fields are left alone.
373
-
374
- ```json
375
- {
376
- "description": "new description",
377
- "config": { "audio_retention": "until_transcribed" }
378
- }
379
- ```
380
-
381
- Response echoes the full vault payload (same shape as `GET /api/vault`).
382
-
383
- ##### `config.audio_retention`
384
-
385
- Controls what the transcription worker does with the audio file on disk
386
- once it reaches a terminal state. The attachment row (including any
387
- recorded transcript) is always preserved — only the file on disk is
388
- affected.
389
-
390
- | Value | Behavior |
391
- |---|---|
392
- | `"keep"` (default) | Never unlink. The original audio stays on disk indefinitely. |
393
- | `"until_transcribed"` | Unlink on successful transcription. On failure the file is kept so you can retry or re-upload. |
394
- | `"never"` | Unlink on any terminal state — **including failure**. Users who opt in accept that losing a bad transcription also loses the source audio. |
395
-
396
- Validation: `audio_retention` must be exactly one of those three strings.
397
- Any other value returns `400 { "error": "invalid_audio_retention" }`.
398
- Vaults created before this setting existed read back as `"keep"`.
399
-
400
- ### Storage
401
-
402
- #### `POST /storage/upload`
403
- Multipart form:
404
- - `file` — required, audio/image, ≤100MB
405
-
406
- Returns `{path, size, mimeType}`.
407
-
408
- #### `GET /storage/{date}/{filename}`
409
- Serves the uploaded file.
410
-
411
- ## CORS
412
-
413
- The server sends permissive CORS headers (`Access-Control-Allow-Origin: *`)
414
- so a static site on any origin can fetch the API. Writes still require a
415
- valid API key.
416
-
417
- ## Pairing with MCP
418
-
419
- Every read endpoint here has a matching MCP tool over `/mcp`
420
- (unified) or `/vaults/{name}/mcp` (scoped):
421
-
422
- | HTTP | MCP tool |
423
- |---------------------------|---------------------|
424
- | `GET /notes` | `read-notes` |
425
- | `GET /notes?ids=...` | `get-note` (ids) |
426
- | `GET /notes/{id}` | `get-note` |
427
- | `GET /links` | `get-links` |
428
- | `GET /graph` | `get-graph` |
429
- | `GET /vaults/{name}` | `get-vault-stats` + `get-vault-description` |
430
- | `GET /tags` | `list-tags` |
431
- | `GET /search?q=` | `search-notes` |
432
-
433
- The MCP tools use the same lean-vs-fat convention (`include_content: true|false`)
434
- and the same snake_case arg names as the HTTP query params.