@openparachute/vault 0.2.4 → 0.3.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 (102) hide show
  1. package/.claude/settings.local.json +2 -25
  2. package/CHANGELOG.md +64 -0
  3. package/CLAUDE.md +17 -7
  4. package/README.md +169 -136
  5. package/core/src/core.test.ts +591 -19
  6. package/core/src/hooks.ts +111 -3
  7. package/core/src/indexed-fields.test.ts +285 -0
  8. package/core/src/indexed-fields.ts +238 -0
  9. package/core/src/mcp.ts +127 -6
  10. package/core/src/notes.ts +153 -11
  11. package/core/src/query-operators.ts +174 -0
  12. package/core/src/schema.ts +69 -2
  13. package/core/src/store.ts +95 -1
  14. package/core/src/tag-schemas.ts +5 -0
  15. package/core/src/types.ts +28 -1
  16. package/docs/HTTP_API.md +105 -1
  17. package/docs/auth-model.md +340 -0
  18. package/package/package.json +32 -0
  19. package/package.json +2 -2
  20. package/src/auth.test.ts +83 -114
  21. package/src/auth.ts +68 -6
  22. package/src/backup-launchd.ts +1 -1
  23. package/src/backup.test.ts +1 -1
  24. package/src/backup.ts +18 -17
  25. package/src/bind.test.ts +28 -0
  26. package/src/bind.ts +19 -0
  27. package/src/cli.ts +228 -133
  28. package/src/config-triggers.test.ts +49 -0
  29. package/src/config.test.ts +317 -2
  30. package/src/config.ts +420 -40
  31. package/src/context.test.ts +136 -0
  32. package/src/context.ts +115 -0
  33. package/src/daemon.ts +17 -16
  34. package/src/doctor.test.ts +9 -7
  35. package/src/launchd.test.ts +1 -1
  36. package/src/launchd.ts +6 -6
  37. package/src/mcp-http.ts +75 -21
  38. package/src/mcp-install.test.ts +125 -0
  39. package/src/mcp-install.ts +60 -0
  40. package/src/mcp-tools.ts +34 -96
  41. package/src/module-config.ts +109 -0
  42. package/src/oauth.test.ts +345 -57
  43. package/src/oauth.ts +155 -35
  44. package/src/published.test.ts +2 -2
  45. package/src/routes.ts +209 -33
  46. package/src/routing.test.ts +817 -300
  47. package/src/routing.ts +204 -202
  48. package/src/scopes.test.ts +294 -0
  49. package/src/scopes.ts +253 -0
  50. package/src/scribe-env.test.ts +49 -0
  51. package/src/scribe-env.ts +33 -0
  52. package/src/server.ts +73 -9
  53. package/src/services-manifest.test.ts +140 -0
  54. package/src/services-manifest.ts +99 -0
  55. package/src/systemd.ts +3 -3
  56. package/src/token-store.ts +42 -9
  57. package/src/transcription-worker.test.ts +864 -0
  58. package/src/transcription-worker.ts +501 -0
  59. package/src/triggers.test.ts +191 -1
  60. package/src/triggers.ts +17 -2
  61. package/src/vault.test.ts +693 -77
  62. package/src/version.test.ts +1 -1
  63. package/.playwright-mcp/console-2026-04-14T04-17-25-395Z.log +0 -2
  64. package/.playwright-mcp/console-2026-04-14T04-18-11-767Z.log +0 -1
  65. package/.playwright-mcp/console-2026-04-14T04-19-07-733Z.log +0 -2
  66. package/.playwright-mcp/console-2026-04-14T04-20-45-440Z.log +0 -2
  67. package/.playwright-mcp/page-2026-04-14T04-17-25-536Z.yml +0 -1
  68. package/.playwright-mcp/page-2026-04-14T04-18-11-816Z.yml +0 -1
  69. package/.playwright-mcp/page-2026-04-14T04-18-31-674Z.yml +0 -211
  70. package/.playwright-mcp/page-2026-04-14T04-19-07-795Z.yml +0 -59
  71. package/.playwright-mcp/page-2026-04-14T04-19-36-239Z.yml +0 -232
  72. package/.playwright-mcp/page-2026-04-14T04-19-58-327Z.yml +0 -182
  73. package/.playwright-mcp/page-2026-04-14T04-20-10-517Z.yml +0 -91
  74. package/.playwright-mcp/page-2026-04-14T04-20-14-796Z.yml +0 -70
  75. package/.playwright-mcp/page-2026-04-14T04-20-45-509Z.yml +0 -59
  76. package/religions-abrahamic-filter.png +0 -0
  77. package/religions-buddhism-v2.png +0 -0
  78. package/religions-buddhism.png +0 -0
  79. package/religions-final.png +0 -0
  80. package/religions-v1.png +0 -0
  81. package/religions-v2.png +0 -0
  82. package/religions-zen.png +0 -0
  83. package/web/README.md +0 -73
  84. package/web/bun.lock +0 -827
  85. package/web/eslint.config.js +0 -23
  86. package/web/index.html +0 -15
  87. package/web/package.json +0 -36
  88. package/web/public/favicon.svg +0 -1
  89. package/web/public/icons.svg +0 -24
  90. package/web/src/App.tsx +0 -149
  91. package/web/src/Graph.tsx +0 -200
  92. package/web/src/NoteView.tsx +0 -155
  93. package/web/src/Sidebar.tsx +0 -186
  94. package/web/src/api.ts +0 -21
  95. package/web/src/index.css +0 -50
  96. package/web/src/main.tsx +0 -10
  97. package/web/src/types.ts +0 -37
  98. package/web/src/utils.ts +0 -107
  99. package/web/tsconfig.app.json +0 -25
  100. package/web/tsconfig.json +0 -7
  101. package/web/tsconfig.node.json +0 -24
  102. package/web/vite.config.ts +0 -16
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Tests for the MCP URL picker used by `mcp-install`. The picker must match
3
+ * vault's advertised OAuth issuer for the origin a client will reach it on —
4
+ * otherwise Claude Code (and any strict RFC 8414 client) rejects the
5
+ * discovery response on issuer/origin mismatch.
6
+ */
7
+
8
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
9
+ import fs from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import { chooseMcpUrl } from "./mcp-install.ts";
13
+
14
+ describe("chooseMcpUrl", () => {
15
+ let tmpHome: string;
16
+ let origHome: string | undefined;
17
+
18
+ beforeEach(() => {
19
+ origHome = process.env.PARACHUTE_HOME;
20
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "vault-mcp-install-"));
21
+ process.env.PARACHUTE_HOME = tmpHome;
22
+ });
23
+
24
+ afterEach(() => {
25
+ if (origHome === undefined) delete process.env.PARACHUTE_HOME;
26
+ else process.env.PARACHUTE_HOME = origHome;
27
+ fs.rmSync(tmpHome, { recursive: true, force: true });
28
+ });
29
+
30
+ test("prefers PARACHUTE_HUB_ORIGIN when set", () => {
31
+ const res = chooseMcpUrl("default", 1940, {
32
+ PARACHUTE_HUB_ORIGIN: "https://hub.example",
33
+ });
34
+ expect(res).toEqual({
35
+ url: "https://hub.example/vault/default/mcp",
36
+ source: "hub-origin",
37
+ });
38
+ });
39
+
40
+ test("strips trailing slash on PARACHUTE_HUB_ORIGIN", () => {
41
+ const res = chooseMcpUrl("default", 1940, {
42
+ PARACHUTE_HUB_ORIGIN: "https://hub.example/",
43
+ });
44
+ expect(res.url).toBe("https://hub.example/vault/default/mcp");
45
+ });
46
+
47
+ test("falls back to tailnet/public FQDN from expose-state.json when hub env unset", () => {
48
+ fs.writeFileSync(
49
+ path.join(tmpHome, "expose-state.json"),
50
+ JSON.stringify({
51
+ version: 1,
52
+ layer: "tailnet",
53
+ mode: "path",
54
+ canonicalFqdn: "parachute.taildf9ce2.ts.net",
55
+ port: 1940,
56
+ funnel: false,
57
+ entries: [],
58
+ }),
59
+ );
60
+ const res = chooseMcpUrl("default", 1940, {});
61
+ expect(res).toEqual({
62
+ url: "https://parachute.taildf9ce2.ts.net/vault/default/mcp",
63
+ source: "expose-state",
64
+ });
65
+ });
66
+
67
+ test("uses public FQDN from expose-state.json when layer is public", () => {
68
+ fs.writeFileSync(
69
+ path.join(tmpHome, "expose-state.json"),
70
+ JSON.stringify({
71
+ version: 1,
72
+ layer: "public",
73
+ mode: "subdomain",
74
+ canonicalFqdn: "vault.parachute.computer",
75
+ port: 1940,
76
+ funnel: true,
77
+ entries: [],
78
+ }),
79
+ );
80
+ const res = chooseMcpUrl("default", 1940, {});
81
+ expect(res.url).toBe("https://vault.parachute.computer/vault/default/mcp");
82
+ expect(res.source).toBe("expose-state");
83
+ });
84
+
85
+ test("falls back to loopback when no hub env and no expose-state", () => {
86
+ const res = chooseMcpUrl("default", 1940, {});
87
+ expect(res).toEqual({
88
+ url: "http://127.0.0.1:1940/vault/default/mcp",
89
+ source: "loopback",
90
+ });
91
+ });
92
+
93
+ test("hub env wins over an active exposure", () => {
94
+ fs.writeFileSync(
95
+ path.join(tmpHome, "expose-state.json"),
96
+ JSON.stringify({
97
+ version: 1,
98
+ layer: "tailnet",
99
+ mode: "path",
100
+ canonicalFqdn: "parachute.taildf9ce2.ts.net",
101
+ port: 1940,
102
+ funnel: false,
103
+ entries: [],
104
+ }),
105
+ );
106
+ const res = chooseMcpUrl("default", 1940, {
107
+ PARACHUTE_HUB_ORIGIN: "https://hub.example",
108
+ });
109
+ expect(res.source).toBe("hub-origin");
110
+ expect(res.url).toBe("https://hub.example/vault/default/mcp");
111
+ });
112
+
113
+ test("falls back to loopback on a malformed expose-state.json", () => {
114
+ fs.writeFileSync(path.join(tmpHome, "expose-state.json"), "{ not json");
115
+ const res = chooseMcpUrl("default", 1940, {});
116
+ expect(res.source).toBe("loopback");
117
+ });
118
+
119
+ test("honors the passed-in vault name in the URL path", () => {
120
+ const res = chooseMcpUrl("work", 1940, {
121
+ PARACHUTE_HUB_ORIGIN: "https://hub.example",
122
+ });
123
+ expect(res.url).toBe("https://hub.example/vault/work/mcp");
124
+ });
125
+ });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * URL picker for `parachute-vault mcp-install`. The URL written into
3
+ * `~/.claude.json` must match vault's advertised OAuth issuer for the origin
4
+ * the client will reach the server on — otherwise strict clients (Claude
5
+ * Code's MCP SDK) reject discovery on origin/issuer mismatch (RFC 8414 §3.1).
6
+ *
7
+ * Selection order:
8
+ * 1. `PARACHUTE_HUB_ORIGIN` env (vault is advertising the hub as issuer).
9
+ * 2. `~/.parachute/expose-state.json` canonical FQDN (active tailnet /
10
+ * public exposure the CLI brought up).
11
+ * 3. Loopback on the configured port.
12
+ */
13
+
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { resolve } from "node:path";
17
+
18
+ export type McpUrlSource = "hub-origin" | "expose-state" | "loopback";
19
+
20
+ export function chooseMcpUrl(
21
+ vaultName: string,
22
+ port: number,
23
+ env: { PARACHUTE_HUB_ORIGIN?: string } = process.env,
24
+ ): { url: string; source: McpUrlSource } {
25
+ const hub = env.PARACHUTE_HUB_ORIGIN?.replace(/\/$/, "");
26
+ if (hub) {
27
+ return { url: `${hub}/vault/${vaultName}/mcp`, source: "hub-origin" };
28
+ }
29
+ const fqdn = readExposedFqdn();
30
+ if (fqdn) {
31
+ return { url: `https://${fqdn}/vault/${vaultName}/mcp`, source: "expose-state" };
32
+ }
33
+ return { url: `http://127.0.0.1:${port}/vault/${vaultName}/mcp`, source: "loopback" };
34
+ }
35
+
36
+ /**
37
+ * Best-effort read of `~/.parachute/expose-state.json` (CLI-owned). Returns
38
+ * the canonical FQDN when an active tailnet/public exposure is configured;
39
+ * returns undefined on any error or when absent — this is advisory, not
40
+ * load-bearing.
41
+ *
42
+ * Re-derives the ecosystem root per-call so tests that flip `PARACHUTE_HOME`
43
+ * see the override — the top-level `CONFIG_DIR` const in config.ts is frozen
44
+ * at module import.
45
+ */
46
+ function readExposedFqdn(): string | undefined {
47
+ try {
48
+ const root = process.env.PARACHUTE_HOME ?? resolve(homedir(), ".parachute");
49
+ const p = resolve(root, "expose-state.json");
50
+ if (!existsSync(p)) return undefined;
51
+ const raw = JSON.parse(readFileSync(p, "utf-8")) as {
52
+ layer?: string;
53
+ canonicalFqdn?: string;
54
+ };
55
+ if ((raw.layer === "tailnet" || raw.layer === "public") && raw.canonicalFqdn) {
56
+ return raw.canonicalFqdn;
57
+ }
58
+ } catch {}
59
+ return undefined;
60
+ }
package/src/mcp-tools.ts CHANGED
@@ -1,25 +1,26 @@
1
1
  /**
2
- * MCP tool generation for multi-vault.
2
+ * MCP tool generation for the scoped (per-vault) MCP endpoint.
3
3
  *
4
- * Wraps core tools with vault resolution (optional `vault` param) and
5
- * overrides vault-info with actual vault config access.
4
+ * Every MCP session is now bound to one vault via `/vault/<name>/mcp`, so
5
+ * tools operate on that vault and vault-info picks up its config directly.
6
6
  */
7
7
 
8
8
  import { generateMcpTools } from "../core/src/mcp.ts";
9
9
  import type { McpToolDef } from "../core/src/mcp.ts";
10
- import { readVaultConfig, writeVaultConfig, listVaults as getVaultNames, resolveDefaultVault } from "./config.ts";
10
+ import { readVaultConfig, writeVaultConfig } from "./config.ts";
11
11
  import { getVaultStore } from "./vault-store.ts";
12
+ import { hasScope, SCOPE_WRITE } from "./scopes.ts";
13
+ import type { AuthResult } from "./auth.ts";
12
14
 
13
15
  /**
14
- * Get the MCP server instruction for a vault (or the default vault).
16
+ * Get the MCP server instruction for a vault.
15
17
  * Sent once at session init — not per tool.
16
18
  */
17
- export function getServerInstruction(vaultName?: string): string {
18
- const name = vaultName ?? resolveDefaultVault() ?? "default";
19
- const config = readVaultConfig(name);
19
+ export function getServerInstruction(vaultName: string): string {
20
+ const config = readVaultConfig(vaultName);
20
21
 
21
22
  const parts: string[] = [
22
- `You are connected to Parachute Vault "${name}".`,
23
+ `You are connected to Parachute Vault "${vaultName}".`,
23
24
  ];
24
25
 
25
26
  if (config?.description) {
@@ -29,111 +30,48 @@ export function getServerInstruction(vaultName?: string): string {
29
30
  return parts.join("\n");
30
31
  }
31
32
 
32
- /**
33
- * Generate the unified MCP tool set.
34
- * Each tool has an optional `vault` param that defaults to the default vault.
35
- */
36
- export function generateUnifiedMcpTools(): McpToolDef[] {
37
- const vaultNames = getVaultNames();
38
- const defaultVault = resolveDefaultVault() ?? "default";
39
- const multiVault = vaultNames.length > 1;
40
-
41
- // Get tool definitions from core (using default vault for schema)
42
- const defaultStore = getVaultStore(defaultVault);
43
- const coreTools = generateMcpTools(defaultStore);
44
-
45
- // Wrap each core tool with vault resolution
46
- const tools: McpToolDef[] = coreTools.map((coreTool) => {
47
- let description = coreTool.description;
48
- if (multiVault) {
49
- description += `\n\nMulti-vault: pass 'vault' to target a specific vault. Default: "${defaultVault}". Available: ${vaultNames.join(", ")}`;
50
- }
51
-
52
- const inputSchema = {
53
- ...coreTool.inputSchema,
54
- properties: {
55
- vault: {
56
- type: "string",
57
- description: `Vault name (default: "${defaultVault}")`,
58
- },
59
- ...(coreTool.inputSchema as any).properties,
60
- },
61
- };
62
-
63
- return {
64
- name: coreTool.name,
65
- description,
66
- inputSchema,
67
- execute: async (params) => {
68
- const vaultName = (params.vault as string) ?? defaultVault;
69
- const config = readVaultConfig(vaultName);
70
- if (!config) {
71
- throw new Error(`Vault "${vaultName}" not found. Available: ${getVaultNames().join(", ")}`);
72
- }
73
- const store = getVaultStore(vaultName);
74
- const vaultTools = generateMcpTools(store);
75
- const tool = vaultTools.find((t) => t.name === coreTool.name)!;
76
- const { vault: _, ...rest } = params;
77
- return await tool.execute(rest);
78
- },
79
- };
80
- });
81
-
82
- // Override vault-info with actual vault config access
83
- overrideVaultInfo(tools, defaultVault);
84
-
85
- // Add list-vaults (multi-vault only, not in core)
86
- if (multiVault) {
87
- tools.push({
88
- name: "list-vaults",
89
- description: "List all available vaults with their descriptions.",
90
- inputSchema: { type: "object", properties: {} },
91
- execute: () => {
92
- const names = getVaultNames();
93
- return names.map((name) => {
94
- const config = readVaultConfig(name);
95
- return {
96
- name,
97
- description: config?.description,
98
- created_at: config?.created_at,
99
- is_default: name === defaultVault,
100
- };
101
- });
102
- },
103
- });
104
- }
105
-
106
- return tools;
107
- }
108
-
109
33
  /**
110
34
  * Generate MCP tools scoped to a single vault.
111
- * No vault param — tools operate on that vault only.
35
+ *
36
+ * `auth` is the resolved token for the caller and is captured by vault-info's
37
+ * execute closure so the description-update branch can perform a secondary
38
+ * scope check: the tool itself is gated at vault:read (so read-only callers
39
+ * can fetch stats), but writing a new description requires vault:write.
40
+ *
41
+ * When omitted (internal callers that only inspect the tool list — no execute
42
+ * path exercised), the description-update branch is disabled entirely.
112
43
  */
113
- export function generateScopedMcpTools(vaultName: string): McpToolDef[] {
44
+ export function generateScopedMcpTools(vaultName: string, auth?: AuthResult): McpToolDef[] {
114
45
  const store = getVaultStore(vaultName);
115
46
  const tools = generateMcpTools(store);
116
47
 
117
- // Override vault-info with actual vault config access
118
- overrideVaultInfo(tools, vaultName);
48
+ overrideVaultInfo(tools, vaultName, auth);
119
49
 
120
50
  return tools;
121
51
  }
122
52
 
123
- /**
124
- * Override vault-info's placeholder execute with real vault config access.
125
- */
126
- function overrideVaultInfo(tools: McpToolDef[], defaultVault: string): void {
53
+ function overrideVaultInfo(
54
+ tools: McpToolDef[],
55
+ vaultName: string,
56
+ auth: AuthResult | undefined,
57
+ ): void {
127
58
  const vaultInfo = tools.find((t) => t.name === "vault-info");
128
59
  if (!vaultInfo) return;
129
60
 
130
61
  vaultInfo.execute = async (params) => {
131
- const vaultName = (params.vault as string) ?? defaultVault;
132
62
  const config = readVaultConfig(vaultName);
133
63
  if (!config) throw new Error(`Vault "${vaultName}" not found`);
134
64
 
135
- // Update description if provided
136
65
  if (params.description !== undefined) {
66
+ // Secondary scope check: vault-info is read-gated so read-only callers
67
+ // can fetch stats, but mutating the vault description requires write.
68
+ // Without this, a vault:read token could bypass the outer gate by
69
+ // passing `description` to a tool the outer gate considers read-only.
70
+ if (!auth || !hasScope(auth.scopes, SCOPE_WRITE)) {
71
+ throw new Error(
72
+ `Forbidden: updating the vault description requires the '${SCOPE_WRITE}' scope. Granted scopes: ${auth?.scopes.join(" ") || "(none)"}.`,
73
+ );
74
+ }
137
75
  config.description = params.description as string;
138
76
  writeVaultConfig(config);
139
77
  }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Module configuration endpoints (Phase 2 of the module architecture).
3
+ *
4
+ * Every Parachute module exposes two paired endpoints:
5
+ *
6
+ * GET /.parachute/config/schema — JSON Schema (draft-07) describing the
7
+ * module's configurable shape. Hub renders
8
+ * a form from this schema. No auth.
9
+ * GET /.parachute/config — current effective values, with
10
+ * `writeOnly` fields excluded. No auth for
11
+ * now (hub is loopback-only through
12
+ * Phase 0–2); gated by `vault:admin` scope
13
+ * once scope enforcement lands in Phase 3.
14
+ *
15
+ * PUT /.parachute/config is Phase 3 — not implemented here.
16
+ *
17
+ * Fields currently described:
18
+ * - audio_retention: per-vault enum, backed by VaultConfig.audio_retention.
19
+ * - scribe_url: env var SCRIBE_URL (read-only for now — there is no
20
+ * yaml slot yet, so PUT won't come online until Phase 3).
21
+ * - scribe_token: env var SCRIBE_TOKEN, writeOnly (never returned).
22
+ * - port: GlobalConfig.port, exposed read-only so the hub can
23
+ * display it without round-tripping through /health.
24
+ */
25
+
26
+ import type { VaultConfig, GlobalConfig } from "./config.ts";
27
+
28
+ export interface ModuleConfigSchema {
29
+ $schema: string;
30
+ type: "object";
31
+ title: string;
32
+ description: string;
33
+ properties: Record<string, unknown>;
34
+ }
35
+
36
+ export function buildConfigSchema(): ModuleConfigSchema {
37
+ return {
38
+ $schema: "http://json-schema.org/draft-07/schema#",
39
+ type: "object",
40
+ title: "Vault configuration",
41
+ description:
42
+ "Settings that control vault's runtime behavior. Hub renders this schema into a configuration form.",
43
+ properties: {
44
+ audio_retention: {
45
+ type: "string",
46
+ enum: ["keep", "until_transcribed", "never"],
47
+ default: "keep",
48
+ title: "Audio retention",
49
+ description:
50
+ "What to do with audio attachments after transcription. `keep` leaves the file on disk; `until_transcribed` unlinks on successful transcribe (keeps on failure for retry); `never` unlinks on any terminal state (including failure — no retries).",
51
+ },
52
+ scribe_url: {
53
+ type: "string",
54
+ format: "uri",
55
+ title: "Scribe URL",
56
+ description:
57
+ "URL of the Scribe service for transcription. Empty disables the background worker. Currently sourced from the SCRIBE_URL env var; a PUT slot lands in Phase 3.",
58
+ readOnly: true,
59
+ },
60
+ scribe_token: {
61
+ type: "string",
62
+ title: "Scribe auth token",
63
+ description:
64
+ "Optional bearer token for Scribe. Stored in the SCRIBE_TOKEN env var today. Write-only — never returned by GET.",
65
+ writeOnly: true,
66
+ },
67
+ port: {
68
+ type: "integer",
69
+ minimum: 1,
70
+ maximum: 65535,
71
+ title: "HTTP port",
72
+ description: "Port the vault server listens on. Set at init time; changing requires a restart.",
73
+ readOnly: true,
74
+ },
75
+ },
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Effective config values, with `writeOnly` fields stripped. `scribe_token` is
81
+ * declared `writeOnly` and is never returned here, even when SCRIBE_TOKEN is
82
+ * set in the environment.
83
+ */
84
+ export function buildConfigValues(
85
+ vaultConfig: VaultConfig,
86
+ globalConfig: GlobalConfig,
87
+ env: { SCRIBE_URL?: string } = process.env,
88
+ ): Record<string, unknown> {
89
+ return {
90
+ audio_retention: vaultConfig.audio_retention ?? "keep",
91
+ scribe_url: env.SCRIBE_URL ?? "",
92
+ port: globalConfig.port,
93
+ };
94
+ }
95
+
96
+ export function handleConfigSchema(): Response {
97
+ return Response.json(buildConfigSchema(), {
98
+ headers: { "Access-Control-Allow-Origin": "*" },
99
+ });
100
+ }
101
+
102
+ export function handleConfig(
103
+ vaultConfig: VaultConfig,
104
+ globalConfig: GlobalConfig,
105
+ ): Response {
106
+ return Response.json(buildConfigValues(vaultConfig, globalConfig), {
107
+ headers: { "Access-Control-Allow-Origin": "*" },
108
+ });
109
+ }