@openparachute/vault 0.4.7-rc.1 → 0.4.8-rc.4
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/README.md +44 -10
- package/core/src/connection-pragmas.test.ts +232 -0
- package/core/src/core.test.ts +257 -0
- package/core/src/cursor.test.ts +160 -0
- package/core/src/cursor.ts +272 -0
- package/core/src/mcp.ts +51 -7
- package/core/src/notes.ts +164 -2
- package/core/src/portable-md.test.ts +247 -0
- package/core/src/portable-md.ts +118 -1
- package/core/src/schema.ts +98 -2
- package/core/src/store.ts +11 -1
- package/core/src/types.ts +32 -0
- package/package.json +1 -1
- package/src/auth-status.ts +4 -0
- package/src/auto-transcribe.test.ts +116 -0
- package/src/auto-transcribe.ts +48 -0
- package/src/cli.ts +151 -50
- package/src/config.test.ts +26 -0
- package/src/config.ts +53 -1
- package/src/db.ts +15 -2
- package/src/export-watch.test.ts +99 -0
- package/src/mcp-install-interactive.test.ts +23 -2
- package/src/mcp-install-interactive.ts +21 -2
- package/src/mcp-install.test.ts +40 -0
- package/src/mcp-tools.ts +17 -1
- package/src/module-config.ts +70 -14
- package/src/module-manifest.test.ts +93 -0
- package/src/module-manifest.ts +94 -0
- package/src/routes.ts +267 -50
- package/src/scribe-discovery.test.ts +77 -0
- package/src/scribe-discovery.ts +91 -0
- package/src/scribe-env.test.ts +66 -1
- package/src/scribe-env.ts +42 -1
- package/src/self-register.test.ts +380 -0
- package/src/self-register.ts +234 -0
- package/src/server.ts +46 -11
- package/src/transcript-note.test.ts +171 -0
- package/src/transcript-note.ts +189 -0
- package/src/transcription-registry.ts +22 -0
- package/src/transcription-worker.test.ts +250 -0
- package/src/transcription-worker.ts +186 -27
- package/src/vault.test.ts +347 -0
package/src/mcp-install.test.ts
CHANGED
|
@@ -522,6 +522,46 @@ describe("mcp-install flag parsing", () => {
|
|
|
522
522
|
expect(res.exitCode).toBe(1);
|
|
523
523
|
expect(res.stderr).toMatch(/No hub origin configured/);
|
|
524
524
|
});
|
|
525
|
+
|
|
526
|
+
test("rejects --mint --scope vault:admin pre-flight (hub policy: per-vault admin is non-requestable)", () => {
|
|
527
|
+
// Regression for the symptom Aaron hit on hub 0.5.12-rc.2 / vault
|
|
528
|
+
// 0.4.7-rc.1: `parachute vault mcp-install` with the "admin" mint
|
|
529
|
+
// option sent `vault:default:admin` to `POST /api/auth/mint-token`,
|
|
530
|
+
// and hub responded:
|
|
531
|
+
//
|
|
532
|
+
// Hub mint-token rejected (HTTP 400, invalid_scope):
|
|
533
|
+
// scope vault:default:admin is not requestable via mint-token;
|
|
534
|
+
// use OAuth flow or operator rotation
|
|
535
|
+
//
|
|
536
|
+
// The combination is invalid by hub policy (see
|
|
537
|
+
// `parachute-hub/src/scope-explanations.ts:VAULT_ADMIN_RE` and
|
|
538
|
+
// `api-mint-token.ts`'s non-requestable guard) — per-vault admin
|
|
539
|
+
// is operator-only, mintable only through the session-cookie-gated
|
|
540
|
+
// `/admin/vault-admin-token/:name` SPA path.
|
|
541
|
+
//
|
|
542
|
+
// The fix rejects the combination pre-flight in vault's mcp-install
|
|
543
|
+
// with a clear remediation pointing at `--legacy-pat --scope vault:admin`
|
|
544
|
+
// (which mints a vault-DB pvt_* with admin scope — the right shape
|
|
545
|
+
// for a local MCP entry needing schema management).
|
|
546
|
+
setupBareVault(tmp, "default");
|
|
547
|
+
fs.writeFileSync(path.join(tmp, "operator.token"), "operator-bearer-stub");
|
|
548
|
+
const res = runCli(
|
|
549
|
+
["mcp-install", "--mint", "--scope", "vault:admin"],
|
|
550
|
+
tmp,
|
|
551
|
+
{ PARACHUTE_HUB_ORIGIN: "https://hub.example.org" },
|
|
552
|
+
);
|
|
553
|
+
expect(res.exitCode).toBe(1);
|
|
554
|
+
// Surface the policy reason so the operator knows why this combo is
|
|
555
|
+
// rejected (not a transient bug).
|
|
556
|
+
expect(res.stderr).toMatch(/not requestable via mint-token/);
|
|
557
|
+
// Point at the working remediation.
|
|
558
|
+
expect(res.stderr).toMatch(/--legacy-pat --scope vault:admin/);
|
|
559
|
+
// Pre-flight must fire BEFORE the operator-token / hub-origin checks
|
|
560
|
+
// pass the request to the network — no "Hub unreachable" / "No hub
|
|
561
|
+
// origin configured" leak.
|
|
562
|
+
expect(res.stderr).not.toMatch(/No hub origin configured/);
|
|
563
|
+
expect(res.stderr).not.toMatch(/Hub unreachable/);
|
|
564
|
+
});
|
|
525
565
|
});
|
|
526
566
|
|
|
527
567
|
// ---------------------------------------------------------------------------
|
package/src/mcp-tools.ts
CHANGED
|
@@ -178,10 +178,26 @@ function applyTagScopeWrappers(
|
|
|
178
178
|
const allowed = await getAllowed();
|
|
179
179
|
const result = await orig(params);
|
|
180
180
|
if (!allowed) return result;
|
|
181
|
-
//
|
|
181
|
+
// Three possible response shapes:
|
|
182
|
+
// - Array (legacy list, no cursor)
|
|
183
|
+
// - `{notes, next_cursor}` (cursor mode, vault#313)
|
|
184
|
+
// - `{...note}` with `id`+`tags` (single-note by id)
|
|
182
185
|
if (Array.isArray(result)) {
|
|
183
186
|
return result.filter((n: any) => noteWithinTagScope(n, allowed, rawTags));
|
|
184
187
|
}
|
|
188
|
+
if (
|
|
189
|
+
result &&
|
|
190
|
+
typeof result === "object" &&
|
|
191
|
+
"notes" in result &&
|
|
192
|
+
Array.isArray((result as any).notes) &&
|
|
193
|
+
"next_cursor" in result
|
|
194
|
+
) {
|
|
195
|
+
const r = result as { notes: any[]; next_cursor: string | null };
|
|
196
|
+
return {
|
|
197
|
+
notes: r.notes.filter((n: any) => noteWithinTagScope(n, allowed, rawTags)),
|
|
198
|
+
next_cursor: r.next_cursor,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
185
201
|
if (result && typeof result === "object" && "id" in result && "tags" in result) {
|
|
186
202
|
return noteWithinTagScope(result as any, allowed, rawTags)
|
|
187
203
|
? result
|
package/src/module-config.ts
CHANGED
|
@@ -15,15 +15,27 @@
|
|
|
15
15
|
* PUT /.parachute/config is Phase 3 — not implemented here.
|
|
16
16
|
*
|
|
17
17
|
* Fields currently described:
|
|
18
|
-
* - audio_retention:
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* - audio_retention: per-vault enum, backed by VaultConfig.audio_retention.
|
|
19
|
+
* - port: GlobalConfig.port, exposed read-only.
|
|
20
|
+
* - autoTranscribe.*: vault↔scribe handoff (vault#353, design 2026-05-21
|
|
21
|
+
* Part 2). Three nested fields per design Q4:
|
|
22
|
+
* - enabled: boolean toggle, default false (persisted in
|
|
23
|
+
* GlobalConfig.auto_transcribe.enabled).
|
|
24
|
+
* - scribeUrl: readOnly — resolved per-process from
|
|
25
|
+
* `~/.parachute/services.json` via
|
|
26
|
+
* `scribe-discovery.ts`. Operators can't point at an
|
|
27
|
+
* arbitrary scribe; the discovery layer is the gate.
|
|
28
|
+
* - scribeBearer: writeOnly — sourced from SCRIBE_AUTH_TOKEN env var.
|
|
29
|
+
* Hub install generates one at first boot
|
|
30
|
+
* (see scribe-env.ts:ensureScribeBearer); manual
|
|
31
|
+
* rotation is via `parachute-vault config set`.
|
|
32
|
+
* - scribe_url / scribe_token (deprecated): kept under their legacy names
|
|
33
|
+
* through one release for the hub admin SPA's prior
|
|
34
|
+
* render path; new code should read autoTranscribe.*.
|
|
24
35
|
*/
|
|
25
36
|
|
|
26
37
|
import type { VaultConfig, GlobalConfig } from "./config.ts";
|
|
38
|
+
import { resolveScribeUrl } from "./scribe-discovery.ts";
|
|
27
39
|
|
|
28
40
|
export interface ModuleConfigSchema {
|
|
29
41
|
$schema: string;
|
|
@@ -49,20 +61,54 @@ export function buildConfigSchema(): ModuleConfigSchema {
|
|
|
49
61
|
description:
|
|
50
62
|
"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
63
|
},
|
|
64
|
+
autoTranscribe: {
|
|
65
|
+
type: "object",
|
|
66
|
+
title: "Auto-transcribe voice uploads",
|
|
67
|
+
description:
|
|
68
|
+
"When enabled, audio attachments (mime-type prefix `audio/`) are automatically sent to scribe and the resulting transcript lands as a sibling `<attachment-path>.transcript.md` note. Scribe must be reachable for transcription to succeed; failures are recorded as a transcript note with `transcript_status: failed`.",
|
|
69
|
+
properties: {
|
|
70
|
+
enabled: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
default: false,
|
|
73
|
+
title: "Enable auto-transcription",
|
|
74
|
+
description:
|
|
75
|
+
"Master toggle. When false, audio uploads land normally without any scribe interaction. Global — persisted in `GlobalConfig.auto_transcribe.enabled` and applies to every vault on this server. Per-vault control is a future enhancement when multi-vault deployments need it.",
|
|
76
|
+
},
|
|
77
|
+
scribeUrl: {
|
|
78
|
+
type: "string",
|
|
79
|
+
format: "uri",
|
|
80
|
+
readOnly: true,
|
|
81
|
+
title: "Scribe URL",
|
|
82
|
+
description:
|
|
83
|
+
"URL of the scribe service. Auto-populated from `~/.parachute/services.json` at vault startup (or from the SCRIBE_URL env var when set). Read-only — operators can't point at an arbitrary scribe.",
|
|
84
|
+
},
|
|
85
|
+
scribeBearer: {
|
|
86
|
+
type: "string",
|
|
87
|
+
writeOnly: true,
|
|
88
|
+
title: "Scribe auth bearer",
|
|
89
|
+
description:
|
|
90
|
+
"Shared bearer for the vault→scribe loopback contract. Hub install generates one at first boot. Write-only — never returned by GET.",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
// Legacy aliases kept for back-compat with callers that read the
|
|
95
|
+
// pre-vault#353 shape. New consumers should read `autoTranscribe.*`.
|
|
52
96
|
scribe_url: {
|
|
53
97
|
type: "string",
|
|
54
98
|
format: "uri",
|
|
55
|
-
title: "Scribe URL",
|
|
99
|
+
title: "Scribe URL (deprecated alias)",
|
|
56
100
|
description:
|
|
57
|
-
"
|
|
101
|
+
"Legacy alias for `autoTranscribe.scribeUrl`. Will be removed in a future release.",
|
|
58
102
|
readOnly: true,
|
|
103
|
+
deprecated: true,
|
|
59
104
|
},
|
|
60
105
|
scribe_token: {
|
|
61
106
|
type: "string",
|
|
62
|
-
title: "Scribe auth token",
|
|
107
|
+
title: "Scribe auth token (deprecated alias)",
|
|
63
108
|
description:
|
|
64
|
-
"
|
|
109
|
+
"Legacy alias for `autoTranscribe.scribeBearer`. Will be removed in a future release.",
|
|
65
110
|
writeOnly: true,
|
|
111
|
+
deprecated: true,
|
|
66
112
|
},
|
|
67
113
|
port: {
|
|
68
114
|
type: "integer",
|
|
@@ -77,18 +123,28 @@ export function buildConfigSchema(): ModuleConfigSchema {
|
|
|
77
123
|
}
|
|
78
124
|
|
|
79
125
|
/**
|
|
80
|
-
* Effective config values, with `writeOnly` fields stripped. `
|
|
81
|
-
* declared `writeOnly` and
|
|
82
|
-
* set in the environment.
|
|
126
|
+
* Effective config values, with `writeOnly` fields stripped. `scribeBearer`
|
|
127
|
+
* (and its legacy alias `scribe_token`) are declared `writeOnly` and never
|
|
128
|
+
* returned, even when set in the environment.
|
|
83
129
|
*/
|
|
84
130
|
export function buildConfigValues(
|
|
85
131
|
vaultConfig: VaultConfig,
|
|
86
132
|
globalConfig: GlobalConfig,
|
|
87
133
|
env: { SCRIBE_URL?: string | undefined } = process.env as { SCRIBE_URL?: string },
|
|
88
134
|
): Record<string, unknown> {
|
|
135
|
+
// Resolve scribe URL through the discovery layer so the GET shape reflects
|
|
136
|
+
// what the worker will actually use (services.json > SCRIBE_URL > unset).
|
|
137
|
+
// Pass env through so the test harness's override is honored.
|
|
138
|
+
const scribeUrl = resolveScribeUrl(env as NodeJS.ProcessEnv) ?? "";
|
|
89
139
|
return {
|
|
90
140
|
audio_retention: vaultConfig.audio_retention ?? "keep",
|
|
91
|
-
|
|
141
|
+
autoTranscribe: {
|
|
142
|
+
enabled: globalConfig.auto_transcribe?.enabled ?? false,
|
|
143
|
+
scribeUrl,
|
|
144
|
+
},
|
|
145
|
+
// Legacy alias mirrors `autoTranscribe.scribeUrl` so hubs reading the
|
|
146
|
+
// pre-vault#353 shape don't regress.
|
|
147
|
+
scribe_url: scribeUrl,
|
|
92
148
|
port: globalConfig.port,
|
|
93
149
|
};
|
|
94
150
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { readSelfManifest, resolvePackageRoot } from "./module-manifest.ts";
|
|
6
|
+
|
|
7
|
+
function withTempPackageRoot(
|
|
8
|
+
manifest: unknown | undefined,
|
|
9
|
+
fn: (root: string) => void,
|
|
10
|
+
): void {
|
|
11
|
+
const root = mkdtempSync(join(tmpdir(), "pvault-manifest-"));
|
|
12
|
+
try {
|
|
13
|
+
if (manifest !== undefined) {
|
|
14
|
+
mkdirSync(join(root, ".parachute"), { recursive: true });
|
|
15
|
+
writeFileSync(
|
|
16
|
+
join(root, ".parachute", "module.json"),
|
|
17
|
+
typeof manifest === "string" ? manifest : JSON.stringify(manifest),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
fn(root);
|
|
21
|
+
} finally {
|
|
22
|
+
rmSync(root, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("module-manifest", () => {
|
|
27
|
+
test("resolvePackageRoot returns the directory containing package.json", () => {
|
|
28
|
+
// In the test env, this module lives at <repo>/src/module-manifest.test.ts —
|
|
29
|
+
// so the resolved root is the repo root. We don't pin the exact path
|
|
30
|
+
// (tests run from various cwds); we just sanity-check it's an absolute
|
|
31
|
+
// directory ending in the vault repo's name.
|
|
32
|
+
const root = resolvePackageRoot();
|
|
33
|
+
expect(root.startsWith("/")).toBe(true);
|
|
34
|
+
expect(root.endsWith("/src")).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("readSelfManifest returns null when .parachute/module.json is missing", () => {
|
|
38
|
+
withTempPackageRoot(undefined, (root) => {
|
|
39
|
+
expect(readSelfManifest(root)).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("readSelfManifest parses a valid manifest", () => {
|
|
44
|
+
withTempPackageRoot(
|
|
45
|
+
{
|
|
46
|
+
name: "vault",
|
|
47
|
+
manifestName: "parachute-vault",
|
|
48
|
+
displayName: "Vault",
|
|
49
|
+
tagline: "Test tagline",
|
|
50
|
+
kind: "api",
|
|
51
|
+
port: 1940,
|
|
52
|
+
paths: ["/vault/default"],
|
|
53
|
+
health: "/vault/default/health",
|
|
54
|
+
},
|
|
55
|
+
(root) => {
|
|
56
|
+
const m = readSelfManifest(root);
|
|
57
|
+
expect(m).not.toBeNull();
|
|
58
|
+
expect(m?.name).toBe("vault");
|
|
59
|
+
expect(m?.manifestName).toBe("parachute-vault");
|
|
60
|
+
expect(m?.displayName).toBe("Vault");
|
|
61
|
+
expect(m?.kind).toBe("api");
|
|
62
|
+
expect(m?.port).toBe(1940);
|
|
63
|
+
expect(m?.paths).toEqual(["/vault/default"]);
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("readSelfManifest throws on malformed JSON", () => {
|
|
69
|
+
withTempPackageRoot("{ not valid json", (root) => {
|
|
70
|
+
expect(() => readSelfManifest(root)).toThrow();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("readSelfManifest throws when required field missing", () => {
|
|
75
|
+
withTempPackageRoot(
|
|
76
|
+
{ name: "vault" /* missing manifestName / port / paths / health / kind */ },
|
|
77
|
+
(root) => {
|
|
78
|
+
expect(() => readSelfManifest(root)).toThrow(/missing required/);
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("readSelfManifest reads the actual shipped manifest in the repo", () => {
|
|
84
|
+
// Smoke test the real shipped file — guards against ever shipping a
|
|
85
|
+
// malformed manifest. Uses the real resolvePackageRoot (which finds
|
|
86
|
+
// the repo root in tests).
|
|
87
|
+
const m = readSelfManifest();
|
|
88
|
+
expect(m).not.toBeNull();
|
|
89
|
+
expect(m?.manifestName).toBe("parachute-vault");
|
|
90
|
+
expect(m?.kind).toBe("api");
|
|
91
|
+
expect(m?.port).toBe(1940);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for the package's own `.parachute/module.json`.
|
|
3
|
+
*
|
|
4
|
+
* Vault ships `module.json` alongside `package.json` at the package root.
|
|
5
|
+
* This module locates the file via `import.meta.url` (which works for both
|
|
6
|
+
* `bun src/cli.ts …` dev runs and the published-package `parachute-vault`
|
|
7
|
+
* binary — the file ships in `package.json` `files` next to `src/`).
|
|
8
|
+
*
|
|
9
|
+
* Used by `self-register.ts` on server boot: vault reads its own manifest
|
|
10
|
+
* + computes the package's `installDir` so the services.json row carries
|
|
11
|
+
* the same metadata that hub's `FIRST_PARTY_FALLBACKS[vault]` provides
|
|
12
|
+
* today. The endgame is that hub's vendored fallback retires once every
|
|
13
|
+
* first-party module self-registers reliably — this is the POC for the
|
|
14
|
+
* pattern.
|
|
15
|
+
*
|
|
16
|
+
* Shape mirrors `parachute-hub/src/module-manifest.ts`. Kept narrow: we
|
|
17
|
+
* only consume the fields vault stamps onto services.json
|
|
18
|
+
* (displayName, tagline, stripPrefix). The full manifest validator lives
|
|
19
|
+
* on the hub side; vault treats its own manifest as authored-by-us +
|
|
20
|
+
* trusts the shape.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import { dirname, join, resolve } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
|
|
27
|
+
export type ModuleKind = "api" | "frontend" | "tool";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Subset of the full manifest schema (see `parachute-hub/src/module-manifest.ts`)
|
|
31
|
+
* — only the fields vault's self-registration consumes today. Adding more is
|
|
32
|
+
* a one-line edit when the surface widens.
|
|
33
|
+
*/
|
|
34
|
+
export interface VaultModuleManifest {
|
|
35
|
+
readonly name: string;
|
|
36
|
+
readonly manifestName: string;
|
|
37
|
+
readonly displayName?: string;
|
|
38
|
+
readonly tagline?: string;
|
|
39
|
+
readonly kind: ModuleKind;
|
|
40
|
+
readonly port: number;
|
|
41
|
+
readonly paths: readonly string[];
|
|
42
|
+
readonly health: string;
|
|
43
|
+
readonly stripPrefix?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the path to the package root — the directory containing both
|
|
48
|
+
* `package.json` and `.parachute/module.json`. Walks up from
|
|
49
|
+
* `import.meta.url` so the answer is correct under both:
|
|
50
|
+
*
|
|
51
|
+
* - dev: `bun src/cli.ts serve` → `src/module-manifest.ts` → parent = repo root
|
|
52
|
+
* - prod: published package → `src/module-manifest.ts` → parent = installed
|
|
53
|
+
* package dir (e.g. `~/.bun/install/global/node_modules/@openparachute/vault`)
|
|
54
|
+
*
|
|
55
|
+
* Exported for tests + the self-register flow that needs to stamp this as
|
|
56
|
+
* `installDir` on the services.json row.
|
|
57
|
+
*/
|
|
58
|
+
export function resolvePackageRoot(): string {
|
|
59
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
60
|
+
// `src/module-manifest.ts` lives one level under the package root.
|
|
61
|
+
return resolve(here, "..");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read `<packageRoot>/.parachute/module.json` if present. Returns null when
|
|
66
|
+
* the file is missing (e.g. during local dev before the file was committed)
|
|
67
|
+
* — callers treat that as "self-registration unavailable, log + continue."
|
|
68
|
+
*
|
|
69
|
+
* Throws on malformed JSON: a corrupt manifest is a deploy bug we want to
|
|
70
|
+
* surface, not silently swallow. The self-register caller catches + logs
|
|
71
|
+
* so a bad manifest doesn't crash server boot.
|
|
72
|
+
*/
|
|
73
|
+
export function readSelfManifest(
|
|
74
|
+
packageRoot: string = resolvePackageRoot(),
|
|
75
|
+
): VaultModuleManifest | null {
|
|
76
|
+
const path = join(packageRoot, ".parachute", "module.json");
|
|
77
|
+
if (!existsSync(path)) return null;
|
|
78
|
+
const raw = readFileSync(path, "utf8");
|
|
79
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
80
|
+
// Minimal shape validation. Only the fields we actually consume — anything
|
|
81
|
+
// else passes through untouched. Strict full-shape validation is the hub's
|
|
82
|
+
// job (it'll fail an install on a malformed manifest); vault treats its
|
|
83
|
+
// own shipped file as authored-by-us.
|
|
84
|
+
if (typeof parsed.name !== "string" || typeof parsed.manifestName !== "string") {
|
|
85
|
+
throw new Error(`${path}: manifest missing required "name" / "manifestName"`);
|
|
86
|
+
}
|
|
87
|
+
if (typeof parsed.port !== "number" || !Array.isArray(parsed.paths)) {
|
|
88
|
+
throw new Error(`${path}: manifest missing required "port" / "paths"`);
|
|
89
|
+
}
|
|
90
|
+
if (typeof parsed.health !== "string" || typeof parsed.kind !== "string") {
|
|
91
|
+
throw new Error(`${path}: manifest missing required "health" / "kind"`);
|
|
92
|
+
}
|
|
93
|
+
return parsed as unknown as VaultModuleManifest;
|
|
94
|
+
}
|