@openparachute/hub 0.7.3-rc.7 → 0.7.3-rc.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/hub",
3
- "version": "0.7.3-rc.7",
3
+ "version": "0.7.3-rc.9",
4
4
  "description": "parachute — the local hub for the Parachute ecosystem (discovery, ports, lifecycle, soon OAuth).",
5
5
  "license": "AGPL-3.0",
6
6
  "publishConfig": {
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Multi-origin iss-set (onboarding-streamline 2026-06-25) — hub side.
3
+ *
4
+ * The hub publishes the SET of origins it legitimately answers on to its
5
+ * supervised resource servers via `PARACHUTE_HUB_ORIGINS` (comma-separated),
6
+ * alongside the single canonical `PARACHUTE_HUB_ORIGIN`. A resource server on
7
+ * scope-guard ≥0.5.0 widens its accepted-`iss` check to this set so a token
8
+ * minted under one URL of a multi-URL box validates via another URL of the
9
+ * SAME box.
10
+ *
11
+ * These tests pin two things:
12
+ * 1. The serialize/parse round-trip + the assembly from hub-controlled
13
+ * inputs (issuer ∪ loopback aliases ∪ expose-state ∪ platform).
14
+ * 2. The SECURITY INVARIANT: the set is built ONLY from operator/hub config
15
+ * and on-disk state — never from an unvalidated request `Host` /
16
+ * `X-Forwarded-Host`. We feed an attacker-controlled "Host" through every
17
+ * input channel a request could plausibly reach and assert it never lands
18
+ * in the published set.
19
+ */
20
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
21
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
22
+ import { tmpdir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { parseHubOrigins, serializeHubOrigins } from "../hub-origin.ts";
25
+ import { buildHubOriginsEnvValue } from "../vault-hub-origin-env.ts";
26
+
27
+ describe("serializeHubOrigins / parseHubOrigins round-trip", () => {
28
+ test("serialize dedupes, drops empties, strips trailing slashes", () => {
29
+ const v = serializeHubOrigins([
30
+ "https://a.example/",
31
+ "https://a.example",
32
+ "",
33
+ "https://b.example",
34
+ ]);
35
+ expect(v).toBe("https://a.example,https://b.example");
36
+ });
37
+
38
+ test("serialize returns undefined when nothing survives", () => {
39
+ expect(serializeHubOrigins([])).toBeUndefined();
40
+ expect(serializeHubOrigins(["", " "])).toBeUndefined();
41
+ });
42
+
43
+ test("parse is the inverse — tolerant of whitespace + trailing slashes + empties", () => {
44
+ expect(parseHubOrigins("https://a.example, https://b.example/ ,,")).toEqual([
45
+ "https://a.example",
46
+ "https://b.example",
47
+ ]);
48
+ });
49
+
50
+ test("parse returns [] for absent/empty/garbage", () => {
51
+ expect(parseHubOrigins(undefined)).toEqual([]);
52
+ expect(parseHubOrigins("")).toEqual([]);
53
+ expect(parseHubOrigins(" , , ")).toEqual([]);
54
+ });
55
+
56
+ test("round-trips a real set", () => {
57
+ const origins = ["https://example.com", "http://127.0.0.1:1939", "http://localhost:1939"];
58
+ const wire = serializeHubOrigins(origins)!;
59
+ expect(parseHubOrigins(wire)).toEqual(origins);
60
+ });
61
+ });
62
+
63
+ describe("buildHubOriginsEnvValue — assembles the hub's legitimate-origin set", () => {
64
+ let dir: string;
65
+ const EXPOSE = () => join(dir, "expose-state.json");
66
+
67
+ /** Write a schema-valid expose-state.json carrying the given hubOrigin. */
68
+ function writeExposeState(hubOrigin: string): void {
69
+ writeFileSync(
70
+ EXPOSE(),
71
+ JSON.stringify({
72
+ version: 1,
73
+ layer: "public",
74
+ mode: "path",
75
+ canonicalFqdn: new URL(hubOrigin).host,
76
+ port: 1939,
77
+ funnel: true,
78
+ entries: [],
79
+ hubOrigin,
80
+ }),
81
+ );
82
+ }
83
+
84
+ beforeEach(() => {
85
+ dir = mkdtempSync(join(tmpdir(), "hub-origins-set-"));
86
+ // No hub.port file in this fresh configDir → readHubPort falls back to
87
+ // HUB_UNIT_DEFAULT_PORT (1939), the deterministic value these cases assert.
88
+ });
89
+ afterEach(() => rmSync(dir, { recursive: true, force: true }));
90
+
91
+ test("issuer ∪ loopback aliases when no expose / platform origin", () => {
92
+ const v = buildHubOriginsEnvValue(dir, "https://example.com", {}, EXPOSE());
93
+ const set = parseHubOrigins(v);
94
+ expect(set).toContain("https://example.com");
95
+ expect(set).toContain("http://127.0.0.1:1939");
96
+ expect(set).toContain("http://localhost:1939");
97
+ expect(set).toHaveLength(3);
98
+ });
99
+
100
+ test("loopback aliases are ALWAYS present (the always-present invariant)", () => {
101
+ // Even with a public issuer, loopback must be in the set so the co-located
102
+ // CLI / loopback-proxied request path validates.
103
+ const set = parseHubOrigins(buildHubOriginsEnvValue(dir, "https://example.com", {}, EXPOSE()));
104
+ expect(set).toContain("http://127.0.0.1:1939");
105
+ expect(set).toContain("http://localhost:1939");
106
+ });
107
+
108
+ test("folds in the expose-state public origin", () => {
109
+ writeExposeState("https://box.sslip.io");
110
+ const set = parseHubOrigins(buildHubOriginsEnvValue(dir, "https://example.com", {}, EXPOSE()));
111
+ expect(set).toContain("https://example.com");
112
+ expect(set).toContain("https://box.sslip.io");
113
+ });
114
+
115
+ test("folds in the platform origin (RENDER_EXTERNAL_URL)", () => {
116
+ const set = parseHubOrigins(
117
+ buildHubOriginsEnvValue(
118
+ dir,
119
+ "https://example.com",
120
+ { RENDER_EXTERNAL_URL: "https://app.onrender.com" },
121
+ EXPOSE(),
122
+ ),
123
+ );
124
+ expect(set).toContain("https://app.onrender.com");
125
+ });
126
+
127
+ test("folds in the composed Fly default origin", () => {
128
+ const set = parseHubOrigins(
129
+ buildHubOriginsEnvValue(dir, "https://example.com", { FLY_APP_NAME: "myapp" }, EXPOSE()),
130
+ );
131
+ expect(set).toContain("https://myapp.fly.dev");
132
+ });
133
+
134
+ test("absent issuer → loopback-only set (still useful, never empty on a normal box)", () => {
135
+ const set = parseHubOrigins(buildHubOriginsEnvValue(dir, undefined, {}, EXPOSE()));
136
+ expect(set).toContain("http://127.0.0.1:1939");
137
+ expect(set).toContain("http://localhost:1939");
138
+ // The empty issuer "" is dropped by buildHubBoundOrigins' URL parse.
139
+ expect(set).not.toContain("");
140
+ });
141
+
142
+ test("a malformed expose-state.json never throws — falls back to issuer + loopback", () => {
143
+ writeFileSync(EXPOSE(), "{ not valid json");
144
+ const set = parseHubOrigins(buildHubOriginsEnvValue(dir, "https://example.com", {}, EXPOSE()));
145
+ expect(set).toContain("https://example.com");
146
+ expect(set).toContain("http://127.0.0.1:1939");
147
+ });
148
+
149
+ describe("SECURITY INVARIANT — request Host never enters the set", () => {
150
+ const ATTACKER = "https://attacker.evil";
151
+
152
+ test("an attacker Host smuggled via expose-state IS honored ONLY because it's operator-written on-disk state — but a Host passed nowhere never appears", () => {
153
+ // There is NO request input to buildHubOriginsEnvValue at all — it takes
154
+ // configDir + issuer + env + expose-state-path. None of those is a
155
+ // request header. We assert the function's surface offers no channel for
156
+ // a request Host: feeding the attacker value to the only inputs an
157
+ // attacker might influence (a stray env var, a header-shaped string)
158
+ // never reaches the set unless it's a legitimate operator-config var.
159
+ const set = parseHubOrigins(
160
+ buildHubOriginsEnvValue(
161
+ dir,
162
+ "https://example.com",
163
+ {
164
+ // Header-shaped env vars an attacker might hope are read — none are
165
+ // consulted by the assembler (only RENDER_EXTERNAL_URL / FLY_APP_NAME).
166
+ HTTP_HOST: ATTACKER,
167
+ HTTP_X_FORWARDED_HOST: ATTACKER,
168
+ X_FORWARDED_HOST: ATTACKER,
169
+ HOST: ATTACKER,
170
+ } as NodeJS.ProcessEnv,
171
+ EXPOSE(),
172
+ ),
173
+ );
174
+ expect(set).not.toContain(ATTACKER);
175
+ expect(set.some((o) => o.includes("attacker"))).toBe(false);
176
+ });
177
+
178
+ test("the set is exactly issuer ∪ loopback ∪ expose ∪ platform — no other source", () => {
179
+ writeExposeState("https://box.sslip.io");
180
+ const set = parseHubOrigins(
181
+ buildHubOriginsEnvValue(
182
+ dir,
183
+ "https://example.com",
184
+ { RENDER_EXTERNAL_URL: "https://app.onrender.com" },
185
+ EXPOSE(),
186
+ ),
187
+ );
188
+ // Every member is one of the four sanctioned sources; nothing else.
189
+ const sanctioned = new Set([
190
+ "https://example.com",
191
+ "http://127.0.0.1:1939",
192
+ "http://localhost:1939",
193
+ "https://box.sslip.io",
194
+ "https://app.onrender.com",
195
+ ]);
196
+ for (const o of set) expect(sanctioned.has(o)).toBe(true);
197
+ });
198
+ });
199
+ });
@@ -3,10 +3,15 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import {
6
+ MIN_RAM_MIB,
6
7
  SCRIBE_DEFAULT_PROVIDER,
7
8
  SCRIBE_PROVIDERS,
8
9
  apiKeyEnvFor,
10
+ clearScribeProvider,
11
+ decideLocalProvider,
9
12
  isKnownScribeProvider,
13
+ platformLocalProvider,
14
+ readAvailableRamMib,
10
15
  readScribeProviderState,
11
16
  scribeConfigPath,
12
17
  scribeEnvPath,
@@ -41,6 +46,118 @@ describe("provider catalog", () => {
41
46
  });
42
47
  });
43
48
 
49
+ describe("platformLocalProvider — the Linux 'local' trap fix", () => {
50
+ test("macOS → parakeet-mlx", () => {
51
+ expect(platformLocalProvider("darwin")).toBe("parakeet-mlx");
52
+ });
53
+
54
+ test("Linux → onnx-asr (NOT the macOS-only parakeet-mlx)", () => {
55
+ expect(platformLocalProvider("linux")).toBe("onnx-asr");
56
+ });
57
+
58
+ test("unsupported platform → null (steer to cloud)", () => {
59
+ expect(platformLocalProvider("win32")).toBeNull();
60
+ });
61
+ });
62
+
63
+ describe("readAvailableRamMib", () => {
64
+ test("non-Linux returns a positive number (totalmem fallback) or null", () => {
65
+ const ram = readAvailableRamMib("darwin");
66
+ // On any real CI/dev box totalmem is well-defined + positive.
67
+ expect(ram === null || (typeof ram === "number" && ram > 0)).toBe(true);
68
+ });
69
+
70
+ test("Linux path reads /proc/meminfo (or null when unreadable)", () => {
71
+ const ram = readAvailableRamMib("linux");
72
+ // On macOS CI there's no /proc/meminfo → null; on Linux CI a positive MiB.
73
+ expect(ram === null || (typeof ram === "number" && ram > 0)).toBe(true);
74
+ });
75
+ });
76
+
77
+ describe("decideLocalProvider — the RAM/platform gate", () => {
78
+ test("Linux with ample RAM → ok, onnx-asr", () => {
79
+ const d = decideLocalProvider("linux", 4096);
80
+ expect(d.ok).toBe(true);
81
+ expect(d.provider).toBe("onnx-asr");
82
+ });
83
+
84
+ test("macOS with ample RAM → ok, parakeet-mlx", () => {
85
+ const d = decideLocalProvider("darwin", 16384);
86
+ expect(d.ok).toBe(true);
87
+ expect(d.provider).toBe("parakeet-mlx");
88
+ });
89
+
90
+ test("below the RAM floor → refused, steers to groq, carries a reason", () => {
91
+ const d = decideLocalProvider("linux", MIN_RAM_MIB - 1);
92
+ expect(d.ok).toBe(false);
93
+ expect(d.steerTo).toBe("groq");
94
+ expect(d.reason).toContain(String(MIN_RAM_MIB));
95
+ });
96
+
97
+ test("exactly at the floor is OK (>= floor)", () => {
98
+ expect(decideLocalProvider("linux", MIN_RAM_MIB).ok).toBe(true);
99
+ });
100
+
101
+ test("unknown RAM (null) does not refuse on a supported platform", () => {
102
+ expect(decideLocalProvider("linux", null).ok).toBe(true);
103
+ });
104
+
105
+ test("unsupported platform → refused regardless of RAM, steers to groq", () => {
106
+ const d = decideLocalProvider("win32", 99999);
107
+ expect(d.ok).toBe(false);
108
+ expect(d.steerTo).toBe("groq");
109
+ });
110
+ });
111
+
112
+ describe("clearScribeProvider", () => {
113
+ test("removes transcribe.provider, preserving other keys", () => {
114
+ const h = makeHarness();
115
+ try {
116
+ mkdirSync(join(h.dir, "scribe"), { recursive: true });
117
+ writeFileSync(
118
+ scribeConfigPath(h.dir),
119
+ JSON.stringify({
120
+ transcribe: { provider: "onnx-asr", language: "en" },
121
+ auth: { required_token: "keep" },
122
+ }),
123
+ );
124
+ clearScribeProvider(h.dir);
125
+ const parsed = JSON.parse(readFileSync(scribeConfigPath(h.dir), "utf8"));
126
+ expect(parsed.transcribe).toEqual({ language: "en" });
127
+ expect(parsed.auth).toEqual({ required_token: "keep" });
128
+ } finally {
129
+ h.cleanup();
130
+ }
131
+ });
132
+
133
+ test("drops the transcribe block entirely when provider was its only key", () => {
134
+ const h = makeHarness();
135
+ try {
136
+ mkdirSync(join(h.dir, "scribe"), { recursive: true });
137
+ writeFileSync(
138
+ scribeConfigPath(h.dir),
139
+ JSON.stringify({ transcribe: { provider: "onnx-asr" }, auth: { required_token: "x" } }),
140
+ );
141
+ clearScribeProvider(h.dir);
142
+ const parsed = JSON.parse(readFileSync(scribeConfigPath(h.dir), "utf8"));
143
+ expect(parsed.transcribe).toBeUndefined();
144
+ expect(parsed.auth).toEqual({ required_token: "x" });
145
+ } finally {
146
+ h.cleanup();
147
+ }
148
+ });
149
+
150
+ test("no-op when the file is absent", () => {
151
+ const h = makeHarness();
152
+ try {
153
+ // No file written.
154
+ expect(() => clearScribeProvider(h.dir)).not.toThrow();
155
+ } finally {
156
+ h.cleanup();
157
+ }
158
+ });
159
+ });
160
+
44
161
  describe("readScribeProviderState", () => {
45
162
  test("missing file: configExists false, no provider", () => {
46
163
  const h = makeHarness();
@@ -125,6 +125,51 @@ describe("bootSupervisedModules", () => {
125
125
  expect(recorder.calls[0]?.env?.PARACHUTE_HUB_ORIGIN).toBe("https://hub.example");
126
126
  });
127
127
 
128
+ test("forwards PARACHUTE_HUB_ORIGINS (the multi-origin iss-set) alongside the single origin", async () => {
129
+ // Multi-origin iss-set (onboarding-streamline 2026-06-25): a resource
130
+ // server on scope-guard ≥0.5.0 widens its accepted-`iss` check to this set
131
+ // so a token minted under one URL of a multi-URL box validates via another.
132
+ // The set always carries the issuer + loopback aliases (the deterministic
133
+ // members); expose-state / platform members vary by box.
134
+ writeManifest({ services: [VAULT_ENTRY] }, h.manifestPath);
135
+ const recorder = makeRecorder();
136
+ const sup = new Supervisor({ spawnFn: recorder.spawn });
137
+
138
+ await bootSupervisedModules(sup, {
139
+ manifestPath: h.manifestPath,
140
+ configDir: h.dir,
141
+ hubOrigin: "https://hub.example",
142
+ });
143
+
144
+ const origins = recorder.calls[0]?.env?.PARACHUTE_HUB_ORIGINS;
145
+ expect(origins).toBeDefined();
146
+ const set = (origins ?? "").split(",");
147
+ expect(set).toContain("https://hub.example");
148
+ // SECURITY: the set is hub-controlled (issuer + loopback + expose +
149
+ // platform) — never a request Host. Loopback aliases are always present.
150
+ expect(set.some((o) => o.startsWith("http://127.0.0.1:"))).toBe(true);
151
+ expect(set.some((o) => o.startsWith("http://localhost:"))).toBe(true);
152
+ // And the single canonical origin is still written for back-compat.
153
+ expect(recorder.calls[0]?.env?.PARACHUTE_HUB_ORIGIN).toBe("https://hub.example");
154
+ });
155
+
156
+ test("no PARACHUTE_HUB_ORIGINS when hubOrigin is unset (no widening, single-origin only)", async () => {
157
+ // Back-compat: a boot with no hubOrigin neither sets PARACHUTE_HUB_ORIGIN
158
+ // nor the set — the child keeps its own loopback default + scope-guard's
159
+ // single-origin behavior.
160
+ writeManifest({ services: [VAULT_ENTRY] }, h.manifestPath);
161
+ const recorder = makeRecorder();
162
+ const sup = new Supervisor({ spawnFn: recorder.spawn });
163
+
164
+ await bootSupervisedModules(sup, {
165
+ manifestPath: h.manifestPath,
166
+ configDir: h.dir,
167
+ });
168
+
169
+ expect(recorder.calls[0]?.env?.PARACHUTE_HUB_ORIGIN).toBeUndefined();
170
+ expect(recorder.calls[0]?.env?.PARACHUTE_HUB_ORIGINS).toBeUndefined();
171
+ });
172
+
128
173
  test("sets PORT in child env from services.json entry (hub#357)", async () => {
129
174
  // Container deploys (Render etc.) set PORT in hub's process.env via
130
175
  // Dockerfile / platform injection. The supervisor's defaultSpawnFn
@@ -1825,6 +1825,24 @@ describe("handleSetupVaultPost", () => {
1825
1825
  expect(cfg?.cleanup).toEqual({ provider: "anthropic", default: true });
1826
1826
  expect(cfg?.cleanupProviders).toEqual({ anthropic: { apiKey: "sk-ant-cleanup-key" } });
1827
1827
  });
1828
+
1829
+ test("scribe transcribe=local resolves to the host platform's backend (the Linux trap fix)", async () => {
1830
+ // The bug: `local` mapped UNCONDITIONALLY to parakeet-mlx (macOS-only),
1831
+ // silently broken on every Linux box. After the fix it resolves to the
1832
+ // platform backend. We assert against the actual host so it's correct on
1833
+ // both Mac (parakeet-mlx) and Linux CI (onnx-asr). The RAM gate only kicks
1834
+ // in below 2 GB — CI/dev boxes clear it, so the choice stays `local`.
1835
+ const { platformLocalProvider } = await import("../scribe-config.ts");
1836
+ const expected = platformLocalProvider(process.platform);
1837
+ const { response } = await postVaultWithFields(h, { scribe_provider: "local" });
1838
+ expect(response.status).toBe(303);
1839
+ const cfg = readScribeConfig(h.dir);
1840
+ if (expected !== null) {
1841
+ // On a supported platform with enough RAM, `local` resolves to the
1842
+ // platform backend — NOT a hardcoded parakeet-mlx on Linux.
1843
+ expect(cfg?.transcribe).toEqual({ provider: expected });
1844
+ }
1845
+ });
1828
1846
  });
1829
1847
 
1830
1848
  // --- end-to-end through hubFetch -----------------------------------------
@@ -76,13 +76,29 @@ describe("persistVaultHubOrigin", () => {
76
76
  expect(existsSync(vaultEnv())).toBe(false);
77
77
  });
78
78
 
79
- test("is idempotent — no rewrite when the value is already current", () => {
79
+ test("is idempotent — no rewrite when both the origin and the set are current", () => {
80
80
  const log: string[] = [];
81
81
  expect(persistVaultHubOrigin(dir, "https://hub.example.com", (l) => log.push(l))).toBe(true);
82
+ // First call wrote BOTH the single origin and the multi-origin set.
83
+ expect(
84
+ log.some((l) => /persisted PARACHUTE_HUB_ORIGIN=https:\/\/hub\.example\.com/.test(l)),
85
+ ).toBe(true);
86
+ expect(log.some((l) => /persisted PARACHUTE_HUB_ORIGINS=/.test(l))).toBe(true);
87
+ const writesAfterFirst = log.length;
88
+ // Second call is a no-op: both values already current → false, no new logs.
82
89
  expect(persistVaultHubOrigin(dir, "https://hub.example.com", (l) => log.push(l))).toBe(false);
83
- // Only the first call logged.
84
- expect(log).toHaveLength(1);
85
- expect(log[0]).toMatch(/persisted PARACHUTE_HUB_ORIGIN=https:\/\/hub\.example\.com/);
90
+ expect(log).toHaveLength(writesAfterFirst);
91
+ });
92
+
93
+ test("writes PARACHUTE_HUB_ORIGINS (multi-origin iss-set) alongside the single origin", () => {
94
+ expect(persistVaultHubOrigin(dir, "https://hub.example.com", () => {})).toBe(true);
95
+ const values = readEnvFileValues(vaultEnv());
96
+ expect(values.PARACHUTE_HUB_ORIGIN).toBe("https://hub.example.com");
97
+ // The set carries the issuer + loopback aliases (always present).
98
+ const set = (values.PARACHUTE_HUB_ORIGINS ?? "").split(",");
99
+ expect(set).toContain("https://hub.example.com");
100
+ expect(set.some((o) => o.startsWith("http://127.0.0.1:"))).toBe(true);
101
+ expect(set.some((o) => o.startsWith("http://localhost:"))).toBe(true);
86
102
  });
87
103
 
88
104
  test("updates a stale origin in-place and preserves sibling keys", () => {
@@ -113,6 +129,24 @@ describe("clearVaultHubOrigin", () => {
113
129
  expect(values.SCRIBE_AUTH_TOKEN).toBe("secret");
114
130
  });
115
131
 
132
+ test("removes BOTH the single origin and the multi-origin set, leaving siblings", () => {
133
+ writeFileSync(
134
+ mkVaultDir(),
135
+ "SCRIBE_AUTH_TOKEN=secret\nPARACHUTE_HUB_ORIGIN=https://hub.example.com\nPARACHUTE_HUB_ORIGINS=https://hub.example.com,http://127.0.0.1:1939\n",
136
+ );
137
+ expect(clearVaultHubOrigin(dir, () => {})).toBe(true);
138
+ const values = readEnvFileValues(vaultEnv());
139
+ expect(values.PARACHUTE_HUB_ORIGIN).toBeUndefined();
140
+ expect(values.PARACHUTE_HUB_ORIGINS).toBeUndefined();
141
+ expect(values.SCRIBE_AUTH_TOKEN).toBe("secret");
142
+ });
143
+
144
+ test("clears a lone PARACHUTE_HUB_ORIGINS even if the single origin is absent", () => {
145
+ writeFileSync(mkVaultDir(), "PARACHUTE_HUB_ORIGINS=https://hub.example.com\n");
146
+ expect(clearVaultHubOrigin(dir, () => {})).toBe(true);
147
+ expect(readEnvFileValues(vaultEnv()).PARACHUTE_HUB_ORIGINS).toBeUndefined();
148
+ });
149
+
116
150
  test("no-op when no origin is present", () => {
117
151
  writeFileSync(mkVaultDir(), "SCRIBE_AUTH_TOKEN=secret\n");
118
152
  expect(clearVaultHubOrigin(dir, () => {})).toBe(false);