@openparachute/hub 0.5.14-rc.14 → 0.5.14-rc.15
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 -1
- package/src/__tests__/account-home-ui.test.ts +23 -1
- package/src/__tests__/hub.test.ts +74 -6
- package/src/account-home-ui.ts +104 -39
- package/src/hub.ts +72 -11
package/package.json
CHANGED
|
@@ -57,6 +57,23 @@ describe("renderAccountHome", () => {
|
|
|
57
57
|
expect(html).not.toContain("Authorization: Bearer");
|
|
58
58
|
// Copy-button progressive-enhancement script is present.
|
|
59
59
|
expect(html).toContain("navigator.clipboard");
|
|
60
|
+
// Friendlier framing: the block leads with "connect your AI assistant"
|
|
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.
|
|
76
|
+
expect(html).toContain('data-testid="open-notes-cta"');
|
|
60
77
|
});
|
|
61
78
|
|
|
62
79
|
test("assigned-vault branch — trailing slash on hubOrigin is normalized", () => {
|
|
@@ -112,11 +129,16 @@ describe("renderAccountHome", () => {
|
|
|
112
129
|
twoFactorEnabled: false,
|
|
113
130
|
});
|
|
114
131
|
expect(html).toContain("Welcome, ghost");
|
|
115
|
-
|
|
132
|
+
// The message explains WHY there's nothing to connect (no vault yet) and
|
|
133
|
+
// gives a clear next step — not just a bare "ask your admin".
|
|
134
|
+
expect(html).toContain("Ask the hub operator to assign you a vault");
|
|
135
|
+
expect(html).toContain("don't have a vault yet");
|
|
116
136
|
// No /admin/ link in this branch — they have no admin role.
|
|
117
137
|
expect(html).not.toContain('href="/admin/"');
|
|
118
138
|
// No Notes CTA.
|
|
119
139
|
expect(html).not.toContain("notes.parachute.computer/add");
|
|
140
|
+
// No connect block — you can't connect a vault you don't have.
|
|
141
|
+
expect(html).not.toContain('data-testid="mcp-connect"');
|
|
120
142
|
});
|
|
121
143
|
|
|
122
144
|
test("account card — change-password link and sign-out form are present", () => {
|
|
@@ -5,7 +5,15 @@ import { join } from "node:path";
|
|
|
5
5
|
import { renderHub, writeHubFile } from "../hub.ts";
|
|
6
6
|
|
|
7
7
|
describe("renderHub", () => {
|
|
8
|
-
|
|
8
|
+
// The verbose discovery body (Get started / Services / Admin) + its
|
|
9
|
+
// data-loading script render only for a signed-in visitor (the signed-out
|
|
10
|
+
// landing is slimmed — see the "signed-out slimming" describe block below).
|
|
11
|
+
// Assertions about that verbose body therefore run against a signed-in
|
|
12
|
+
// render; assertions about the page shell (doctype, styles, brand) hold for
|
|
13
|
+
// both and use whichever render is convenient.
|
|
14
|
+
const html = renderHub({
|
|
15
|
+
session: { displayName: "operator", csrfToken: "csrf-shell" },
|
|
16
|
+
});
|
|
9
17
|
|
|
10
18
|
test("is a self-contained HTML document with inline styles and script", () => {
|
|
11
19
|
expect(html).toStartWith("<!doctype html>");
|
|
@@ -162,12 +170,72 @@ describe("renderHub", () => {
|
|
|
162
170
|
});
|
|
163
171
|
|
|
164
172
|
test("default render (no session) emits the 'Sign in' affordance", () => {
|
|
165
|
-
|
|
166
|
-
expect(
|
|
167
|
-
expect(
|
|
173
|
+
const out = renderHub();
|
|
174
|
+
expect(out).toContain('class="auth-indicator"');
|
|
175
|
+
expect(out).toContain("Sign in");
|
|
176
|
+
expect(out).toContain('href="/login?next=/"');
|
|
168
177
|
// No POST form, no CSRF input — those only appear when signed in.
|
|
169
|
-
expect(
|
|
170
|
-
expect(
|
|
178
|
+
expect(out).not.toContain('action="/logout"');
|
|
179
|
+
expect(out).not.toContain("__csrf");
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("renderHub — signed-out slimming (operator feedback)", () => {
|
|
184
|
+
// A signed-out visitor should see a clean, minimal landing: brand +
|
|
185
|
+
// tagline (in the header) + a single clear "Sign in" call. The hub's
|
|
186
|
+
// internal detail — the service catalog, vault listings, admin surfaces,
|
|
187
|
+
// and the well-known-driven loading script — must NOT render until the
|
|
188
|
+
// visitor authenticates. The signed-in render is unchanged.
|
|
189
|
+
const signedOut = renderHub();
|
|
190
|
+
const signedIn = renderHub({
|
|
191
|
+
session: { displayName: "operator", csrfToken: "csrf-xyz" },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("signed-out: brand wordmark + tagline still render (the slim landing keeps the brand)", () => {
|
|
195
|
+
expect(signedOut).toContain("<h1>Parachute</h1>");
|
|
196
|
+
expect(signedOut).toContain("Truly personal computing. Your knowledge belongs with you.");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("signed-out: a clear 'Sign in' call is the primary affordance", () => {
|
|
200
|
+
expect(signedOut).toContain('data-testid="signed-out-signin"');
|
|
201
|
+
expect(signedOut).toContain('href="/login?next=/"');
|
|
202
|
+
expect(signedOut).toContain("Sign in");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("signed-out: the verbose Services / Admin / Get started sections are absent", () => {
|
|
206
|
+
expect(signedOut).not.toContain('id="services-section"');
|
|
207
|
+
expect(signedOut).not.toContain('id="admin-section"');
|
|
208
|
+
expect(signedOut).not.toContain('id="get-started-section"');
|
|
209
|
+
expect(signedOut).not.toContain("<h2>Services</h2>");
|
|
210
|
+
expect(signedOut).not.toContain("<h2>Admin</h2>");
|
|
211
|
+
// Admin links / token surface must not be exposed pre-auth.
|
|
212
|
+
expect(signedOut).not.toContain("/admin/vaults");
|
|
213
|
+
expect(signedOut).not.toContain("/admin/tokens");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("signed-out: the well-known service-catalog loading script is not emitted", () => {
|
|
217
|
+
// No data-driven discovery body to populate when signed out → no script.
|
|
218
|
+
// (The brand mark is an inline SVG, not a <script>; assert on the IIFE's
|
|
219
|
+
// load function rather than a blanket "no <script>".) The footer's
|
|
220
|
+
// public "discovery" anchor → /.well-known/parachute.json stays — it's a
|
|
221
|
+
// plain link, not the catalog-fetching script — so assert on the fetch
|
|
222
|
+
// call + the loader function, not the URL string.
|
|
223
|
+
expect(signedOut).not.toContain("loadServices");
|
|
224
|
+
expect(signedOut).not.toContain("renderServices");
|
|
225
|
+
expect(signedOut).not.toContain("fetch('/.well-known/parachute.json'");
|
|
226
|
+
expect(signedOut).not.toContain("<script>");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("signed-in: the verbose sections + loading script DO render (signed-in view unchanged)", () => {
|
|
230
|
+
expect(signedIn).toContain('id="services-section"');
|
|
231
|
+
expect(signedIn).toContain('id="admin-section"');
|
|
232
|
+
expect(signedIn).toContain('id="get-started-section"');
|
|
233
|
+
expect(signedIn).toContain("/admin/vaults");
|
|
234
|
+
expect(signedIn).toContain("/.well-known/parachute.json");
|
|
235
|
+
expect(signedIn).toContain("loadServices");
|
|
236
|
+
// And the signed-out lede / standalone Sign-in CTA is gone (the
|
|
237
|
+
// auth-indicator carries sign-out instead).
|
|
238
|
+
expect(signedIn).not.toContain('data-testid="signed-out-signin"');
|
|
171
239
|
});
|
|
172
240
|
});
|
|
173
241
|
|
package/src/account-home-ui.ts
CHANGED
|
@@ -186,14 +186,20 @@ function renderVaultCard(opts: VaultCardOpts): string {
|
|
|
186
186
|
|
|
187
187
|
if (assignedVaults.length > 0) {
|
|
188
188
|
// One vault tile per assignment (multi-user Phase 2 PR 2). Each tile
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
189
|
+
// leads with a friendly "connect your AI assistant to this vault" block
|
|
190
|
+
// that covers BOTH connect paths a non-technical friend is likely to
|
|
191
|
+
// use — Claude Code (the `claude mcp add` CLI command) and Claude.ai on
|
|
192
|
+
// the web (Settings → Connectors → Add custom connector, pointed at the
|
|
193
|
+
// endpoint). Both are the OAuth path — no token to paste, the first
|
|
194
|
+
// connection opens a browser to sign in + approve. The Notes "Open" CTA
|
|
195
|
+
// sits alongside as the browser-UI option. Phrasing mirrors
|
|
196
|
+
// parachute.computer/install.njk's #connect-mcp-clients section so the
|
|
197
|
+
// operator docs and the friend's account page stay consistent.
|
|
198
|
+
//
|
|
199
|
+
// This closes the multi-user gap where the friend tile read as MCP
|
|
200
|
+
// jargon ("Connect an MCP client") rather than "here's how to connect
|
|
201
|
+
// this to your AI" — and where the web (Claude.ai) path was entirely
|
|
202
|
+
// missing, only the Claude Code CLI command was offered.
|
|
197
203
|
const heading = assignedVaults.length === 1 ? "<h2>Your vault</h2>" : "<h2>Your vaults</h2>";
|
|
198
204
|
const tiles = assignedVaults
|
|
199
205
|
.map((vaultName) => {
|
|
@@ -206,42 +212,56 @@ function renderVaultCard(opts: VaultCardOpts): string {
|
|
|
206
212
|
return `
|
|
207
213
|
<div class="vault-tile" data-testid="vault-tile" data-vault-name="${safeVault}">
|
|
208
214
|
<p class="vault-name"><strong>${safeVault}</strong></p>
|
|
209
|
-
<p>
|
|
210
|
-
<a class="btn btn-primary" href="https://notes.parachute.computer/add?url=${vaultUrlForAdd}"
|
|
211
|
-
target="_blank" rel="noopener" data-testid="open-notes-cta">Open Notes ↗</a>
|
|
212
|
-
</p>
|
|
213
215
|
<div class="mcp-connect" data-testid="mcp-connect">
|
|
214
|
-
<p class="mcp-connect-label">Connect
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
</div>
|
|
223
|
-
<div class="mcp-field">
|
|
224
|
-
<span class="mcp-field-label">Claude Code</span>
|
|
216
|
+
<p class="mcp-connect-label" data-testid="connect-ai-heading">Connect your AI
|
|
217
|
+
assistant to this vault</p>
|
|
218
|
+
<p class="mcp-connect-intro">Two common ways. Both sign you in to this hub over
|
|
219
|
+
HTTPS and ask you to approve access the first time — no token to copy.</p>
|
|
220
|
+
|
|
221
|
+
<div class="mcp-method" data-testid="connect-method-claude-code">
|
|
222
|
+
<p class="mcp-method-title">Claude Code (terminal)</p>
|
|
223
|
+
<p class="mcp-method-sub">Run this in your terminal:</p>
|
|
225
224
|
<div class="copy-row">
|
|
226
225
|
<code data-testid="mcp-add-command">${safeAddCmd}</code>
|
|
227
226
|
<button type="button" class="btn btn-copy" data-copy="${safeAddCmd}"
|
|
228
227
|
data-testid="copy-mcp-add-command">Copy</button>
|
|
229
228
|
</div>
|
|
230
229
|
</div>
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
|
|
231
|
+
<div class="mcp-method" data-testid="connect-method-claude-ai">
|
|
232
|
+
<p class="mcp-method-title">Claude.ai (web)</p>
|
|
233
|
+
<p class="mcp-method-sub">In Claude.ai, open <strong>Settings → Connectors</strong>,
|
|
234
|
+
choose <strong>Add custom connector</strong>, and paste this endpoint:</p>
|
|
235
|
+
<div class="copy-row">
|
|
236
|
+
<code data-testid="mcp-endpoint">${safeEndpoint}</code>
|
|
237
|
+
<button type="button" class="btn btn-copy" data-copy="${safeEndpoint}"
|
|
238
|
+
data-testid="copy-mcp-endpoint">Copy</button>
|
|
239
|
+
</div>
|
|
240
|
+
<p class="mcp-method-note">Claude.ai then redirects you here to sign in and
|
|
241
|
+
approve. (Your hub must be reachable from the web for this.)</p>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<p class="mcp-connect-hint" data-testid="connect-any-client-hint">Any other MCP
|
|
245
|
+
client (Codex, Goose, Cursor, your own agent): point it at the same endpoint
|
|
246
|
+
above over HTTP.</p>
|
|
233
247
|
</div>
|
|
248
|
+
<p class="vault-notes-cta">
|
|
249
|
+
<a class="btn btn-primary" href="https://notes.parachute.computer/add?url=${vaultUrlForAdd}"
|
|
250
|
+
target="_blank" rel="noopener" data-testid="open-notes-cta">Open Notes ↗</a>
|
|
251
|
+
<span class="vault-notes-cta-sub">Prefer a browser UI? Open Notes to browse +
|
|
252
|
+
capture in this vault.</span>
|
|
253
|
+
</p>
|
|
234
254
|
</div>`;
|
|
235
255
|
})
|
|
236
256
|
.join("");
|
|
237
257
|
return `
|
|
238
258
|
<section class="section" data-testid="vault-card">
|
|
239
259
|
${heading}
|
|
240
|
-
<p>
|
|
260
|
+
<p>Connect Claude (or any AI assistant) to your vault${
|
|
241
261
|
assignedVaults.length === 1 ? "" : "s"
|
|
242
|
-
} —
|
|
243
|
-
|
|
244
|
-
hub over HTTPS and
|
|
262
|
+
} — pick Claude Code or
|
|
263
|
+
Claude.ai below — or open Notes for a browser UI. The first connection signs you in
|
|
264
|
+
to your hub over HTTPS and asks you to approve access.</p>
|
|
245
265
|
<div class="vault-tiles">${tiles}
|
|
246
266
|
</div>
|
|
247
267
|
</section>${COPY_SCRIPT}`;
|
|
@@ -262,8 +282,11 @@ function renderVaultCard(opts: VaultCardOpts): string {
|
|
|
262
282
|
return `
|
|
263
283
|
<section class="section" data-testid="no-vault-card">
|
|
264
284
|
<h2>Your vault</h2>
|
|
265
|
-
<p>
|
|
266
|
-
|
|
285
|
+
<p>You don't have a vault yet, so there's nothing to connect to. A vault
|
|
286
|
+
is your personal knowledge store on this hub — once the operator
|
|
287
|
+
assigns you one, this page will show you how to connect Claude (or
|
|
288
|
+
any AI assistant) to it.</p>
|
|
289
|
+
<p><strong>Ask the hub operator to assign you a vault.</strong></p>
|
|
267
290
|
</section>`;
|
|
268
291
|
}
|
|
269
292
|
|
|
@@ -433,15 +456,40 @@ const STYLES = `
|
|
|
433
456
|
.vault-tile p:last-child { margin-top: 0.5rem; }
|
|
434
457
|
|
|
435
458
|
.mcp-connect {
|
|
436
|
-
margin-
|
|
437
|
-
padding-top: 0.6rem;
|
|
438
|
-
border-top: 1px solid ${PALETTE.borderLight};
|
|
459
|
+
margin-bottom: 0.75rem;
|
|
439
460
|
}
|
|
440
461
|
.mcp-connect-label {
|
|
462
|
+
font-family: ${FONT_SERIF};
|
|
463
|
+
font-size: 1.05rem;
|
|
464
|
+
font-weight: 400;
|
|
465
|
+
color: ${PALETTE.fg};
|
|
466
|
+
margin: 0 0 0.3rem;
|
|
467
|
+
}
|
|
468
|
+
.mcp-connect-intro {
|
|
441
469
|
font-size: 0.85rem;
|
|
442
|
-
|
|
470
|
+
color: ${PALETTE.fgMuted};
|
|
471
|
+
margin: 0 0 0.75rem;
|
|
472
|
+
}
|
|
473
|
+
.mcp-method {
|
|
474
|
+
margin: 0.75rem 0;
|
|
475
|
+
padding-top: 0.6rem;
|
|
476
|
+
border-top: 1px solid ${PALETTE.borderLight};
|
|
477
|
+
}
|
|
478
|
+
.mcp-method-title {
|
|
479
|
+
font-size: 0.9rem;
|
|
480
|
+
font-weight: 600;
|
|
443
481
|
color: ${PALETTE.fg};
|
|
444
|
-
margin: 0 0 0.
|
|
482
|
+
margin: 0 0 0.15rem;
|
|
483
|
+
}
|
|
484
|
+
.mcp-method-sub {
|
|
485
|
+
font-size: 0.82rem;
|
|
486
|
+
color: ${PALETTE.fgMuted};
|
|
487
|
+
margin: 0 0 0.4rem;
|
|
488
|
+
}
|
|
489
|
+
.mcp-method-note {
|
|
490
|
+
font-size: 0.78rem;
|
|
491
|
+
color: ${PALETTE.fgMuted};
|
|
492
|
+
margin: 0.35rem 0 0;
|
|
445
493
|
}
|
|
446
494
|
.mcp-field { margin: 0.5rem 0; }
|
|
447
495
|
.mcp-field-label {
|
|
@@ -453,6 +501,20 @@ const STYLES = `
|
|
|
453
501
|
font-family: ${FONT_MONO};
|
|
454
502
|
margin-bottom: 0.2rem;
|
|
455
503
|
}
|
|
504
|
+
.vault-notes-cta {
|
|
505
|
+
margin: 0.9rem 0 0;
|
|
506
|
+
padding-top: 0.6rem;
|
|
507
|
+
border-top: 1px solid ${PALETTE.borderLight};
|
|
508
|
+
display: flex;
|
|
509
|
+
align-items: center;
|
|
510
|
+
flex-wrap: wrap;
|
|
511
|
+
gap: 0.5rem 0.75rem;
|
|
512
|
+
}
|
|
513
|
+
.vault-notes-cta-sub {
|
|
514
|
+
font-size: 0.82rem;
|
|
515
|
+
color: ${PALETTE.fgMuted};
|
|
516
|
+
flex: 1 1 12rem;
|
|
517
|
+
}
|
|
456
518
|
.copy-row {
|
|
457
519
|
display: flex;
|
|
458
520
|
align-items: center;
|
|
@@ -564,11 +626,14 @@ const STYLES = `
|
|
|
564
626
|
body { background: #1a1815; color: #e8e4dc; }
|
|
565
627
|
.card { background: #25221d; border-color: #3a362f; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); }
|
|
566
628
|
h1, h2 { color: #f0ece4; }
|
|
567
|
-
.subtitle, .kv dt, .mcp-field-label, .mcp-connect-hint
|
|
568
|
-
.
|
|
629
|
+
.subtitle, .kv dt, .mcp-field-label, .mcp-connect-hint,
|
|
630
|
+
.mcp-connect-intro, .mcp-method-sub, .mcp-method-note,
|
|
631
|
+
.vault-notes-cta-sub { color: #a8a29a; }
|
|
632
|
+
.vault-name strong, .mcp-connect-label, .mcp-method-title { color: #f0ece4; }
|
|
569
633
|
code { background: #1f1c18; color: #e8e4dc; }
|
|
570
634
|
.copy-row code { background: transparent; }
|
|
571
|
-
.section
|
|
635
|
+
.section { border-top-color: #3a362f; }
|
|
636
|
+
.mcp-method, .vault-notes-cta { border-top-color: #3a362f; }
|
|
572
637
|
.brand-tag { border-color: #3a362f; color: #a8a29a; }
|
|
573
638
|
.copy-row { background: #1f1c18; border-color: #3a362f; }
|
|
574
639
|
.btn-secondary, .btn-copy { color: #e8e4dc; border-color: #3a362f; }
|
package/src/hub.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { CANONICAL_TAGLINE, WORDMARK_TEXT, brandMarkSvg } from "./brand.ts";
|
|
4
4
|
import { CONFIG_DIR } from "./config.ts";
|
|
5
5
|
import { CSRF_FIELD_NAME } from "./csrf.ts";
|
|
6
6
|
|
|
@@ -84,7 +84,19 @@ function buildHtml({ session }: RenderHubOpts): string {
|
|
|
84
84
|
const authBlock = session
|
|
85
85
|
? renderSignedIn(session.displayName, session.csrfToken)
|
|
86
86
|
: renderSignedOut();
|
|
87
|
-
|
|
87
|
+
// Gate the verbose discovery sections (Get started / Services / Admin)
|
|
88
|
+
// and their data-loading script on auth state. A signed-out visitor sees
|
|
89
|
+
// a clean, minimal landing — brand + tagline + a clear "Sign in" call —
|
|
90
|
+
// not the hub's service catalog, vault listings, or admin links. The
|
|
91
|
+
// detail un-gates the moment they sign in (the server already knows auth
|
|
92
|
+
// state from the session cookie, so this stays a no-JS-required,
|
|
93
|
+
// session-aware render). Operator feedback from a live multi-user deploy:
|
|
94
|
+
// the signed-out page exposed too much to anonymous visitors.
|
|
95
|
+
const body = session ? SIGNED_IN_BODY : SIGNED_OUT_BODY;
|
|
96
|
+
const script = session ? DISCOVERY_SCRIPT : "";
|
|
97
|
+
return HTML_TEMPLATE.replace("<!--AUTH-INDICATOR-->", authBlock)
|
|
98
|
+
.replace("<!--DISCOVERY-BODY-->", body)
|
|
99
|
+
.replace("<!--DISCOVERY-SCRIPT-->", script);
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
function renderSignedIn(displayName: string, csrfToken: string): string {
|
|
@@ -269,6 +281,34 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
269
281
|
font-size: 0.92rem;
|
|
270
282
|
margin: 0 0 1.25rem;
|
|
271
283
|
}
|
|
284
|
+
/* Signed-out landing: a single centered "Sign in" call under the brand.
|
|
285
|
+
Minimal by design — the service catalog + admin surfaces stay hidden
|
|
286
|
+
until the visitor authenticates. */
|
|
287
|
+
.signed-out-cta {
|
|
288
|
+
text-align: center;
|
|
289
|
+
margin-bottom: 0;
|
|
290
|
+
}
|
|
291
|
+
.signed-out-lede {
|
|
292
|
+
color: var(--fg-muted);
|
|
293
|
+
font-size: 1.05rem;
|
|
294
|
+
margin: 0 0 1.5rem;
|
|
295
|
+
}
|
|
296
|
+
.btn-signin {
|
|
297
|
+
display: inline-block;
|
|
298
|
+
background: var(--accent);
|
|
299
|
+
color: var(--card-bg);
|
|
300
|
+
font-family: var(--sans);
|
|
301
|
+
font-size: 1rem;
|
|
302
|
+
font-weight: 500;
|
|
303
|
+
text-decoration: none;
|
|
304
|
+
padding: 0.65rem 1.6rem;
|
|
305
|
+
border-radius: 8px;
|
|
306
|
+
transition: background 0.15s ease, transform 0.15s ease;
|
|
307
|
+
}
|
|
308
|
+
.btn-signin:hover {
|
|
309
|
+
background: var(--accent-hover);
|
|
310
|
+
transform: translateY(-1px);
|
|
311
|
+
}
|
|
272
312
|
.grid {
|
|
273
313
|
display: grid;
|
|
274
314
|
gap: 1.25rem;
|
|
@@ -378,7 +418,22 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
378
418
|
<h1>${WORDMARK_TEXT}</h1>
|
|
379
419
|
<p class="tagline">${CANONICAL_TAGLINE}</p>
|
|
380
420
|
</header>
|
|
421
|
+
<!--DISCOVERY-BODY-->
|
|
422
|
+
<footer>
|
|
423
|
+
<a href="/.well-known/parachute.json">discovery</a>
|
|
424
|
+
</footer>
|
|
425
|
+
</main>
|
|
426
|
+
<!--DISCOVERY-SCRIPT-->
|
|
427
|
+
</body>
|
|
428
|
+
</html>
|
|
429
|
+
`;
|
|
381
430
|
|
|
431
|
+
// The verbose discovery body — the service catalog, admin surfaces, and the
|
|
432
|
+
// "Get started" CTA. Rendered ONLY for a signed-in visitor (`buildHtml`
|
|
433
|
+
// selects this vs SIGNED_OUT_BODY on `session`). Anonymous visitors get the
|
|
434
|
+
// slim landing below instead, so the hub's internal surface isn't exposed
|
|
435
|
+
// pre-auth.
|
|
436
|
+
const SIGNED_IN_BODY = `
|
|
382
437
|
<section class="section" id="get-started-section" hidden>
|
|
383
438
|
<h2>Get started</h2>
|
|
384
439
|
<p class="section-sub">Jump straight into what you came here for.</p>
|
|
@@ -398,12 +453,21 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
398
453
|
<p class="section-sub">Manage this hub — vaults, permissions, tokens.</p>
|
|
399
454
|
<div class="grid" id="admin-grid"></div>
|
|
400
455
|
</section>
|
|
456
|
+
`;
|
|
401
457
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
<
|
|
458
|
+
// The slim signed-out landing. Brand + tagline (in the header above) plus a
|
|
459
|
+
// single clear "Sign in" call — no service catalog, no vault listings, no
|
|
460
|
+
// admin links. Keep it tasteful and minimal; the detail un-gates on sign-in.
|
|
461
|
+
const SIGNED_OUT_BODY = `
|
|
462
|
+
<section class="section signed-out-cta" id="signed-out-cta">
|
|
463
|
+
<p class="signed-out-lede">Sign in to reach your vault and the services on this hub.</p>
|
|
464
|
+
<a href="/login?next=/" class="btn-signin" data-testid="signed-out-signin">Sign in →</a>
|
|
465
|
+
</section>
|
|
466
|
+
`;
|
|
467
|
+
|
|
468
|
+
// The data-loading script for the signed-in discovery body. Emitted only
|
|
469
|
+
// when signed in (the signed-out body has nothing for it to populate).
|
|
470
|
+
const DISCOVERY_SCRIPT = `<script>
|
|
407
471
|
(async () => {
|
|
408
472
|
const servicesGrid = document.getElementById('services-grid');
|
|
409
473
|
const adminGrid = document.getElementById('admin-grid');
|
|
@@ -614,7 +678,4 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
614
678
|
|
|
615
679
|
void loadServices();
|
|
616
680
|
})();
|
|
617
|
-
</script
|
|
618
|
-
</body>
|
|
619
|
-
</html>
|
|
620
|
-
`;
|
|
681
|
+
</script>`;
|