@openparachute/hub 0.6.4-rc.1 → 0.6.4-rc.10
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 +1 -2
- package/src/__tests__/account-home-ui.test.ts +344 -110
- package/src/__tests__/account-mirror.test.ts +156 -0
- package/src/__tests__/account-setup.test.ts +273 -2
- package/src/__tests__/api-account.test.ts +112 -1
- package/src/__tests__/api-invites.test.ts +37 -0
- package/src/__tests__/api-modules-ops.test.ts +8 -2
- package/src/__tests__/cloudflare-state.test.ts +104 -0
- package/src/__tests__/expose-cloudflare.test.ts +130 -5
- package/src/__tests__/expose-interactive.test.ts +234 -7
- package/src/__tests__/expose-supervisor-version.test.ts +104 -0
- package/src/__tests__/grants.test.ts +197 -8
- package/src/__tests__/hub-server.test.ts +133 -0
- package/src/__tests__/hub-unit.test.ts +181 -0
- package/src/__tests__/init.test.ts +579 -3
- package/src/__tests__/install.test.ts +448 -2
- package/src/__tests__/migrate-cutover.test.ts +1 -0
- package/src/__tests__/setup-wizard.test.ts +110 -0
- package/src/__tests__/supervisor.test.ts +197 -0
- package/src/__tests__/users.test.ts +39 -0
- package/src/__tests__/well-known.test.ts +25 -0
- package/src/__tests__/wizard.test.ts +72 -1
- package/src/account-home-ui.ts +430 -257
- package/src/account-mirror.ts +126 -0
- package/src/account-setup.ts +46 -7
- package/src/account-vault-token.ts +9 -0
- package/src/admin-login-ui.ts +33 -6
- package/src/api-account.ts +64 -0
- package/src/api-invites.ts +12 -14
- package/src/cli.ts +6 -2
- package/src/cloudflare/detect.ts +1 -1
- package/src/cloudflare/state.ts +104 -8
- package/src/commands/expose-cloudflare.ts +103 -36
- package/src/commands/expose-interactive.ts +163 -17
- package/src/commands/expose-supervisor.ts +45 -0
- package/src/commands/init.ts +183 -4
- package/src/commands/install.ts +321 -3
- package/src/commands/wizard.ts +36 -2
- package/src/grants.ts +113 -0
- package/src/help.ts +18 -5
- package/src/hub-server.ts +39 -0
- package/src/hub-settings.ts +3 -3
- package/src/hub-unit.ts +255 -0
- package/src/service-spec.ts +9 -1
- package/src/setup-wizard.ts +34 -2
- package/src/supervisor.ts +148 -0
- package/src/users.ts +12 -4
- package/src/well-known.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openparachute/hub",
|
|
3
|
-
"version": "0.6.4-rc.
|
|
3
|
+
"version": "0.6.4-rc.10",
|
|
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": {
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"format": "biome format --write .",
|
|
33
33
|
"typecheck": "tsc --noEmit",
|
|
34
34
|
"build:spa": "cd web/ui && bun install --frozen-lockfile && bun run build",
|
|
35
|
-
"postinstall": "if [ -d web/ui ]; then bun run build:spa; fi",
|
|
36
35
|
"prepack": "bun run build:spa"
|
|
37
36
|
},
|
|
38
37
|
"devDependencies": {
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* tests pin the load-bearing shape:
|
|
5
5
|
*
|
|
6
6
|
* - Assigned-vault branch: Notes CTA href encodes the hub+vault URL,
|
|
7
|
-
* vault name shows in the body,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* vault name shows in the body, the backup-state line surfaces, and a
|
|
8
|
+
* "build your own UI" hint links the surface-build starter. The connect
|
|
9
|
+
* instructions live ONCE in the onboarding checklist (not duplicated in
|
|
10
|
+
* the vault card); token-minting is gone from /account (OAuth-first);
|
|
11
|
+
* a single "Advanced vault settings ↗" deep-link covers advanced needs.
|
|
10
12
|
* - Admin (no assigned vault) branch: link to /admin/ visible.
|
|
11
13
|
* - Defensive third branch (non-admin + no vault): "ask the operator"
|
|
12
14
|
* copy renders.
|
|
@@ -44,35 +46,9 @@ describe("renderAccountHome", () => {
|
|
|
44
46
|
expect(html).toContain(`https://notes.parachute.computer/add?url=${encodedVaultUrl}`);
|
|
45
47
|
expect(html).toContain('target="_blank"');
|
|
46
48
|
expect(html).toContain('rel="noopener"');
|
|
47
|
-
// The friend-connect surface: MCP endpoint + `claude mcp add` command,
|
|
48
|
-
// each with a copy button. OAuth path (no token in the command).
|
|
49
|
-
expect(html).toContain(`${HUB_ORIGIN}/vault/alice/mcp`);
|
|
50
|
-
expect(html).toContain(
|
|
51
|
-
`claude mcp add --transport http parachute-alice ${HUB_ORIGIN}/vault/alice/mcp`,
|
|
52
|
-
);
|
|
53
|
-
expect(html).toContain('data-testid="copy-mcp-endpoint"');
|
|
54
|
-
expect(html).toContain('data-testid="copy-mcp-add-command"');
|
|
55
|
-
// The connect command must NOT embed a token — the OAuth path needs none.
|
|
56
|
-
expect(html).not.toContain("--header");
|
|
57
|
-
expect(html).not.toContain("Authorization: Bearer");
|
|
58
49
|
// Copy-button progressive-enhancement script is present.
|
|
59
50
|
expect(html).toContain("navigator.clipboard");
|
|
60
|
-
//
|
|
61
|
-
// rather than MCP jargon up top.
|
|
62
|
-
expect(html).toContain('data-testid="connect-ai-heading"');
|
|
63
|
-
expect(html).toContain("Connect your AI");
|
|
64
|
-
// BOTH connect methods render as distinct, labelled blocks.
|
|
65
|
-
expect(html).toContain('data-testid="connect-method-claude-code"');
|
|
66
|
-
expect(html).toContain("Claude Code");
|
|
67
|
-
expect(html).toContain('data-testid="connect-method-claude-ai"');
|
|
68
|
-
expect(html).toContain("Claude.ai");
|
|
69
|
-
// The Claude.ai path mirrors the install.njk canonical phrasing
|
|
70
|
-
// (Settings → Connectors → Add custom connector, paste the endpoint).
|
|
71
|
-
expect(html).toContain("Connectors");
|
|
72
|
-
expect(html).toContain("Add custom connector");
|
|
73
|
-
// A brief "any other MCP client" line is present (no bloat — just one).
|
|
74
|
-
expect(html).toContain('data-testid="connect-any-client-hint"');
|
|
75
|
-
// Notes CTA still present, now framed as the browser-UI option.
|
|
51
|
+
// Notes CTA present, framed as the browser-UI option.
|
|
76
52
|
expect(html).toContain('data-testid="open-notes-cta"');
|
|
77
53
|
// Import-notes CTA deep-links to the Notes-UI /import route for the same
|
|
78
54
|
// vault, mirroring the Open-Notes target-resolution (same hosted origin,
|
|
@@ -81,6 +57,78 @@ describe("renderAccountHome", () => {
|
|
|
81
57
|
expect(html).toContain('data-testid="import-notes-cta"');
|
|
82
58
|
});
|
|
83
59
|
|
|
60
|
+
test("dedup — the full connect block lives ONCE (in the checklist), NOT in the vault card", () => {
|
|
61
|
+
const html = renderAccountHome({
|
|
62
|
+
username: "alice",
|
|
63
|
+
assignedVaults: ["alice"],
|
|
64
|
+
passwordChanged: true,
|
|
65
|
+
hubOrigin: HUB_ORIGIN,
|
|
66
|
+
isFirstAdmin: false,
|
|
67
|
+
csrfToken: CSRF,
|
|
68
|
+
twoFactorEnabled: false,
|
|
69
|
+
connectedVault: false,
|
|
70
|
+
});
|
|
71
|
+
// The checklist owns "Connect your AI" — the endpoint + the `claude mcp add`
|
|
72
|
+
// command (OAuth, no token) appear there.
|
|
73
|
+
expect(html).toContain('data-testid="onboarding-mcp-endpoint"');
|
|
74
|
+
expect(html).toContain('data-testid="onboarding-mcp-add-command"');
|
|
75
|
+
expect(html).toContain(`${HUB_ORIGIN}/vault/alice/mcp`);
|
|
76
|
+
// The vault card must NOT repeat the connect instructions. None of the
|
|
77
|
+
// old mcp-connect block markers may appear — no duplicated connect surface.
|
|
78
|
+
expect(html).not.toContain('data-testid="mcp-connect"');
|
|
79
|
+
expect(html).not.toContain('data-testid="connect-ai-heading"');
|
|
80
|
+
expect(html).not.toContain('data-testid="connect-method-claude-code"');
|
|
81
|
+
expect(html).not.toContain('data-testid="connect-method-claude-ai"');
|
|
82
|
+
expect(html).not.toContain('data-testid="connect-any-client-hint"');
|
|
83
|
+
// The connect instructions live only in the checklist, never duplicated in
|
|
84
|
+
// the vault card: split the page at the vault card and assert the endpoint
|
|
85
|
+
// doesn't reappear in that slice.
|
|
86
|
+
const cardIdx = html.indexOf('data-testid="vault-card"');
|
|
87
|
+
expect(cardIdx).toBeGreaterThan(-1);
|
|
88
|
+
const vaultCardSlice = html.slice(cardIdx);
|
|
89
|
+
expect(vaultCardSlice).not.toContain(`${HUB_ORIGIN}/vault/alice/mcp`);
|
|
90
|
+
expect(vaultCardSlice).not.toContain("claude mcp add");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("token-mint — the mint affordance is gone from /account (OAuth-first)", () => {
|
|
94
|
+
const html = renderAccountHome({
|
|
95
|
+
username: "alice",
|
|
96
|
+
assignedVaults: ["alice"],
|
|
97
|
+
passwordChanged: true,
|
|
98
|
+
hubOrigin: HUB_ORIGIN,
|
|
99
|
+
isFirstAdmin: false,
|
|
100
|
+
csrfToken: CSRF,
|
|
101
|
+
twoFactorEnabled: false,
|
|
102
|
+
mintableVerbs: { alice: ["read", "write", "admin"] },
|
|
103
|
+
});
|
|
104
|
+
// No token-mint <details> block, no mint form, no verb radios — minting a
|
|
105
|
+
// header-auth token is a script/advanced concern that lives in the SPA.
|
|
106
|
+
expect(html).not.toContain('data-testid="token-mint"');
|
|
107
|
+
expect(html).not.toContain('data-testid="mint-form"');
|
|
108
|
+
expect(html).not.toContain('data-testid="mint-verb-read"');
|
|
109
|
+
expect(html).not.toContain('data-testid="mint-verb-admin"');
|
|
110
|
+
expect(html).not.toContain("Mint an access token");
|
|
111
|
+
// The single advanced entry point covers the advanced needs.
|
|
112
|
+
expect(html).toContain('data-testid="vault-admin-button"');
|
|
113
|
+
expect(html).toContain("Advanced vault settings");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("build-your-own-UI — Notes is one way; the hint links the surface-build starter", () => {
|
|
117
|
+
const html = renderAccountHome({
|
|
118
|
+
username: "alice",
|
|
119
|
+
assignedVaults: ["alice"],
|
|
120
|
+
passwordChanged: true,
|
|
121
|
+
hubOrigin: HUB_ORIGIN,
|
|
122
|
+
isFirstAdmin: false,
|
|
123
|
+
csrfToken: CSRF,
|
|
124
|
+
twoFactorEnabled: false,
|
|
125
|
+
});
|
|
126
|
+
expect(html).toContain('data-testid="build-your-own-ui"');
|
|
127
|
+
expect(html).toContain('data-testid="build-your-own-ui-link"');
|
|
128
|
+
expect(html).toContain("https://parachute.computer/onboarding/surface-build/");
|
|
129
|
+
expect(html).toContain("build you a custom UI");
|
|
130
|
+
});
|
|
131
|
+
|
|
84
132
|
test("assigned-vault branch — import-notes CTA gated alongside open-notes (no dead link)", () => {
|
|
85
133
|
// Both CTAs render together for an assigned vault and are absent together
|
|
86
134
|
// in the no-vault branches — so we never surface an Import link that points
|
|
@@ -175,7 +223,7 @@ describe("renderAccountHome", () => {
|
|
|
175
223
|
expect(html).not.toContain('data-testid="mcp-connect"');
|
|
176
224
|
});
|
|
177
225
|
|
|
178
|
-
test("get-started card — links to the two onboarding prompts, placed
|
|
226
|
+
test("get-started card — links to the two onboarding prompts, placed AFTER the vault card", () => {
|
|
179
227
|
const html = renderAccountHome({
|
|
180
228
|
username: "alice",
|
|
181
229
|
assignedVaults: ["alice"],
|
|
@@ -195,8 +243,10 @@ describe("renderAccountHome", () => {
|
|
|
195
243
|
expect(html).toContain('data-testid="starter-surface-build"');
|
|
196
244
|
// External links open safely.
|
|
197
245
|
expect(html).toContain('rel="noopener"');
|
|
198
|
-
//
|
|
199
|
-
|
|
246
|
+
// Connect-before-prompts: the prompts are only useful once connected, so
|
|
247
|
+
// they now sit AFTER the vault card in document order (and after the
|
|
248
|
+
// onboarding checklist, which leads the page).
|
|
249
|
+
expect(html.indexOf('data-testid="get-started-card"')).toBeGreaterThan(
|
|
200
250
|
html.indexOf('data-testid="vault-card"'),
|
|
201
251
|
);
|
|
202
252
|
});
|
|
@@ -231,8 +281,30 @@ describe("renderAccountHome", () => {
|
|
|
231
281
|
expect(noVault).toContain('data-testid="no-vault-card"');
|
|
232
282
|
});
|
|
233
283
|
|
|
234
|
-
test("
|
|
235
|
-
const
|
|
284
|
+
test("backup state — renders the backup line + omits when no mirror entry", () => {
|
|
285
|
+
const backedUp = renderAccountHome({
|
|
286
|
+
username: "alice",
|
|
287
|
+
assignedVaults: ["alice"],
|
|
288
|
+
passwordChanged: true,
|
|
289
|
+
hubOrigin: HUB_ORIGIN,
|
|
290
|
+
isFirstAdmin: false,
|
|
291
|
+
csrfToken: CSRF,
|
|
292
|
+
twoFactorEnabled: false,
|
|
293
|
+
mintableVerbs: { alice: ["read", "write", "admin"] },
|
|
294
|
+
mirrorLines: { alice: "Backed up — full version history" },
|
|
295
|
+
});
|
|
296
|
+
// The warm, plain-language backup line surfaces on the tile.
|
|
297
|
+
expect(backedUp).toContain('data-testid="backup-state-line"');
|
|
298
|
+
expect(backedUp).toContain("Backed up — full version history");
|
|
299
|
+
// Not pushing yet → a "Back up to GitHub ↗" action (reuses the
|
|
300
|
+
// vault-admin-token deep-link, gated on admin).
|
|
301
|
+
expect(backedUp).toContain('data-testid="backup-github-button"');
|
|
302
|
+
expect(backedUp).toContain("Back up to GitHub");
|
|
303
|
+
expect(backedUp).toContain("/account/vault-admin-token/alice");
|
|
304
|
+
|
|
305
|
+
// GitHub variant: when pushing, the line says so and the action is dropped.
|
|
306
|
+
// Suppression is gated on the `mirrorPushing` boolean (NOT the line string).
|
|
307
|
+
const pushing = renderAccountHome({
|
|
236
308
|
username: "alice",
|
|
237
309
|
assignedVaults: ["alice"],
|
|
238
310
|
passwordChanged: true,
|
|
@@ -240,14 +312,59 @@ describe("renderAccountHome", () => {
|
|
|
240
312
|
isFirstAdmin: false,
|
|
241
313
|
csrfToken: CSRF,
|
|
242
314
|
twoFactorEnabled: false,
|
|
315
|
+
mintableVerbs: { alice: ["read", "write", "admin"] },
|
|
316
|
+
mirrorLines: { alice: "Backed up — version history + GitHub" },
|
|
317
|
+
mirrorPushing: { alice: true },
|
|
243
318
|
});
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
319
|
+
expect(pushing).toContain("version history + GitHub");
|
|
320
|
+
expect(pushing).not.toContain('data-testid="backup-github-button"');
|
|
321
|
+
|
|
322
|
+
// Omitted silently when the mirror fetch returned nothing (no entry).
|
|
323
|
+
const noMirror = renderAccountHome({
|
|
324
|
+
username: "alice",
|
|
325
|
+
assignedVaults: ["alice"],
|
|
326
|
+
passwordChanged: true,
|
|
327
|
+
hubOrigin: HUB_ORIGIN,
|
|
328
|
+
isFirstAdmin: false,
|
|
329
|
+
csrfToken: CSRF,
|
|
330
|
+
twoFactorEnabled: false,
|
|
331
|
+
mintableVerbs: { alice: ["read", "write", "admin"] },
|
|
332
|
+
});
|
|
333
|
+
expect(noMirror).not.toContain('data-testid="backup-state-line"');
|
|
334
|
+
expect(noMirror).not.toContain('data-testid="vault-backup"');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("backup action — gated on the mirrorPushing boolean, not the line string", () => {
|
|
338
|
+
// Regression guard: the "Back up to GitHub ↗" suppression must follow the
|
|
339
|
+
// proper `mirrorPushing` boolean, NOT substring-match the display line.
|
|
340
|
+
// (a) line mentions "GitHub" but mirrorPushing is false → action STILL shows.
|
|
341
|
+
const lineLies = renderAccountHome({
|
|
342
|
+
username: "alice",
|
|
343
|
+
assignedVaults: ["alice"],
|
|
344
|
+
passwordChanged: true,
|
|
345
|
+
hubOrigin: HUB_ORIGIN,
|
|
346
|
+
isFirstAdmin: false,
|
|
347
|
+
csrfToken: CSRF,
|
|
348
|
+
twoFactorEnabled: false,
|
|
349
|
+
mintableVerbs: { alice: ["read", "write", "admin"] },
|
|
350
|
+
mirrorLines: { alice: "Backed up — full version history (GitHub disabled)" },
|
|
351
|
+
mirrorPushing: { alice: false },
|
|
352
|
+
});
|
|
353
|
+
expect(lineLies).toContain('data-testid="backup-github-button"');
|
|
354
|
+
// (b) mirrorPushing true but the line lacks "GitHub" → action suppressed.
|
|
355
|
+
const boolWins = renderAccountHome({
|
|
356
|
+
username: "alice",
|
|
357
|
+
assignedVaults: ["alice"],
|
|
358
|
+
passwordChanged: true,
|
|
359
|
+
hubOrigin: HUB_ORIGIN,
|
|
360
|
+
isFirstAdmin: false,
|
|
361
|
+
csrfToken: CSRF,
|
|
362
|
+
twoFactorEnabled: false,
|
|
363
|
+
mintableVerbs: { alice: ["read", "write", "admin"] },
|
|
364
|
+
mirrorLines: { alice: "Backed up — full version history" },
|
|
365
|
+
mirrorPushing: { alice: true },
|
|
366
|
+
});
|
|
367
|
+
expect(boolWins).not.toContain('data-testid="backup-github-button"');
|
|
251
368
|
});
|
|
252
369
|
|
|
253
370
|
test("account card — security actions collapse into a secondary <details>", () => {
|
|
@@ -345,18 +462,13 @@ describe("renderAccountHome", () => {
|
|
|
345
462
|
const familyEncoded = encodeURIComponent(`${HUB_ORIGIN}/vault/family`);
|
|
346
463
|
expect(html).toContain(`https://notes.parachute.computer/add?url=${personalEncoded}`);
|
|
347
464
|
expect(html).toContain(`https://notes.parachute.computer/add?url=${familyEncoded}`);
|
|
348
|
-
//
|
|
349
|
-
expect(html).
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
`claude mcp add --transport http parachute-family ${HUB_ORIGIN}/vault/family/mcp`,
|
|
356
|
-
);
|
|
357
|
-
// Two tiles → two copy-endpoint buttons.
|
|
358
|
-
expect(html.split('data-testid="copy-mcp-endpoint"').length - 1).toBe(2);
|
|
359
|
-
// The copy script is emitted once at the section level, not per-tile.
|
|
465
|
+
// Two tiles → two Open-Notes CTAs.
|
|
466
|
+
expect(html.split('data-testid="open-notes-cta"').length - 1).toBe(2);
|
|
467
|
+
// The vault card does NOT repeat the connect block — that lives in the
|
|
468
|
+
// checklist (which uses the first/primary vault only).
|
|
469
|
+
expect(html).not.toContain('data-testid="mcp-connect"');
|
|
470
|
+
expect(html).not.toContain(`${HUB_ORIGIN}/vault/family/mcp`);
|
|
471
|
+
// The copy script is emitted once at the page level, not per-tile.
|
|
360
472
|
expect(html.split("<script>").length - 1).toBe(1);
|
|
361
473
|
});
|
|
362
474
|
|
|
@@ -394,9 +506,9 @@ describe("renderAccountHome", () => {
|
|
|
394
506
|
expect(html).toContain("parachute-<vault>");
|
|
395
507
|
});
|
|
396
508
|
|
|
397
|
-
// ---
|
|
509
|
+
// --- single advanced entry point (gated on the admin verb) ---------------
|
|
398
510
|
|
|
399
|
-
test("
|
|
511
|
+
test("advanced settings link — present + gated on the admin verb", () => {
|
|
400
512
|
const html = renderAccountHome({
|
|
401
513
|
username: "alice",
|
|
402
514
|
assignedVaults: ["work"],
|
|
@@ -405,45 +517,22 @@ describe("renderAccountHome", () => {
|
|
|
405
517
|
isFirstAdmin: false,
|
|
406
518
|
csrfToken: CSRF,
|
|
407
519
|
twoFactorEnabled: false,
|
|
408
|
-
mintableVerbs: { work: ["read", "write"] },
|
|
520
|
+
mintableVerbs: { work: ["read", "write", "admin"] },
|
|
409
521
|
});
|
|
410
|
-
//
|
|
411
|
-
expect(html).toContain('data-testid="
|
|
412
|
-
expect(html).toContain("
|
|
413
|
-
expect(html).toContain("
|
|
414
|
-
|
|
415
|
-
expect(html).toContain('data-testid="mint-verb-read"');
|
|
416
|
-
expect(html).toContain('data-testid="mint-verb-write"');
|
|
417
|
-
// Form POSTs to the per-vault endpoint with the CSRF token embedded.
|
|
418
|
-
expect(html).toContain('action="/account/vault-token/work"');
|
|
522
|
+
// One clearly-labelled "Advanced vault settings ↗" link → the SPA deep-link.
|
|
523
|
+
expect(html).toContain('data-testid="vault-admin-form"');
|
|
524
|
+
expect(html).toContain('data-testid="vault-admin-button"');
|
|
525
|
+
expect(html).toContain("Advanced vault settings");
|
|
526
|
+
expect(html).toContain('action="/account/vault-admin-token/work"');
|
|
419
527
|
expect(html).toContain('method="POST"');
|
|
420
|
-
expect(html).toContain('data-testid="mint-form"');
|
|
421
528
|
expect(html).toContain(CSRF);
|
|
422
|
-
//
|
|
423
|
-
expect(html).toContain("
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
test("mint affordance — a read-only role offers ONLY the read verb", () => {
|
|
427
|
-
// Today every assignment is write-role, but the renderer is verb-blind to
|
|
428
|
-
// the role: it shows exactly the verbs it's handed. A read-only cap must
|
|
429
|
-
// never surface a write radio (the server would reject it anyway).
|
|
430
|
-
const html = renderAccountHome({
|
|
431
|
-
username: "alice",
|
|
432
|
-
assignedVaults: ["work"],
|
|
433
|
-
passwordChanged: true,
|
|
434
|
-
hubOrigin: HUB_ORIGIN,
|
|
435
|
-
isFirstAdmin: false,
|
|
436
|
-
csrfToken: CSRF,
|
|
437
|
-
twoFactorEnabled: false,
|
|
438
|
-
mintableVerbs: { work: ["read"] },
|
|
439
|
-
});
|
|
440
|
-
expect(html).toContain('data-testid="mint-verb-read"');
|
|
441
|
-
expect(html).not.toContain('data-testid="mint-verb-write"');
|
|
529
|
+
// The old dual-purpose "Configure / back up this vault" label is gone.
|
|
530
|
+
expect(html).not.toContain("Configure / back up this vault");
|
|
442
531
|
});
|
|
443
532
|
|
|
444
|
-
test("
|
|
445
|
-
//
|
|
446
|
-
//
|
|
533
|
+
test("advanced settings link — absent when the user lacks the admin verb", () => {
|
|
534
|
+
// A read/write-only assignment must not surface the admin deep-link (the
|
|
535
|
+
// POST handler would 403 the admin token mint).
|
|
447
536
|
const html = renderAccountHome({
|
|
448
537
|
username: "alice",
|
|
449
538
|
assignedVaults: ["work"],
|
|
@@ -452,14 +541,10 @@ describe("renderAccountHome", () => {
|
|
|
452
541
|
isFirstAdmin: false,
|
|
453
542
|
csrfToken: CSRF,
|
|
454
543
|
twoFactorEnabled: false,
|
|
455
|
-
mintableVerbs: { work: ["read", "write"
|
|
544
|
+
mintableVerbs: { work: ["read", "write"] },
|
|
456
545
|
});
|
|
457
|
-
expect(html).toContain('
|
|
458
|
-
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
test("mint affordance — absent when no mintable verbs (admin / no-vault / unmapped role)", () => {
|
|
462
|
-
// Admin branch: no tiles at all, so no mint block.
|
|
546
|
+
expect(html).not.toContain('data-testid="vault-admin-button"');
|
|
547
|
+
// Admin branch: no tiles at all, so no advanced link.
|
|
463
548
|
const admin = renderAccountHome({
|
|
464
549
|
username: "admin",
|
|
465
550
|
assignedVaults: [],
|
|
@@ -469,19 +554,7 @@ describe("renderAccountHome", () => {
|
|
|
469
554
|
csrfToken: CSRF,
|
|
470
555
|
twoFactorEnabled: false,
|
|
471
556
|
});
|
|
472
|
-
expect(admin).not.toContain('data-testid="
|
|
473
|
-
// Assigned vault but empty verb list (fail-closed unknown role) → no block.
|
|
474
|
-
const empty = renderAccountHome({
|
|
475
|
-
username: "alice",
|
|
476
|
-
assignedVaults: ["work"],
|
|
477
|
-
passwordChanged: true,
|
|
478
|
-
hubOrigin: HUB_ORIGIN,
|
|
479
|
-
isFirstAdmin: false,
|
|
480
|
-
csrfToken: CSRF,
|
|
481
|
-
twoFactorEnabled: false,
|
|
482
|
-
mintableVerbs: { work: [] },
|
|
483
|
-
});
|
|
484
|
-
expect(empty).not.toContain('data-testid="token-mint"');
|
|
557
|
+
expect(admin).not.toContain('data-testid="vault-admin-button"');
|
|
485
558
|
});
|
|
486
559
|
|
|
487
560
|
test("minted-token banner — shows the token once with a save-it warning, no revoke claim", () => {
|
|
@@ -529,4 +602,165 @@ describe("renderAccountHome", () => {
|
|
|
529
602
|
// Error render must NOT also show a token.
|
|
530
603
|
expect(html).not.toContain('data-testid="minted-token-banner"');
|
|
531
604
|
});
|
|
605
|
+
|
|
606
|
+
// --- first-run onboarding checklist --------------------------------------
|
|
607
|
+
|
|
608
|
+
test("onboarding checklist — renders 3 steps with the correct /mcp endpoint (not connected)", () => {
|
|
609
|
+
const html = renderAccountHome({
|
|
610
|
+
username: "alice",
|
|
611
|
+
assignedVaults: ["alice"],
|
|
612
|
+
passwordChanged: true,
|
|
613
|
+
hubOrigin: HUB_ORIGIN,
|
|
614
|
+
isFirstAdmin: false,
|
|
615
|
+
csrfToken: CSRF,
|
|
616
|
+
twoFactorEnabled: false,
|
|
617
|
+
connectedVault: false,
|
|
618
|
+
});
|
|
619
|
+
expect(html).toContain('data-testid="onboarding-checklist"');
|
|
620
|
+
expect(html).toContain('data-connected="false"');
|
|
621
|
+
// All three numbered steps render.
|
|
622
|
+
expect(html).toContain('data-testid="onboarding-step-1"');
|
|
623
|
+
expect(html).toContain('data-testid="onboarding-step-2"');
|
|
624
|
+
expect(html).toContain('data-testid="onboarding-step-3"');
|
|
625
|
+
expect(html).toContain("Your account is ready");
|
|
626
|
+
expect(html).toContain("Connect your AI");
|
|
627
|
+
expect(html).toContain("Set up your vault");
|
|
628
|
+
// Step ② shows the canonical /vault/<name>/mcp endpoint inline — the /mcp
|
|
629
|
+
// suffix is load-bearing (only it returns the WWW-Authenticate header).
|
|
630
|
+
expect(html).toContain(`${HUB_ORIGIN}/vault/alice/mcp`);
|
|
631
|
+
expect(html).toMatch(/data-testid="onboarding-mcp-endpoint">[^<]*\/vault\/alice\/mcp</);
|
|
632
|
+
// Both connect methods are inline in step ②.
|
|
633
|
+
expect(html).toContain('data-testid="onboarding-mcp-add-command"');
|
|
634
|
+
expect(html).toContain("Add custom connector");
|
|
635
|
+
expect(html).toContain("claude mcp add");
|
|
636
|
+
// Step ③ links the vault-setup starter prompt.
|
|
637
|
+
expect(html).toContain('data-testid="onboarding-vault-setup-link"');
|
|
638
|
+
expect(html).toContain("https://parachute.computer/onboarding/vault-setup/");
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test("onboarding checklist — condenses to 'you're connected' when a grant exists", () => {
|
|
642
|
+
const html = renderAccountHome({
|
|
643
|
+
username: "alice",
|
|
644
|
+
assignedVaults: ["alice"],
|
|
645
|
+
passwordChanged: true,
|
|
646
|
+
hubOrigin: HUB_ORIGIN,
|
|
647
|
+
isFirstAdmin: false,
|
|
648
|
+
csrfToken: CSRF,
|
|
649
|
+
twoFactorEnabled: false,
|
|
650
|
+
connectedVault: true,
|
|
651
|
+
});
|
|
652
|
+
// Still the same section, but in its condensed done-state.
|
|
653
|
+
expect(html).toContain('data-testid="onboarding-checklist"');
|
|
654
|
+
expect(html).toContain('data-connected="true"');
|
|
655
|
+
expect(html).toContain('data-testid="onboarding-done-line"');
|
|
656
|
+
expect(html).toContain("You're connected");
|
|
657
|
+
// The full 3-step list is gone (no nagging) — but the vault card below
|
|
658
|
+
// remains the working surface.
|
|
659
|
+
expect(html).not.toContain('data-testid="onboarding-step-2"');
|
|
660
|
+
expect(html).toContain('data-testid="vault-card"');
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test("onboarding condensed state keeps a 'Connect another AI' expander with the full instructions (hub#583)", () => {
|
|
664
|
+
const html = renderAccountHome({
|
|
665
|
+
username: "alice",
|
|
666
|
+
assignedVaults: ["alice"],
|
|
667
|
+
passwordChanged: true,
|
|
668
|
+
hubOrigin: HUB_ORIGIN,
|
|
669
|
+
isFirstAdmin: false,
|
|
670
|
+
csrfToken: CSRF,
|
|
671
|
+
twoFactorEnabled: false,
|
|
672
|
+
connectedVault: true,
|
|
673
|
+
});
|
|
674
|
+
// The expander itself...
|
|
675
|
+
expect(html).toContain('data-testid="onboarding-connect-another"');
|
|
676
|
+
expect(html).toContain('data-testid="onboarding-connect-another-summary"');
|
|
677
|
+
expect(html).toContain("Connect another AI");
|
|
678
|
+
// ...re-reveals the endpoint + BOTH connect methods that the condensed
|
|
679
|
+
// line used to delete entirely (the hub#583 defect).
|
|
680
|
+
expect(html).toContain('data-testid="onboarding-mcp-endpoint"');
|
|
681
|
+
expect(html).toContain('data-testid="onboarding-mcp-add-command"');
|
|
682
|
+
expect(html).toContain("Claude.ai (web)");
|
|
683
|
+
expect(html).toContain("Claude Code (terminal)");
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test("onboarding NON-condensed (not connected) state has no 'Connect another AI' expander (hub#583)", () => {
|
|
687
|
+
const html = renderAccountHome({
|
|
688
|
+
username: "alice",
|
|
689
|
+
assignedVaults: ["alice"],
|
|
690
|
+
passwordChanged: true,
|
|
691
|
+
hubOrigin: HUB_ORIGIN,
|
|
692
|
+
isFirstAdmin: false,
|
|
693
|
+
csrfToken: CSRF,
|
|
694
|
+
twoFactorEnabled: false,
|
|
695
|
+
connectedVault: false,
|
|
696
|
+
});
|
|
697
|
+
// Full checklist already shows the inline instructions in step 2, so the
|
|
698
|
+
// expander is condensed-state-only.
|
|
699
|
+
expect(html).not.toContain('data-testid="onboarding-connect-another"');
|
|
700
|
+
expect(html).toContain('data-testid="onboarding-step-2"');
|
|
701
|
+
expect(html).toContain('data-testid="onboarding-mcp-endpoint"');
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test("onboarding checklist — leads the page: BEFORE the vault card and the starter prompts", () => {
|
|
705
|
+
const html = renderAccountHome({
|
|
706
|
+
username: "alice",
|
|
707
|
+
assignedVaults: ["alice"],
|
|
708
|
+
passwordChanged: true,
|
|
709
|
+
hubOrigin: HUB_ORIGIN,
|
|
710
|
+
isFirstAdmin: false,
|
|
711
|
+
csrfToken: CSRF,
|
|
712
|
+
twoFactorEnabled: false,
|
|
713
|
+
connectedVault: false,
|
|
714
|
+
});
|
|
715
|
+
const checklistIdx = html.indexOf('data-testid="onboarding-checklist"');
|
|
716
|
+
const vaultIdx = html.indexOf('data-testid="vault-card"');
|
|
717
|
+
const promptsIdx = html.indexOf('data-testid="get-started-card"');
|
|
718
|
+
// Net first-run order: checklist (connect) → vault details → prompts.
|
|
719
|
+
expect(checklistIdx).toBeGreaterThanOrEqual(0);
|
|
720
|
+
expect(checklistIdx).toBeLessThan(vaultIdx);
|
|
721
|
+
expect(vaultIdx).toBeLessThan(promptsIdx);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
test("onboarding checklist — absent on the admin and no-vault branches", () => {
|
|
725
|
+
const admin = renderAccountHome({
|
|
726
|
+
username: "admin",
|
|
727
|
+
assignedVaults: [],
|
|
728
|
+
passwordChanged: true,
|
|
729
|
+
hubOrigin: HUB_ORIGIN,
|
|
730
|
+
isFirstAdmin: true,
|
|
731
|
+
csrfToken: CSRF,
|
|
732
|
+
twoFactorEnabled: false,
|
|
733
|
+
});
|
|
734
|
+
expect(admin).not.toContain('data-testid="onboarding-checklist"');
|
|
735
|
+
|
|
736
|
+
const noVault = renderAccountHome({
|
|
737
|
+
username: "ghost",
|
|
738
|
+
assignedVaults: [],
|
|
739
|
+
passwordChanged: true,
|
|
740
|
+
hubOrigin: HUB_ORIGIN,
|
|
741
|
+
isFirstAdmin: false,
|
|
742
|
+
csrfToken: CSRF,
|
|
743
|
+
twoFactorEnabled: false,
|
|
744
|
+
});
|
|
745
|
+
expect(noVault).not.toContain('data-testid="onboarding-checklist"');
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
test("onboarding checklist — multi-vault uses the first vault for the connect step", () => {
|
|
749
|
+
const html = renderAccountHome({
|
|
750
|
+
username: "alice",
|
|
751
|
+
assignedVaults: ["personal", "family"],
|
|
752
|
+
passwordChanged: true,
|
|
753
|
+
hubOrigin: HUB_ORIGIN,
|
|
754
|
+
isFirstAdmin: false,
|
|
755
|
+
csrfToken: CSRF,
|
|
756
|
+
twoFactorEnabled: false,
|
|
757
|
+
connectedVault: false,
|
|
758
|
+
});
|
|
759
|
+
// The checklist's connect step references the first/primary vault; the
|
|
760
|
+
// per-vault tiles below still list every vault (by name + Notes CTA), but
|
|
761
|
+
// no longer repeat the connect endpoint (dedup — connect lives only here).
|
|
762
|
+
expect(html).toMatch(/data-testid="onboarding-mcp-endpoint">[^<]*\/vault\/personal\/mcp</);
|
|
763
|
+
expect(html).toContain("<strong>family</strong>"); // second vault still has a tile
|
|
764
|
+
expect(html).not.toContain(`${HUB_ORIGIN}/vault/family/mcp`); // no duplicated connect
|
|
765
|
+
});
|
|
532
766
|
});
|