@openparachute/vault 0.6.0-rc.1 → 0.6.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/.parachute/module.json +14 -3
- package/README.md +7 -7
- package/core/src/core.test.ts +279 -26
- package/core/src/expand-visibility.test.ts +102 -0
- package/core/src/expand.ts +31 -3
- package/core/src/indexed-fields.ts +1 -1
- package/core/src/link-count.test.ts +301 -0
- package/core/src/links.ts +97 -2
- package/core/src/mcp.ts +201 -33
- package/core/src/notes.ts +44 -8
- package/core/src/obsidian-alignment.test.ts +375 -0
- package/core/src/obsidian.ts +234 -14
- package/core/src/portable-md.test.ts +40 -0
- package/core/src/portable-md.ts +142 -16
- package/core/src/schema.ts +58 -11
- package/core/src/store.ts +69 -22
- package/core/src/tag-expand-axis.test.ts +301 -0
- package/core/src/tag-hierarchy.ts +80 -0
- package/core/src/tag-schemas.ts +61 -46
- package/core/src/triggers-store.test.ts +100 -0
- package/core/src/triggers-store.ts +165 -0
- package/core/src/types.ts +68 -4
- package/core/src/vault-projection.ts +20 -0
- package/core/src/wikilinks.ts +2 -2
- package/package.json +2 -3
- package/src/admin-spa.test.ts +100 -10
- package/src/admin-spa.ts +48 -3
- package/src/auth-hub-jwt.test.ts +8 -1
- package/src/auth-status.ts +2 -2
- package/src/auth.test.ts +39 -3
- package/src/auth.ts +31 -2
- package/src/auto-transcribe.test.ts +51 -0
- package/src/auto-transcribe.ts +24 -6
- package/src/autostart.test.ts +75 -0
- package/src/autostart.ts +84 -0
- package/src/cli.ts +434 -140
- package/src/config.test.ts +109 -0
- package/src/config.ts +157 -10
- package/src/export-watch.test.ts +23 -0
- package/src/export-watch.ts +14 -0
- package/src/git-preflight.test.ts +70 -0
- package/src/git-preflight.ts +68 -0
- package/src/hub-jwt.test.ts +75 -2
- package/src/hub-jwt.ts +43 -6
- package/src/init-summary.test.ts +120 -5
- package/src/init-summary.ts +67 -25
- package/src/live-match.test.ts +198 -0
- package/src/live-match.ts +310 -0
- package/src/mcp-install.test.ts +93 -0
- package/src/mcp-install.ts +106 -0
- package/src/mcp-tools.ts +80 -7
- package/src/mirror-config.test.ts +14 -0
- package/src/mirror-config.ts +11 -0
- package/src/mirror-import.test.ts +110 -0
- package/src/mirror-import.ts +71 -13
- package/src/mirror-manager.test.ts +51 -0
- package/src/mirror-manager.ts +73 -11
- package/src/mirror-routes.test.ts +463 -1
- package/src/mirror-routes.ts +474 -4
- package/src/oauth-discovery.test.ts +55 -0
- package/src/oauth-discovery.ts +24 -5
- package/src/routes.ts +696 -121
- package/src/routing.test.ts +451 -5
- package/src/routing.ts +113 -5
- package/src/scopes.ts +1 -1
- package/src/server.ts +66 -4
- package/src/storage.test.ts +162 -0
- package/src/subscribe.test.ts +588 -0
- package/src/subscribe.ts +248 -0
- package/src/subscriptions.ts +295 -0
- package/src/tag-expand-routes.test.ts +45 -0
- package/src/tag-scope.ts +68 -1
- package/src/token-store.ts +7 -7
- package/src/transcription-worker.test.ts +471 -5
- package/src/transcription-worker.ts +212 -44
- package/src/triggers-api.test.ts +533 -0
- package/src/triggers-api.ts +295 -0
- package/src/triggers.ts +93 -7
- package/src/usage.test.ts +362 -0
- package/src/usage.ts +318 -0
- package/src/vault-create.test.ts +340 -12
- package/src/vault-name.test.ts +61 -3
- package/src/vault-name.ts +62 -14
- package/src/vault-remove.test.ts +187 -0
- package/src/vault-store.ts +10 -3
- package/src/vault.test.ts +1353 -62
- package/web/ui/dist/assets/index-CGL256oe.js +60 -0
- package/web/ui/dist/assets/index-J0pVP7I-.css +1 -0
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-DBe8Xiah.css +0 -1
- package/web/ui/dist/assets/index-DDRo6F4u.js +0 -60
package/src/auth.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth invariants — vault as a pure hub resource-server (vault#282 Stage 2).
|
|
3
3
|
*
|
|
4
|
-
* The `pvt_*` opaque vault-DB token was dropped at 0.
|
|
4
|
+
* The `pvt_*` opaque vault-DB token was dropped at 0.5.0: vault no longer
|
|
5
5
|
* mints or validates it. The surviving auth surfaces tested here are:
|
|
6
6
|
* - VAULT_AUTH_TOKEN — the server-wide operator bearer.
|
|
7
7
|
* - Legacy YAML api_keys (vault.yaml / config.yaml) — hashed keys.
|
|
@@ -26,7 +26,8 @@ import {
|
|
|
26
26
|
hashKey,
|
|
27
27
|
} from "./config.ts";
|
|
28
28
|
import { getVaultStore, clearVaultStoreCache } from "./vault-store.ts";
|
|
29
|
-
import { authenticateVaultRequest, authenticateGlobalRequest } from "./auth.ts";
|
|
29
|
+
import { authenticateVaultRequest, authenticateGlobalRequest, warnLegacyGlobalApiKeys } from "./auth.ts";
|
|
30
|
+
import type { StoredKey } from "./config.ts";
|
|
30
31
|
|
|
31
32
|
let tmpHome: string;
|
|
32
33
|
let prevHome: string | undefined;
|
|
@@ -91,7 +92,7 @@ describe("auth — pvt_* tokens are unvalidatable (fail closed)", () => {
|
|
|
91
92
|
// API key" a non-pvt_ bad token gets) — the prefix is the user-meaningful
|
|
92
93
|
// signal that the mechanism was dropped, not that the key was mistyped.
|
|
93
94
|
const PVT_MESSAGE =
|
|
94
|
-
"pvt_* tokens are no longer supported (vault 0.
|
|
95
|
+
"pvt_* tokens are no longer supported (vault 0.5.0). Re-add this vault via your hub to get an access token.";
|
|
95
96
|
|
|
96
97
|
test("a pvt_* bearer is 401-rejected with the dropped-token message on the per-vault surface", async () => {
|
|
97
98
|
seedVault("journal");
|
|
@@ -442,3 +443,38 @@ describe("auth — VAULT_AUTH_TOKEN server-wide operator bearer", () => {
|
|
|
442
443
|
expect("error" in result).toBe(true);
|
|
443
444
|
});
|
|
444
445
|
});
|
|
446
|
+
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
// Legacy GLOBAL api_keys boot warning (security review — multi-user
|
|
449
|
+
// hardening). Cross-vault credentials in config.yaml must be surfaced loudly
|
|
450
|
+
// at boot, but never altered. Pure-function unit tests (no server boot).
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
describe("warnLegacyGlobalApiKeys (legacy cross-vault key boot warning)", () => {
|
|
453
|
+
function key(id: string): StoredKey {
|
|
454
|
+
return {
|
|
455
|
+
id,
|
|
456
|
+
label: id,
|
|
457
|
+
key_hash: `sha256:${id}`,
|
|
458
|
+
scope: "full",
|
|
459
|
+
created_at: new Date().toISOString(),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
test("warns when global api_keys are present", () => {
|
|
464
|
+
const msgs: string[] = [];
|
|
465
|
+
const count = warnLegacyGlobalApiKeys([key("a"), key("b")], (m) => msgs.push(m));
|
|
466
|
+
expect(count).toBe(2);
|
|
467
|
+
expect(msgs).toHaveLength(1);
|
|
468
|
+
expect(msgs[0]).toContain("legacy GLOBAL api_key");
|
|
469
|
+
expect(msgs[0]).toContain("CROSS-VAULT");
|
|
470
|
+
// Heads-up only — must signal it does NOT alter the keys.
|
|
471
|
+
expect(msgs[0]).toContain("remain active");
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("silent when there are no global api_keys", () => {
|
|
475
|
+
const msgs: string[] = [];
|
|
476
|
+
expect(warnLegacyGlobalApiKeys([], (m) => msgs.push(m))).toBe(0);
|
|
477
|
+
expect(warnLegacyGlobalApiKeys(undefined, (m) => msgs.push(m))).toBe(0);
|
|
478
|
+
expect(msgs).toHaveLength(0);
|
|
479
|
+
});
|
|
480
|
+
});
|
package/src/auth.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authentication and authorization for the vault server.
|
|
3
3
|
*
|
|
4
|
-
* As of 0.
|
|
4
|
+
* As of 0.5.0 vault is a PURE HUB RESOURCE-SERVER (vault#282 Stage 2). The
|
|
5
5
|
* opaque `pvt_*` vault-DB token was dropped — vault no longer mints or
|
|
6
6
|
* validates it. Three auth paths survive:
|
|
7
7
|
*
|
|
@@ -171,6 +171,35 @@ export function warnLegacyOnce(cacheKey: string, context: string): void {
|
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Boot-time warning for legacy GLOBAL `api_keys` in `config.yaml` (security
|
|
176
|
+
* review — multi-user hardening). Those keys are CROSS-VAULT credentials: a
|
|
177
|
+
* single key authenticates against EVERY vault on this server (see the global
|
|
178
|
+
* `api_keys` branch in `authenticate` ~L283). That predates per-vault keys +
|
|
179
|
+
* tag-scoped hub JWTs and is a confidentiality hazard once a server hosts
|
|
180
|
+
* multiple users' vaults — one user's global key reads another's vault.
|
|
181
|
+
*
|
|
182
|
+
* WARNING ONLY — never touches the keys (the operator owns them). The
|
|
183
|
+
* verification flagged 6 such keys on the live box; this surfaces them at
|
|
184
|
+
* boot so they're rotated/removed before multi-user sharing. Returns the
|
|
185
|
+
* count it warned about (0 = silent) so callers / tests can assert.
|
|
186
|
+
*/
|
|
187
|
+
export function warnLegacyGlobalApiKeys(
|
|
188
|
+
globalApiKeys: StoredKey[] | undefined,
|
|
189
|
+
warn: (msg: string) => void = console.warn,
|
|
190
|
+
): number {
|
|
191
|
+
const count = globalApiKeys?.length ?? 0;
|
|
192
|
+
if (count === 0) return 0;
|
|
193
|
+
warn(
|
|
194
|
+
`[auth] WARNING: ${count} legacy GLOBAL api_key(s) found in config.yaml. ` +
|
|
195
|
+
"These are CROSS-VAULT credentials (each grants access to every vault on this server) " +
|
|
196
|
+
"and predate per-vault keys + tag-scoped hub JWTs. Before multi-user sharing, ROTATE or " +
|
|
197
|
+
"REMOVE them — a global key leaks one user's vault to another. They remain active (the " +
|
|
198
|
+
"operator owns them); this is a heads-up, not an automatic change.",
|
|
199
|
+
);
|
|
200
|
+
return count;
|
|
201
|
+
}
|
|
202
|
+
|
|
174
203
|
/** Read-only tools (the only tools allowed for "read" permission). */
|
|
175
204
|
const READ_TOOLS = new Set([
|
|
176
205
|
"query-notes",
|
|
@@ -310,7 +339,7 @@ function droppedPvtTokenResponse(): Response {
|
|
|
310
339
|
{
|
|
311
340
|
error: "Unauthorized",
|
|
312
341
|
message:
|
|
313
|
-
"pvt_* tokens are no longer supported (vault 0.
|
|
342
|
+
"pvt_* tokens are no longer supported (vault 0.5.0). Re-add this vault via your hub to get an access token.",
|
|
314
343
|
},
|
|
315
344
|
{ status: 401 },
|
|
316
345
|
);
|
|
@@ -118,4 +118,55 @@ describe("shouldAutoTranscribe", () => {
|
|
|
118
118
|
enabledOverride: false,
|
|
119
119
|
})).toBe(false);
|
|
120
120
|
});
|
|
121
|
+
|
|
122
|
+
describe("per-vault precedence (per-vault → global → true)", () => {
|
|
123
|
+
test("per-vault true wins even when global is false", () => {
|
|
124
|
+
expect(shouldAutoTranscribe("audio/wav", {
|
|
125
|
+
readGlobalConfigImpl: readGlobalConfig(false),
|
|
126
|
+
getCachedScribeUrlImpl: scribePresent,
|
|
127
|
+
perVaultEnabled: true,
|
|
128
|
+
})).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("per-vault false wins even when global is true", () => {
|
|
132
|
+
// The whole point: linking scribe to vault X (perVault true) elsewhere
|
|
133
|
+
// must not force-on a vault that set its own false.
|
|
134
|
+
expect(shouldAutoTranscribe("audio/wav", {
|
|
135
|
+
readGlobalConfigImpl: readGlobalConfig(true),
|
|
136
|
+
getCachedScribeUrlImpl: scribePresent,
|
|
137
|
+
perVaultEnabled: false,
|
|
138
|
+
})).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("per-vault unset falls back to global", () => {
|
|
142
|
+
expect(shouldAutoTranscribe("audio/wav", {
|
|
143
|
+
readGlobalConfigImpl: readGlobalConfig(true),
|
|
144
|
+
getCachedScribeUrlImpl: scribePresent,
|
|
145
|
+
perVaultEnabled: undefined,
|
|
146
|
+
})).toBe(true);
|
|
147
|
+
expect(shouldAutoTranscribe("audio/wav", {
|
|
148
|
+
readGlobalConfigImpl: readGlobalConfig(false),
|
|
149
|
+
getCachedScribeUrlImpl: scribePresent,
|
|
150
|
+
perVaultEnabled: undefined,
|
|
151
|
+
})).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("both per-vault and global unset falls back to true (no regression)", () => {
|
|
155
|
+
expect(shouldAutoTranscribe("audio/wav", {
|
|
156
|
+
readGlobalConfigImpl: readGlobalConfig(undefined),
|
|
157
|
+
getCachedScribeUrlImpl: scribePresent,
|
|
158
|
+
perVaultEnabled: undefined,
|
|
159
|
+
})).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("enabledOverride still hard-overrides the per-vault value", () => {
|
|
163
|
+
// The explicit caller-opt-in path beats everything.
|
|
164
|
+
expect(shouldAutoTranscribe("audio/wav", {
|
|
165
|
+
readGlobalConfigImpl: readGlobalConfig(true),
|
|
166
|
+
getCachedScribeUrlImpl: scribePresent,
|
|
167
|
+
perVaultEnabled: false,
|
|
168
|
+
enabledOverride: true,
|
|
169
|
+
})).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
121
172
|
});
|
package/src/auto-transcribe.ts
CHANGED
|
@@ -19,11 +19,18 @@ import { getCachedScribeUrl } from "./scribe-discovery.ts";
|
|
|
19
19
|
*
|
|
20
20
|
* Returns `true` only when ALL three conditions hold:
|
|
21
21
|
* 1. mime-type starts with `audio/` (case-insensitive).
|
|
22
|
-
* 2.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* 2. The resolved auto-transcribe toggle is not `false`. Resolution is
|
|
23
|
+
* **per-vault → global → true**:
|
|
24
|
+
* - `perVaultEnabled` (the owning vault's own `auto_transcribe.enabled`)
|
|
25
|
+
* wins when set — this is what makes scribe's "link to vault X" affect
|
|
26
|
+
* only X, not the whole server.
|
|
27
|
+
* - else the server-wide `globalConfig.auto_transcribe?.enabled`.
|
|
28
|
+
* - else `true` (default ON — once scribe is reachable, audio
|
|
29
|
+
* transcribes without a separate config step). Operators who want it
|
|
30
|
+
* OFF set `auto_transcribe.enabled: false` explicitly (per-vault or
|
|
31
|
+
* globally).
|
|
32
|
+
* `enabledOverride`, when present, hard-overrides the whole chain (used
|
|
33
|
+
* by the explicit caller-opt-in path).
|
|
27
34
|
* 3. Scribe is discoverable (services.json entry OR SCRIBE_URL env).
|
|
28
35
|
*
|
|
29
36
|
* The three conditions are independent guards: a single `false` is sufficient
|
|
@@ -35,7 +42,17 @@ export function shouldAutoTranscribe(
|
|
|
35
42
|
/** Injection seam for tests — defaults to live globals. */
|
|
36
43
|
readGlobalConfigImpl?: typeof readGlobalConfig;
|
|
37
44
|
getCachedScribeUrlImpl?: () => string | undefined;
|
|
38
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* The owning vault's per-vault `auto_transcribe.enabled` (vault.yaml).
|
|
47
|
+
* Takes precedence over the global toggle when set, so enabling/disabling
|
|
48
|
+
* one vault doesn't move the rest. `undefined` (the vault left it unset)
|
|
49
|
+
* falls through to the global toggle.
|
|
50
|
+
*/
|
|
51
|
+
perVaultEnabled?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Hard override of the entire per-vault→global→true chain. Used by the
|
|
54
|
+
* explicit caller-opt-in path; not part of the normal precedence ladder.
|
|
55
|
+
*/
|
|
39
56
|
enabledOverride?: boolean;
|
|
40
57
|
} = {},
|
|
41
58
|
): boolean {
|
|
@@ -43,6 +60,7 @@ export function shouldAutoTranscribe(
|
|
|
43
60
|
return false;
|
|
44
61
|
}
|
|
45
62
|
const enabled = opts.enabledOverride
|
|
63
|
+
?? opts.perVaultEnabled
|
|
46
64
|
?? (opts.readGlobalConfigImpl ?? readGlobalConfig)().auto_transcribe?.enabled
|
|
47
65
|
?? true;
|
|
48
66
|
if (!enabled) return false;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { decideAutostart } from "./autostart.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pure matrix for the autostart decision (ParachuteComputer/parachute-hub#580
|
|
6
|
+
* item 2). No launchd/systemd is touched — `decideAutostart` is side-effect
|
|
7
|
+
* free; the CLI consumes its result to register or skip.
|
|
8
|
+
*/
|
|
9
|
+
describe("decideAutostart", () => {
|
|
10
|
+
test("hub present, no flag, no persisted → default OFF (#580)", () => {
|
|
11
|
+
const d = decideAutostart({ flagOn: false, flagOff: false, persisted: undefined, hubPresent: true });
|
|
12
|
+
expect(d.enabled).toBe(false);
|
|
13
|
+
expect(d.reason).toBe("hub-default-off");
|
|
14
|
+
// Per-run inference — not persisted, so a later standalone re-run registers.
|
|
15
|
+
expect(d.persist).toBe(false);
|
|
16
|
+
expect(d.overrodeHub).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("hub absent, no flag, no persisted → default ON (standalone)", () => {
|
|
20
|
+
const d = decideAutostart({ flagOn: false, flagOff: false, persisted: undefined, hubPresent: false });
|
|
21
|
+
expect(d.enabled).toBe(true);
|
|
22
|
+
expect(d.reason).toBe("default-on");
|
|
23
|
+
expect(d.persist).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("explicit --autostart forces ON even under a hub (operator override + warn flag)", () => {
|
|
27
|
+
const d = decideAutostart({ flagOn: true, flagOff: false, persisted: undefined, hubPresent: true });
|
|
28
|
+
expect(d.enabled).toBe(true);
|
|
29
|
+
expect(d.reason).toBe("flag-on");
|
|
30
|
+
expect(d.persist).toBe(true);
|
|
31
|
+
expect(d.overrodeHub).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("explicit --autostart with no hub does not set overrodeHub", () => {
|
|
35
|
+
const d = decideAutostart({ flagOn: true, flagOff: false, persisted: undefined, hubPresent: false });
|
|
36
|
+
expect(d.enabled).toBe(true);
|
|
37
|
+
expect(d.overrodeHub).toBe(false);
|
|
38
|
+
expect(d.persist).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("explicit --no-autostart forces OFF and persists (even under a hub)", () => {
|
|
42
|
+
const d = decideAutostart({ flagOn: false, flagOff: true, persisted: undefined, hubPresent: true });
|
|
43
|
+
expect(d.enabled).toBe(false);
|
|
44
|
+
expect(d.reason).toBe("flag-off");
|
|
45
|
+
expect(d.persist).toBe(true);
|
|
46
|
+
expect(d.overrodeHub).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("--no-autostart wins over --autostart on the same line (safer default)", () => {
|
|
50
|
+
const d = decideAutostart({ flagOn: true, flagOff: true, persisted: undefined, hubPresent: false });
|
|
51
|
+
expect(d.enabled).toBe(false);
|
|
52
|
+
expect(d.reason).toBe("flag-off");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("persisted=false honored over hub-present default", () => {
|
|
56
|
+
const d = decideAutostart({ flagOn: false, flagOff: false, persisted: false, hubPresent: true });
|
|
57
|
+
expect(d.enabled).toBe(false);
|
|
58
|
+
expect(d.reason).toBe("persisted");
|
|
59
|
+
expect(d.persist).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("persisted=true honored even when a hub is present (prior explicit choice)", () => {
|
|
63
|
+
const d = decideAutostart({ flagOn: false, flagOff: false, persisted: true, hubPresent: true });
|
|
64
|
+
expect(d.enabled).toBe(true);
|
|
65
|
+
expect(d.reason).toBe("persisted");
|
|
66
|
+
expect(d.persist).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("flag beats persisted: --no-autostart over persisted=true", () => {
|
|
70
|
+
const d = decideAutostart({ flagOn: false, flagOff: true, persisted: true, hubPresent: false });
|
|
71
|
+
expect(d.enabled).toBe(false);
|
|
72
|
+
expect(d.reason).toBe("flag-off");
|
|
73
|
+
expect(d.persist).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
package/src/autostart.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether `parachute-vault init` should register a boot/restart daemon
|
|
3
|
+
* (launchd on macOS, systemd on Linux).
|
|
4
|
+
*
|
|
5
|
+
* Pure: extracted so the flag × persisted-config × hub-presence matrix can be
|
|
6
|
+
* unit-tested without spawning the CLI or touching launchd/systemd. The caller
|
|
7
|
+
* passes in the resolved `hubPresent` signal (from `detectHubPresence`) so this
|
|
8
|
+
* function stays side-effect-free.
|
|
9
|
+
*
|
|
10
|
+
* Why hub-presence flips the default (ParachuteComputer/parachute-hub#580):
|
|
11
|
+
* under hub-as-supervisor the hub owns the vault lifecycle — it spawns vault as
|
|
12
|
+
* a supervised child. If init *also* registers a launchd/systemd unit with
|
|
13
|
+
* `KeepAlive`/`RunAtLoad`, two lifecycles race for :1940: the supervisor's
|
|
14
|
+
* child and the platform manager's respawn. `parachute stop` kills the
|
|
15
|
+
* supervised one, launchd resurrects the other (EADDRINUSE crash-loop, pidfile
|
|
16
|
+
* records the loser, the rogue holds the port with no injected
|
|
17
|
+
* PARACHUTE_HUB_ORIGIN → iss mismatch). Defaulting autostart OFF when a hub is
|
|
18
|
+
* present makes the standalone daemon an explicit opt-in.
|
|
19
|
+
*
|
|
20
|
+
* Precedence (first match wins):
|
|
21
|
+
* 1. `--no-autostart` on this run → off (persisted; safer-default
|
|
22
|
+
* precedence beats --autostart)
|
|
23
|
+
* 2. `--autostart` on this run → on (persisted; operator
|
|
24
|
+
* override — caller warns if a
|
|
25
|
+
* supervised hub was detected)
|
|
26
|
+
* 3. Existing `config.autostart` (boolean) → that value (honor prior choice)
|
|
27
|
+
* 4. Hub present, no flag, no persisted value → off (the hub supervisor owns
|
|
28
|
+
* the lifecycle — #580)
|
|
29
|
+
* 5. Default → on (standalone deploys
|
|
30
|
+
* genuinely need a daemon)
|
|
31
|
+
*
|
|
32
|
+
* `persist` is true only when the choice came from an explicit flag (cases 1+2)
|
|
33
|
+
* — matching the prior behavior where a flagless re-run never rewrote the
|
|
34
|
+
* persisted value. The hub-present default (case 4) is intentionally NOT
|
|
35
|
+
* persisted: it's a per-run inference, so a later standalone re-run (no hub)
|
|
36
|
+
* falls back to the register default rather than being stuck off.
|
|
37
|
+
*
|
|
38
|
+
* `overrodeHub` is true only for case 2 when a hub was detected — the signal the
|
|
39
|
+
* caller uses to log the "supervised hub detected, registering anyway" warning.
|
|
40
|
+
*/
|
|
41
|
+
export interface AutostartDecisionInput {
|
|
42
|
+
/** `--autostart` present on this invocation. */
|
|
43
|
+
flagOn: boolean;
|
|
44
|
+
/** `--no-autostart` present on this invocation. */
|
|
45
|
+
flagOff: boolean;
|
|
46
|
+
/** Persisted `config.autostart` from a prior run, if a boolean. */
|
|
47
|
+
persisted?: boolean | undefined;
|
|
48
|
+
/** Whether a hub supervisor was detected (from `detectHubPresence`). */
|
|
49
|
+
hubPresent: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AutostartDecision {
|
|
53
|
+
/** Final resolved value. */
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
/** Whether the caller should write `config.autostart` to disk. */
|
|
56
|
+
persist: boolean;
|
|
57
|
+
/** True when `--autostart` forced registration despite a detected hub. */
|
|
58
|
+
overrodeHub: boolean;
|
|
59
|
+
/** Which precedence rule decided the outcome (for testing + copy). */
|
|
60
|
+
reason: "flag-off" | "flag-on" | "persisted" | "hub-default-off" | "default-on";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function decideAutostart(input: AutostartDecisionInput): AutostartDecision {
|
|
64
|
+
const { flagOn, flagOff, persisted, hubPresent } = input;
|
|
65
|
+
|
|
66
|
+
if (flagOff) {
|
|
67
|
+
return { enabled: false, persist: true, overrodeHub: false, reason: "flag-off" };
|
|
68
|
+
}
|
|
69
|
+
if (flagOn) {
|
|
70
|
+
return {
|
|
71
|
+
enabled: true,
|
|
72
|
+
persist: true,
|
|
73
|
+
overrodeHub: hubPresent,
|
|
74
|
+
reason: "flag-on",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (typeof persisted === "boolean") {
|
|
78
|
+
return { enabled: persisted, persist: false, overrodeHub: false, reason: "persisted" };
|
|
79
|
+
}
|
|
80
|
+
if (hubPresent) {
|
|
81
|
+
return { enabled: false, persist: false, overrodeHub: false, reason: "hub-default-off" };
|
|
82
|
+
}
|
|
83
|
+
return { enabled: true, persist: false, overrodeHub: false, reason: "default-on" };
|
|
84
|
+
}
|