@openparachute/vault 0.2.3 → 0.3.0-rc.1
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/.claude/settings.local.json +8 -0
- package/CHANGELOG.md +70 -0
- package/CLAUDE.md +17 -7
- package/README.md +169 -136
- package/core/src/core.test.ts +603 -19
- package/core/src/indexed-fields.test.ts +285 -0
- package/core/src/indexed-fields.ts +238 -0
- package/core/src/mcp.ts +127 -6
- package/core/src/notes.ts +157 -11
- package/core/src/query-operators.ts +174 -0
- package/core/src/schema.ts +69 -2
- package/core/src/store.ts +92 -0
- package/core/src/tag-schemas.ts +5 -0
- package/core/src/types.ts +29 -1
- package/docs/HTTP_API.md +105 -1
- package/package/package.json +32 -0
- package/package.json +2 -2
- package/src/auth.test.ts +83 -114
- package/src/auth.ts +68 -6
- package/src/backup-launchd.ts +1 -1
- package/src/backup.test.ts +1 -1
- package/src/backup.ts +18 -17
- package/src/cli.ts +179 -121
- package/src/config-triggers.test.ts +49 -0
- package/src/config.test.ts +317 -2
- package/src/config.ts +420 -40
- package/src/context.test.ts +136 -0
- package/src/context.ts +115 -0
- package/src/daemon.ts +17 -16
- package/src/doctor.test.ts +9 -7
- package/src/launchd.test.ts +1 -1
- package/src/launchd.ts +6 -6
- package/src/mcp-http.ts +75 -21
- package/src/mcp-install.test.ts +125 -0
- package/src/mcp-install.ts +60 -0
- package/src/mcp-tools.ts +34 -96
- package/src/module-config.ts +109 -0
- package/src/oauth.test.ts +345 -57
- package/src/oauth.ts +155 -35
- package/src/published.test.ts +2 -2
- package/src/routes.ts +209 -33
- package/src/routing.test.ts +817 -300
- package/src/routing.ts +204 -202
- package/src/scopes.test.ts +136 -0
- package/src/scopes.ts +105 -0
- package/src/scribe-env.test.ts +49 -0
- package/src/scribe-env.ts +33 -0
- package/src/server.ts +57 -5
- package/src/services-manifest.test.ts +140 -0
- package/src/services-manifest.ts +99 -0
- package/src/systemd.ts +3 -3
- package/src/token-store.ts +42 -9
- package/src/transcription-worker.test.ts +583 -0
- package/src/transcription-worker.ts +346 -0
- package/src/triggers.test.ts +191 -1
- package/src/triggers.ts +17 -2
- package/src/vault.test.ts +693 -77
- package/src/version.test.ts +1 -1
|
@@ -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
|
|
2
|
+
* MCP tool generation for the scoped (per-vault) MCP endpoint.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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
|
|
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
|
|
18
|
-
const
|
|
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 "${
|
|
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
|
-
*
|
|
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
|
-
|
|
118
|
-
overrideVaultInfo(tools, vaultName);
|
|
48
|
+
overrideVaultInfo(tools, vaultName, auth);
|
|
119
49
|
|
|
120
50
|
return tools;
|
|
121
51
|
}
|
|
122
52
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
}
|