@openparachute/vault 0.3.3 → 0.4.3
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/.parachute/module.json +15 -0
- package/README.md +133 -0
- package/core/src/core.test.ts +2990 -92
- package/core/src/links.ts +1 -1
- package/core/src/mcp.ts +413 -68
- package/core/src/notes.ts +693 -42
- package/core/src/obsidian.ts +3 -3
- package/core/src/paths.ts +1 -1
- package/core/src/query-operators.ts +23 -7
- package/core/src/schema-defaults.ts +331 -0
- package/core/src/schema.ts +467 -11
- package/core/src/store.ts +262 -8
- package/core/src/tag-hierarchy.ts +171 -0
- package/core/src/tag-schemas.ts +242 -42
- package/core/src/types.ts +96 -7
- package/core/src/vault-projection.ts +309 -0
- package/core/src/wikilinks.ts +3 -3
- package/package.json +13 -3
- package/src/admin-spa.test.ts +161 -0
- package/src/admin-spa.ts +161 -0
- package/src/auth-hub-jwt.test.ts +360 -0
- package/src/auth-status.ts +84 -0
- package/src/auth.test.ts +135 -23
- package/src/auth.ts +173 -15
- package/src/backup.ts +4 -7
- package/src/cli.ts +322 -57
- package/src/config.test.ts +44 -0
- package/src/config.ts +68 -40
- package/src/hub-jwt.test.ts +307 -0
- package/src/hub-jwt.ts +88 -0
- package/src/init.test.ts +216 -0
- package/src/mcp-http.ts +33 -29
- package/src/mcp-install.ts +1 -1
- package/src/mcp-tools.ts +318 -19
- package/src/module-config.ts +1 -1
- package/src/oauth.test.ts +345 -0
- package/src/oauth.ts +85 -14
- package/src/owner-auth.ts +57 -1
- package/src/prompt.ts +6 -5
- package/src/routes.ts +796 -61
- package/src/routing.test.ts +466 -1
- package/src/routing.ts +106 -24
- package/src/scopes.test.ts +66 -8
- package/src/scopes.ts +163 -37
- package/src/server.ts +24 -2
- package/src/services-manifest.test.ts +20 -0
- package/src/services-manifest.ts +9 -2
- package/src/stop-signal.test.ts +85 -0
- package/src/storage.test.ts +92 -0
- package/src/tag-scope.ts +118 -0
- package/src/token-store.test.ts +47 -0
- package/src/token-store.ts +128 -13
- package/src/tokens-routes.test.ts +727 -0
- package/src/tokens-routes.ts +392 -0
- package/src/transcription-worker.test.ts +5 -0
- package/src/triggers.ts +1 -1
- package/src/two-factor.ts +2 -2
- package/src/vault-create.test.ts +193 -0
- package/src/vault-name.test.ts +123 -0
- package/src/vault-name.ts +80 -0
- package/src/vault.test.ts +1626 -183
- package/tsconfig.json +8 -1
- package/.claude/settings.local.json +0 -8
- package/.dockerignore +0 -8
- package/.env.example +0 -9
- package/CHANGELOG.md +0 -175
- package/CLAUDE.md +0 -125
- package/Caddyfile +0 -3
- package/Dockerfile +0 -22
- package/bun.lock +0 -219
- package/bunfig.toml +0 -2
- package/deploy/parachute-vault.service +0 -20
- package/docker-compose.yml +0 -50
- package/docs/HTTP_API.md +0 -434
- package/docs/auth-model.md +0 -340
- package/fly.toml +0 -24
- package/package/package.json +0 -32
- package/railway.json +0 -14
- package/scripts/migrate-audio-to-opus.test.ts +0 -237
- package/scripts/migrate-audio-to-opus.ts +0 -499
package/src/config.test.ts
CHANGED
|
@@ -180,6 +180,50 @@ describe("config", () => {
|
|
|
180
180
|
writeGlobalConfig({ port: 1940, discovery: "disabled" });
|
|
181
181
|
expect(readGlobalConfig().discovery).toBe("disabled");
|
|
182
182
|
});
|
|
183
|
+
|
|
184
|
+
test("global api_keys round-trip scope: read|write and default missing scope to write", () => {
|
|
185
|
+
// Regression for the silent privilege-escalation bug: the global YAML
|
|
186
|
+
// parser used to drop the `scope` field, leaving `globalKey.scope`
|
|
187
|
+
// undefined. Auth then resolved any non-"read" value to "full", so a
|
|
188
|
+
// user-authored `scope: read` global key would silently get full access.
|
|
189
|
+
writeGlobalConfig({
|
|
190
|
+
port: 1940,
|
|
191
|
+
api_keys: [
|
|
192
|
+
{ id: "k_r", label: "reader", scope: "read", key_hash: "sha256:r", created_at: "2026-01-01T00:00:00.000Z" },
|
|
193
|
+
{ id: "k_w", label: "writer", scope: "write", key_hash: "sha256:w", created_at: "2026-01-01T00:00:00.000Z" },
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
const loaded = readGlobalConfig();
|
|
197
|
+
expect(loaded.api_keys?.find((k) => k.id === "k_r")?.scope).toBe("read");
|
|
198
|
+
expect(loaded.api_keys?.find((k) => k.id === "k_w")?.scope).toBe("write");
|
|
199
|
+
|
|
200
|
+
// Missing-scope branch: a hand-edited config.yaml entry without a
|
|
201
|
+
// `scope:` line must default to "write" (matches vault-level parser
|
|
202
|
+
// and the historical behavior the writer round-trips).
|
|
203
|
+
const fs = require("fs");
|
|
204
|
+
const path = `${process.env.PARACHUTE_HOME}/vault/config.yaml`;
|
|
205
|
+
fs.writeFileSync(
|
|
206
|
+
path,
|
|
207
|
+
`port: 1940\napi_keys:\n - id: k_legacy\n label: legacy\n key_hash: sha256:legacy\n created_at: "2026-01-01T00:00:00.000Z"\n`,
|
|
208
|
+
);
|
|
209
|
+
const reloaded = readGlobalConfig();
|
|
210
|
+
expect(reloaded.api_keys?.find((k) => k.id === "k_legacy")?.scope).toBe("write");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("round-trips autostart: true|false (#113)", () => {
|
|
214
|
+
// Default: absent means autostart-on (init registers the daemon).
|
|
215
|
+
writeGlobalConfig({ port: 1940 });
|
|
216
|
+
expect(readGlobalConfig().autostart).toBeUndefined();
|
|
217
|
+
|
|
218
|
+
// Explicit true (e.g., user re-enabled after disabling).
|
|
219
|
+
writeGlobalConfig({ port: 1940, autostart: true });
|
|
220
|
+
expect(readGlobalConfig().autostart).toBe(true);
|
|
221
|
+
|
|
222
|
+
// Explicit false — the opt-out signal init writes when --no-autostart is
|
|
223
|
+
// passed. Daemon registration is then skipped on this and future inits.
|
|
224
|
+
writeGlobalConfig({ port: 1940, autostart: false });
|
|
225
|
+
expect(readGlobalConfig().autostart).toBe(false);
|
|
226
|
+
});
|
|
183
227
|
});
|
|
184
228
|
|
|
185
229
|
// ---------------------------------------------------------------------------
|
package/src/config.ts
CHANGED
|
@@ -86,6 +86,14 @@ export const ERR_PATH = join(LOGS_DIR, "vault.err");
|
|
|
86
86
|
export const DEFAULT_PORT = 1940;
|
|
87
87
|
export const ASSETS_DIR = join(VAULT_HOME, "assets");
|
|
88
88
|
|
|
89
|
+
// Filesystem sentinel for graceful shutdown. `parachute-vault stop` writes
|
|
90
|
+
// this file; the running server polls for it and exits cleanly when it
|
|
91
|
+
// appears. Resolved per-call so PARACHUTE_HOME overrides (tests, Docker)
|
|
92
|
+
// match between writer and reader.
|
|
93
|
+
export function stopSignalPath(): string {
|
|
94
|
+
return join(vaultHomePath(), "stop.signal");
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
export function vaultDir(name: string): string {
|
|
90
98
|
return join(dataDirPath(), name);
|
|
91
99
|
}
|
|
@@ -243,6 +251,16 @@ export interface GlobalConfig {
|
|
|
243
251
|
* callers.
|
|
244
252
|
*/
|
|
245
253
|
discovery?: "enabled" | "disabled";
|
|
254
|
+
/**
|
|
255
|
+
* Whether `parachute-vault init` registers the daemon with launchd / systemd
|
|
256
|
+
* (which then auto-starts on boot AND auto-restarts on crash). Defaults to
|
|
257
|
+
* `true` (preserve historical behavior). When `false`, init skips daemon
|
|
258
|
+
* registration AND removes any prior registration — for CI, dev sandboxes,
|
|
259
|
+
* Docker/K8s setups, or environments where another supervisor manages the
|
|
260
|
+
* process. The user is expected to run `parachute-vault serve` manually or
|
|
261
|
+
* point their own supervisor at it.
|
|
262
|
+
*/
|
|
263
|
+
autostart?: boolean;
|
|
246
264
|
/** Backup configuration: schedule, retention, destinations. */
|
|
247
265
|
backup?: BackupConfig;
|
|
248
266
|
}
|
|
@@ -403,13 +421,13 @@ function parseVaultConfig(yaml: string, name: string): VaultConfig {
|
|
|
403
421
|
};
|
|
404
422
|
|
|
405
423
|
const nameMatch = yaml.match(/^name:\s*(.+)/m);
|
|
406
|
-
if (nameMatch) config.name = nameMatch[1]
|
|
424
|
+
if (nameMatch) config.name = nameMatch[1]!.trim();
|
|
407
425
|
|
|
408
426
|
const createdMatch = yaml.match(/^created_at:\s*"?([^"\n]+)"?/m);
|
|
409
|
-
if (createdMatch) config.created_at = createdMatch[1]
|
|
427
|
+
if (createdMatch) config.created_at = createdMatch[1]!;
|
|
410
428
|
|
|
411
429
|
const pubTagMatch = yaml.match(/^published_tag:\s*(\S+)/m);
|
|
412
|
-
if (pubTagMatch) config.published_tag = pubTagMatch[1]
|
|
430
|
+
if (pubTagMatch) config.published_tag = pubTagMatch[1]!;
|
|
413
431
|
|
|
414
432
|
const retentionMatch = yaml.match(/^audio_retention:\s*(\S+)/m);
|
|
415
433
|
if (retentionMatch) {
|
|
@@ -420,14 +438,14 @@ function parseVaultConfig(yaml: string, name: string): VaultConfig {
|
|
|
420
438
|
// Parse description (block scalar)
|
|
421
439
|
const descMatch = yaml.match(/^description:\s*\|\s*\n((?:\s{2}.+\n?)+)/m);
|
|
422
440
|
if (descMatch) {
|
|
423
|
-
config.description = descMatch[1]
|
|
441
|
+
config.description = descMatch[1]!
|
|
424
442
|
.split("\n")
|
|
425
443
|
.map((l) => l.replace(/^\s{2}/, ""))
|
|
426
444
|
.join("\n")
|
|
427
445
|
.trim();
|
|
428
446
|
} else {
|
|
429
447
|
const descSimple = yaml.match(/^description:\s*(.+)/m);
|
|
430
|
-
if (descSimple) config.description = descSimple[1]
|
|
448
|
+
if (descSimple) config.description = descSimple[1]!.trim().replace(/^"(.*)"$/, "$1");
|
|
431
449
|
}
|
|
432
450
|
|
|
433
451
|
// Parse api_keys
|
|
@@ -442,10 +460,10 @@ function parseVaultConfig(yaml: string, name: string): VaultConfig {
|
|
|
442
460
|
|
|
443
461
|
if (idMatch && hashMatch) {
|
|
444
462
|
config.api_keys.push({
|
|
445
|
-
id: idMatch[1]
|
|
463
|
+
id: idMatch[1]!,
|
|
446
464
|
label: (labelMatch?.[1] ?? "default").trim(),
|
|
447
465
|
scope: (scopeMatch?.[1] as KeyScope) ?? "write",
|
|
448
|
-
key_hash: hashMatch[1]
|
|
466
|
+
key_hash: hashMatch[1]!,
|
|
449
467
|
created_at: createdAtMatch?.[1] ?? new Date().toISOString(),
|
|
450
468
|
last_used_at: lastUsedMatch?.[1],
|
|
451
469
|
});
|
|
@@ -499,12 +517,12 @@ function parseTranscriptionContext(yaml: string): TriggerIncludeContext[] | unde
|
|
|
499
517
|
if (itemStart) {
|
|
500
518
|
pushCurrent();
|
|
501
519
|
current = { tag: "" };
|
|
502
|
-
applyContextField(current, itemStart[1]
|
|
520
|
+
applyContextField(current, itemStart[1]!, itemStart[2]!);
|
|
503
521
|
continue;
|
|
504
522
|
}
|
|
505
523
|
const fieldMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
506
524
|
if (fieldMatch && current) {
|
|
507
|
-
applyContextField(current, fieldMatch[1]
|
|
525
|
+
applyContextField(current, fieldMatch[1]!, fieldMatch[2]!);
|
|
508
526
|
continue;
|
|
509
527
|
}
|
|
510
528
|
}
|
|
@@ -533,50 +551,52 @@ function parseTagSchemas(yaml: string): Record<string, TagSchema> | undefined {
|
|
|
533
551
|
if (line.match(/^\S/) && line.trim().length > 0) break;
|
|
534
552
|
if (line.trim().length === 0) continue;
|
|
535
553
|
|
|
536
|
-
const indent = line.match(/^(\s*)/)?.[1]
|
|
554
|
+
const indent = line.match(/^(\s*)/)?.[1]?.length ?? 0;
|
|
537
555
|
|
|
538
556
|
if (indent === 2) {
|
|
539
557
|
// Tag name (e.g., " person:")
|
|
540
558
|
const tagMatch = line.match(/^\s{2}(\S+):\s*$/);
|
|
541
559
|
if (tagMatch) {
|
|
542
|
-
currentTag = tagMatch[1]
|
|
560
|
+
currentTag = tagMatch[1]!;
|
|
543
561
|
currentField = null;
|
|
544
562
|
schemas[currentTag] = {};
|
|
545
563
|
}
|
|
546
564
|
} else if (indent === 4 && currentTag) {
|
|
547
565
|
// Tag-level property (description, fields:)
|
|
566
|
+
const schema = schemas[currentTag]!;
|
|
548
567
|
const descMatch = line.match(/^\s{4}description:\s*"?([^"]*)"?\s*$/);
|
|
549
568
|
if (descMatch) {
|
|
550
|
-
|
|
569
|
+
schema.description = descMatch[1]!;
|
|
551
570
|
continue;
|
|
552
571
|
}
|
|
553
572
|
const fieldsMatch = line.match(/^\s{4}fields:\s*$/);
|
|
554
573
|
if (fieldsMatch) {
|
|
555
|
-
|
|
574
|
+
schema.fields = schema.fields ?? {};
|
|
556
575
|
currentField = null;
|
|
557
576
|
}
|
|
558
|
-
} else if (indent === 6 && currentTag && schemas[currentTag]
|
|
577
|
+
} else if (indent === 6 && currentTag && schemas[currentTag]?.fields !== undefined) {
|
|
559
578
|
// Field name (e.g., " first_appeared:")
|
|
560
579
|
const fieldMatch = line.match(/^\s{6}(\S+):\s*$/);
|
|
561
580
|
if (fieldMatch) {
|
|
562
|
-
currentField = fieldMatch[1]
|
|
563
|
-
schemas[currentTag]
|
|
581
|
+
currentField = fieldMatch[1]!;
|
|
582
|
+
schemas[currentTag]!.fields![currentField] = { type: "string" };
|
|
564
583
|
}
|
|
565
|
-
} else if (indent === 8 && currentTag && currentField && schemas[currentTag]
|
|
584
|
+
} else if (indent === 8 && currentTag && currentField && schemas[currentTag]?.fields) {
|
|
566
585
|
// Field property (type, description, enum)
|
|
586
|
+
const field = schemas[currentTag]!.fields![currentField]!;
|
|
567
587
|
const typeMatch = line.match(/^\s{8}type:\s*(\S+)/);
|
|
568
588
|
if (typeMatch) {
|
|
569
|
-
|
|
589
|
+
field.type = typeMatch[1]!;
|
|
570
590
|
continue;
|
|
571
591
|
}
|
|
572
592
|
const fdescMatch = line.match(/^\s{8}description:\s*"?([^"]*)"?\s*$/);
|
|
573
593
|
if (fdescMatch) {
|
|
574
|
-
|
|
594
|
+
field.description = fdescMatch[1]!;
|
|
575
595
|
continue;
|
|
576
596
|
}
|
|
577
597
|
const enumMatch = line.match(/^\s{8}enum:\s*\[([^\]]*)\]/);
|
|
578
598
|
if (enumMatch) {
|
|
579
|
-
|
|
599
|
+
field.enum = enumMatch[1]!
|
|
580
600
|
.split(",")
|
|
581
601
|
.map((s) => s.trim().replace(/^"(.*)"$/, "$1"));
|
|
582
602
|
}
|
|
@@ -610,7 +630,7 @@ function applyContextField(
|
|
|
610
630
|
if (key === "exclude_tag") { item.exclude_tag = value.replace(/^"(.*)"$/, "$1"); return; }
|
|
611
631
|
if (key === "include_metadata") {
|
|
612
632
|
const listMatch = value.match(/^\[([^\]]*)\]/);
|
|
613
|
-
if (listMatch) item.include_metadata = parseYamlList(listMatch[1]);
|
|
633
|
+
if (listMatch) item.include_metadata = parseYamlList(listMatch[1]!);
|
|
614
634
|
return;
|
|
615
635
|
}
|
|
616
636
|
}
|
|
@@ -655,7 +675,7 @@ function parseTriggers(yaml: string): TriggerConfig[] | undefined {
|
|
|
655
675
|
console.warn(`[config] trigger "${current.name}" has no webhook URL — skipping`);
|
|
656
676
|
}
|
|
657
677
|
}
|
|
658
|
-
current = { name: nameMatch[1]
|
|
678
|
+
current = { name: nameMatch[1]!.trim(), when: {}, action: undefined as unknown as TriggerAction };
|
|
659
679
|
section = "top";
|
|
660
680
|
continue;
|
|
661
681
|
}
|
|
@@ -677,37 +697,37 @@ function parseTriggers(yaml: string): TriggerConfig[] | undefined {
|
|
|
677
697
|
// Top-level trigger field
|
|
678
698
|
const eventsMatch = trimmed.match(/^events:\s*\[([^\]]*)\]/);
|
|
679
699
|
if (eventsMatch) {
|
|
680
|
-
current.events = parseYamlList(eventsMatch[1]) as Array<"created" | "updated">;
|
|
700
|
+
current.events = parseYamlList(eventsMatch[1]!) as Array<"created" | "updated">;
|
|
681
701
|
continue;
|
|
682
702
|
}
|
|
683
703
|
|
|
684
704
|
// When fields
|
|
685
705
|
if (section === "when") {
|
|
686
706
|
const tagsMatch = trimmed.match(/^tags:\s*\[([^\]]*)\]/);
|
|
687
|
-
if (tagsMatch) { current.when!.tags = parseYamlList(tagsMatch[1]); continue; }
|
|
707
|
+
if (tagsMatch) { current.when!.tags = parseYamlList(tagsMatch[1]!); continue; }
|
|
688
708
|
const hasContentMatch = trimmed.match(/^has_content:\s*(true|false)/);
|
|
689
709
|
if (hasContentMatch) { current.when!.has_content = hasContentMatch[1] === "true"; continue; }
|
|
690
710
|
const missingMetaMatch = trimmed.match(/^missing_metadata:\s*\[([^\]]*)\]/);
|
|
691
|
-
if (missingMetaMatch) { current.when!.missing_metadata = parseYamlList(missingMetaMatch[1]); continue; }
|
|
711
|
+
if (missingMetaMatch) { current.when!.missing_metadata = parseYamlList(missingMetaMatch[1]!); continue; }
|
|
692
712
|
const hasMetaMatch = trimmed.match(/^has_metadata:\s*\[([^\]]*)\]/);
|
|
693
|
-
if (hasMetaMatch) { current.when!.has_metadata = parseYamlList(hasMetaMatch[1]); continue; }
|
|
713
|
+
if (hasMetaMatch) { current.when!.has_metadata = parseYamlList(hasMetaMatch[1]!); continue; }
|
|
694
714
|
}
|
|
695
715
|
|
|
696
716
|
// Action fields
|
|
697
717
|
if (section === "action") {
|
|
698
718
|
const webhookMatch = trimmed.match(/^webhook:\s*(.+)/);
|
|
699
719
|
if (webhookMatch) {
|
|
700
|
-
current.action = { ...(current.action ?? {}), webhook: webhookMatch[1]
|
|
720
|
+
current.action = { ...(current.action ?? {}), webhook: webhookMatch[1]!.trim() } as TriggerAction;
|
|
701
721
|
continue;
|
|
702
722
|
}
|
|
703
723
|
const timeoutMatch = trimmed.match(/^timeout:\s*(\d+)/);
|
|
704
724
|
if (timeoutMatch && current.action) {
|
|
705
|
-
current.action.timeout = parseInt(timeoutMatch[1]
|
|
725
|
+
current.action.timeout = parseInt(timeoutMatch[1]!, 10);
|
|
706
726
|
continue;
|
|
707
727
|
}
|
|
708
728
|
const sendMatch = trimmed.match(/^send:\s*(\S+)/);
|
|
709
729
|
if (sendMatch && current.action) {
|
|
710
|
-
current.action.send = sendMatch[1] as TriggerAction["send"];
|
|
730
|
+
current.action.send = sendMatch[1]! as TriggerAction["send"];
|
|
711
731
|
continue;
|
|
712
732
|
}
|
|
713
733
|
}
|
|
@@ -719,12 +739,12 @@ function parseTriggers(yaml: string): TriggerConfig[] | undefined {
|
|
|
719
739
|
if (itemStart) {
|
|
720
740
|
pushContextItem();
|
|
721
741
|
currentContext = { tag: "" };
|
|
722
|
-
applyContextField(currentContext, itemStart[1]
|
|
742
|
+
applyContextField(currentContext, itemStart[1]!, itemStart[2]!);
|
|
723
743
|
continue;
|
|
724
744
|
}
|
|
725
745
|
const fieldMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
726
746
|
if (fieldMatch && currentContext) {
|
|
727
|
-
applyContextField(currentContext, fieldMatch[1]
|
|
747
|
+
applyContextField(currentContext, fieldMatch[1]!, fieldMatch[2]!);
|
|
728
748
|
continue;
|
|
729
749
|
}
|
|
730
750
|
}
|
|
@@ -843,7 +863,7 @@ function parseBackup(yaml: string): BackupConfig | undefined {
|
|
|
843
863
|
const tierMatch = trimmed.match(/^(daily|weekly|monthly|yearly):\s*(\S+)/);
|
|
844
864
|
if (tierMatch) {
|
|
845
865
|
const tier = tierMatch[1] as keyof RetentionPolicy;
|
|
846
|
-
const raw = tierMatch[2]
|
|
866
|
+
const raw = tierMatch[2]!.trim();
|
|
847
867
|
// "null" / "~" / "unbounded" all mean "keep every year" for the
|
|
848
868
|
// yearly tier. For the other tiers they'd be meaningless; we
|
|
849
869
|
// silently treat them as disabled (0) rather than erroring.
|
|
@@ -867,13 +887,13 @@ function parseBackup(yaml: string): BackupConfig | undefined {
|
|
|
867
887
|
pushDest();
|
|
868
888
|
hasDest = true;
|
|
869
889
|
currentDest = {};
|
|
870
|
-
(currentDest as Record<string, string>)[itemMatch[1]] = itemMatch[2]
|
|
890
|
+
(currentDest as Record<string, string>)[itemMatch[1]!] = itemMatch[2]!.trim();
|
|
871
891
|
continue;
|
|
872
892
|
}
|
|
873
893
|
// Continuation line inside the current list item.
|
|
874
894
|
const fieldMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
875
895
|
if (fieldMatch && hasDest) {
|
|
876
|
-
(currentDest as Record<string, string>)[fieldMatch[1]] = fieldMatch[2]
|
|
896
|
+
(currentDest as Record<string, string>)[fieldMatch[1]!] = fieldMatch[2]!.trim();
|
|
877
897
|
continue;
|
|
878
898
|
}
|
|
879
899
|
}
|
|
@@ -1106,14 +1126,18 @@ export function readGlobalConfig(): GlobalConfig {
|
|
|
1106
1126
|
const passwordHashMatch = yaml.match(/^owner_password_hash:\s*"([^"]+)"/m);
|
|
1107
1127
|
const totpSecretMatch = yaml.match(/^totp_secret:\s*"([^"]+)"/m);
|
|
1108
1128
|
const discoveryMatch = yaml.match(/^discovery:\s*(enabled|disabled)/m);
|
|
1129
|
+
const autostartMatch = yaml.match(/^autostart:\s*(true|false)/m);
|
|
1109
1130
|
const config: GlobalConfig = {
|
|
1110
|
-
port: portMatch ? parseInt(portMatch[1]
|
|
1131
|
+
port: portMatch ? parseInt(portMatch[1]!, 10) : DEFAULT_PORT,
|
|
1111
1132
|
default_vault: defaultVaultMatch?.[1],
|
|
1112
1133
|
owner_password_hash: passwordHashMatch?.[1],
|
|
1113
1134
|
totp_secret: totpSecretMatch?.[1],
|
|
1114
1135
|
};
|
|
1115
1136
|
if (discoveryMatch) {
|
|
1116
|
-
config.discovery = discoveryMatch[1] as "enabled" | "disabled";
|
|
1137
|
+
config.discovery = discoveryMatch[1]! as "enabled" | "disabled";
|
|
1138
|
+
}
|
|
1139
|
+
if (autostartMatch) {
|
|
1140
|
+
config.autostart = autostartMatch[1]! === "true";
|
|
1117
1141
|
}
|
|
1118
1142
|
|
|
1119
1143
|
// Parse backup_codes: a YAML list of quoted bcrypt hashes under
|
|
@@ -1128,7 +1152,7 @@ export function readGlobalConfig(): GlobalConfig {
|
|
|
1128
1152
|
for (const line of lines) {
|
|
1129
1153
|
if (line.match(/^\S/) && line.trim().length > 0) break; // next top-level key
|
|
1130
1154
|
const m = line.match(/^\s+-\s+"([^"]+)"/);
|
|
1131
|
-
if (m) codes.push(m[1]);
|
|
1155
|
+
if (m) codes.push(m[1]!);
|
|
1132
1156
|
}
|
|
1133
1157
|
if (codes.length > 0) config.backup_codes = codes;
|
|
1134
1158
|
}
|
|
@@ -1140,14 +1164,16 @@ export function readGlobalConfig(): GlobalConfig {
|
|
|
1140
1164
|
for (const block of keyBlocks) {
|
|
1141
1165
|
const idMatch = block.match(/^(\S+)/);
|
|
1142
1166
|
const labelMatch = block.match(/label:\s*(.+)/);
|
|
1167
|
+
const scopeMatch = block.match(/scope:\s*(\S+)/);
|
|
1143
1168
|
const hashMatch = block.match(/key_hash:\s*(\S+)/);
|
|
1144
1169
|
const createdAtMatch = block.match(/created_at:\s*"?([^"\n]+)"?/);
|
|
1145
1170
|
const lastUsedMatch = block.match(/last_used_at:\s*"?([^"\n]+)"?/);
|
|
1146
1171
|
if (idMatch && hashMatch) {
|
|
1147
1172
|
config.api_keys.push({
|
|
1148
|
-
id: idMatch[1]
|
|
1173
|
+
id: idMatch[1]!,
|
|
1149
1174
|
label: (labelMatch?.[1] ?? "default").trim(),
|
|
1150
|
-
|
|
1175
|
+
scope: (scopeMatch?.[1] as KeyScope) ?? "write",
|
|
1176
|
+
key_hash: hashMatch[1]!,
|
|
1151
1177
|
created_at: createdAtMatch?.[1] ?? new Date().toISOString(),
|
|
1152
1178
|
last_used_at: lastUsedMatch?.[1],
|
|
1153
1179
|
});
|
|
@@ -1172,6 +1198,7 @@ export function writeGlobalConfig(config: GlobalConfig): void {
|
|
|
1172
1198
|
const lines = [`port: ${config.port}`];
|
|
1173
1199
|
if (config.default_vault) lines.push(`default_vault: ${config.default_vault}`);
|
|
1174
1200
|
if (config.discovery) lines.push(`discovery: ${config.discovery}`);
|
|
1201
|
+
if (config.autostart !== undefined) lines.push(`autostart: ${config.autostart}`);
|
|
1175
1202
|
if (config.owner_password_hash) {
|
|
1176
1203
|
lines.push(`owner_password_hash: "${config.owner_password_hash}"`);
|
|
1177
1204
|
}
|
|
@@ -1190,6 +1217,7 @@ export function writeGlobalConfig(config: GlobalConfig): void {
|
|
|
1190
1217
|
for (const key of config.api_keys) {
|
|
1191
1218
|
lines.push(` - id: ${key.id}`);
|
|
1192
1219
|
lines.push(` label: ${key.label}`);
|
|
1220
|
+
lines.push(` scope: ${key.scope ?? "write"}`);
|
|
1193
1221
|
lines.push(` key_hash: ${key.key_hash}`);
|
|
1194
1222
|
lines.push(` created_at: "${key.created_at}"`);
|
|
1195
1223
|
if (key.last_used_at) {
|
|
@@ -1420,7 +1448,7 @@ export function resolveDefaultVault(): string | null {
|
|
|
1420
1448
|
return globalConfig.default_vault;
|
|
1421
1449
|
}
|
|
1422
1450
|
if (vaults.length === 1) {
|
|
1423
|
-
return vaults[0]
|
|
1451
|
+
return vaults[0]!;
|
|
1424
1452
|
}
|
|
1425
1453
|
return null;
|
|
1426
1454
|
}
|