@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.
Files changed (91) hide show
  1. package/.parachute/module.json +14 -3
  2. package/README.md +7 -7
  3. package/core/src/core.test.ts +279 -26
  4. package/core/src/expand-visibility.test.ts +102 -0
  5. package/core/src/expand.ts +31 -3
  6. package/core/src/indexed-fields.ts +1 -1
  7. package/core/src/link-count.test.ts +301 -0
  8. package/core/src/links.ts +97 -2
  9. package/core/src/mcp.ts +201 -33
  10. package/core/src/notes.ts +44 -8
  11. package/core/src/obsidian-alignment.test.ts +375 -0
  12. package/core/src/obsidian.ts +234 -14
  13. package/core/src/portable-md.test.ts +40 -0
  14. package/core/src/portable-md.ts +142 -16
  15. package/core/src/schema.ts +58 -11
  16. package/core/src/store.ts +69 -22
  17. package/core/src/tag-expand-axis.test.ts +301 -0
  18. package/core/src/tag-hierarchy.ts +80 -0
  19. package/core/src/tag-schemas.ts +61 -46
  20. package/core/src/triggers-store.test.ts +100 -0
  21. package/core/src/triggers-store.ts +165 -0
  22. package/core/src/types.ts +68 -4
  23. package/core/src/vault-projection.ts +20 -0
  24. package/core/src/wikilinks.ts +2 -2
  25. package/package.json +2 -3
  26. package/src/admin-spa.test.ts +100 -10
  27. package/src/admin-spa.ts +48 -3
  28. package/src/auth-hub-jwt.test.ts +8 -1
  29. package/src/auth-status.ts +2 -2
  30. package/src/auth.test.ts +39 -3
  31. package/src/auth.ts +31 -2
  32. package/src/auto-transcribe.test.ts +51 -0
  33. package/src/auto-transcribe.ts +24 -6
  34. package/src/autostart.test.ts +75 -0
  35. package/src/autostart.ts +84 -0
  36. package/src/cli.ts +434 -140
  37. package/src/config.test.ts +109 -0
  38. package/src/config.ts +157 -10
  39. package/src/export-watch.test.ts +23 -0
  40. package/src/export-watch.ts +14 -0
  41. package/src/git-preflight.test.ts +70 -0
  42. package/src/git-preflight.ts +68 -0
  43. package/src/hub-jwt.test.ts +75 -2
  44. package/src/hub-jwt.ts +43 -6
  45. package/src/init-summary.test.ts +120 -5
  46. package/src/init-summary.ts +67 -25
  47. package/src/live-match.test.ts +198 -0
  48. package/src/live-match.ts +310 -0
  49. package/src/mcp-install.test.ts +93 -0
  50. package/src/mcp-install.ts +106 -0
  51. package/src/mcp-tools.ts +80 -7
  52. package/src/mirror-config.test.ts +14 -0
  53. package/src/mirror-config.ts +11 -0
  54. package/src/mirror-import.test.ts +110 -0
  55. package/src/mirror-import.ts +71 -13
  56. package/src/mirror-manager.test.ts +51 -0
  57. package/src/mirror-manager.ts +73 -11
  58. package/src/mirror-routes.test.ts +463 -1
  59. package/src/mirror-routes.ts +474 -4
  60. package/src/oauth-discovery.test.ts +55 -0
  61. package/src/oauth-discovery.ts +24 -5
  62. package/src/routes.ts +696 -121
  63. package/src/routing.test.ts +451 -5
  64. package/src/routing.ts +113 -5
  65. package/src/scopes.ts +1 -1
  66. package/src/server.ts +66 -4
  67. package/src/storage.test.ts +162 -0
  68. package/src/subscribe.test.ts +588 -0
  69. package/src/subscribe.ts +248 -0
  70. package/src/subscriptions.ts +295 -0
  71. package/src/tag-expand-routes.test.ts +45 -0
  72. package/src/tag-scope.ts +68 -1
  73. package/src/token-store.ts +7 -7
  74. package/src/transcription-worker.test.ts +471 -5
  75. package/src/transcription-worker.ts +212 -44
  76. package/src/triggers-api.test.ts +533 -0
  77. package/src/triggers-api.ts +295 -0
  78. package/src/triggers.ts +93 -7
  79. package/src/usage.test.ts +362 -0
  80. package/src/usage.ts +318 -0
  81. package/src/vault-create.test.ts +340 -12
  82. package/src/vault-name.test.ts +61 -3
  83. package/src/vault-name.ts +62 -14
  84. package/src/vault-remove.test.ts +187 -0
  85. package/src/vault-store.ts +10 -3
  86. package/src/vault.test.ts +1353 -62
  87. package/web/ui/dist/assets/index-CGL256oe.js +60 -0
  88. package/web/ui/dist/assets/index-J0pVP7I-.css +1 -0
  89. package/web/ui/dist/index.html +2 -2
  90. package/web/ui/dist/assets/index-DBe8Xiah.css +0 -1
  91. 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.6.0: vault no longer
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.6.0). Re-add this vault via your hub to get an access token.";
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.6.0 vault is a PURE HUB RESOURCE-SERVER (vault#282 Stage 2). The
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.6.0). Re-add this vault via your hub to get an access token.",
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
  });
@@ -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. `globalConfig.auto_transcribe?.enabled` is not explicitly false.
23
- * Default behavior (when unset) is ON — once an operator has scribe
24
- * reachable, audio attachments transcribe automatically without a
25
- * separate config step. Operators who want it OFF set
26
- * `auto_transcribe.enabled: false` explicitly.
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
- /** Allow per-call enabled override — used by the explicit-opt-in path. */
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
+ });
@@ -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
+ }