@openparachute/hub 0.5.14-rc.2 → 0.5.14-rc.21
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 +109 -15
- package/package.json +7 -3
- package/src/__tests__/account-home-ui.test.ts +251 -15
- package/src/__tests__/account-vault-token.test.ts +355 -0
- package/src/__tests__/admin-vaults.test.ts +70 -4
- package/src/__tests__/api-mint-token.test.ts +693 -5
- package/src/__tests__/api-modules-config.test.ts +16 -10
- package/src/__tests__/api-modules-ops.test.ts +45 -0
- package/src/__tests__/api-modules.test.ts +92 -75
- package/src/__tests__/api-ready.test.ts +135 -0
- package/src/__tests__/api-revoke-token.test.ts +384 -0
- package/src/__tests__/api-users.test.ts +7 -2
- package/src/__tests__/auth.test.ts +157 -30
- package/src/__tests__/cli.test.ts +44 -5
- package/src/__tests__/cloudflare-detect.test.ts +60 -5
- package/src/__tests__/expose-2fa-warning.test.ts +31 -17
- package/src/__tests__/expose-auth-preflight.test.ts +71 -72
- package/src/__tests__/expose-cloudflare.test.ts +582 -11
- package/src/__tests__/expose-interactive.test.ts +10 -4
- package/src/__tests__/expose-public-auto.test.ts +5 -1
- package/src/__tests__/expose.test.ts +52 -2
- package/src/__tests__/hub-server.test.ts +396 -10
- package/src/__tests__/hub.test.ts +85 -6
- package/src/__tests__/init.test.ts +928 -0
- package/src/__tests__/lifecycle.test.ts +464 -2
- package/src/__tests__/migrate.test.ts +433 -51
- package/src/__tests__/oauth-handlers.test.ts +1252 -83
- package/src/__tests__/oauth-ui.test.ts +12 -1
- package/src/__tests__/operator-token-issuer-self-heal.test.ts +412 -0
- package/src/__tests__/proxy-error-ui.test.ts +212 -0
- package/src/__tests__/proxy-state.test.ts +192 -0
- package/src/__tests__/resource-binding.test.ts +97 -0
- package/src/__tests__/scope-explanations.test.ts +77 -12
- package/src/__tests__/services-manifest.test.ts +122 -4
- package/src/__tests__/setup-wizard.test.ts +633 -53
- package/src/__tests__/status.test.ts +36 -0
- package/src/__tests__/two-factor-flow.test.ts +602 -0
- package/src/__tests__/two-factor.test.ts +183 -0
- package/src/__tests__/upgrade.test.ts +78 -1
- package/src/__tests__/users.test.ts +68 -0
- package/src/__tests__/vault-auth-status.test.ts +312 -11
- package/src/__tests__/vault-hub-origin-env.test.ts +263 -0
- package/src/__tests__/wizard.test.ts +372 -0
- package/src/account-home-ui.ts +488 -38
- package/src/account-vault-token.ts +282 -0
- package/src/admin-handlers.ts +159 -4
- package/src/admin-login-ui.ts +49 -5
- package/src/admin-vaults.ts +48 -15
- package/src/api-account.ts +14 -0
- package/src/api-mint-token.ts +132 -24
- package/src/api-modules-ops.ts +49 -11
- package/src/api-modules.ts +29 -12
- package/src/api-ready.ts +102 -0
- package/src/api-revoke-token.ts +107 -21
- package/src/api-users.ts +29 -3
- package/src/cli.ts +112 -25
- package/src/clients.ts +18 -6
- package/src/cloudflare/config.ts +10 -4
- package/src/cloudflare/detect.ts +82 -20
- package/src/commands/auth.ts +165 -24
- package/src/commands/expose-2fa-warning.ts +34 -32
- package/src/commands/expose-auth-preflight.ts +89 -78
- package/src/commands/expose-cloudflare.ts +471 -16
- package/src/commands/expose-interactive.ts +10 -11
- package/src/commands/expose-public-auto.ts +6 -4
- package/src/commands/expose.ts +8 -0
- package/src/commands/init.ts +594 -0
- package/src/commands/install.ts +33 -2
- package/src/commands/lifecycle.ts +386 -17
- package/src/commands/migrate.ts +293 -41
- package/src/commands/status.ts +22 -0
- package/src/commands/upgrade.ts +55 -11
- package/src/commands/wizard.ts +847 -0
- package/src/env-file.ts +10 -0
- package/src/help.ts +157 -15
- package/src/hub-db.ts +39 -1
- package/src/hub-server.ts +119 -13
- package/src/hub-settings.ts +11 -0
- package/src/hub.ts +82 -14
- package/src/oauth-handlers.ts +298 -21
- package/src/oauth-ui.ts +10 -0
- package/src/operator-token.ts +151 -0
- package/src/pending-login.ts +116 -0
- package/src/proxy-error-ui.ts +506 -0
- package/src/proxy-state.ts +131 -0
- package/src/rate-limit.ts +51 -0
- package/src/resource-binding.ts +134 -0
- package/src/scope-attenuation.ts +85 -0
- package/src/scope-explanations.ts +131 -14
- package/src/services-manifest.ts +112 -0
- package/src/setup-wizard.ts +738 -125
- package/src/tailscale/run.ts +28 -11
- package/src/totp.ts +201 -0
- package/src/two-factor-handlers.ts +287 -0
- package/src/two-factor-store.ts +181 -0
- package/src/two-factor-ui.ts +462 -0
- package/src/users.ts +58 -0
- package/src/vault/auth-status.ts +200 -25
- package/src/vault-hub-origin-env.ts +163 -0
- package/web/ui/dist/assets/index-BiBlvEaj.css +1 -0
- package/web/ui/dist/assets/index-CIN3mnmf.js +61 -0
- package/web/ui/dist/index.html +2 -2
- package/src/__tests__/vault-tokens-create-interactive.test.ts +0 -183
- package/src/commands/vault-tokens-create-interactive.ts +0 -143
- package/web/ui/dist/assets/index-7DtAXz7y.css +0 -1
- package/web/ui/dist/assets/index-tRmPbbC7.js +0 -61
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
getDefaultOperationsRegistry,
|
|
24
24
|
} from "../api-modules-ops.ts";
|
|
25
25
|
import { CSRF_COOKIE_NAME, CSRF_FIELD_NAME } from "../csrf.ts";
|
|
26
|
+
import { type ExposeState, readExposeState, writeExposeState } from "../expose-state.ts";
|
|
26
27
|
import { hubDbPath, openHubDb } from "../hub-db.ts";
|
|
27
28
|
import { hubFetch } from "../hub-server.ts";
|
|
28
29
|
import { getSetting, setSetting } from "../hub-settings.ts";
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
handleSetupGet,
|
|
37
38
|
handleSetupInstallPost,
|
|
38
39
|
handleSetupVaultPost,
|
|
40
|
+
postVaultImportImpl,
|
|
39
41
|
} from "../setup-wizard.ts";
|
|
40
42
|
import { Supervisor } from "../supervisor.ts";
|
|
41
43
|
import { createUser, getUserByUsername, userCount } from "../users.ts";
|
|
@@ -43,6 +45,20 @@ import { createUser, getUserByUsername, userCount } from "../users.ts";
|
|
|
43
45
|
interface Harness {
|
|
44
46
|
dir: string;
|
|
45
47
|
manifestPath: string;
|
|
48
|
+
/**
|
|
49
|
+
* Hermetic expose-state reader scoped to the harness's tmp dir. The
|
|
50
|
+
* production `readExposeState()` defaults to the operator's real
|
|
51
|
+
* `~/.parachute/expose-state.json` (a module-load constant), so a
|
|
52
|
+
* wizard test that omits an injected reader would auto-seed
|
|
53
|
+
* `setup_expose_mode` from the developer's LIVE exposure (hub#406) and
|
|
54
|
+
* flip expose-step assertions nondeterministically. Threading this
|
|
55
|
+
* harness reader keeps every wizard test isolated from the real
|
|
56
|
+
* filesystem — same isolation the harness already gives DB + manifest.
|
|
57
|
+
* Defaults to "no live exposure" (the tmp file doesn't exist) unless a
|
|
58
|
+
* test writes one via `writeExposeState(state, h.exposeStatePath)`.
|
|
59
|
+
*/
|
|
60
|
+
exposeStatePath: string;
|
|
61
|
+
readExposeStateFn: () => ExposeState | undefined;
|
|
46
62
|
cleanup: () => void;
|
|
47
63
|
}
|
|
48
64
|
|
|
@@ -51,9 +67,12 @@ function makeHarness(): Harness {
|
|
|
51
67
|
writeFileSync(join(dir, "hub.html"), "<html>discovery</html>");
|
|
52
68
|
const manifestPath = join(dir, "services.json");
|
|
53
69
|
writeManifest({ services: [] }, manifestPath);
|
|
70
|
+
const exposeStatePath = join(dir, "expose-state.json");
|
|
54
71
|
return {
|
|
55
72
|
dir,
|
|
56
73
|
manifestPath,
|
|
74
|
+
exposeStatePath,
|
|
75
|
+
readExposeStateFn: () => readExposeState(exposeStatePath),
|
|
57
76
|
cleanup: () => rmSync(dir, { recursive: true, force: true }),
|
|
58
77
|
};
|
|
59
78
|
}
|
|
@@ -123,7 +142,11 @@ describe("deriveWizardState", () => {
|
|
|
123
142
|
test("welcome step when no admin and no vault", () => {
|
|
124
143
|
const db = openHubDb(hubDbPath(h.dir));
|
|
125
144
|
try {
|
|
126
|
-
const s = deriveWizardState({
|
|
145
|
+
const s = deriveWizardState({
|
|
146
|
+
db,
|
|
147
|
+
manifestPath: h.manifestPath,
|
|
148
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
149
|
+
});
|
|
127
150
|
expect(s.step).toBe("welcome");
|
|
128
151
|
expect(s.hasAdmin).toBe(false);
|
|
129
152
|
expect(s.hasVault).toBe(false);
|
|
@@ -136,7 +159,11 @@ describe("deriveWizardState", () => {
|
|
|
136
159
|
const db = openHubDb(hubDbPath(h.dir));
|
|
137
160
|
try {
|
|
138
161
|
await createUser(db, "owner", "pw");
|
|
139
|
-
const s = deriveWizardState({
|
|
162
|
+
const s = deriveWizardState({
|
|
163
|
+
db,
|
|
164
|
+
manifestPath: h.manifestPath,
|
|
165
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
166
|
+
});
|
|
140
167
|
expect(s.step).toBe("vault");
|
|
141
168
|
expect(s.hasAdmin).toBe(true);
|
|
142
169
|
expect(s.hasVault).toBe(false);
|
|
@@ -163,7 +190,11 @@ describe("deriveWizardState", () => {
|
|
|
163
190
|
},
|
|
164
191
|
h.manifestPath,
|
|
165
192
|
);
|
|
166
|
-
const s = deriveWizardState({
|
|
193
|
+
const s = deriveWizardState({
|
|
194
|
+
db,
|
|
195
|
+
manifestPath: h.manifestPath,
|
|
196
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
197
|
+
});
|
|
167
198
|
expect(s.step).toBe("expose");
|
|
168
199
|
expect(s.hasAdmin).toBe(true);
|
|
169
200
|
expect(s.hasVault).toBe(true);
|
|
@@ -200,7 +231,12 @@ describe("deriveWizardState", () => {
|
|
|
200
231
|
);
|
|
201
232
|
// Simulate Render env. detectAutoExposeMode reads RENDER_EXTERNAL_URL.
|
|
202
233
|
const renderEnv = { RENDER_EXTERNAL_URL: "https://parachute-hub.onrender.com" };
|
|
203
|
-
const s = deriveWizardState({
|
|
234
|
+
const s = deriveWizardState({
|
|
235
|
+
db,
|
|
236
|
+
manifestPath: h.manifestPath,
|
|
237
|
+
env: renderEnv,
|
|
238
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
239
|
+
});
|
|
204
240
|
expect(s.step).toBe("done");
|
|
205
241
|
expect(s.hasExposeMode).toBe(true);
|
|
206
242
|
} finally {
|
|
@@ -226,7 +262,12 @@ describe("deriveWizardState", () => {
|
|
|
226
262
|
},
|
|
227
263
|
h.manifestPath,
|
|
228
264
|
);
|
|
229
|
-
const s = deriveWizardState({
|
|
265
|
+
const s = deriveWizardState({
|
|
266
|
+
db,
|
|
267
|
+
manifestPath: h.manifestPath,
|
|
268
|
+
env: {},
|
|
269
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
270
|
+
});
|
|
230
271
|
// Local install path — the operator still gets to choose
|
|
231
272
|
expect(s.step).toBe("expose");
|
|
232
273
|
expect(s.hasExposeMode).toBe(false);
|
|
@@ -235,6 +276,188 @@ describe("deriveWizardState", () => {
|
|
|
235
276
|
}
|
|
236
277
|
});
|
|
237
278
|
|
|
279
|
+
test("auto-seeds expose mode from a live `parachute expose tailnet` (hub#406 team-onboarding bug)", async () => {
|
|
280
|
+
// Team-onboarding bug: an operator ran `parachute expose tailnet`
|
|
281
|
+
// BEFORE opening the wizard. That writes expose-state.json
|
|
282
|
+
// (layer=tailnet) but never the `setup_expose_mode` hub_setting —
|
|
283
|
+
// the two are orthogonal axes. Pre-fix, the wizard consulted only
|
|
284
|
+
// the setting and re-rendered "How will this hub be reached?" though
|
|
285
|
+
// tailnet was already live. deriveWizardState now reads the live
|
|
286
|
+
// exposure layer and auto-seeds the setting, so the expose step is
|
|
287
|
+
// treated as satisfied and the wizard advances to done.
|
|
288
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
289
|
+
try {
|
|
290
|
+
await createUser(db, "owner", "pw");
|
|
291
|
+
writeManifest(
|
|
292
|
+
{
|
|
293
|
+
services: [
|
|
294
|
+
{
|
|
295
|
+
name: "parachute-vault",
|
|
296
|
+
version: "0.1.0",
|
|
297
|
+
port: 1940,
|
|
298
|
+
paths: ["/vault/default"],
|
|
299
|
+
health: "/health",
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
h.manifestPath,
|
|
304
|
+
);
|
|
305
|
+
// Simulate `parachute expose tailnet`: write a real expose-state
|
|
306
|
+
// file (round-trips through readExposeState's validator) into the
|
|
307
|
+
// harness tmp path. No env signal (not Render/Fly), no setting.
|
|
308
|
+
writeExposeState(
|
|
309
|
+
{
|
|
310
|
+
version: 1,
|
|
311
|
+
layer: "tailnet",
|
|
312
|
+
mode: "path",
|
|
313
|
+
canonicalFqdn: "my-mac.tailnet-name.ts.net",
|
|
314
|
+
port: 1939,
|
|
315
|
+
funnel: false,
|
|
316
|
+
entries: [],
|
|
317
|
+
},
|
|
318
|
+
h.exposeStatePath,
|
|
319
|
+
);
|
|
320
|
+
const s = deriveWizardState({
|
|
321
|
+
db,
|
|
322
|
+
manifestPath: h.manifestPath,
|
|
323
|
+
env: {},
|
|
324
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
325
|
+
});
|
|
326
|
+
expect(s.step).toBe("done");
|
|
327
|
+
expect(s.hasExposeMode).toBe(true);
|
|
328
|
+
// The setting was auto-seeded from the live exposure layer.
|
|
329
|
+
expect(getSetting(db, "setup_expose_mode")).toBe("tailnet");
|
|
330
|
+
} finally {
|
|
331
|
+
db.close();
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("auto-seeds expose mode = public from a live public exposure", async () => {
|
|
336
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
337
|
+
try {
|
|
338
|
+
await createUser(db, "owner", "pw");
|
|
339
|
+
writeManifest(
|
|
340
|
+
{
|
|
341
|
+
services: [
|
|
342
|
+
{
|
|
343
|
+
name: "parachute-vault",
|
|
344
|
+
version: "0.1.0",
|
|
345
|
+
port: 1940,
|
|
346
|
+
paths: ["/vault/default"],
|
|
347
|
+
health: "/health",
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
},
|
|
351
|
+
h.manifestPath,
|
|
352
|
+
);
|
|
353
|
+
writeExposeState(
|
|
354
|
+
{
|
|
355
|
+
version: 1,
|
|
356
|
+
layer: "public",
|
|
357
|
+
mode: "path",
|
|
358
|
+
canonicalFqdn: "hub.example.com",
|
|
359
|
+
port: 1939,
|
|
360
|
+
funnel: true,
|
|
361
|
+
entries: [],
|
|
362
|
+
},
|
|
363
|
+
h.exposeStatePath,
|
|
364
|
+
);
|
|
365
|
+
const s = deriveWizardState({
|
|
366
|
+
db,
|
|
367
|
+
manifestPath: h.manifestPath,
|
|
368
|
+
env: {},
|
|
369
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
370
|
+
});
|
|
371
|
+
expect(s.step).toBe("done");
|
|
372
|
+
expect(s.hasExposeMode).toBe(true);
|
|
373
|
+
expect(getSetting(db, "setup_expose_mode")).toBe("public");
|
|
374
|
+
} finally {
|
|
375
|
+
db.close();
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("still asks the expose step when no live exposure + no setting (unchanged)", async () => {
|
|
380
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
381
|
+
try {
|
|
382
|
+
await createUser(db, "owner", "pw");
|
|
383
|
+
writeManifest(
|
|
384
|
+
{
|
|
385
|
+
services: [
|
|
386
|
+
{
|
|
387
|
+
name: "parachute-vault",
|
|
388
|
+
version: "0.1.0",
|
|
389
|
+
port: 1940,
|
|
390
|
+
paths: ["/vault/default"],
|
|
391
|
+
health: "/health",
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
},
|
|
395
|
+
h.manifestPath,
|
|
396
|
+
);
|
|
397
|
+
// No env signal, no expose-state file written (reader returns
|
|
398
|
+
// undefined), no setting → the operator still gets the expose step.
|
|
399
|
+
const s = deriveWizardState({
|
|
400
|
+
db,
|
|
401
|
+
manifestPath: h.manifestPath,
|
|
402
|
+
env: {},
|
|
403
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
404
|
+
});
|
|
405
|
+
expect(s.step).toBe("expose");
|
|
406
|
+
expect(s.hasExposeMode).toBe(false);
|
|
407
|
+
expect(getSetting(db, "setup_expose_mode")).toBeUndefined();
|
|
408
|
+
} finally {
|
|
409
|
+
db.close();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("an explicit setup_expose_mode wins over a live exposure (no clobber)", async () => {
|
|
414
|
+
// If the operator already answered the expose step (or it was seeded
|
|
415
|
+
// by a prior call), a later live-exposure read must not overwrite the
|
|
416
|
+
// recorded answer. Guards the `=== undefined` gate.
|
|
417
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
418
|
+
try {
|
|
419
|
+
await createUser(db, "owner", "pw");
|
|
420
|
+
writeManifest(
|
|
421
|
+
{
|
|
422
|
+
services: [
|
|
423
|
+
{
|
|
424
|
+
name: "parachute-vault",
|
|
425
|
+
version: "0.1.0",
|
|
426
|
+
port: 1940,
|
|
427
|
+
paths: ["/vault/default"],
|
|
428
|
+
health: "/health",
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
h.manifestPath,
|
|
433
|
+
);
|
|
434
|
+
setSetting(db, "setup_expose_mode", "localhost");
|
|
435
|
+
writeExposeState(
|
|
436
|
+
{
|
|
437
|
+
version: 1,
|
|
438
|
+
layer: "public",
|
|
439
|
+
mode: "path",
|
|
440
|
+
canonicalFqdn: "hub.example.com",
|
|
441
|
+
port: 1939,
|
|
442
|
+
funnel: true,
|
|
443
|
+
entries: [],
|
|
444
|
+
},
|
|
445
|
+
h.exposeStatePath,
|
|
446
|
+
);
|
|
447
|
+
const s = deriveWizardState({
|
|
448
|
+
db,
|
|
449
|
+
manifestPath: h.manifestPath,
|
|
450
|
+
env: {},
|
|
451
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
452
|
+
});
|
|
453
|
+
expect(s.step).toBe("done");
|
|
454
|
+
// Recorded answer is preserved, not overwritten by the live layer.
|
|
455
|
+
expect(getSetting(db, "setup_expose_mode")).toBe("localhost");
|
|
456
|
+
} finally {
|
|
457
|
+
db.close();
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
238
461
|
test("done step once admin + vault + expose mode all exist", async () => {
|
|
239
462
|
const db = openHubDb(hubDbPath(h.dir));
|
|
240
463
|
try {
|
|
@@ -254,7 +477,11 @@ describe("deriveWizardState", () => {
|
|
|
254
477
|
h.manifestPath,
|
|
255
478
|
);
|
|
256
479
|
setSetting(db, "setup_expose_mode", "localhost");
|
|
257
|
-
const s = deriveWizardState({
|
|
480
|
+
const s = deriveWizardState({
|
|
481
|
+
db,
|
|
482
|
+
manifestPath: h.manifestPath,
|
|
483
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
484
|
+
});
|
|
258
485
|
expect(s.step).toBe("done");
|
|
259
486
|
expect(s.hasAdmin).toBe(true);
|
|
260
487
|
expect(s.hasVault).toBe(true);
|
|
@@ -282,6 +509,7 @@ describe("handleSetupGet", () => {
|
|
|
282
509
|
db,
|
|
283
510
|
manifestPath: h.manifestPath,
|
|
284
511
|
configDir: h.dir,
|
|
512
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
285
513
|
issuer: "https://hub.example",
|
|
286
514
|
registry: getDefaultOperationsRegistry(),
|
|
287
515
|
});
|
|
@@ -303,6 +531,7 @@ describe("handleSetupGet", () => {
|
|
|
303
531
|
db,
|
|
304
532
|
manifestPath: h.manifestPath,
|
|
305
533
|
configDir: h.dir,
|
|
534
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
306
535
|
issuer: "https://hub.example",
|
|
307
536
|
registry: getDefaultOperationsRegistry(),
|
|
308
537
|
});
|
|
@@ -353,6 +582,7 @@ describe("handleSetupGet", () => {
|
|
|
353
582
|
db,
|
|
354
583
|
manifestPath: h.manifestPath,
|
|
355
584
|
configDir: h.dir,
|
|
585
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
356
586
|
issuer: "https://hub.example",
|
|
357
587
|
registry: getDefaultOperationsRegistry(),
|
|
358
588
|
});
|
|
@@ -385,6 +615,7 @@ describe("handleSetupGet", () => {
|
|
|
385
615
|
db,
|
|
386
616
|
manifestPath: h.manifestPath,
|
|
387
617
|
configDir: h.dir,
|
|
618
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
388
619
|
issuer: "https://hub.example",
|
|
389
620
|
registry: getDefaultOperationsRegistry(),
|
|
390
621
|
});
|
|
@@ -433,6 +664,7 @@ describe("handleSetupGet", () => {
|
|
|
433
664
|
db,
|
|
434
665
|
manifestPath: h.manifestPath,
|
|
435
666
|
configDir: h.dir,
|
|
667
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
436
668
|
issuer: "https://hub.example",
|
|
437
669
|
registry: getDefaultOperationsRegistry(),
|
|
438
670
|
},
|
|
@@ -482,6 +714,7 @@ describe("handleSetupGet", () => {
|
|
|
482
714
|
db,
|
|
483
715
|
manifestPath: h.manifestPath,
|
|
484
716
|
configDir: h.dir,
|
|
717
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
485
718
|
issuer: "https://hub.example",
|
|
486
719
|
registry: getDefaultOperationsRegistry(),
|
|
487
720
|
},
|
|
@@ -523,6 +756,7 @@ describe("handleSetupGet", () => {
|
|
|
523
756
|
db,
|
|
524
757
|
manifestPath: h.manifestPath,
|
|
525
758
|
configDir: h.dir,
|
|
759
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
526
760
|
issuer: "https://hub.example",
|
|
527
761
|
registry: getDefaultOperationsRegistry(),
|
|
528
762
|
},
|
|
@@ -547,6 +781,7 @@ describe("handleSetupGet", () => {
|
|
|
547
781
|
db,
|
|
548
782
|
manifestPath: h.manifestPath,
|
|
549
783
|
configDir: h.dir,
|
|
784
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
550
785
|
issuer: "https://hub.example",
|
|
551
786
|
registry: reg,
|
|
552
787
|
});
|
|
@@ -576,6 +811,7 @@ describe("handleSetupGet", () => {
|
|
|
576
811
|
db,
|
|
577
812
|
manifestPath: h.manifestPath,
|
|
578
813
|
configDir: h.dir,
|
|
814
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
579
815
|
issuer: "https://hub.example",
|
|
580
816
|
registry: reg,
|
|
581
817
|
});
|
|
@@ -607,6 +843,7 @@ describe("handleSetupAccountPost", () => {
|
|
|
607
843
|
db,
|
|
608
844
|
manifestPath: h.manifestPath,
|
|
609
845
|
configDir: h.dir,
|
|
846
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
610
847
|
issuer: "https://hub.example",
|
|
611
848
|
registry: getDefaultOperationsRegistry(),
|
|
612
849
|
});
|
|
@@ -631,6 +868,7 @@ describe("handleSetupAccountPost", () => {
|
|
|
631
868
|
db,
|
|
632
869
|
manifestPath: h.manifestPath,
|
|
633
870
|
configDir: h.dir,
|
|
871
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
634
872
|
issuer: "https://hub.example",
|
|
635
873
|
registry: getDefaultOperationsRegistry(),
|
|
636
874
|
},
|
|
@@ -659,6 +897,7 @@ describe("handleSetupAccountPost", () => {
|
|
|
659
897
|
db,
|
|
660
898
|
manifestPath: h.manifestPath,
|
|
661
899
|
configDir: h.dir,
|
|
900
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
662
901
|
issuer: "https://hub.example",
|
|
663
902
|
registry: getDefaultOperationsRegistry(),
|
|
664
903
|
});
|
|
@@ -679,6 +918,7 @@ describe("handleSetupAccountPost", () => {
|
|
|
679
918
|
db,
|
|
680
919
|
manifestPath: h.manifestPath,
|
|
681
920
|
configDir: h.dir,
|
|
921
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
682
922
|
issuer: "https://hub.example",
|
|
683
923
|
registry: getDefaultOperationsRegistry(),
|
|
684
924
|
},
|
|
@@ -711,6 +951,7 @@ describe("handleSetupAccountPost", () => {
|
|
|
711
951
|
db,
|
|
712
952
|
manifestPath: h.manifestPath,
|
|
713
953
|
configDir: h.dir,
|
|
954
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
714
955
|
issuer: "https://hub.example",
|
|
715
956
|
registry: getDefaultOperationsRegistry(),
|
|
716
957
|
},
|
|
@@ -730,6 +971,7 @@ describe("handleSetupAccountPost", () => {
|
|
|
730
971
|
db,
|
|
731
972
|
manifestPath: h.manifestPath,
|
|
732
973
|
configDir: h.dir,
|
|
974
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
733
975
|
issuer: "https://hub.example",
|
|
734
976
|
registry: getDefaultOperationsRegistry(),
|
|
735
977
|
});
|
|
@@ -750,6 +992,7 @@ describe("handleSetupAccountPost", () => {
|
|
|
750
992
|
db,
|
|
751
993
|
manifestPath: h.manifestPath,
|
|
752
994
|
configDir: h.dir,
|
|
995
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
753
996
|
issuer: "https://hub.example",
|
|
754
997
|
registry: getDefaultOperationsRegistry(),
|
|
755
998
|
},
|
|
@@ -774,10 +1017,15 @@ describe("handleSetupVaultPost", () => {
|
|
|
774
1017
|
});
|
|
775
1018
|
afterEach(() => h.cleanup());
|
|
776
1019
|
|
|
777
|
-
test("requires a supervisor (CLI mode rejects)", async () => {
|
|
1020
|
+
test("requires a supervisor (CLI mode rejects create/import; allows skip — hub#168 Cut 2)", async () => {
|
|
778
1021
|
const db = openHubDb(hubDbPath(h.dir));
|
|
779
1022
|
try {
|
|
780
1023
|
await createUser(db, "owner", "pw");
|
|
1024
|
+
// Bare POST (no CSRF, no session) still 400s, but on the new
|
|
1025
|
+
// CSRF-first ordering it stops at the CSRF check rather than the
|
|
1026
|
+
// supervisor check. That's correct posture — refuse the
|
|
1027
|
+
// unauthenticated request before tendering an architectural
|
|
1028
|
+
// explanation.
|
|
781
1029
|
const post = await handleSetupVaultPost(
|
|
782
1030
|
req("/admin/setup/vault", {
|
|
783
1031
|
method: "POST",
|
|
@@ -788,13 +1036,15 @@ describe("handleSetupVaultPost", () => {
|
|
|
788
1036
|
db,
|
|
789
1037
|
manifestPath: h.manifestPath,
|
|
790
1038
|
configDir: h.dir,
|
|
1039
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
791
1040
|
issuer: "https://hub.example",
|
|
792
1041
|
registry: getDefaultOperationsRegistry(),
|
|
793
1042
|
},
|
|
794
1043
|
);
|
|
795
1044
|
expect(post.status).toBe(400);
|
|
796
1045
|
const html = await post.text();
|
|
797
|
-
|
|
1046
|
+
// CSRF-first: the bare request bounces at the CSRF gate.
|
|
1047
|
+
expect(html).toContain("Invalid form submission");
|
|
798
1048
|
} finally {
|
|
799
1049
|
db.close();
|
|
800
1050
|
}
|
|
@@ -808,6 +1058,7 @@ describe("handleSetupVaultPost", () => {
|
|
|
808
1058
|
db,
|
|
809
1059
|
manifestPath: h.manifestPath,
|
|
810
1060
|
configDir: h.dir,
|
|
1061
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
811
1062
|
issuer: "https://hub.example",
|
|
812
1063
|
registry: getDefaultOperationsRegistry(),
|
|
813
1064
|
});
|
|
@@ -827,6 +1078,7 @@ describe("handleSetupVaultPost", () => {
|
|
|
827
1078
|
db,
|
|
828
1079
|
manifestPath: h.manifestPath,
|
|
829
1080
|
configDir: h.dir,
|
|
1081
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
830
1082
|
issuer: "https://hub.example",
|
|
831
1083
|
supervisor: makeSupervisor(),
|
|
832
1084
|
registry: getDefaultOperationsRegistry(),
|
|
@@ -856,6 +1108,7 @@ describe("handleSetupVaultPost", () => {
|
|
|
856
1108
|
db,
|
|
857
1109
|
manifestPath: h.manifestPath,
|
|
858
1110
|
configDir: h.dir,
|
|
1111
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
859
1112
|
issuer: "https://hub.example",
|
|
860
1113
|
registry: getDefaultOperationsRegistry(),
|
|
861
1114
|
});
|
|
@@ -880,6 +1133,7 @@ describe("handleSetupVaultPost", () => {
|
|
|
880
1133
|
db,
|
|
881
1134
|
manifestPath: h.manifestPath,
|
|
882
1135
|
configDir: h.dir,
|
|
1136
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
883
1137
|
issuer: "https://hub.example",
|
|
884
1138
|
supervisor: makeSupervisor(),
|
|
885
1139
|
registry: getDefaultOperationsRegistry(),
|
|
@@ -933,6 +1187,7 @@ describe("handleSetupVaultPost", () => {
|
|
|
933
1187
|
db,
|
|
934
1188
|
manifestPath: h.manifestPath,
|
|
935
1189
|
configDir: h.dir,
|
|
1190
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
936
1191
|
issuer: "https://hub.example",
|
|
937
1192
|
registry: getDefaultOperationsRegistry(),
|
|
938
1193
|
});
|
|
@@ -959,6 +1214,7 @@ describe("handleSetupVaultPost", () => {
|
|
|
959
1214
|
db,
|
|
960
1215
|
manifestPath: h.manifestPath,
|
|
961
1216
|
configDir: h.dir,
|
|
1217
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
962
1218
|
issuer: "https://hub.example",
|
|
963
1219
|
supervisor: makeSupervisor(),
|
|
964
1220
|
registry: getDefaultOperationsRegistry(),
|
|
@@ -1002,6 +1258,7 @@ describe("handleSetupVaultPost", () => {
|
|
|
1002
1258
|
db,
|
|
1003
1259
|
manifestPath: h.manifestPath,
|
|
1004
1260
|
configDir: h.dir,
|
|
1261
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1005
1262
|
issuer: "https://hub.example",
|
|
1006
1263
|
registry: getDefaultOperationsRegistry(),
|
|
1007
1264
|
});
|
|
@@ -1027,6 +1284,7 @@ describe("handleSetupVaultPost", () => {
|
|
|
1027
1284
|
db,
|
|
1028
1285
|
manifestPath: h.manifestPath,
|
|
1029
1286
|
configDir: h.dir,
|
|
1287
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1030
1288
|
issuer: "https://hub.example",
|
|
1031
1289
|
supervisor: makeSupervisor(),
|
|
1032
1290
|
registry: getDefaultOperationsRegistry(),
|
|
@@ -1063,6 +1321,7 @@ describe("handleSetupVaultPost", () => {
|
|
|
1063
1321
|
db,
|
|
1064
1322
|
manifestPath: h.manifestPath,
|
|
1065
1323
|
configDir: h.dir,
|
|
1324
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1066
1325
|
issuer: "https://hub.example",
|
|
1067
1326
|
registry: getDefaultOperationsRegistry(),
|
|
1068
1327
|
});
|
|
@@ -1092,6 +1351,7 @@ describe("handleSetupVaultPost", () => {
|
|
|
1092
1351
|
db,
|
|
1093
1352
|
manifestPath: h.manifestPath,
|
|
1094
1353
|
configDir: h.dir,
|
|
1354
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1095
1355
|
issuer: "https://hub.example",
|
|
1096
1356
|
supervisor,
|
|
1097
1357
|
registry: getDefaultOperationsRegistry(),
|
|
@@ -1146,6 +1406,7 @@ describe("handleSetupVaultPost", () => {
|
|
|
1146
1406
|
db,
|
|
1147
1407
|
manifestPath: h.manifestPath,
|
|
1148
1408
|
configDir: h.dir,
|
|
1409
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1149
1410
|
issuer: "https://hub.example",
|
|
1150
1411
|
registry: getDefaultOperationsRegistry(),
|
|
1151
1412
|
});
|
|
@@ -1171,6 +1432,7 @@ describe("handleSetupVaultPost", () => {
|
|
|
1171
1432
|
db,
|
|
1172
1433
|
manifestPath: h.manifestPath,
|
|
1173
1434
|
configDir: h.dir,
|
|
1435
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1174
1436
|
issuer: "https://hub.example",
|
|
1175
1437
|
supervisor: makeSupervisor(),
|
|
1176
1438
|
registry: getDefaultOperationsRegistry(),
|
|
@@ -1425,6 +1687,7 @@ describe("handleSetupExposePost", () => {
|
|
|
1425
1687
|
db,
|
|
1426
1688
|
manifestPath: h.manifestPath,
|
|
1427
1689
|
configDir: h.dir,
|
|
1690
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1428
1691
|
issuer: "https://hub.example",
|
|
1429
1692
|
registry: getDefaultOperationsRegistry(),
|
|
1430
1693
|
});
|
|
@@ -1453,6 +1716,7 @@ describe("handleSetupExposePost", () => {
|
|
|
1453
1716
|
db,
|
|
1454
1717
|
manifestPath: h.manifestPath,
|
|
1455
1718
|
configDir: h.dir,
|
|
1719
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1456
1720
|
issuer: "https://hub.example",
|
|
1457
1721
|
registry: getDefaultOperationsRegistry(),
|
|
1458
1722
|
},
|
|
@@ -1488,6 +1752,7 @@ describe("handleSetupExposePost", () => {
|
|
|
1488
1752
|
db,
|
|
1489
1753
|
manifestPath: h.manifestPath,
|
|
1490
1754
|
configDir: h.dir,
|
|
1755
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1491
1756
|
issuer: "https://hub.example",
|
|
1492
1757
|
registry: getDefaultOperationsRegistry(),
|
|
1493
1758
|
},
|
|
@@ -1526,6 +1791,7 @@ describe("handleSetupExposePost", () => {
|
|
|
1526
1791
|
db,
|
|
1527
1792
|
manifestPath: h.manifestPath,
|
|
1528
1793
|
configDir: h.dir,
|
|
1794
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1529
1795
|
issuer: "https://hub.example",
|
|
1530
1796
|
registry: getDefaultOperationsRegistry(),
|
|
1531
1797
|
},
|
|
@@ -1560,6 +1826,7 @@ describe("handleSetupExposePost", () => {
|
|
|
1560
1826
|
db,
|
|
1561
1827
|
manifestPath: h.manifestPath,
|
|
1562
1828
|
configDir: h.dir,
|
|
1829
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1563
1830
|
issuer: "https://hub.example",
|
|
1564
1831
|
registry: getDefaultOperationsRegistry(),
|
|
1565
1832
|
},
|
|
@@ -1596,6 +1863,7 @@ describe("handleSetupExposePost", () => {
|
|
|
1596
1863
|
db,
|
|
1597
1864
|
manifestPath: h.manifestPath,
|
|
1598
1865
|
configDir: h.dir,
|
|
1866
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1599
1867
|
issuer: "https://hub.example",
|
|
1600
1868
|
registry: getDefaultOperationsRegistry(),
|
|
1601
1869
|
},
|
|
@@ -1646,6 +1914,7 @@ describe("done screen auto-minted token (hub#272 Item A)", () => {
|
|
|
1646
1914
|
db,
|
|
1647
1915
|
manifestPath: h.manifestPath,
|
|
1648
1916
|
configDir: h.dir,
|
|
1917
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1649
1918
|
issuer: "https://hub.example",
|
|
1650
1919
|
registry: getDefaultOperationsRegistry(),
|
|
1651
1920
|
});
|
|
@@ -1674,6 +1943,7 @@ describe("done screen auto-minted token (hub#272 Item A)", () => {
|
|
|
1674
1943
|
db,
|
|
1675
1944
|
manifestPath: h.manifestPath,
|
|
1676
1945
|
configDir: h.dir,
|
|
1946
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1677
1947
|
issuer: "https://hub.example",
|
|
1678
1948
|
registry: getDefaultOperationsRegistry(),
|
|
1679
1949
|
},
|
|
@@ -1720,6 +1990,7 @@ describe("done screen auto-minted token (hub#272 Item A)", () => {
|
|
|
1720
1990
|
db,
|
|
1721
1991
|
manifestPath: h.manifestPath,
|
|
1722
1992
|
configDir: h.dir,
|
|
1993
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1723
1994
|
issuer: "https://hub.example",
|
|
1724
1995
|
registry: getDefaultOperationsRegistry(),
|
|
1725
1996
|
},
|
|
@@ -1785,16 +2056,21 @@ describe("done screen auto-minted token (hub#272 Item A)", () => {
|
|
|
1785
2056
|
db,
|
|
1786
2057
|
manifestPath: h.manifestPath,
|
|
1787
2058
|
configDir: h.dir,
|
|
2059
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1788
2060
|
issuer: "https://hub.example",
|
|
1789
2061
|
registry: getDefaultOperationsRegistry(),
|
|
1790
2062
|
},
|
|
1791
2063
|
);
|
|
1792
2064
|
const html = await res.text();
|
|
1793
2065
|
expect(html).toContain("claude mcp add --transport http parachute-default");
|
|
1794
|
-
// The fallback explanatory text
|
|
1795
|
-
//
|
|
1796
|
-
//
|
|
1797
|
-
|
|
2066
|
+
// The fallback explanatory text leads with the OAuth path (no token
|
|
2067
|
+
// needed) and, for headless clients, references a hub JWT placeholder
|
|
2068
|
+
// — NOT the retired `pvt_*` format (gap #4). The `--header` flag must
|
|
2069
|
+
// also NOT be appended to the command line itself.
|
|
2070
|
+
expect(html).toContain("browser OAuth");
|
|
2071
|
+
expect(html).toContain("Bearer <token>");
|
|
2072
|
+
expect(html).not.toContain("pvt_");
|
|
2073
|
+
expect(html).toContain("parachute auth mint-token");
|
|
1798
2074
|
expect(html).toContain("/admin/tokens");
|
|
1799
2075
|
// Specifically no Copy button — that's a token-present surface.
|
|
1800
2076
|
expect(html).not.toContain('id="mcp-cmd"');
|
|
@@ -1829,6 +2105,7 @@ describe("done screen auto-minted token (hub#272 Item A)", () => {
|
|
|
1829
2105
|
db,
|
|
1830
2106
|
manifestPath: h.manifestPath,
|
|
1831
2107
|
configDir: h.dir,
|
|
2108
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1832
2109
|
issuer: "https://hub.example",
|
|
1833
2110
|
registry: getDefaultOperationsRegistry(),
|
|
1834
2111
|
};
|
|
@@ -1885,6 +2162,7 @@ describe("done screen auto-minted token (hub#272 Item A)", () => {
|
|
|
1885
2162
|
db,
|
|
1886
2163
|
manifestPath: h.manifestPath,
|
|
1887
2164
|
configDir: h.dir,
|
|
2165
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1888
2166
|
issuer: "https://hub.example",
|
|
1889
2167
|
registry: getDefaultOperationsRegistry(),
|
|
1890
2168
|
},
|
|
@@ -1950,6 +2228,7 @@ describe("done screen auto-minted token (hub#272 Item A)", () => {
|
|
|
1950
2228
|
db,
|
|
1951
2229
|
manifestPath: h.manifestPath,
|
|
1952
2230
|
configDir: h.dir,
|
|
2231
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1953
2232
|
issuer: "https://hub.example",
|
|
1954
2233
|
registry: getDefaultOperationsRegistry(),
|
|
1955
2234
|
},
|
|
@@ -2008,6 +2287,7 @@ describe("done screen auto-minted token (hub#272 Item A)", () => {
|
|
|
2008
2287
|
db,
|
|
2009
2288
|
manifestPath: h.manifestPath,
|
|
2010
2289
|
configDir: h.dir,
|
|
2290
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2011
2291
|
issuer: "https://hub.example",
|
|
2012
2292
|
registry: getDefaultOperationsRegistry(),
|
|
2013
2293
|
},
|
|
@@ -2069,6 +2349,7 @@ describe("done screen auto-minted token (hub#272 Item A)", () => {
|
|
|
2069
2349
|
db,
|
|
2070
2350
|
manifestPath: h.manifestPath,
|
|
2071
2351
|
configDir: h.dir,
|
|
2352
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2072
2353
|
issuer: "https://hub.example",
|
|
2073
2354
|
registry: getDefaultOperationsRegistry(),
|
|
2074
2355
|
});
|
|
@@ -2132,6 +2413,7 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2132
2413
|
db,
|
|
2133
2414
|
manifestPath: h.manifestPath,
|
|
2134
2415
|
configDir: h.dir,
|
|
2416
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2135
2417
|
issuer: "https://hub.example",
|
|
2136
2418
|
registry: getDefaultOperationsRegistry(),
|
|
2137
2419
|
},
|
|
@@ -2162,6 +2444,9 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2162
2444
|
const db = openHubDb(hubDbPath(h.dir));
|
|
2163
2445
|
try {
|
|
2164
2446
|
const user = await createUser(db, "owner", "pw");
|
|
2447
|
+
// Seed services.json with `parachute-scribe` so the wizard's scribe
|
|
2448
|
+
// install tile renders the already-installed shape. Post-2026-05-27
|
|
2449
|
+
// CURATED trim scribe is the only non-vault install tile.
|
|
2165
2450
|
writeManifest(
|
|
2166
2451
|
{
|
|
2167
2452
|
services: [
|
|
@@ -2172,15 +2457,12 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2172
2457
|
paths: ["/vault/default"],
|
|
2173
2458
|
health: "/health",
|
|
2174
2459
|
},
|
|
2175
|
-
// hub#323: app replaces notes as the wizard's first install tile.
|
|
2176
|
-
// Seeding services.json with `parachute-app` exercises the
|
|
2177
|
-
// already-installed render path on the wizard's first tile.
|
|
2178
2460
|
{
|
|
2179
|
-
name: "parachute-
|
|
2180
|
-
version: "0.
|
|
2181
|
-
port:
|
|
2182
|
-
paths: ["/
|
|
2183
|
-
health: "/
|
|
2461
|
+
name: "parachute-scribe",
|
|
2462
|
+
version: "0.4.4",
|
|
2463
|
+
port: 1943,
|
|
2464
|
+
paths: ["/scribe"],
|
|
2465
|
+
health: "/scribe/health",
|
|
2184
2466
|
},
|
|
2185
2467
|
],
|
|
2186
2468
|
},
|
|
@@ -2197,19 +2479,23 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2197
2479
|
db,
|
|
2198
2480
|
manifestPath: h.manifestPath,
|
|
2199
2481
|
configDir: h.dir,
|
|
2482
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2200
2483
|
issuer: "https://hub.example",
|
|
2201
2484
|
registry: getDefaultOperationsRegistry(),
|
|
2202
2485
|
},
|
|
2203
2486
|
);
|
|
2204
2487
|
const html = await res.text();
|
|
2205
2488
|
expect(html).toContain("Already installed");
|
|
2206
|
-
|
|
2489
|
+
// The scribe tile rendered the installed shape, not the install form.
|
|
2490
|
+
expect(html).not.toContain('action="/admin/setup/install/scribe"');
|
|
2491
|
+
// "Manage in admin" is the secondary link on the already-installed tile.
|
|
2492
|
+
expect(html).toContain("Manage in admin");
|
|
2207
2493
|
} finally {
|
|
2208
2494
|
db.close();
|
|
2209
2495
|
}
|
|
2210
2496
|
});
|
|
2211
2497
|
|
|
2212
|
-
test("done screen renders op-poll panel when ?
|
|
2498
|
+
test("done screen renders op-poll panel when ?op_scribe=<id> matches a registry op", async () => {
|
|
2213
2499
|
const db = openHubDb(hubDbPath(h.dir));
|
|
2214
2500
|
try {
|
|
2215
2501
|
const user = await createUser(db, "owner", "pw");
|
|
@@ -2229,21 +2515,23 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2229
2515
|
);
|
|
2230
2516
|
setSetting(db, "setup_expose_mode", "localhost");
|
|
2231
2517
|
const reg = getDefaultOperationsRegistry();
|
|
2232
|
-
//
|
|
2233
|
-
//
|
|
2234
|
-
//
|
|
2235
|
-
|
|
2236
|
-
reg.
|
|
2518
|
+
// Post-2026-05-27 CURATED trim, scribe is the only non-vault wizard
|
|
2519
|
+
// install tile, so it carries the op-poll panel. Same shape as the
|
|
2520
|
+
// prior `op_app=<id>` / `op_notes=<id>` flows — the rendering code
|
|
2521
|
+
// is per-`?op_<short>=<id>` query and tile-row agnostic.
|
|
2522
|
+
const op = reg.create("install", "scribe");
|
|
2523
|
+
reg.update(op.id, { status: "running" }, "running bun add -g @openparachute/scribe@latest");
|
|
2237
2524
|
const { createSession } = await import("../sessions.ts");
|
|
2238
2525
|
const session = createSession(db, { userId: user.id });
|
|
2239
2526
|
const res = handleSetupGet(
|
|
2240
|
-
req(`/admin/setup?just_finished=1&
|
|
2527
|
+
req(`/admin/setup?just_finished=1&op_scribe=${op.id}`, {
|
|
2241
2528
|
headers: { cookie: `${SESSION_COOKIE_NAME}=${session.id}` },
|
|
2242
2529
|
}),
|
|
2243
2530
|
{
|
|
2244
2531
|
db,
|
|
2245
2532
|
manifestPath: h.manifestPath,
|
|
2246
2533
|
configDir: h.dir,
|
|
2534
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2247
2535
|
issuer: "https://hub.example",
|
|
2248
2536
|
registry: reg,
|
|
2249
2537
|
},
|
|
@@ -2283,6 +2571,7 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2283
2571
|
db,
|
|
2284
2572
|
manifestPath: h.manifestPath,
|
|
2285
2573
|
configDir: h.dir,
|
|
2574
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2286
2575
|
issuer: "https://hub.example",
|
|
2287
2576
|
registry: getDefaultOperationsRegistry(),
|
|
2288
2577
|
});
|
|
@@ -2293,7 +2582,7 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2293
2582
|
return 0;
|
|
2294
2583
|
};
|
|
2295
2584
|
const post = await handleSetupInstallPost(
|
|
2296
|
-
req("/admin/setup/install/
|
|
2585
|
+
req("/admin/setup/install/scribe", {
|
|
2297
2586
|
method: "POST",
|
|
2298
2587
|
body: new URLSearchParams({ [CSRF_FIELD_NAME]: csrf }).toString(),
|
|
2299
2588
|
headers: {
|
|
@@ -2301,11 +2590,12 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2301
2590
|
cookie: `${CSRF_COOKIE_NAME}=${csrf}; ${SESSION_COOKIE_NAME}=${session.id}`,
|
|
2302
2591
|
},
|
|
2303
2592
|
}),
|
|
2304
|
-
"
|
|
2593
|
+
"scribe",
|
|
2305
2594
|
{
|
|
2306
2595
|
db,
|
|
2307
2596
|
manifestPath: h.manifestPath,
|
|
2308
2597
|
configDir: h.dir,
|
|
2598
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2309
2599
|
issuer: "https://hub.example",
|
|
2310
2600
|
supervisor: makeSupervisor(),
|
|
2311
2601
|
registry: getDefaultOperationsRegistry(),
|
|
@@ -2315,10 +2605,10 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2315
2605
|
);
|
|
2316
2606
|
expect(post.status).toBe(303);
|
|
2317
2607
|
const location = post.headers.get("location") ?? "";
|
|
2318
|
-
expect(location).toMatch(/^\/admin\/setup\?just_finished=1&
|
|
2608
|
+
expect(location).toMatch(/^\/admin\/setup\?just_finished=1&op_scribe=/);
|
|
2319
2609
|
await new Promise((r) => setTimeout(r, 50));
|
|
2320
2610
|
expect(runCalls.length).toBeGreaterThan(0);
|
|
2321
|
-
expect(runCalls[0]?.join(" ")).toContain("bun add -g @openparachute/
|
|
2611
|
+
expect(runCalls[0]?.join(" ")).toContain("bun add -g @openparachute/scribe@latest");
|
|
2322
2612
|
} finally {
|
|
2323
2613
|
db.close();
|
|
2324
2614
|
}
|
|
@@ -2334,6 +2624,7 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2334
2624
|
db,
|
|
2335
2625
|
manifestPath: h.manifestPath,
|
|
2336
2626
|
configDir: h.dir,
|
|
2627
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2337
2628
|
issuer: "https://hub.example",
|
|
2338
2629
|
registry: getDefaultOperationsRegistry(),
|
|
2339
2630
|
});
|
|
@@ -2352,6 +2643,7 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2352
2643
|
db,
|
|
2353
2644
|
manifestPath: h.manifestPath,
|
|
2354
2645
|
configDir: h.dir,
|
|
2646
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2355
2647
|
issuer: "https://hub.example",
|
|
2356
2648
|
supervisor: makeSupervisor(),
|
|
2357
2649
|
registry: getDefaultOperationsRegistry(),
|
|
@@ -2379,6 +2671,7 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2379
2671
|
db,
|
|
2380
2672
|
manifestPath: h.manifestPath,
|
|
2381
2673
|
configDir: h.dir,
|
|
2674
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2382
2675
|
issuer: "https://hub.example",
|
|
2383
2676
|
supervisor: makeSupervisor(),
|
|
2384
2677
|
registry: getDefaultOperationsRegistry(),
|
|
@@ -2400,12 +2693,13 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2400
2693
|
db,
|
|
2401
2694
|
manifestPath: h.manifestPath,
|
|
2402
2695
|
configDir: h.dir,
|
|
2696
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2403
2697
|
issuer: "https://hub.example",
|
|
2404
2698
|
registry: getDefaultOperationsRegistry(),
|
|
2405
2699
|
});
|
|
2406
2700
|
const csrf = setCookie(get, CSRF_COOKIE_NAME) ?? "";
|
|
2407
2701
|
const post = await handleSetupInstallPost(
|
|
2408
|
-
req("/admin/setup/install/
|
|
2702
|
+
req("/admin/setup/install/scribe", {
|
|
2409
2703
|
method: "POST",
|
|
2410
2704
|
body: new URLSearchParams({ [CSRF_FIELD_NAME]: csrf }).toString(),
|
|
2411
2705
|
headers: {
|
|
@@ -2413,11 +2707,12 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2413
2707
|
cookie: `${CSRF_COOKIE_NAME}=${csrf}`,
|
|
2414
2708
|
},
|
|
2415
2709
|
}),
|
|
2416
|
-
"
|
|
2710
|
+
"scribe",
|
|
2417
2711
|
{
|
|
2418
2712
|
db,
|
|
2419
2713
|
manifestPath: h.manifestPath,
|
|
2420
2714
|
configDir: h.dir,
|
|
2715
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2421
2716
|
issuer: "https://hub.example",
|
|
2422
2717
|
supervisor: makeSupervisor(),
|
|
2423
2718
|
registry: getDefaultOperationsRegistry(),
|
|
@@ -2446,6 +2741,7 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2446
2741
|
db,
|
|
2447
2742
|
manifestPath: h.manifestPath,
|
|
2448
2743
|
configDir: h.dir,
|
|
2744
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2449
2745
|
issuer: "https://hub.example",
|
|
2450
2746
|
registry: getDefaultOperationsRegistry(),
|
|
2451
2747
|
},
|
|
@@ -2479,6 +2775,7 @@ describe("typed vault name (hub#267)", () => {
|
|
|
2479
2775
|
db,
|
|
2480
2776
|
manifestPath: h.manifestPath,
|
|
2481
2777
|
configDir: h.dir,
|
|
2778
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2482
2779
|
issuer: "https://hub.example",
|
|
2483
2780
|
registry: getDefaultOperationsRegistry(),
|
|
2484
2781
|
});
|
|
@@ -2521,6 +2818,7 @@ describe("typed vault name (hub#267)", () => {
|
|
|
2521
2818
|
db,
|
|
2522
2819
|
manifestPath: h.manifestPath,
|
|
2523
2820
|
configDir: h.dir,
|
|
2821
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2524
2822
|
issuer: "https://hub.example",
|
|
2525
2823
|
supervisor,
|
|
2526
2824
|
registry: getDefaultOperationsRegistry(),
|
|
@@ -2554,6 +2852,7 @@ describe("typed vault name (hub#267)", () => {
|
|
|
2554
2852
|
db,
|
|
2555
2853
|
manifestPath: h.manifestPath,
|
|
2556
2854
|
configDir: h.dir,
|
|
2855
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2557
2856
|
issuer: "https://hub.example",
|
|
2558
2857
|
registry: getDefaultOperationsRegistry(),
|
|
2559
2858
|
});
|
|
@@ -2574,6 +2873,7 @@ describe("typed vault name (hub#267)", () => {
|
|
|
2574
2873
|
db,
|
|
2575
2874
|
manifestPath: h.manifestPath,
|
|
2576
2875
|
configDir: h.dir,
|
|
2876
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2577
2877
|
issuer: "https://hub.example",
|
|
2578
2878
|
supervisor: makeSupervisor(),
|
|
2579
2879
|
registry: getDefaultOperationsRegistry(),
|
|
@@ -2599,6 +2899,7 @@ describe("typed vault name (hub#267)", () => {
|
|
|
2599
2899
|
db,
|
|
2600
2900
|
manifestPath: h.manifestPath,
|
|
2601
2901
|
configDir: h.dir,
|
|
2902
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2602
2903
|
issuer: "https://hub.example",
|
|
2603
2904
|
registry: getDefaultOperationsRegistry(),
|
|
2604
2905
|
});
|
|
@@ -2639,6 +2940,7 @@ describe("typed vault name (hub#267)", () => {
|
|
|
2639
2940
|
db,
|
|
2640
2941
|
manifestPath: h.manifestPath,
|
|
2641
2942
|
configDir: h.dir,
|
|
2943
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2642
2944
|
issuer: "https://hub.example",
|
|
2643
2945
|
supervisor,
|
|
2644
2946
|
registry: getDefaultOperationsRegistry(),
|
|
@@ -2701,6 +3003,7 @@ describe("typed vault name (hub#267)", () => {
|
|
|
2701
3003
|
db,
|
|
2702
3004
|
manifestPath: h.manifestPath,
|
|
2703
3005
|
configDir: h.dir,
|
|
3006
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2704
3007
|
issuer: "https://hub.example",
|
|
2705
3008
|
registry: getDefaultOperationsRegistry(),
|
|
2706
3009
|
},
|
|
@@ -2752,6 +3055,7 @@ describe("typed vault name (hub#267)", () => {
|
|
|
2752
3055
|
db,
|
|
2753
3056
|
manifestPath: h.manifestPath,
|
|
2754
3057
|
configDir: h.dir,
|
|
3058
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2755
3059
|
issuer: "https://hub.example",
|
|
2756
3060
|
registry: getDefaultOperationsRegistry(),
|
|
2757
3061
|
},
|
|
@@ -2803,6 +3107,7 @@ describe("typed vault name (hub#267)", () => {
|
|
|
2803
3107
|
db,
|
|
2804
3108
|
manifestPath: h.manifestPath,
|
|
2805
3109
|
configDir: h.dir,
|
|
3110
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2806
3111
|
issuer: "https://hub.example",
|
|
2807
3112
|
registry: getDefaultOperationsRegistry(),
|
|
2808
3113
|
},
|
|
@@ -2873,6 +3178,7 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
2873
3178
|
db,
|
|
2874
3179
|
manifestPath: h.manifestPath,
|
|
2875
3180
|
configDir: h.dir,
|
|
3181
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2876
3182
|
issuer: "https://hub.example",
|
|
2877
3183
|
registry: getDefaultOperationsRegistry(),
|
|
2878
3184
|
});
|
|
@@ -2897,6 +3203,7 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
2897
3203
|
db,
|
|
2898
3204
|
manifestPath: h.manifestPath,
|
|
2899
3205
|
configDir: h.dir,
|
|
3206
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2900
3207
|
issuer: "https://hub.example",
|
|
2901
3208
|
registry: getDefaultOperationsRegistry(),
|
|
2902
3209
|
});
|
|
@@ -2922,6 +3229,7 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
2922
3229
|
db,
|
|
2923
3230
|
manifestPath: h.manifestPath,
|
|
2924
3231
|
configDir: h.dir,
|
|
3232
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2925
3233
|
issuer: "https://hub.example",
|
|
2926
3234
|
registry: getDefaultOperationsRegistry(),
|
|
2927
3235
|
});
|
|
@@ -2943,6 +3251,7 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
2943
3251
|
db,
|
|
2944
3252
|
manifestPath: h.manifestPath,
|
|
2945
3253
|
configDir: h.dir,
|
|
3254
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2946
3255
|
issuer: "https://hub.example",
|
|
2947
3256
|
registry: getDefaultOperationsRegistry(),
|
|
2948
3257
|
},
|
|
@@ -2965,6 +3274,7 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
2965
3274
|
db,
|
|
2966
3275
|
manifestPath: h.manifestPath,
|
|
2967
3276
|
configDir: h.dir,
|
|
3277
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2968
3278
|
issuer: "https://hub.example",
|
|
2969
3279
|
registry: getDefaultOperationsRegistry(),
|
|
2970
3280
|
});
|
|
@@ -2986,6 +3296,7 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
2986
3296
|
db,
|
|
2987
3297
|
manifestPath: h.manifestPath,
|
|
2988
3298
|
configDir: h.dir,
|
|
3299
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
2989
3300
|
issuer: "https://hub.example",
|
|
2990
3301
|
registry: getDefaultOperationsRegistry(),
|
|
2991
3302
|
},
|
|
@@ -3015,6 +3326,7 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
3015
3326
|
db,
|
|
3016
3327
|
manifestPath: h.manifestPath,
|
|
3017
3328
|
configDir: h.dir,
|
|
3329
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3018
3330
|
issuer: "https://hub.example",
|
|
3019
3331
|
registry: getDefaultOperationsRegistry(),
|
|
3020
3332
|
});
|
|
@@ -3036,6 +3348,7 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
3036
3348
|
db,
|
|
3037
3349
|
manifestPath: h.manifestPath,
|
|
3038
3350
|
configDir: h.dir,
|
|
3351
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3039
3352
|
issuer: "https://hub.example",
|
|
3040
3353
|
registry: getDefaultOperationsRegistry(),
|
|
3041
3354
|
},
|
|
@@ -3063,6 +3376,7 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
3063
3376
|
db,
|
|
3064
3377
|
manifestPath: h.manifestPath,
|
|
3065
3378
|
configDir: h.dir,
|
|
3379
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3066
3380
|
issuer: "https://hub.example",
|
|
3067
3381
|
registry: getDefaultOperationsRegistry(),
|
|
3068
3382
|
});
|
|
@@ -3084,6 +3398,7 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
3084
3398
|
db,
|
|
3085
3399
|
manifestPath: h.manifestPath,
|
|
3086
3400
|
configDir: h.dir,
|
|
3401
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3087
3402
|
issuer: "https://hub.example",
|
|
3088
3403
|
registry: getDefaultOperationsRegistry(),
|
|
3089
3404
|
},
|
|
@@ -3109,6 +3424,7 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
3109
3424
|
db,
|
|
3110
3425
|
manifestPath: h.manifestPath,
|
|
3111
3426
|
configDir: h.dir,
|
|
3427
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3112
3428
|
issuer: "https://hub.example",
|
|
3113
3429
|
registry: getDefaultOperationsRegistry(),
|
|
3114
3430
|
});
|
|
@@ -3129,6 +3445,7 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
3129
3445
|
db,
|
|
3130
3446
|
manifestPath: h.manifestPath,
|
|
3131
3447
|
configDir: h.dir,
|
|
3448
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3132
3449
|
issuer: "https://hub.example",
|
|
3133
3450
|
registry: getDefaultOperationsRegistry(),
|
|
3134
3451
|
},
|
|
@@ -3175,6 +3492,7 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
3175
3492
|
db,
|
|
3176
3493
|
manifestPath: h.manifestPath,
|
|
3177
3494
|
configDir: h.dir,
|
|
3495
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3178
3496
|
issuer: "https://hub.example",
|
|
3179
3497
|
registry: getDefaultOperationsRegistry(),
|
|
3180
3498
|
});
|
|
@@ -3196,6 +3514,7 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
3196
3514
|
db,
|
|
3197
3515
|
manifestPath: h.manifestPath,
|
|
3198
3516
|
configDir: h.dir,
|
|
3517
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3199
3518
|
issuer: "https://hub.example",
|
|
3200
3519
|
registry: getDefaultOperationsRegistry(),
|
|
3201
3520
|
};
|
|
@@ -3279,6 +3598,7 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3279
3598
|
db,
|
|
3280
3599
|
manifestPath: h.manifestPath,
|
|
3281
3600
|
configDir: h.dir,
|
|
3601
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3282
3602
|
issuer: "https://hub.example",
|
|
3283
3603
|
registry: getDefaultOperationsRegistry(),
|
|
3284
3604
|
},
|
|
@@ -3296,7 +3616,14 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3296
3616
|
}
|
|
3297
3617
|
});
|
|
3298
3618
|
|
|
3299
|
-
test("
|
|
3619
|
+
test("lead tile always points at notes.parachute.computer (canonical hosted PWA) regardless of local module installs", async () => {
|
|
3620
|
+
// Pre-2026-05-27 the lead tile flipped to `/surface/notes/` when the
|
|
3621
|
+
// Surface module was installed locally. Aaron's launch-focus
|
|
3622
|
+
// directive: notes.parachute.computer is the canonical user-facing
|
|
3623
|
+
// UI, and the wizard should always point operators at it (rather
|
|
3624
|
+
// than maybe-or-maybe-not-installed local Surface). This test pins
|
|
3625
|
+
// that the lead tile is invariant under the install state of
|
|
3626
|
+
// uncurated modules.
|
|
3300
3627
|
const db = openHubDb(hubDbPath(h.dir));
|
|
3301
3628
|
try {
|
|
3302
3629
|
const user = await createUser(db, "owner", "pw");
|
|
@@ -3310,6 +3637,9 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3310
3637
|
paths: ["/vault/default"],
|
|
3311
3638
|
health: "/health",
|
|
3312
3639
|
},
|
|
3640
|
+
// Even with parachute-surface installed locally (an uncurated
|
|
3641
|
+
// module post-trim), the lead tile must NOT flip to a local
|
|
3642
|
+
// path.
|
|
3313
3643
|
{
|
|
3314
3644
|
name: "parachute-surface",
|
|
3315
3645
|
version: "0.2.0",
|
|
@@ -3332,21 +3662,34 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3332
3662
|
db,
|
|
3333
3663
|
manifestPath: h.manifestPath,
|
|
3334
3664
|
configDir: h.dir,
|
|
3665
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3335
3666
|
issuer: "https://hub.example",
|
|
3336
3667
|
registry: getDefaultOperationsRegistry(),
|
|
3337
3668
|
},
|
|
3338
3669
|
);
|
|
3339
3670
|
const html = await res.text();
|
|
3340
3671
|
expect(html).toContain("Start using your vault");
|
|
3341
|
-
//
|
|
3342
|
-
expect(html).toContain(
|
|
3672
|
+
// Lead CTA always targets the hosted PWA.
|
|
3673
|
+
expect(html).toContain("https://notes.parachute.computer/add?url=");
|
|
3343
3674
|
expect(html).toContain("Open Notes");
|
|
3675
|
+
// The pre-trim local-surface fallback is gone — the lead tile does
|
|
3676
|
+
// NOT link to /surface/notes/ anymore.
|
|
3677
|
+
expect(html).not.toContain('href="/surface/notes/"');
|
|
3344
3678
|
} finally {
|
|
3345
3679
|
db.close();
|
|
3346
3680
|
}
|
|
3347
3681
|
});
|
|
3348
3682
|
|
|
3349
|
-
test("succeeded install op renders
|
|
3683
|
+
test("succeeded install op renders 'Manage modules' link (no 'Use it now' for modules without a hosted surface)", async () => {
|
|
3684
|
+
// Pre-2026-05-27 the surface module had a USE_IT_NOW_URLS entry
|
|
3685
|
+
// pointing at `/surface/notes/`, so a succeeded surface install tile
|
|
3686
|
+
// rendered a primary "Use it now" link. Post-trim only scribe + vault
|
|
3687
|
+
// are curated; vault has its own lead tile (above the install row);
|
|
3688
|
+
// scribe doesn't ship a user-facing landing surface today
|
|
3689
|
+
// (scribe#53 tracks the eventual admin SPA), so USE_IT_NOW_URLS is
|
|
3690
|
+
// empty and a succeeded scribe install renders only the "Manage
|
|
3691
|
+
// modules" secondary affordance. Future per-module surfaces can
|
|
3692
|
+
// re-add an entry to that map.
|
|
3350
3693
|
const db = openHubDb(hubDbPath(h.dir));
|
|
3351
3694
|
try {
|
|
3352
3695
|
const user = await createUser(db, "owner", "pw");
|
|
@@ -3366,35 +3709,43 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3366
3709
|
);
|
|
3367
3710
|
setSetting(db, "setup_expose_mode", "localhost");
|
|
3368
3711
|
const reg = getDefaultOperationsRegistry();
|
|
3369
|
-
const op = reg.create("install", "
|
|
3370
|
-
reg.update(op.id, { status: "succeeded" }, "installed @openparachute/
|
|
3712
|
+
const op = reg.create("install", "scribe");
|
|
3713
|
+
reg.update(op.id, { status: "succeeded" }, "installed @openparachute/scribe");
|
|
3371
3714
|
const { createSession } = await import("../sessions.ts");
|
|
3372
3715
|
const session = createSession(db, { userId: user.id });
|
|
3373
3716
|
const res = handleSetupGet(
|
|
3374
|
-
req(`/admin/setup?just_finished=1&
|
|
3717
|
+
req(`/admin/setup?just_finished=1&op_scribe=${op.id}`, {
|
|
3375
3718
|
headers: { cookie: `${SESSION_COOKIE_NAME}=${session.id}` },
|
|
3376
3719
|
}),
|
|
3377
3720
|
{
|
|
3378
3721
|
db,
|
|
3379
3722
|
manifestPath: h.manifestPath,
|
|
3380
3723
|
configDir: h.dir,
|
|
3724
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3381
3725
|
issuer: "https://hub.example",
|
|
3382
3726
|
registry: reg,
|
|
3383
3727
|
},
|
|
3384
3728
|
);
|
|
3385
3729
|
const html = await res.text();
|
|
3386
3730
|
expect(html).toContain("status: succeeded");
|
|
3387
|
-
//
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3731
|
+
// No "Use it now" — scribe has no entry in USE_IT_NOW_URLS today.
|
|
3732
|
+
expect(html).not.toContain(">Use it now<");
|
|
3733
|
+
// "Manage modules" secondary link is always present on a terminal-
|
|
3734
|
+
// succeeded install tile.
|
|
3391
3735
|
expect(html).toContain(">Manage modules<");
|
|
3392
3736
|
} finally {
|
|
3393
3737
|
db.close();
|
|
3394
3738
|
}
|
|
3395
3739
|
});
|
|
3396
3740
|
|
|
3397
|
-
test("'Already installed' tile
|
|
3741
|
+
test("'Already installed' tile renders without a 'Use it now' link when the module has no hosted surface", async () => {
|
|
3742
|
+
// Post-2026-05-27 CURATED trim, USE_IT_NOW_URLS is empty (scribe has
|
|
3743
|
+
// no first-class user-facing landing surface yet; vault gets its
|
|
3744
|
+
// own lead tile, not an install tile). The already-installed tile
|
|
3745
|
+
// therefore renders only the "Manage in admin" secondary link. Pre-
|
|
3746
|
+
// trim the surface module had a USE_IT_NOW_URLS entry that drove
|
|
3747
|
+
// this surface, so the test now pins the absence rather than the
|
|
3748
|
+
// presence.
|
|
3398
3749
|
const db = openHubDb(hubDbPath(h.dir));
|
|
3399
3750
|
try {
|
|
3400
3751
|
const user = await createUser(db, "owner", "pw");
|
|
@@ -3409,11 +3760,11 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3409
3760
|
health: "/health",
|
|
3410
3761
|
},
|
|
3411
3762
|
{
|
|
3412
|
-
name: "parachute-
|
|
3413
|
-
version: "0.
|
|
3414
|
-
port:
|
|
3415
|
-
paths: ["/
|
|
3416
|
-
health: "/
|
|
3763
|
+
name: "parachute-scribe",
|
|
3764
|
+
version: "0.4.4",
|
|
3765
|
+
port: 1943,
|
|
3766
|
+
paths: ["/scribe"],
|
|
3767
|
+
health: "/scribe/health",
|
|
3417
3768
|
},
|
|
3418
3769
|
],
|
|
3419
3770
|
},
|
|
@@ -3430,14 +3781,17 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3430
3781
|
db,
|
|
3431
3782
|
manifestPath: h.manifestPath,
|
|
3432
3783
|
configDir: h.dir,
|
|
3784
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3433
3785
|
issuer: "https://hub.example",
|
|
3434
3786
|
registry: getDefaultOperationsRegistry(),
|
|
3435
3787
|
},
|
|
3436
3788
|
);
|
|
3437
3789
|
const html = await res.text();
|
|
3438
3790
|
expect(html).toContain("Already installed");
|
|
3439
|
-
//
|
|
3440
|
-
expect(html).toContain(
|
|
3791
|
+
// No "Use it now" on the scribe already-installed tile.
|
|
3792
|
+
expect(html).not.toContain(">Use it now<");
|
|
3793
|
+
// Secondary affordance still present.
|
|
3794
|
+
expect(html).toContain("Manage in admin");
|
|
3441
3795
|
} finally {
|
|
3442
3796
|
db.close();
|
|
3443
3797
|
}
|
|
@@ -3455,6 +3809,7 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3455
3809
|
db,
|
|
3456
3810
|
manifestPath: h.manifestPath,
|
|
3457
3811
|
configDir: h.dir,
|
|
3812
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3458
3813
|
issuer: "https://hub.example",
|
|
3459
3814
|
registry: getDefaultOperationsRegistry(),
|
|
3460
3815
|
});
|
|
@@ -3529,3 +3884,228 @@ describe("detectAutoExposeMode — Fly env detection (patterns#100)", () => {
|
|
|
3529
3884
|
).toBe("public");
|
|
3530
3885
|
});
|
|
3531
3886
|
});
|
|
3887
|
+
|
|
3888
|
+
// hub#168 Cut 2/3: vault-step three branches (create/import/skip) + JSON
|
|
3889
|
+
// content-type acceptance. The handleSetupVaultPost handler is shared
|
|
3890
|
+
// between browser and CLI surfaces — branching is by mode field +
|
|
3891
|
+
// content-type. These tests drive the JSON surface directly to keep the
|
|
3892
|
+
// behavior locked.
|
|
3893
|
+
|
|
3894
|
+
describe("setup-wizard JSON surface (hub#168 Cuts 2/3)", () => {
|
|
3895
|
+
let h: Harness;
|
|
3896
|
+
beforeEach(() => {
|
|
3897
|
+
h = makeHarness();
|
|
3898
|
+
_resetOperationsRegistryForTests();
|
|
3899
|
+
});
|
|
3900
|
+
afterEach(() => h.cleanup());
|
|
3901
|
+
|
|
3902
|
+
test("GET /admin/setup returns JSON envelope when Accept: application/json", () => {
|
|
3903
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
3904
|
+
try {
|
|
3905
|
+
const deps = {
|
|
3906
|
+
db,
|
|
3907
|
+
manifestPath: h.manifestPath,
|
|
3908
|
+
configDir: h.dir,
|
|
3909
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3910
|
+
issuer: "http://127.0.0.1:1939",
|
|
3911
|
+
registry: getDefaultOperationsRegistry(),
|
|
3912
|
+
};
|
|
3913
|
+
const res = handleSetupGet(
|
|
3914
|
+
req("/admin/setup", { headers: { accept: "application/json" } }),
|
|
3915
|
+
deps,
|
|
3916
|
+
);
|
|
3917
|
+
expect(res.status).toBe(200);
|
|
3918
|
+
expect(res.headers.get("content-type")).toContain("application/json");
|
|
3919
|
+
} finally {
|
|
3920
|
+
db.close();
|
|
3921
|
+
}
|
|
3922
|
+
});
|
|
3923
|
+
|
|
3924
|
+
test("vault step skip mode short-circuits + persists setup_vault_skipped", async () => {
|
|
3925
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
3926
|
+
try {
|
|
3927
|
+
// Seed: admin exists so the wizard's vault step is reachable.
|
|
3928
|
+
await createUser(db, "owner", "pw");
|
|
3929
|
+
// Get a session cookie via a CSRF token GET first.
|
|
3930
|
+
const supervisor = makeSupervisor();
|
|
3931
|
+
const baseDeps = {
|
|
3932
|
+
db,
|
|
3933
|
+
manifestPath: h.manifestPath,
|
|
3934
|
+
configDir: h.dir,
|
|
3935
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3936
|
+
issuer: "http://127.0.0.1:1939",
|
|
3937
|
+
registry: getDefaultOperationsRegistry(),
|
|
3938
|
+
supervisor,
|
|
3939
|
+
};
|
|
3940
|
+
const getRes = handleSetupGet(
|
|
3941
|
+
req("/admin/setup", { headers: { accept: "application/json" } }),
|
|
3942
|
+
baseDeps,
|
|
3943
|
+
);
|
|
3944
|
+
const csrf = setCookie(getRes, CSRF_COOKIE_NAME) ?? "";
|
|
3945
|
+
const envelope = (await getRes.json()) as { csrfToken: string };
|
|
3946
|
+
// Build a session for the operator (proxy what an account POST
|
|
3947
|
+
// would do).
|
|
3948
|
+
const { createSession, buildSessionCookie, SESSION_TTL_MS } = await import("../sessions.ts");
|
|
3949
|
+
const user = (await import("../users.ts")).getUserByUsername(db, "owner");
|
|
3950
|
+
if (!user) throw new Error("user missing");
|
|
3951
|
+
const session = createSession(db, { userId: user.id });
|
|
3952
|
+
const cookieHeader = `${SESSION_COOKIE_NAME}=${session.id}; ${CSRF_COOKIE_NAME}=${csrf}`;
|
|
3953
|
+
const postRes = await handleSetupVaultPost(
|
|
3954
|
+
req("/admin/setup/vault", {
|
|
3955
|
+
method: "POST",
|
|
3956
|
+
headers: {
|
|
3957
|
+
accept: "application/json",
|
|
3958
|
+
"content-type": "application/json",
|
|
3959
|
+
cookie: cookieHeader,
|
|
3960
|
+
},
|
|
3961
|
+
body: JSON.stringify({
|
|
3962
|
+
[CSRF_FIELD_NAME]: envelope.csrfToken,
|
|
3963
|
+
mode: "skip",
|
|
3964
|
+
}),
|
|
3965
|
+
}),
|
|
3966
|
+
baseDeps,
|
|
3967
|
+
);
|
|
3968
|
+
expect(postRes.status).toBe(200);
|
|
3969
|
+
expect(postRes.headers.get("content-type")).toContain("application/json");
|
|
3970
|
+
const body = (await postRes.json()) as { step: string };
|
|
3971
|
+
expect(body.step).toBe("expose");
|
|
3972
|
+
// The skip flag is persisted.
|
|
3973
|
+
expect(getSetting(db, "setup_vault_skipped")).toBe("true");
|
|
3974
|
+
// deriveWizardState advances past the vault step.
|
|
3975
|
+
const s = deriveWizardState({
|
|
3976
|
+
db,
|
|
3977
|
+
manifestPath: h.manifestPath,
|
|
3978
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3979
|
+
});
|
|
3980
|
+
expect(s.hasVault).toBe(true);
|
|
3981
|
+
expect(s.step).toBe("expose");
|
|
3982
|
+
} finally {
|
|
3983
|
+
db.close();
|
|
3984
|
+
}
|
|
3985
|
+
});
|
|
3986
|
+
|
|
3987
|
+
test("vault step import mode requires remote_url (400 on empty)", async () => {
|
|
3988
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
3989
|
+
try {
|
|
3990
|
+
await createUser(db, "owner", "pw");
|
|
3991
|
+
const supervisor = makeSupervisor();
|
|
3992
|
+
const baseDeps = {
|
|
3993
|
+
db,
|
|
3994
|
+
manifestPath: h.manifestPath,
|
|
3995
|
+
configDir: h.dir,
|
|
3996
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3997
|
+
issuer: "http://127.0.0.1:1939",
|
|
3998
|
+
registry: getDefaultOperationsRegistry(),
|
|
3999
|
+
supervisor,
|
|
4000
|
+
};
|
|
4001
|
+
const { createSession } = await import("../sessions.ts");
|
|
4002
|
+
const user = (await import("../users.ts")).getUserByUsername(db, "owner");
|
|
4003
|
+
if (!user) throw new Error("user missing");
|
|
4004
|
+
const session = createSession(db, { userId: user.id });
|
|
4005
|
+
// Need CSRF cookie value matching the body field. Pull a token
|
|
4006
|
+
// through a GET first.
|
|
4007
|
+
const getRes = handleSetupGet(
|
|
4008
|
+
req("/admin/setup", { headers: { accept: "application/json" } }),
|
|
4009
|
+
baseDeps,
|
|
4010
|
+
);
|
|
4011
|
+
const csrf = setCookie(getRes, CSRF_COOKIE_NAME) ?? "";
|
|
4012
|
+
const envelope = (await getRes.json()) as { csrfToken: string };
|
|
4013
|
+
const cookieHeader = `${SESSION_COOKIE_NAME}=${session.id}; ${CSRF_COOKIE_NAME}=${csrf}`;
|
|
4014
|
+
const postRes = await handleSetupVaultPost(
|
|
4015
|
+
req("/admin/setup/vault", {
|
|
4016
|
+
method: "POST",
|
|
4017
|
+
headers: {
|
|
4018
|
+
accept: "application/json",
|
|
4019
|
+
"content-type": "application/json",
|
|
4020
|
+
cookie: cookieHeader,
|
|
4021
|
+
},
|
|
4022
|
+
body: JSON.stringify({
|
|
4023
|
+
[CSRF_FIELD_NAME]: envelope.csrfToken,
|
|
4024
|
+
mode: "import",
|
|
4025
|
+
vault_name: "imported",
|
|
4026
|
+
remote_url: "",
|
|
4027
|
+
}),
|
|
4028
|
+
}),
|
|
4029
|
+
baseDeps,
|
|
4030
|
+
);
|
|
4031
|
+
expect(postRes.status).toBe(400);
|
|
4032
|
+
const body = (await postRes.json()) as { error: string; message: string };
|
|
4033
|
+
expect(body.error).toContain("Remote URL required");
|
|
4034
|
+
} finally {
|
|
4035
|
+
db.close();
|
|
4036
|
+
}
|
|
4037
|
+
});
|
|
4038
|
+
|
|
4039
|
+
// hub#168 fold (PR #447 reviewer): the import POST to vault MUST carry
|
|
4040
|
+
// a Bearer — vault's `authenticateVaultRequest` rejects 401 before
|
|
4041
|
+
// scope check on missing auth. Asserts the header is present, names
|
|
4042
|
+
// the vault, and the body shape is intact.
|
|
4043
|
+
test("postVaultImportImpl sends Authorization: Bearer + correct body to vault", async () => {
|
|
4044
|
+
let capturedUrl: string | undefined;
|
|
4045
|
+
let capturedHeaders: Headers | undefined;
|
|
4046
|
+
let capturedBody: unknown;
|
|
4047
|
+
const stubFetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
4048
|
+
capturedUrl = typeof input === "string" ? input : input.toString();
|
|
4049
|
+
capturedHeaders = new Headers(init?.headers ?? {});
|
|
4050
|
+
capturedBody = JSON.parse((init?.body as string) ?? "{}");
|
|
4051
|
+
return new Response(
|
|
4052
|
+
JSON.stringify({
|
|
4053
|
+
notes_imported: 7,
|
|
4054
|
+
tags_imported: 2,
|
|
4055
|
+
attachments_imported: 0,
|
|
4056
|
+
warnings: [],
|
|
4057
|
+
}),
|
|
4058
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
4059
|
+
);
|
|
4060
|
+
}) as typeof fetch;
|
|
4061
|
+
|
|
4062
|
+
const result = await postVaultImportImpl({
|
|
4063
|
+
vaultName: "imported",
|
|
4064
|
+
vaultPort: 1940,
|
|
4065
|
+
bearerToken: "stub-jwt-abc",
|
|
4066
|
+
remoteUrl: "https://github.com/owner/repo.git",
|
|
4067
|
+
mode: "merge",
|
|
4068
|
+
pat: "ghp_stub",
|
|
4069
|
+
fetcher: stubFetch,
|
|
4070
|
+
});
|
|
4071
|
+
|
|
4072
|
+
expect(result.notes_imported).toBe(7);
|
|
4073
|
+
expect(capturedUrl).toBe("http://127.0.0.1:1940/vault/imported/.parachute/mirror/import");
|
|
4074
|
+
expect(capturedHeaders?.get("authorization")).toBe("Bearer stub-jwt-abc");
|
|
4075
|
+
expect(capturedHeaders?.get("content-type")).toBe("application/json");
|
|
4076
|
+
expect(capturedBody).toEqual({
|
|
4077
|
+
remote_url: "https://github.com/owner/repo.git",
|
|
4078
|
+
mode: "merge",
|
|
4079
|
+
credentials: { kind: "pat", token: "ghp_stub" },
|
|
4080
|
+
});
|
|
4081
|
+
});
|
|
4082
|
+
|
|
4083
|
+
// No-PAT branch — public repo import. Sends `credentials: null`,
|
|
4084
|
+
// which vault interprets as "use stored credentials" (or none).
|
|
4085
|
+
// Reviewer-flagged coverage gap on the rc.8 fold.
|
|
4086
|
+
test("postVaultImportImpl sends credentials: null when no PAT is provided", async () => {
|
|
4087
|
+
let capturedBody: unknown;
|
|
4088
|
+
const stubFetch = (async (_: string | URL | Request, init?: RequestInit) => {
|
|
4089
|
+
capturedBody = JSON.parse((init?.body as string) ?? "{}");
|
|
4090
|
+
return new Response(JSON.stringify({ notes_imported: 1 }), {
|
|
4091
|
+
status: 200,
|
|
4092
|
+
headers: { "content-type": "application/json" },
|
|
4093
|
+
});
|
|
4094
|
+
}) as typeof fetch;
|
|
4095
|
+
|
|
4096
|
+
await postVaultImportImpl({
|
|
4097
|
+
vaultName: "public-import",
|
|
4098
|
+
vaultPort: 1940,
|
|
4099
|
+
bearerToken: "stub",
|
|
4100
|
+
remoteUrl: "https://github.com/owner/public.git",
|
|
4101
|
+
mode: "replace",
|
|
4102
|
+
fetcher: stubFetch,
|
|
4103
|
+
});
|
|
4104
|
+
|
|
4105
|
+
expect(capturedBody).toEqual({
|
|
4106
|
+
remote_url: "https://github.com/owner/public.git",
|
|
4107
|
+
mode: "replace",
|
|
4108
|
+
credentials: null,
|
|
4109
|
+
});
|
|
4110
|
+
});
|
|
4111
|
+
});
|