@openparachute/vault 0.4.8 → 0.4.9-rc.11
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/core.test.ts +4 -1
- package/core/src/hooks.test.ts +320 -1
- package/core/src/hooks.ts +243 -38
- package/core/src/indexed-fields.test.ts +151 -0
- package/core/src/indexed-fields.ts +98 -0
- package/core/src/mcp.ts +99 -41
- package/core/src/notes.ts +26 -2
- package/core/src/portable-md.test.ts +304 -1
- package/core/src/portable-md.ts +418 -2
- package/core/src/schema.ts +114 -2
- package/core/src/store.ts +185 -2
- package/core/src/types.ts +28 -0
- package/package.json +2 -2
- package/src/auth-hub-jwt.test.ts +147 -0
- package/src/auth.ts +121 -1
- package/src/auto-transcribe.test.ts +7 -2
- package/src/auto-transcribe.ts +6 -2
- package/src/cli.ts +131 -36
- package/src/config.ts +12 -4
- package/src/export-watch.test.ts +74 -0
- package/src/export-watch.ts +108 -7
- package/src/github-device-flow.test.ts +404 -0
- package/src/github-device-flow.ts +415 -0
- package/src/hub-jwt.test.ts +27 -2
- package/src/hub-jwt.ts +10 -0
- package/src/mcp-http.ts +48 -39
- package/src/mcp-install-interactive.test.ts +10 -21
- package/src/mcp-install-interactive.ts +12 -21
- package/src/mcp-install.test.ts +141 -30
- package/src/mcp-install.ts +109 -3
- package/src/mcp-tools.ts +460 -3
- package/src/mirror-config.test.ts +277 -14
- package/src/mirror-config.ts +482 -31
- package/src/mirror-credentials.test.ts +601 -0
- package/src/mirror-credentials.ts +700 -0
- package/src/mirror-deps.ts +67 -17
- package/src/mirror-import.test.ts +550 -0
- package/src/mirror-import.ts +487 -0
- package/src/mirror-manager.test.ts +423 -12
- package/src/mirror-manager.ts +621 -72
- package/src/mirror-per-vault.test.ts +519 -0
- package/src/mirror-registry.ts +91 -14
- package/src/mirror-routes.test.ts +966 -10
- package/src/mirror-routes.ts +1111 -7
- package/src/module-config.ts +11 -5
- package/src/routes.ts +38 -1
- package/src/routing.test.ts +92 -1
- package/src/routing.ts +193 -20
- package/src/server.ts +116 -35
- package/src/storage.test.ts +132 -7
- package/src/token-store.ts +300 -5
- package/src/transcription-worker.ts +9 -4
- package/src/triggers.ts +16 -3
- package/src/vault.test.ts +681 -2
- package/web/ui/dist/assets/index-Cn-PPMRv.js +60 -0
- package/web/ui/dist/assets/{index-BOa-JJtV.css → index-DBe8Xiah.css} +1 -1
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-BzA5LgE3.js +0 -60
|
@@ -12,6 +12,10 @@ import os from "node:os";
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
|
+
DEFAULT_SAFETY_NET_SECONDS,
|
|
16
|
+
MAX_SAFETY_NET_SECONDS,
|
|
17
|
+
MIN_SAFETY_NET_SECONDS,
|
|
18
|
+
commentOutMirrorBlock,
|
|
15
19
|
defaultMirrorConfig,
|
|
16
20
|
parseMirrorConfig,
|
|
17
21
|
resolveMirrorPath,
|
|
@@ -40,11 +44,14 @@ describe("defaultMirrorConfig", () => {
|
|
|
40
44
|
expect(d.enabled).toBe(false);
|
|
41
45
|
expect(d.location).toBe("internal");
|
|
42
46
|
expect(d.external_path).toBeNull();
|
|
43
|
-
|
|
47
|
+
// Post event-driven shift: sync_mode replaces watch. "events" is the
|
|
48
|
+
// new default — when an operator flips enabled on, hooks subscribe
|
|
49
|
+
// automatically.
|
|
50
|
+
expect(d.sync_mode).toBe("events");
|
|
44
51
|
expect(d.auto_commit).toBe(true);
|
|
45
52
|
expect(d.auto_push).toBe(false);
|
|
46
53
|
expect(d.commit_template).toContain("{{date}}");
|
|
47
|
-
expect(d.
|
|
54
|
+
expect(d.safety_net_seconds).toBe(DEFAULT_SAFETY_NET_SECONDS);
|
|
48
55
|
});
|
|
49
56
|
});
|
|
50
57
|
|
|
@@ -58,41 +65,68 @@ describe("parseMirrorConfig", () => {
|
|
|
58
65
|
expect(parseMirrorConfig("")).toBeUndefined();
|
|
59
66
|
});
|
|
60
67
|
|
|
61
|
-
test("parses a fully-specified mirror block", () => {
|
|
68
|
+
test("parses a fully-specified mirror block (post-event-driven shape)", () => {
|
|
62
69
|
const yaml = [
|
|
63
70
|
"port: 1940",
|
|
64
71
|
"mirror:",
|
|
65
72
|
" enabled: true",
|
|
66
73
|
" location: external",
|
|
67
74
|
" external_path: /home/aaron/mirrors/gitcoin",
|
|
68
|
-
"
|
|
75
|
+
" sync_mode: events",
|
|
69
76
|
" auto_commit: true",
|
|
70
77
|
" auto_push: true",
|
|
71
78
|
' commit_template: "vault: {{notes_changed}} note{{plural}}"',
|
|
72
|
-
"
|
|
79
|
+
" safety_net_seconds: 3600",
|
|
73
80
|
].join("\n");
|
|
74
81
|
const m = parseMirrorConfig(yaml);
|
|
75
82
|
expect(m).toEqual({
|
|
76
83
|
enabled: true,
|
|
77
84
|
location: "external",
|
|
78
85
|
external_path: "/home/aaron/mirrors/gitcoin",
|
|
79
|
-
|
|
86
|
+
sync_mode: "events",
|
|
80
87
|
auto_commit: true,
|
|
81
88
|
auto_push: true,
|
|
82
89
|
commit_template: "vault: {{notes_changed}} note{{plural}}",
|
|
83
|
-
|
|
90
|
+
safety_net_seconds: 3600,
|
|
84
91
|
});
|
|
85
92
|
});
|
|
86
93
|
|
|
87
94
|
test("partial mirror block fills missing fields from defaults", () => {
|
|
88
|
-
const yaml = "mirror:\n enabled: true\n
|
|
95
|
+
const yaml = "mirror:\n enabled: true\n sync_mode: manual\n";
|
|
89
96
|
const m = parseMirrorConfig(yaml)!;
|
|
90
97
|
expect(m.enabled).toBe(true);
|
|
91
|
-
expect(m.
|
|
98
|
+
expect(m.sync_mode).toBe("manual");
|
|
92
99
|
expect(m.location).toBe("internal");
|
|
93
100
|
expect(m.auto_commit).toBe(true);
|
|
94
101
|
});
|
|
95
102
|
|
|
103
|
+
test("legacy `watch: true` translates to sync_mode: events", () => {
|
|
104
|
+
const m = parseMirrorConfig("mirror:\n enabled: true\n watch: true\n")!;
|
|
105
|
+
expect(m.sync_mode).toBe("events");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("legacy `watch: false` translates to sync_mode: manual", () => {
|
|
109
|
+
const m = parseMirrorConfig("mirror:\n enabled: true\n watch: false\n")!;
|
|
110
|
+
expect(m.sync_mode).toBe("manual");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("explicit sync_mode wins over legacy watch", () => {
|
|
114
|
+
const yaml = "mirror:\n enabled: true\n watch: true\n sync_mode: manual\n";
|
|
115
|
+
const m = parseMirrorConfig(yaml)!;
|
|
116
|
+
expect(m.sync_mode).toBe("manual");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("legacy `interval_seconds: 5` clamps up to MIN_SAFETY_NET_SECONDS", () => {
|
|
120
|
+
const m = parseMirrorConfig("mirror:\n enabled: true\n interval_seconds: 5\n")!;
|
|
121
|
+
expect(m.safety_net_seconds).toBe(MIN_SAFETY_NET_SECONDS);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("explicit safety_net_seconds wins over legacy interval_seconds", () => {
|
|
125
|
+
const yaml = "mirror:\n enabled: true\n interval_seconds: 5\n safety_net_seconds: 1800\n";
|
|
126
|
+
const m = parseMirrorConfig(yaml)!;
|
|
127
|
+
expect(m.safety_net_seconds).toBe(1800);
|
|
128
|
+
});
|
|
129
|
+
|
|
96
130
|
test("external_path: null is interpreted as null", () => {
|
|
97
131
|
const m = parseMirrorConfig(
|
|
98
132
|
"mirror:\n enabled: true\n external_path: null\n",
|
|
@@ -120,11 +154,11 @@ describe("serializeMirrorConfig", () => {
|
|
|
120
154
|
enabled: true,
|
|
121
155
|
location: "external" as const,
|
|
122
156
|
external_path: "/home/aaron/team-brain",
|
|
123
|
-
|
|
157
|
+
sync_mode: "events" as const,
|
|
124
158
|
auto_commit: true,
|
|
125
159
|
auto_push: false,
|
|
126
160
|
commit_template: "export: {{date}} ({{notes_changed}} note{{plural}})",
|
|
127
|
-
|
|
161
|
+
safety_net_seconds: 3600,
|
|
128
162
|
};
|
|
129
163
|
const yaml = serializeMirrorConfig(original).join("\n") + "\n";
|
|
130
164
|
const parsed = parseMirrorConfig(yaml);
|
|
@@ -251,10 +285,147 @@ describe("validateMirrorConfigShape", () => {
|
|
|
251
285
|
if (!r.ok) expect(r.field).toBe("enabled");
|
|
252
286
|
});
|
|
253
287
|
|
|
254
|
-
test("rejects non-integer
|
|
255
|
-
const r = validateMirrorConfigShape({
|
|
288
|
+
test("rejects non-integer safety_net_seconds", () => {
|
|
289
|
+
const r = validateMirrorConfigShape({ safety_net_seconds: 0.5 });
|
|
290
|
+
expect(r.ok).toBe(false);
|
|
291
|
+
if (!r.ok) expect(r.field).toBe("safety_net_seconds");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("rejects safety_net_seconds below MIN", () => {
|
|
295
|
+
const r = validateMirrorConfigShape({ safety_net_seconds: MIN_SAFETY_NET_SECONDS - 1 });
|
|
296
|
+
expect(r.ok).toBe(false);
|
|
297
|
+
if (!r.ok) expect(r.field).toBe("safety_net_seconds");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("rejects safety_net_seconds above MAX", () => {
|
|
301
|
+
const r = validateMirrorConfigShape({ safety_net_seconds: MAX_SAFETY_NET_SECONDS + 1 });
|
|
302
|
+
expect(r.ok).toBe(false);
|
|
303
|
+
if (!r.ok) expect(r.field).toBe("safety_net_seconds");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("legacy interval_seconds field clamps + migrates to safety_net_seconds", () => {
|
|
307
|
+
// Hand-edited config supplies the old field; we still accept it but
|
|
308
|
+
// route it through the safety-net clamp range.
|
|
309
|
+
const r = validateMirrorConfigShape({ interval_seconds: 5 });
|
|
310
|
+
expect(r.ok).toBe(true);
|
|
311
|
+
if (r.ok) expect(r.config.safety_net_seconds).toBe(MIN_SAFETY_NET_SECONDS);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("rejects unknown sync_mode", () => {
|
|
315
|
+
const r = validateMirrorConfigShape({ sync_mode: "interval" });
|
|
256
316
|
expect(r.ok).toBe(false);
|
|
257
|
-
if (!r.ok) expect(r.field).toBe("
|
|
317
|
+
if (!r.ok) expect(r.field).toBe("sync_mode");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("accepts sync_mode events / manual", () => {
|
|
321
|
+
expect((validateMirrorConfigShape({ sync_mode: "events" }) as { config: { sync_mode: string } }).config.sync_mode).toBe("events");
|
|
322
|
+
expect((validateMirrorConfigShape({ sync_mode: "manual" }) as { config: { sync_mode: string } }).config.sync_mode).toBe("manual");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("legacy watch: true translates to sync_mode: events", () => {
|
|
326
|
+
const r = validateMirrorConfigShape({ watch: true });
|
|
327
|
+
expect(r.ok).toBe(true);
|
|
328
|
+
if (r.ok) expect(r.config.sync_mode).toBe("events");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("legacy watch: false translates to sync_mode: manual", () => {
|
|
332
|
+
const r = validateMirrorConfigShape({ watch: false });
|
|
333
|
+
expect(r.ok).toBe(true);
|
|
334
|
+
if (r.ok) expect(r.config.sync_mode).toBe("manual");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("rejects auto_push + internal location WHEN no credentials are configured", () => {
|
|
338
|
+
// Pre-credentials shape: auto_push + internal was rejected outright
|
|
339
|
+
// (internal mirror = no remote = push would silently fail). Once
|
|
340
|
+
// credentials are wired (PAT or GitHub OAuth), the credential save
|
|
341
|
+
// path sets `origin` on the internal repo too — so push IS
|
|
342
|
+
// meaningful. We keep the rejection only on the no-credentials path,
|
|
343
|
+
// with a clear error pointing the operator at the credential flow.
|
|
344
|
+
const r = validateMirrorConfigShape(
|
|
345
|
+
{
|
|
346
|
+
enabled: true,
|
|
347
|
+
location: "internal",
|
|
348
|
+
auto_push: true,
|
|
349
|
+
},
|
|
350
|
+
{ readCredentials: () => null },
|
|
351
|
+
);
|
|
352
|
+
expect(r.ok).toBe(false);
|
|
353
|
+
if (!r.ok) {
|
|
354
|
+
expect(r.field).toBe("auto_push");
|
|
355
|
+
expect(r.error).toContain("credentials");
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("auto_push + internal IS accepted when PAT credentials are configured", () => {
|
|
360
|
+
// The three-stacking-gaps bug Aaron hit: History preset (internal
|
|
361
|
+
// location) + PAT saved → expected pushes to fire. validation was
|
|
362
|
+
// the first blocker. Now the combination passes when credentials
|
|
363
|
+
// are present.
|
|
364
|
+
const r = validateMirrorConfigShape(
|
|
365
|
+
{
|
|
366
|
+
enabled: true,
|
|
367
|
+
location: "internal",
|
|
368
|
+
auto_push: true,
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
readCredentials: () => ({
|
|
372
|
+
active_method: "pat",
|
|
373
|
+
github_oauth: null,
|
|
374
|
+
pat: {
|
|
375
|
+
token: "ghp_xxxxxxxxxxxxxxxx",
|
|
376
|
+
remote_url: "https://x-access-token:ghp_xxxxxxxxxxxxxxxx@github.com/a/b.git",
|
|
377
|
+
label: "test",
|
|
378
|
+
},
|
|
379
|
+
}),
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
expect(r.ok).toBe(true);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("auto_push + internal IS accepted when github_oauth credentials are configured", () => {
|
|
386
|
+
const r = validateMirrorConfigShape(
|
|
387
|
+
{
|
|
388
|
+
enabled: true,
|
|
389
|
+
location: "internal",
|
|
390
|
+
auto_push: true,
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
readCredentials: () => ({
|
|
394
|
+
active_method: "github_oauth",
|
|
395
|
+
github_oauth: {
|
|
396
|
+
access_token: "gho_xxxxxxxxxxxx",
|
|
397
|
+
scope: "repo",
|
|
398
|
+
authorized_at: "2026-05-28T03:14:15.000Z",
|
|
399
|
+
user_login: "aaron",
|
|
400
|
+
user_id: 1,
|
|
401
|
+
},
|
|
402
|
+
pat: null,
|
|
403
|
+
}),
|
|
404
|
+
},
|
|
405
|
+
);
|
|
406
|
+
expect(r.ok).toBe(true);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("auto_push + external location is fine", () => {
|
|
410
|
+
const r = validateMirrorConfigShape({
|
|
411
|
+
enabled: true,
|
|
412
|
+
location: "external",
|
|
413
|
+
external_path: "/tmp/foo",
|
|
414
|
+
auto_push: true,
|
|
415
|
+
});
|
|
416
|
+
expect(r.ok).toBe(true);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("auto_push + disabled never errors", () => {
|
|
420
|
+
// Cross-field rule gates on `enabled`. A disabled config with stale
|
|
421
|
+
// auto_push: true + internal is the upgrade-path shape; operators
|
|
422
|
+
// shouldn't have to clear the field to disable.
|
|
423
|
+
const r = validateMirrorConfigShape({
|
|
424
|
+
enabled: false,
|
|
425
|
+
location: "internal",
|
|
426
|
+
auto_push: true,
|
|
427
|
+
});
|
|
428
|
+
expect(r.ok).toBe(true);
|
|
258
429
|
});
|
|
259
430
|
|
|
260
431
|
test("rejects empty commit_template", () => {
|
|
@@ -326,3 +497,95 @@ describe("validateExternalPath", () => {
|
|
|
326
497
|
if (r.ok) expect(r.resolved_path).toBe(dir);
|
|
327
498
|
});
|
|
328
499
|
});
|
|
500
|
+
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// commentOutMirrorBlock — vault#400 migration YAML rewrite (extracted from
|
|
503
|
+
// server.ts per vault#408 review N3). Runs against the operator's real
|
|
504
|
+
// config.yaml, so it gets direct coverage here.
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
|
|
507
|
+
describe("commentOutMirrorBlock", () => {
|
|
508
|
+
test("comments out a real serializer-shaped mirror block; leaves other keys intact", () => {
|
|
509
|
+
// Build the block exactly as serializeMirrorConfig emits it — pins the
|
|
510
|
+
// real production shape rather than a hand-written approximation.
|
|
511
|
+
const block = serializeMirrorConfig({
|
|
512
|
+
...defaultMirrorConfig(),
|
|
513
|
+
enabled: true,
|
|
514
|
+
location: "external",
|
|
515
|
+
external_path: "/home/aaron/mirrors/brain",
|
|
516
|
+
auto_push: true,
|
|
517
|
+
}).join("\n");
|
|
518
|
+
const yaml = `port: 1940
|
|
519
|
+
default_vault: brain
|
|
520
|
+
${block}
|
|
521
|
+
auto_transcribe:
|
|
522
|
+
enabled: true
|
|
523
|
+
`;
|
|
524
|
+
const out = commentOutMirrorBlock(yaml);
|
|
525
|
+
|
|
526
|
+
// No LIVE mirror block survives (the parser anchor won't match).
|
|
527
|
+
expect(parseMirrorConfig(out)).toBeUndefined();
|
|
528
|
+
// Every mirror line is commented.
|
|
529
|
+
expect(out).toContain("# mirror:");
|
|
530
|
+
expect(out).toContain("# enabled: true");
|
|
531
|
+
expect(out).toContain("# external_path: /home/aaron/mirrors/brain");
|
|
532
|
+
expect(out).toContain("# auto_push: true");
|
|
533
|
+
// Provenance marker added.
|
|
534
|
+
expect(out).toContain("# [vault#400] migrated to per-vault");
|
|
535
|
+
// Non-mirror top-level keys untouched (byte-for-byte).
|
|
536
|
+
expect(out).toContain("port: 1940");
|
|
537
|
+
expect(out).toContain("default_vault: brain");
|
|
538
|
+
expect(out).toContain("auto_transcribe:");
|
|
539
|
+
expect(out).toContain(" enabled: true");
|
|
540
|
+
// The mirror block must NOT have swallowed the auto_transcribe block —
|
|
541
|
+
// its child line stays a live (uncommented) 2-space-indent field.
|
|
542
|
+
expect(out).not.toContain("# enabled: true\n# auto_transcribe");
|
|
543
|
+
const at = out.indexOf("auto_transcribe:");
|
|
544
|
+
expect(out.slice(at)).toContain("\n enabled: true");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("idempotent — running on already-commented output is a no-op", () => {
|
|
548
|
+
const block = serializeMirrorConfig({
|
|
549
|
+
...defaultMirrorConfig(),
|
|
550
|
+
enabled: true,
|
|
551
|
+
}).join("\n");
|
|
552
|
+
const yaml = `port: 1940\n${block}\ndiscovery: enabled\n`;
|
|
553
|
+
const once = commentOutMirrorBlock(yaml);
|
|
554
|
+
const twice = commentOutMirrorBlock(once);
|
|
555
|
+
expect(twice).toBe(once); // second pass changes nothing
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("no mirror block → returns input unchanged", () => {
|
|
559
|
+
const yaml = `port: 1940
|
|
560
|
+
default_vault: brain
|
|
561
|
+
discovery: enabled
|
|
562
|
+
`;
|
|
563
|
+
expect(commentOutMirrorBlock(yaml)).toBe(yaml);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test("mirror block at EOF (no trailing key) is fully commented", () => {
|
|
567
|
+
const block = serializeMirrorConfig({
|
|
568
|
+
...defaultMirrorConfig(),
|
|
569
|
+
enabled: true,
|
|
570
|
+
auto_commit: false,
|
|
571
|
+
}).join("\n");
|
|
572
|
+
const yaml = `port: 1940\n${block}\n`;
|
|
573
|
+
const out = commentOutMirrorBlock(yaml);
|
|
574
|
+
expect(parseMirrorConfig(out)).toBeUndefined();
|
|
575
|
+
expect(out).toContain("# auto_commit: false");
|
|
576
|
+
expect(out).toContain("port: 1940"); // live, untouched
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test("preserves a blank line between the mirror block and the next key", () => {
|
|
580
|
+
const block = serializeMirrorConfig({
|
|
581
|
+
...defaultMirrorConfig(),
|
|
582
|
+
enabled: true,
|
|
583
|
+
}).join("\n");
|
|
584
|
+
// Blank line separates the block from `discovery:` — must stay blank
|
|
585
|
+
// (not commented) and `discovery:` must stay live.
|
|
586
|
+
const yaml = `port: 1940\n${block}\n\ndiscovery: enabled\n`;
|
|
587
|
+
const out = commentOutMirrorBlock(yaml);
|
|
588
|
+
expect(out).toContain("\n\ndiscovery: enabled"); // blank line preserved, key live
|
|
589
|
+
expect(parseMirrorConfig(out)).toBeUndefined();
|
|
590
|
+
});
|
|
591
|
+
});
|