@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
@@ -0,0 +1,232 @@
1
+ /**
2
+ * CRUD for the v15 `note_schemas` and `schema_mappings` tables.
3
+ *
4
+ * Replaces the v14-and-earlier `_schemas/<name>` and `_schema_defaults`
5
+ * notes-as-config convention. The legacy notes are LEFT IN PLACE post-v15
6
+ * but are no longer read by the resolver (see `schema-defaults.ts`).
7
+ *
8
+ * `note_schemas` rows are the schema definitions themselves; `schema_mappings`
9
+ * rows are the rules that decide which notes a schema applies to (by path
10
+ * prefix or by tag). One schema can be referenced by many mappings.
11
+ */
12
+
13
+ import { Database } from "bun:sqlite";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Types
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /** Allowed values for `schema_mappings.match_kind`. */
20
+ export const MAPPING_KINDS = ["path_prefix", "tag"] as const;
21
+ export type SchemaMappingKind = (typeof MAPPING_KINDS)[number];
22
+
23
+ export interface NoteSchemaField {
24
+ type?: "string" | "number" | "boolean" | "array" | "object";
25
+ enum?: string[];
26
+ description?: string;
27
+ }
28
+
29
+ export interface NoteSchemaRecord {
30
+ name: string;
31
+ description: string | null;
32
+ fields: Record<string, NoteSchemaField> | null;
33
+ required: string[] | null;
34
+ created_at: string | null;
35
+ updated_at: string | null;
36
+ }
37
+
38
+ export interface SchemaMappingRecord {
39
+ schema_name: string;
40
+ match_kind: SchemaMappingKind;
41
+ match_value: string;
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Row parsing
46
+ // ---------------------------------------------------------------------------
47
+
48
+ interface SchemaRow {
49
+ name: string;
50
+ description: string | null;
51
+ fields: string | null;
52
+ required: string | null;
53
+ created_at: string | null;
54
+ updated_at: string | null;
55
+ }
56
+
57
+ function parseFields(raw: string | null): Record<string, NoteSchemaField> | null {
58
+ if (!raw) return null;
59
+ try {
60
+ const parsed = JSON.parse(raw);
61
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
62
+ return parsed as Record<string, NoteSchemaField>;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function parseRequired(raw: string | null): string[] | null {
69
+ if (!raw) return null;
70
+ try {
71
+ const parsed = JSON.parse(raw);
72
+ if (!Array.isArray(parsed)) return null;
73
+ const cleaned = parsed.filter((x): x is string => typeof x === "string");
74
+ return cleaned.length > 0 ? cleaned : null;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function rowToRecord(row: SchemaRow): NoteSchemaRecord {
81
+ return {
82
+ name: row.name,
83
+ description: row.description,
84
+ fields: parseFields(row.fields),
85
+ required: parseRequired(row.required),
86
+ created_at: row.created_at,
87
+ updated_at: row.updated_at,
88
+ };
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // note_schemas: read
93
+ // ---------------------------------------------------------------------------
94
+
95
+ export function listNoteSchemas(db: Database): NoteSchemaRecord[] {
96
+ const rows = db.prepare(
97
+ "SELECT name, description, fields, required, created_at, updated_at FROM note_schemas ORDER BY name",
98
+ ).all() as SchemaRow[];
99
+ return rows.map(rowToRecord);
100
+ }
101
+
102
+ export function getNoteSchema(db: Database, name: string): NoteSchemaRecord | null {
103
+ const row = db.prepare(
104
+ "SELECT name, description, fields, required, created_at, updated_at FROM note_schemas WHERE name = ?",
105
+ ).get(name) as SchemaRow | undefined;
106
+ return row ? rowToRecord(row) : null;
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // note_schemas: write — partial upsert (mirrors upsertTagRecord shape)
111
+ // ---------------------------------------------------------------------------
112
+
113
+ export interface NoteSchemaPatch {
114
+ description?: string | null;
115
+ fields?: Record<string, NoteSchemaField> | null;
116
+ required?: string[] | null;
117
+ }
118
+
119
+ /**
120
+ * Partial-upsert a note schema. Auto-creates the row if missing. Any patch
121
+ * field left undefined is preserved; pass null to clear. Empty `required: []`
122
+ * collapses to null so the read path treats "no required fields" the same
123
+ * as "required not declared."
124
+ */
125
+ export function upsertNoteSchema(
126
+ db: Database,
127
+ name: string,
128
+ patch: NoteSchemaPatch,
129
+ ): NoteSchemaRecord {
130
+ if (!name || typeof name !== "string") {
131
+ throw new Error("note schema name must be a non-empty string");
132
+ }
133
+
134
+ const now = new Date().toISOString();
135
+ const existing = getNoteSchema(db, name);
136
+
137
+ const description = patch.description !== undefined ? patch.description : existing?.description ?? null;
138
+ const fields =
139
+ patch.fields !== undefined
140
+ ? (patch.fields ? JSON.stringify(patch.fields) : null)
141
+ : (existing?.fields ? JSON.stringify(existing.fields) : null);
142
+ let requiredRaw: string | null;
143
+ if (patch.required !== undefined) {
144
+ requiredRaw = patch.required && patch.required.length > 0 ? JSON.stringify(patch.required) : null;
145
+ } else {
146
+ requiredRaw = existing?.required && existing.required.length > 0 ? JSON.stringify(existing.required) : null;
147
+ }
148
+
149
+ if (existing) {
150
+ db.prepare(
151
+ "UPDATE note_schemas SET description = ?, fields = ?, required = ?, updated_at = ? WHERE name = ?",
152
+ ).run(description, fields, requiredRaw, now, name);
153
+ } else {
154
+ db.prepare(
155
+ "INSERT INTO note_schemas (name, description, fields, required, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
156
+ ).run(name, description, fields, requiredRaw, now, now);
157
+ }
158
+
159
+ return getNoteSchema(db, name)!;
160
+ }
161
+
162
+ /**
163
+ * Delete a note schema and all its mappings (via ON DELETE CASCADE).
164
+ * Returns true if the row existed.
165
+ */
166
+ export function deleteNoteSchema(db: Database, name: string): boolean {
167
+ const deleted = db.prepare(
168
+ "DELETE FROM note_schemas WHERE name = ? RETURNING name",
169
+ ).get(name) as { name: string } | null;
170
+ return deleted !== null;
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // schema_mappings: read + write
175
+ // ---------------------------------------------------------------------------
176
+
177
+ export interface ListMappingsOpts {
178
+ schema_name?: string;
179
+ match_kind?: SchemaMappingKind;
180
+ }
181
+
182
+ export function listSchemaMappings(db: Database, opts: ListMappingsOpts = {}): SchemaMappingRecord[] {
183
+ const where: string[] = [];
184
+ const params: string[] = [];
185
+ if (opts.schema_name) {
186
+ where.push("schema_name = ?");
187
+ params.push(opts.schema_name);
188
+ }
189
+ if (opts.match_kind) {
190
+ where.push("match_kind = ?");
191
+ params.push(opts.match_kind);
192
+ }
193
+ const sql = `SELECT schema_name, match_kind, match_value FROM schema_mappings${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY schema_name, match_kind, match_value`;
194
+ return db.prepare(sql).all(...params) as SchemaMappingRecord[];
195
+ }
196
+
197
+ /**
198
+ * Idempotent insert. Composite PK on (schema, kind, value) — re-setting
199
+ * the same triple is a no-op. Throws if `schema_name` doesn't reference
200
+ * an existing `note_schemas` row (FK constraint).
201
+ */
202
+ export function setSchemaMapping(
203
+ db: Database,
204
+ schema_name: string,
205
+ match_kind: SchemaMappingKind,
206
+ match_value: string,
207
+ ): void {
208
+ if (!MAPPING_KINDS.includes(match_kind)) {
209
+ throw new Error(`match_kind must be one of: ${MAPPING_KINDS.join(", ")}`);
210
+ }
211
+ if (!match_value || typeof match_value !== "string") {
212
+ throw new Error("match_value must be a non-empty string");
213
+ }
214
+ db.prepare(
215
+ "INSERT OR IGNORE INTO schema_mappings (schema_name, match_kind, match_value) VALUES (?, ?, ?)",
216
+ ).run(schema_name, match_kind, match_value);
217
+ }
218
+
219
+ /**
220
+ * Delete a single mapping. Returns true if a row was removed.
221
+ */
222
+ export function deleteSchemaMapping(
223
+ db: Database,
224
+ schema_name: string,
225
+ match_kind: SchemaMappingKind,
226
+ match_value: string,
227
+ ): boolean {
228
+ const deleted = db.prepare(
229
+ "DELETE FROM schema_mappings WHERE schema_name = ? AND match_kind = ? AND match_value = ? RETURNING schema_name",
230
+ ).get(schema_name, match_kind, match_value) as { schema_name: string } | null;
231
+ return deleted !== null;
232
+ }