@openparachute/vault 0.3.0-rc.1 → 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.
- package/core/src/hooks.ts +111 -3
- package/core/src/store.ts +3 -1
- package/docs/auth-model.md +340 -0
- package/package.json +1 -1
- package/src/bind.test.ts +28 -0
- package/src/bind.ts +19 -0
- package/src/cli.ts +52 -15
- package/src/scopes.test.ts +158 -0
- package/src/scopes.ts +148 -0
- package/src/server.ts +19 -7
- package/src/transcription-worker.test.ts +282 -1
- package/src/transcription-worker.ts +171 -16
package/src/cli.ts
CHANGED
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
import { confirm, ask, askPassword, choose } from "./prompt.ts";
|
|
80
80
|
import { generateToken, createToken, listTokens, revokeToken, migrateVaultKeys } from "./token-store.ts";
|
|
81
81
|
import type { TokenPermission } from "./token-store.ts";
|
|
82
|
+
import { resolveCreateTokenFlags, VAULT_SCOPES } from "./scopes.ts";
|
|
82
83
|
import { getVaultStore } from "./vault-store.ts";
|
|
83
84
|
import { upsertService, ServicesManifestError } from "./services-manifest.ts";
|
|
84
85
|
import {
|
|
@@ -135,7 +136,7 @@ if (!SKIP_MIGRATION.has(command)) {
|
|
|
135
136
|
|
|
136
137
|
switch (command) {
|
|
137
138
|
case "init":
|
|
138
|
-
await cmdInit();
|
|
139
|
+
await cmdInit(cmdArgs);
|
|
139
140
|
break;
|
|
140
141
|
case "create":
|
|
141
142
|
cmdCreate(cmdArgs);
|
|
@@ -216,9 +217,16 @@ switch (command) {
|
|
|
216
217
|
// Command implementations
|
|
217
218
|
// ---------------------------------------------------------------------------
|
|
218
219
|
|
|
219
|
-
async function cmdInit() {
|
|
220
|
+
async function cmdInit(args: string[] = []) {
|
|
220
221
|
ensureConfigDirSync();
|
|
221
222
|
|
|
223
|
+
// Flags: --mcp installs MCP in ~/.claude.json without prompting;
|
|
224
|
+
// --no-mcp skips it without prompting. If both passed, --no-mcp wins
|
|
225
|
+
// (safer default). Neither → prompt in a TTY, default-yes in a
|
|
226
|
+
// non-TTY for back-compat with existing piped install scripts.
|
|
227
|
+
const flagMcpOn = args.includes("--mcp");
|
|
228
|
+
const flagMcpOff = args.includes("--no-mcp");
|
|
229
|
+
|
|
222
230
|
const isMac = process.platform === "darwin";
|
|
223
231
|
const isLinux = process.platform === "linux";
|
|
224
232
|
const isFirstRun = !existsSync(ENV_PATH);
|
|
@@ -341,9 +349,28 @@ async function cmdInit() {
|
|
|
341
349
|
}
|
|
342
350
|
console.log(` Listening on http://0.0.0.0:${globalConfig.port || DEFAULT_PORT}`);
|
|
343
351
|
|
|
344
|
-
// 7. Install MCP for Claude Code (with token for auth)
|
|
345
|
-
|
|
346
|
-
|
|
352
|
+
// 7. Install MCP for Claude Code (with token for auth) — user confirms
|
|
353
|
+
// unless --mcp / --no-mcp explicitly passed. Writing to ~/.claude.json
|
|
354
|
+
// is a side effect some users don't want; default-yes in a TTY since
|
|
355
|
+
// most users installing vault want Claude Code to see it, but ask.
|
|
356
|
+
let addMcp: boolean;
|
|
357
|
+
if (flagMcpOff) {
|
|
358
|
+
addMcp = false;
|
|
359
|
+
} else if (flagMcpOn) {
|
|
360
|
+
addMcp = true;
|
|
361
|
+
} else if (process.stdin.isTTY) {
|
|
362
|
+
addMcp = await confirm("Add Vault MCP to Claude Code (~/.claude.json)?", true);
|
|
363
|
+
} else {
|
|
364
|
+
addMcp = true; // non-interactive: preserve the installable-via-pipe default
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (addMcp) {
|
|
368
|
+
installMcpConfig(apiKey);
|
|
369
|
+
console.log(` MCP server added to ~/.claude.json`);
|
|
370
|
+
} else {
|
|
371
|
+
console.log(" Skipped adding MCP to ~/.claude.json.");
|
|
372
|
+
console.log(" Run `parachute-vault mcp-install` later if you want it.");
|
|
373
|
+
}
|
|
347
374
|
|
|
348
375
|
// 8. Summary
|
|
349
376
|
console.log("\n---");
|
|
@@ -818,7 +845,8 @@ function cmdTokens(args: string[]) {
|
|
|
818
845
|
return;
|
|
819
846
|
}
|
|
820
847
|
|
|
821
|
-
// parachute-vault tokens create --vault <name>
|
|
848
|
+
// parachute-vault tokens create --vault <name>
|
|
849
|
+
// [--scope vault:read,vault:write | --read | --permission full|read]
|
|
822
850
|
// [--expires <duration>] [--label <label>]
|
|
823
851
|
if (subcmd === "create") {
|
|
824
852
|
const vaultFlag = args.indexOf("--vault");
|
|
@@ -830,15 +858,17 @@ function cmdTokens(args: string[]) {
|
|
|
830
858
|
process.exit(1);
|
|
831
859
|
}
|
|
832
860
|
|
|
833
|
-
// --read
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
const
|
|
838
|
-
if (
|
|
839
|
-
console.error(
|
|
861
|
+
// Combining --scope / --read / --permission is always an error: a
|
|
862
|
+
// user minting a token expects exactly one narrowing signal, and
|
|
863
|
+
// silently picking one would mint the opposite of what the other
|
|
864
|
+
// reading intended. See resolveCreateTokenFlags.
|
|
865
|
+
const resolved = resolveCreateTokenFlags(args);
|
|
866
|
+
if (resolved.error) {
|
|
867
|
+
console.error(resolved.error);
|
|
840
868
|
process.exit(1);
|
|
841
869
|
}
|
|
870
|
+
const scopes = resolved.scopes;
|
|
871
|
+
const permission: TokenPermission = resolved.permission;
|
|
842
872
|
|
|
843
873
|
const expiresFlag = args.indexOf("--expires");
|
|
844
874
|
let expiresAt: string | null = null;
|
|
@@ -859,12 +889,15 @@ function cmdTokens(args: string[]) {
|
|
|
859
889
|
createToken(store.db, fullToken, {
|
|
860
890
|
label,
|
|
861
891
|
permission,
|
|
892
|
+
scopes,
|
|
862
893
|
expires_at: expiresAt,
|
|
863
894
|
});
|
|
864
895
|
|
|
896
|
+
const displayScopes = scopes ?? [...VAULT_SCOPES];
|
|
865
897
|
console.log(`Created token for vault "${vaultName}":`);
|
|
866
898
|
console.log(` Token: ${fullToken}`);
|
|
867
899
|
console.log(` Permission: ${permission}`);
|
|
900
|
+
console.log(` Scopes: ${displayScopes.join(" ")}`);
|
|
868
901
|
if (expiresAt) console.log(` Expires: ${expiresAt}`);
|
|
869
902
|
console.log(` Label: ${label}`);
|
|
870
903
|
console.log();
|
|
@@ -2031,7 +2064,7 @@ data, and debugging.
|
|
|
2031
2064
|
── Standard use ───────────────────────────────────────────────────────
|
|
2032
2065
|
|
|
2033
2066
|
Setup:
|
|
2034
|
-
parachute-vault init
|
|
2067
|
+
parachute-vault init [--mcp | --no-mcp] Set up everything (one command, idempotent)
|
|
2035
2068
|
parachute-vault doctor Diagnose install/config issues
|
|
2036
2069
|
parachute-vault uninstall [--wipe] [--yes]
|
|
2037
2070
|
Remove daemon + MCP entry; --wipe also removes vaults, .env,
|
|
@@ -2050,7 +2083,11 @@ Tokens:
|
|
|
2050
2083
|
parachute-vault tokens List all tokens
|
|
2051
2084
|
parachute-vault tokens create Create a full-access token in the default vault
|
|
2052
2085
|
parachute-vault tokens create --vault <name> Create a token in a specific vault
|
|
2053
|
-
parachute-vault tokens create --read Read-only token
|
|
2086
|
+
parachute-vault tokens create --read Read-only token (shorthand for --scope vault:read)
|
|
2087
|
+
parachute-vault tokens create --scope vault:write
|
|
2088
|
+
Narrow the token's scopes. Accepts a comma-separated
|
|
2089
|
+
list or repeated --scope flags. Valid scopes:
|
|
2090
|
+
vault:read, vault:write, vault:admin.
|
|
2054
2091
|
parachute-vault tokens create --label x Set a label
|
|
2055
2092
|
parachute-vault tokens create --expires 30d Expiring token
|
|
2056
2093
|
parachute-vault tokens revoke <token-id> Revoke a token (default vault)
|
package/src/scopes.test.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
SCOPE_WRITE,
|
|
11
11
|
SCOPE_ADMIN,
|
|
12
12
|
parseScopes,
|
|
13
|
+
parseScopeFlags,
|
|
14
|
+
resolveCreateTokenFlags,
|
|
13
15
|
hasScope,
|
|
14
16
|
scopeForMethod,
|
|
15
17
|
legacyPermissionToScopes,
|
|
@@ -123,6 +125,162 @@ describe("legacyPermissionToScopes", () => {
|
|
|
123
125
|
});
|
|
124
126
|
});
|
|
125
127
|
|
|
128
|
+
describe("parseScopeFlags", () => {
|
|
129
|
+
test("returns null scopes when --scope is absent", () => {
|
|
130
|
+
expect(parseScopeFlags([])).toEqual({ scopes: null, error: null });
|
|
131
|
+
expect(parseScopeFlags(["--vault", "default", "--label", "x"]))
|
|
132
|
+
.toEqual({ scopes: null, error: null });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("single --scope with one value", () => {
|
|
136
|
+
expect(parseScopeFlags(["--scope", "vault:read"]))
|
|
137
|
+
.toEqual({ scopes: [SCOPE_READ], error: null });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("single --scope with comma-separated values", () => {
|
|
141
|
+
expect(parseScopeFlags(["--scope", "vault:read,vault:write"]))
|
|
142
|
+
.toEqual({ scopes: [SCOPE_READ, SCOPE_WRITE], error: null });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("repeated --scope flags", () => {
|
|
146
|
+
expect(parseScopeFlags(["--scope", "vault:read", "--scope", "vault:write"]))
|
|
147
|
+
.toEqual({ scopes: [SCOPE_READ, SCOPE_WRITE], error: null });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("dedupes while preserving first-occurrence order", () => {
|
|
151
|
+
expect(parseScopeFlags(["--scope", "vault:write,vault:read,vault:write"]))
|
|
152
|
+
.toEqual({ scopes: [SCOPE_WRITE, SCOPE_READ], error: null });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("trims whitespace around each scope", () => {
|
|
156
|
+
expect(parseScopeFlags(["--scope", " vault:read , vault:write "]))
|
|
157
|
+
.toEqual({ scopes: [SCOPE_READ, SCOPE_WRITE], error: null });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("rejects unknown scopes with a helpful error", () => {
|
|
161
|
+
const result = parseScopeFlags(["--scope", "vault:frob"]);
|
|
162
|
+
expect(result.scopes).toBeNull();
|
|
163
|
+
expect(result.error).toContain("Unknown scope");
|
|
164
|
+
expect(result.error).toContain("vault:frob");
|
|
165
|
+
expect(result.error).toContain("vault:read, vault:write, vault:admin");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("rejects a mixed list with one invalid scope", () => {
|
|
169
|
+
const result = parseScopeFlags(["--scope", "vault:read,admin"]);
|
|
170
|
+
expect(result.scopes).toBeNull();
|
|
171
|
+
expect(result.error).toContain("admin");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("rejects --scope with no value (end of argv)", () => {
|
|
175
|
+
const result = parseScopeFlags(["--scope"]);
|
|
176
|
+
expect(result.scopes).toBeNull();
|
|
177
|
+
expect(result.error).toContain("--scope requires a value");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("rejects --scope followed by another flag", () => {
|
|
181
|
+
const result = parseScopeFlags(["--scope", "--label", "x"]);
|
|
182
|
+
expect(result.scopes).toBeNull();
|
|
183
|
+
expect(result.error).toContain("--scope requires a value");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("rejects --scope with an empty value", () => {
|
|
187
|
+
const result = parseScopeFlags(["--scope", ""]);
|
|
188
|
+
expect(result.scopes).toBeNull();
|
|
189
|
+
expect(result.error).toContain("empty");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("rejects --scope with only commas", () => {
|
|
193
|
+
const result = parseScopeFlags(["--scope", ",,"]);
|
|
194
|
+
expect(result.scopes).toBeNull();
|
|
195
|
+
expect(result.error).toContain("empty");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("resolveCreateTokenFlags", () => {
|
|
200
|
+
test("no flags → full scope, full permission (historical default)", () => {
|
|
201
|
+
expect(resolveCreateTokenFlags([]))
|
|
202
|
+
.toEqual({ scopes: undefined, permission: "full", error: null });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("--read alone → [vault:read], read permission", () => {
|
|
206
|
+
expect(resolveCreateTokenFlags(["--read"]))
|
|
207
|
+
.toEqual({ scopes: [SCOPE_READ], permission: "read", error: null });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("--scope vault:read alone → read permission", () => {
|
|
211
|
+
expect(resolveCreateTokenFlags(["--scope", "vault:read"]))
|
|
212
|
+
.toEqual({ scopes: [SCOPE_READ], permission: "read", error: null });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("--scope vault:write,vault:read → full permission (any write surface → full)", () => {
|
|
216
|
+
expect(resolveCreateTokenFlags(["--scope", "vault:write,vault:read"]))
|
|
217
|
+
.toEqual({ scopes: [SCOPE_WRITE, SCOPE_READ], permission: "full", error: null });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("--scope vault:admin alone → full permission", () => {
|
|
221
|
+
expect(resolveCreateTokenFlags(["--scope", "vault:admin"]))
|
|
222
|
+
.toEqual({ scopes: [SCOPE_ADMIN], permission: "full", error: null });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("--permission read → no scopes (token-store default), read permission", () => {
|
|
226
|
+
expect(resolveCreateTokenFlags(["--permission", "read"]))
|
|
227
|
+
.toEqual({ scopes: undefined, permission: "read", error: null });
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("--permission full → no scopes, full permission", () => {
|
|
231
|
+
expect(resolveCreateTokenFlags(["--permission", "full"]))
|
|
232
|
+
.toEqual({ scopes: undefined, permission: "full", error: null });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("--scope + --read errors and mentions both flags", () => {
|
|
236
|
+
const result = resolveCreateTokenFlags(["--scope", "vault:write", "--read"]);
|
|
237
|
+
expect(result.scopes).toBeUndefined();
|
|
238
|
+
expect(result.error).toContain("--scope");
|
|
239
|
+
expect(result.error).toContain("--read");
|
|
240
|
+
expect(result.error).toContain("cannot be combined");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("--scope + --permission errors", () => {
|
|
244
|
+
const result = resolveCreateTokenFlags(["--scope", "vault:read", "--permission", "full"]);
|
|
245
|
+
expect(result.scopes).toBeUndefined();
|
|
246
|
+
expect(result.error).toContain("--scope");
|
|
247
|
+
expect(result.error).toContain("--permission");
|
|
248
|
+
expect(result.error).toContain("cannot be combined");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("--read + --permission errors", () => {
|
|
252
|
+
const result = resolveCreateTokenFlags(["--read", "--permission", "full"]);
|
|
253
|
+
expect(result.scopes).toBeUndefined();
|
|
254
|
+
expect(result.error).toContain("--read");
|
|
255
|
+
expect(result.error).toContain("--permission");
|
|
256
|
+
expect(result.error).toContain("cannot be combined");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("invalid --permission value errors with prefer-scope hint", () => {
|
|
260
|
+
const result = resolveCreateTokenFlags(["--permission", "admin"]);
|
|
261
|
+
expect(result.scopes).toBeUndefined();
|
|
262
|
+
expect(result.error).toContain("admin");
|
|
263
|
+
expect(result.error).toContain("full");
|
|
264
|
+
expect(result.error).toContain("read");
|
|
265
|
+
expect(result.error).toContain("--scope");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("--permission with no value errors", () => {
|
|
269
|
+
expect(resolveCreateTokenFlags(["--permission"]).error).toContain("requires a value");
|
|
270
|
+
expect(resolveCreateTokenFlags(["--permission", "--label", "x"]).error).toContain("requires a value");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("surfaces parseScopeFlags errors unchanged", () => {
|
|
274
|
+
const result = resolveCreateTokenFlags(["--scope", "vault:frob"]);
|
|
275
|
+
expect(result.error).toContain("Unknown scope");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("ignores unrelated flags", () => {
|
|
279
|
+
const result = resolveCreateTokenFlags(["--vault", "journal", "--label", "r", "--read"]);
|
|
280
|
+
expect(result).toEqual({ scopes: [SCOPE_READ], permission: "read", error: null });
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
126
284
|
describe("serializeScopes — round-trips with parseScopes", () => {
|
|
127
285
|
test("joins with spaces", () => {
|
|
128
286
|
expect(serializeScopes([SCOPE_READ, SCOPE_WRITE])).toBe("vault:read vault:write");
|
package/src/scopes.ts
CHANGED
|
@@ -103,3 +103,151 @@ export function legacyPermissionToScopes(permission: string): string[] {
|
|
|
103
103
|
export function serializeScopes(scopes: string[]): string {
|
|
104
104
|
return scopes.join(" ");
|
|
105
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parse `--scope` flag values from an argv list into a validated scope list.
|
|
109
|
+
*
|
|
110
|
+
* Accepts repeatable `--scope vault:read --scope vault:write` and
|
|
111
|
+
* comma-separated `--scope vault:read,vault:write` (and a mix of the two).
|
|
112
|
+
* Scopes are validated against `VAULT_SCOPES` — we refuse to mint a token
|
|
113
|
+
* with a scope the server has no way to enforce.
|
|
114
|
+
*
|
|
115
|
+
* Return shape: `{scopes}` is `null` when no `--scope` appears anywhere, so
|
|
116
|
+
* the caller can distinguish "flag not set" from "flag set to empty." On
|
|
117
|
+
* validation failure, `error` is a human-readable message suitable for
|
|
118
|
+
* `console.error` + `process.exit(1)`.
|
|
119
|
+
*/
|
|
120
|
+
export function parseScopeFlags(
|
|
121
|
+
args: string[],
|
|
122
|
+
): { scopes: string[] | null; error: string | null } {
|
|
123
|
+
const validList = VAULT_SCOPES.join(", ");
|
|
124
|
+
const raw: string[] = [];
|
|
125
|
+
for (let i = 0; i < args.length; i++) {
|
|
126
|
+
if (args[i] !== "--scope") continue;
|
|
127
|
+
const val = args[i + 1];
|
|
128
|
+
if (val === undefined || val.startsWith("--")) {
|
|
129
|
+
return { scopes: null, error: `--scope requires a value. Valid scopes: ${validList}` };
|
|
130
|
+
}
|
|
131
|
+
raw.push(val);
|
|
132
|
+
i++;
|
|
133
|
+
}
|
|
134
|
+
if (raw.length === 0) return { scopes: null, error: null };
|
|
135
|
+
|
|
136
|
+
const expanded = raw
|
|
137
|
+
.flatMap((v) => v.split(","))
|
|
138
|
+
.map((s) => s.trim())
|
|
139
|
+
.filter((s) => s.length > 0);
|
|
140
|
+
if (expanded.length === 0) {
|
|
141
|
+
return { scopes: null, error: `--scope value was empty. Valid scopes: ${validList}` };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const validSet = new Set<string>(VAULT_SCOPES);
|
|
145
|
+
const invalid = expanded.filter((s) => !validSet.has(s));
|
|
146
|
+
if (invalid.length > 0) {
|
|
147
|
+
return {
|
|
148
|
+
scopes: null,
|
|
149
|
+
error: `Unknown scope(s): ${invalid.join(", ")}. Valid scopes: ${validList}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const seen = new Set<string>();
|
|
154
|
+
const deduped: string[] = [];
|
|
155
|
+
for (const s of expanded) {
|
|
156
|
+
if (!seen.has(s)) {
|
|
157
|
+
seen.add(s);
|
|
158
|
+
deduped.push(s);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { scopes: deduped, error: null };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Resolve `parachute vault tokens create` argv into a concrete scope set +
|
|
166
|
+
* legacy `permission` column value, or an actionable error.
|
|
167
|
+
*
|
|
168
|
+
* Precedence is **exclusive**: `--scope`, `--read`, and `--permission` all
|
|
169
|
+
* narrow the token, but combining them is always an error — a user who
|
|
170
|
+
* writes `--scope vault:write --read` almost certainly expects one of the
|
|
171
|
+
* two to win, and silently picking would mint the opposite of what at
|
|
172
|
+
* least one reading intended. Fail loud for anything token-minting.
|
|
173
|
+
*
|
|
174
|
+
* With no narrowing flag, falls back to a full-scope token for back-compat.
|
|
175
|
+
*/
|
|
176
|
+
export function resolveCreateTokenFlags(args: string[]): {
|
|
177
|
+
scopes: string[] | undefined;
|
|
178
|
+
permission: "full" | "read";
|
|
179
|
+
error: string | null;
|
|
180
|
+
} {
|
|
181
|
+
const scopeResult = parseScopeFlags(args);
|
|
182
|
+
if (scopeResult.error) {
|
|
183
|
+
return { scopes: undefined, permission: "full", error: scopeResult.error };
|
|
184
|
+
}
|
|
185
|
+
const hasScopeFlag = scopeResult.scopes !== null;
|
|
186
|
+
const hasReadFlag = args.includes("--read");
|
|
187
|
+
const permIdx = args.indexOf("--permission");
|
|
188
|
+
const hasPermFlag = permIdx !== -1;
|
|
189
|
+
|
|
190
|
+
if (hasScopeFlag && hasReadFlag) {
|
|
191
|
+
return {
|
|
192
|
+
scopes: undefined,
|
|
193
|
+
permission: "full",
|
|
194
|
+
error:
|
|
195
|
+
"--scope and --read cannot be combined. Pick one:\n" +
|
|
196
|
+
" --read # shorthand for --scope vault:read\n" +
|
|
197
|
+
" --scope vault:read # equivalent, explicit\n" +
|
|
198
|
+
" --scope vault:write # write scope",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (hasScopeFlag && hasPermFlag) {
|
|
202
|
+
return {
|
|
203
|
+
scopes: undefined,
|
|
204
|
+
permission: "full",
|
|
205
|
+
error:
|
|
206
|
+
"--scope and --permission cannot be combined. --scope is the canonical way to narrow a token; --permission is legacy.",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (hasReadFlag && hasPermFlag) {
|
|
210
|
+
return {
|
|
211
|
+
scopes: undefined,
|
|
212
|
+
permission: "full",
|
|
213
|
+
error: "--read and --permission cannot be combined. --read is a shorthand for --permission read.",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (hasPermFlag) {
|
|
218
|
+
const rawPerm = args[permIdx + 1];
|
|
219
|
+
if (!rawPerm || rawPerm.startsWith("--")) {
|
|
220
|
+
return {
|
|
221
|
+
scopes: undefined,
|
|
222
|
+
permission: "full",
|
|
223
|
+
error: `--permission requires a value ("full" or "read"). Prefer --scope for new scripts.`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (!["full", "read"].includes(rawPerm)) {
|
|
227
|
+
return {
|
|
228
|
+
scopes: undefined,
|
|
229
|
+
permission: "full",
|
|
230
|
+
error: `Invalid --permission: ${rawPerm}. Must be "full" or "read". Prefer --scope for new scripts.`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (scopeResult.scopes) {
|
|
236
|
+
const scopes = scopeResult.scopes;
|
|
237
|
+
const permission: "full" | "read" =
|
|
238
|
+
scopes.includes(SCOPE_WRITE) || scopes.includes(SCOPE_ADMIN) ? "full" : "read";
|
|
239
|
+
return { scopes, permission, error: null };
|
|
240
|
+
}
|
|
241
|
+
if (hasReadFlag) {
|
|
242
|
+
return { scopes: [SCOPE_READ], permission: "read", error: null };
|
|
243
|
+
}
|
|
244
|
+
if (hasPermFlag) {
|
|
245
|
+
const rawPerm = args[permIdx + 1];
|
|
246
|
+
return {
|
|
247
|
+
scopes: undefined,
|
|
248
|
+
permission: rawPerm === "read" ? "read" : "full",
|
|
249
|
+
error: null,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return { scopes: undefined, permission: "full", error: null };
|
|
253
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
|
|
18
18
|
import { readVaultConfig, readGlobalConfig, writeGlobalConfig, writeVaultConfig, listVaults, DEFAULT_PORT, ensureConfigDirSync, loadEnvFile, generateApiKey, hashKey } from "./config.ts";
|
|
19
19
|
import { migrateVaultKeys } from "./token-store.ts";
|
|
20
|
-
import { getVaultStore } from "./vault-store.ts";
|
|
20
|
+
import { getVaultStore, getVaultNameForStore } from "./vault-store.ts";
|
|
21
21
|
import { defaultHookRegistry } from "../core/src/hooks.ts";
|
|
22
22
|
import { registerTriggers } from "./triggers.ts";
|
|
23
23
|
import { route } from "./routing.ts";
|
|
24
|
-
import { startTranscriptionWorker, type TranscriptionWorker } from "./transcription-worker.ts";
|
|
24
|
+
import { startTranscriptionWorker, registerTranscriptionHook, type TranscriptionWorker } from "./transcription-worker.ts";
|
|
25
25
|
import { assetsDir } from "./routes.ts";
|
|
26
26
|
import { resolveScribeAuthToken } from "./scribe-env.ts";
|
|
27
|
+
import { resolveBindHostname } from "./bind.ts";
|
|
27
28
|
|
|
28
29
|
// Register webhook triggers from global config. Replaces the old hardcoded
|
|
29
30
|
// tts-hook and transcription-hook with config-driven webhooks.
|
|
@@ -59,6 +60,12 @@ function safeHost(url: string): string | null {
|
|
|
59
60
|
try { return new URL(url).host; } catch { return null; }
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
// Load .env before anything reads process.env — otherwise SCRIBE_URL and
|
|
64
|
+
// friends configured in ~/.parachute/vault/.env are invisible to the
|
|
65
|
+
// transcription-worker check and the trigger double-fire warning below.
|
|
66
|
+
ensureConfigDirSync();
|
|
67
|
+
loadEnvFile();
|
|
68
|
+
|
|
62
69
|
registerConfiguredTriggers();
|
|
63
70
|
|
|
64
71
|
/**
|
|
@@ -78,14 +85,18 @@ if (process.env.SCRIBE_URL) {
|
|
|
78
85
|
getAudioRetention: (vault) => readVaultConfig(vault)?.audio_retention ?? "keep",
|
|
79
86
|
getContextPredicates: (vault) => readVaultConfig(vault)?.transcription?.context,
|
|
80
87
|
});
|
|
88
|
+
// Event-driven hot path — the `attachment:created` hook fires the worker
|
|
89
|
+
// in a microtask instead of waiting for the 30s sweep.
|
|
90
|
+
registerTranscriptionHook(
|
|
91
|
+
defaultHookRegistry,
|
|
92
|
+
transcriptionWorker,
|
|
93
|
+
(store) => getVaultNameForStore(store as never),
|
|
94
|
+
);
|
|
81
95
|
console.log(`[transcribe] worker started → ${process.env.SCRIBE_URL}`);
|
|
82
96
|
} else {
|
|
83
97
|
console.log("[transcribe] worker disabled (set SCRIBE_URL to enable)");
|
|
84
98
|
}
|
|
85
99
|
|
|
86
|
-
ensureConfigDirSync();
|
|
87
|
-
loadEnvFile();
|
|
88
|
-
|
|
89
100
|
// Auto-init: create a default vault if none exist (first run in Docker)
|
|
90
101
|
if (listVaults().length === 0) {
|
|
91
102
|
const globalConfig = readGlobalConfig();
|
|
@@ -159,10 +170,11 @@ for (const vaultName of listVaults()) {
|
|
|
159
170
|
|
|
160
171
|
const globalConfig = readGlobalConfig();
|
|
161
172
|
const port = parseInt(process.env.PORT ?? "") || globalConfig.port || DEFAULT_PORT;
|
|
173
|
+
const hostname = resolveBindHostname();
|
|
162
174
|
|
|
163
175
|
const server = Bun.serve({
|
|
164
176
|
port,
|
|
165
|
-
hostname
|
|
177
|
+
hostname,
|
|
166
178
|
idleTimeout: 120, // seconds — webhook triggers can take a while
|
|
167
179
|
async fetch(req, server) {
|
|
168
180
|
const url = new URL(req.url);
|
|
@@ -208,7 +220,7 @@ const server = Bun.serve({
|
|
|
208
220
|
},
|
|
209
221
|
});
|
|
210
222
|
|
|
211
|
-
console.log(`Parachute Vault server listening on http
|
|
223
|
+
console.log(`Parachute Vault server listening on http://${hostname}:${server.port}`);
|
|
212
224
|
|
|
213
225
|
// Graceful shutdown — best-effort drain of in-flight note-mutation hooks.
|
|
214
226
|
async function shutdown(signal: string): Promise<void> {
|