@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,123 @@
1
+ /**
2
+ * Unit tests for `validateVaultName` — the rule enforced by the `init`
3
+ * prompt and the `--vault-name` flag. Covers each rejection branch plus
4
+ * the happy paths the prompt has to accept (default, hyphens, underscores).
5
+ */
6
+
7
+ import { describe, test, expect } from "bun:test";
8
+ import { validateVaultName, decideInitVaultName } from "./vault-name.ts";
9
+
10
+ describe("validateVaultName", () => {
11
+ describe("accepts", () => {
12
+ test.each([
13
+ "default",
14
+ "aaron",
15
+ "personal",
16
+ "work",
17
+ "a",
18
+ "vault-1",
19
+ "my_vault",
20
+ "a-b_c-1",
21
+ "abc123",
22
+ ])("%s", (name) => {
23
+ const result = validateVaultName(name);
24
+ expect(result.ok).toBe(true);
25
+ if (result.ok) expect(result.name).toBe(name);
26
+ });
27
+
28
+ test("trims surrounding whitespace before validating", () => {
29
+ const result = validateVaultName(" aaron ");
30
+ expect(result.ok).toBe(true);
31
+ if (result.ok) expect(result.name).toBe("aaron");
32
+ });
33
+ });
34
+
35
+ describe("rejects", () => {
36
+ test("empty string", () => {
37
+ const result = validateVaultName("");
38
+ expect(result.ok).toBe(false);
39
+ if (!result.ok) expect(result.error).toContain("empty");
40
+ });
41
+
42
+ test("whitespace-only", () => {
43
+ const result = validateVaultName(" ");
44
+ expect(result.ok).toBe(false);
45
+ if (!result.ok) expect(result.error).toContain("empty");
46
+ });
47
+
48
+ test.each([
49
+ ["uppercase", "Aaron"],
50
+ ["mixed case", "MyVault"],
51
+ ["space inside", "my vault"],
52
+ ["slash", "team/work"],
53
+ ["dot", "vault.1"],
54
+ ["backslash", "team\\work"],
55
+ ["question mark", "vault?"],
56
+ ["hash", "vault#1"],
57
+ ["leading symbol disallowed by regex", "@aaron"],
58
+ ["unicode", "café"],
59
+ ])("%s (%s)", (_label, name) => {
60
+ const result = validateVaultName(name);
61
+ expect(result.ok).toBe(false);
62
+ if (!result.ok) {
63
+ expect(result.error).toContain(
64
+ "lowercase alphanumeric with hyphens or underscores",
65
+ );
66
+ }
67
+ });
68
+
69
+ test("reserved name 'list'", () => {
70
+ const result = validateVaultName("list");
71
+ expect(result.ok).toBe(false);
72
+ if (!result.ok) expect(result.error).toContain("reserved");
73
+ });
74
+ });
75
+ });
76
+
77
+ describe("decideInitVaultName", () => {
78
+ test("--vault-name=aaron resolves to name 'aaron'", () => {
79
+ const d = decideInitVaultName(["--vault-name", "aaron"], { isTTY: true });
80
+ expect(d).toEqual({ kind: "name", name: "aaron" });
81
+ });
82
+
83
+ test("--vault-name=default preserves the existing default", () => {
84
+ const d = decideInitVaultName(["--vault-name", "default"], { isTTY: false });
85
+ expect(d).toEqual({ kind: "name", name: "default" });
86
+ });
87
+
88
+ test("--vault-name with no value errors out", () => {
89
+ const d = decideInitVaultName(["--vault-name"], { isTTY: true });
90
+ expect(d.kind).toBe("error");
91
+ if (d.kind === "error") expect(d.message).toContain("requires a value");
92
+ });
93
+
94
+ test("--vault-name=My Vault errors out (uppercase + space)", () => {
95
+ const d = decideInitVaultName(["--vault-name", "My Vault"], { isTTY: true });
96
+ expect(d.kind).toBe("error");
97
+ if (d.kind === "error") {
98
+ expect(d.message).toContain("--vault-name:");
99
+ expect(d.message).toContain("lowercase alphanumeric");
100
+ }
101
+ });
102
+
103
+ test("--vault-name=list errors out (reserved)", () => {
104
+ const d = decideInitVaultName(["--vault-name", "list"], { isTTY: true });
105
+ expect(d.kind).toBe("error");
106
+ if (d.kind === "error") expect(d.message).toContain("reserved");
107
+ });
108
+
109
+ test("no flag + non-TTY falls back to 'default' (piped install)", () => {
110
+ const d = decideInitVaultName(["--no-mcp"], { isTTY: false });
111
+ expect(d).toEqual({ kind: "name", name: "default" });
112
+ });
113
+
114
+ test("no flag + TTY signals the caller to prompt", () => {
115
+ const d = decideInitVaultName([], { isTTY: true });
116
+ expect(d).toEqual({ kind: "prompt" });
117
+ });
118
+
119
+ test("--vault-name with leading whitespace is trimmed and accepted", () => {
120
+ const d = decideInitVaultName(["--vault-name", " aaron "], { isTTY: true });
121
+ expect(d).toEqual({ kind: "name", name: "aaron" });
122
+ });
123
+ });
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Validation for vault names.
3
+ *
4
+ * Vault names appear in URLs (`/vault/<name>/mcp`, `/vault/<name>/api/*`),
5
+ * the SQLite filename, and the OAuth consent page — anything that breaks
6
+ * URL routing or filesystem assumptions has to be rejected up front.
7
+ *
8
+ * Used by the `init` prompt and the `--vault-name` flag. `cmdCreate` keeps
9
+ * its own (slightly more permissive, legacy) regex for backward compat —
10
+ * tightening it would reject names existing users may already have minted.
11
+ */
12
+
13
+ const VAULT_NAME_RE = /^[a-z0-9_-]+$/;
14
+
15
+ const RESERVED_NAMES = new Set([
16
+ // Collides with the `/vaults/list` discovery endpoint historically; the
17
+ // routes have since moved under `/vault/<name>/`, but `cmdCreate` still
18
+ // rejects "list" and consistency is cheap.
19
+ "list",
20
+ ]);
21
+
22
+ export type VaultNameValidation =
23
+ | { ok: true; name: string }
24
+ | { ok: false; error: string };
25
+
26
+ export function validateVaultName(raw: string): VaultNameValidation {
27
+ const name = raw.trim();
28
+ if (!name) {
29
+ return { ok: false, error: "vault name cannot be empty." };
30
+ }
31
+ if (!VAULT_NAME_RE.test(name)) {
32
+ return {
33
+ ok: false,
34
+ error:
35
+ "vault names must be lowercase alphanumeric with hyphens or underscores. Try again.",
36
+ };
37
+ }
38
+ if (RESERVED_NAMES.has(name)) {
39
+ return { ok: false, error: `"${name}" is a reserved vault name.` };
40
+ }
41
+ return { ok: true, name };
42
+ }
43
+
44
+ /**
45
+ * Decide what vault name `init` should use, based on `--vault-name` and
46
+ * whether we're attached to a TTY. Pure: extracted so the flag/TTY matrix
47
+ * can be unit-tested without spawning the CLI or touching the filesystem.
48
+ *
49
+ * - flag present + valid → `{ kind: "name", name }`
50
+ * - flag present + invalid (or missing value) → `{ kind: "error", message }`
51
+ * - no flag, non-TTY → `{ kind: "name", name: "default" }` (piped install)
52
+ * - no flag, TTY → `{ kind: "prompt" }` (caller runs an interactive prompt)
53
+ */
54
+ export type VaultNameDecision =
55
+ | { kind: "name"; name: string }
56
+ | { kind: "prompt" }
57
+ | { kind: "error"; message: string };
58
+
59
+ export function decideInitVaultName(
60
+ args: string[],
61
+ opts: { isTTY: boolean },
62
+ ): VaultNameDecision {
63
+ const idx = args.indexOf("--vault-name");
64
+ if (idx !== -1) {
65
+ const raw = args[idx + 1];
66
+ if (raw === undefined) {
67
+ return {
68
+ kind: "error",
69
+ message: "--vault-name requires a value, e.g. --vault-name aaron",
70
+ };
71
+ }
72
+ const v = validateVaultName(raw);
73
+ if (!v.ok) return { kind: "error", message: `--vault-name: ${v.error}` };
74
+ return { kind: "name", name: v.name };
75
+ }
76
+ if (!opts.isTTY) {
77
+ return { kind: "name", name: "default" };
78
+ }
79
+ return { kind: "prompt" };
80
+ }