@openparachute/hub 0.5.14-rc.8 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-ops.test.ts +45 -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__/expose-2fa-warning.test.ts +31 -17
- package/src/__tests__/expose-auth-preflight.test.ts +71 -72
- package/src/__tests__/expose-cloudflare.test.ts +482 -14
- package/src/__tests__/expose.test.ts +52 -2
- package/src/__tests__/hub-server.test.ts +97 -0
- package/src/__tests__/hub.test.ts +85 -6
- package/src/__tests__/init.test.ts +102 -1
- package/src/__tests__/lifecycle.test.ts +464 -2
- 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__/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 +335 -15
- 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 +47 -6
- package/src/__tests__/vault-hub-origin-env.test.ts +263 -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-revoke-token.ts +107 -21
- package/src/api-users.ts +29 -3
- package/src/cli.ts +26 -21
- package/src/clients.ts +18 -6
- package/src/cloudflare/config.ts +10 -4
- package/src/cloudflare/detect.ts +39 -44
- 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 +370 -12
- package/src/commands/expose.ts +8 -0
- package/src/commands/init.ts +33 -2
- package/src/commands/lifecycle.ts +386 -17
- package/src/commands/status.ts +22 -0
- package/src/commands/upgrade.ts +55 -11
- package/src/commands/wizard.ts +8 -4
- package/src/env-file.ts +10 -0
- package/src/help.ts +3 -1
- package/src/hub-db.ts +39 -1
- package/src/hub-server.ts +52 -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/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 +77 -7
- 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 +71 -19
- 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
package/src/commands/wizard.ts
CHANGED
|
@@ -76,8 +76,9 @@ export interface RunCliWizardOpts {
|
|
|
76
76
|
sleep?: (ms: number) => Promise<void>;
|
|
77
77
|
/**
|
|
78
78
|
* Non-interactive escape hatch: pre-supply the account-step answers.
|
|
79
|
-
* Username defaults to `
|
|
80
|
-
*
|
|
79
|
+
* Username defaults to `owner` when unset (aligned with
|
|
80
|
+
* `parachute auth set-password` + the operator.token convention); password
|
|
81
|
+
* is required (no default, no prompt → exit-with-error). Mirrors the
|
|
81
82
|
* `PARACHUTE_INITIAL_ADMIN_*` env-seed shape.
|
|
82
83
|
*/
|
|
83
84
|
accountUsername?: string;
|
|
@@ -400,8 +401,11 @@ async function walkAccountStep(
|
|
|
400
401
|
log(" Set up the operator account that owns this hub.");
|
|
401
402
|
let username = opts.accountUsername;
|
|
402
403
|
if (username === undefined) {
|
|
403
|
-
|
|
404
|
-
|
|
404
|
+
// Default to "owner" — aligns with `parachute auth set-password` and the
|
|
405
|
+
// operator.token convention (the earliest-created user is the operator).
|
|
406
|
+
// The web wizard lets the operator name it freely; so does this prompt.
|
|
407
|
+
const raw = (await opts.prompt(" username [owner]: ")).trim();
|
|
408
|
+
username = raw === "" ? "owner" : raw;
|
|
405
409
|
}
|
|
406
410
|
let password = opts.accountPassword;
|
|
407
411
|
if (password === undefined) {
|
package/src/env-file.ts
CHANGED
|
@@ -68,6 +68,16 @@ export function upsertEnvLine(lines: string[], key: string, value: string): stri
|
|
|
68
68
|
return next;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Drop every `KEY=…` line for `key`, preserving the order of the rest.
|
|
73
|
+
* Returns a new array; the input is untouched. No-op (returns a copy) when
|
|
74
|
+
* the key isn't present.
|
|
75
|
+
*/
|
|
76
|
+
export function removeEnvLine(lines: string[], key: string): string[] {
|
|
77
|
+
const prefix = `${key}=`;
|
|
78
|
+
return lines.filter((line) => !line.startsWith(prefix));
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
export function writeEnvFile(path: string, lines: readonly string[]): void {
|
|
72
82
|
mkdirSync(dirname(path), { recursive: true });
|
|
73
83
|
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
package/src/help.ts
CHANGED
|
@@ -194,7 +194,7 @@ Flags:
|
|
|
194
194
|
http://127.0.0.1:1939). \`parachute init\`
|
|
195
195
|
passes this in when chaining; standalone
|
|
196
196
|
callers supply it explicitly.
|
|
197
|
-
--account-username <name> pre-supply the admin username (default:
|
|
197
|
+
--account-username <name> pre-supply the admin username (default: owner)
|
|
198
198
|
--account-password <pw> pre-supply the admin password (required when
|
|
199
199
|
non-interactive)
|
|
200
200
|
--bootstrap-token <token> one-time bootstrap token when the hub is in
|
|
@@ -326,6 +326,7 @@ Usage:
|
|
|
326
326
|
parachute expose public --tailnet
|
|
327
327
|
parachute expose public --cloudflare --domain <hostname>
|
|
328
328
|
parachute expose public off --cloudflare
|
|
329
|
+
parachute expose cloudflare --domain <hostname> # alias for the above
|
|
329
330
|
|
|
330
331
|
Status:
|
|
331
332
|
tailnet is the supported exposure shape. The hub's OAuth + per-module
|
|
@@ -381,6 +382,7 @@ Examples:
|
|
|
381
382
|
parachute expose public off # stop the Funnel
|
|
382
383
|
parachute expose public --cloudflare --domain vault.example.com
|
|
383
384
|
# stable URL via cloudflared
|
|
385
|
+
parachute expose cloudflare --domain vault.example.com # alias for the line above
|
|
384
386
|
parachute expose public off --cloudflare # stop the cloudflared tunnel
|
|
385
387
|
|
|
386
388
|
Tailscale Funnel constraints:
|
package/src/hub-db.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Hub-local SQLite database. Opens `~/.parachute/hub.db` (overridable via
|
|
3
3
|
* `$PARACHUTE_HOME`). Holds everything the hub owns as the ecosystem's OAuth
|
|
4
4
|
* issuer — signing keys (v1), users + opaque refresh tokens (v2), OAuth
|
|
5
|
-
* clients + auth-codes + grants + browser sessions (v3)
|
|
5
|
+
* clients + auth-codes + grants + browser sessions (v3), and TOTP 2FA
|
|
6
|
+
* enrollment on the users row (v11, hub#473).
|
|
6
7
|
*
|
|
7
8
|
* Each open() runs `migrate()` to bring the schema up to date. A
|
|
8
9
|
* `schema_version` table records every applied migration so re-opens are
|
|
@@ -320,6 +321,43 @@ const MIGRATIONS: readonly Migration[] = [
|
|
|
320
321
|
ALTER TABLE users DROP COLUMN assigned_vault;
|
|
321
322
|
`,
|
|
322
323
|
},
|
|
324
|
+
{
|
|
325
|
+
version: 11,
|
|
326
|
+
sql: `
|
|
327
|
+
-- Real TOTP 2FA at the hub login layer (hub#473). Three nullable
|
|
328
|
+
-- columns on \`users\`:
|
|
329
|
+
--
|
|
330
|
+
-- * totp_secret (TEXT, nullable) — the base32-encoded RFC 6238 TOTP
|
|
331
|
+
-- secret. NULL means "2FA not enrolled" — the canonical "is 2FA on
|
|
332
|
+
-- for this user?" signal. Stored as the plaintext base32 string
|
|
333
|
+
-- (not encrypted at rest): hub.db already holds the argon2id
|
|
334
|
+
-- password hashes AND the OAuth signing private keys in plaintext
|
|
335
|
+
-- PEM (signing_keys.private_key_pem), so the TOTP secret sits at
|
|
336
|
+
-- the same operator-local trust boundary — encrypting one column
|
|
337
|
+
-- while leaving the signing key in the clear would be security
|
|
338
|
+
-- theatre. A future at-rest-encryption pass (hub#474 follow-up)
|
|
339
|
+
-- would cover all three (password hashes are already one-way; the
|
|
340
|
+
-- signing key + TOTP secret are the recoverable secrets).
|
|
341
|
+
-- * totp_backup_codes (TEXT, nullable) — JSON array of argon2id-HASHED
|
|
342
|
+
-- single-use recovery codes. Same hash family as passwords
|
|
343
|
+
-- (@node-rs/argon2). Plaintext codes are shown to the user exactly
|
|
344
|
+
-- once at enrollment and never stored. A code is removed from the
|
|
345
|
+
-- array when consumed. NULL / "[]" means "no backup codes left."
|
|
346
|
+
-- * totp_enrolled_at (TEXT, nullable) — ISO-8601 timestamp of the
|
|
347
|
+
-- last successful enrollment. NULL until first enroll; informational
|
|
348
|
+
-- (admin UI / account page "2FA enabled since …").
|
|
349
|
+
--
|
|
350
|
+
-- Backfill: every existing user pre-dates this migration and gets NULL
|
|
351
|
+
-- for all three — i.e. "2FA not enrolled." Their /login flow stays
|
|
352
|
+
-- password-only (the login handler only requires a TOTP step when
|
|
353
|
+
-- totp_secret IS NOT NULL), so existing operators keep signing in
|
|
354
|
+
-- exactly as before. No backfill UPDATE needed — the column default is
|
|
355
|
+
-- NULL.
|
|
356
|
+
ALTER TABLE users ADD COLUMN totp_secret TEXT;
|
|
357
|
+
ALTER TABLE users ADD COLUMN totp_backup_codes TEXT;
|
|
358
|
+
ALTER TABLE users ADD COLUMN totp_enrolled_at TEXT;
|
|
359
|
+
`,
|
|
360
|
+
},
|
|
323
361
|
];
|
|
324
362
|
|
|
325
363
|
export function openHubDb(path: string = hubDbPath()): Database {
|
package/src/hub-server.ts
CHANGED
|
@@ -67,11 +67,20 @@
|
|
|
67
67
|
* /api/users/<id> (DELETE) → hard-delete user + revoke tokens (host:admin)
|
|
68
68
|
* /api/users/<id>/reset-password (POST) → admin-initiated password reset (host:admin)
|
|
69
69
|
* /login (GET + POST) → operator password login
|
|
70
|
+
* /login/2fa (POST) → second-factor (TOTP/backup) step
|
|
71
|
+
* (hub#473; reached after a correct
|
|
72
|
+
* password for a 2FA-enrolled user)
|
|
70
73
|
* /logout (POST) → end admin session
|
|
71
74
|
* /account/change-password (GET + POST) → user self-service change-password
|
|
72
75
|
* (force-redirect target for users
|
|
73
76
|
* with password_changed=false; also
|
|
74
77
|
* reachable directly to rotate)
|
|
78
|
+
* /account/2fa (GET + POST) → user self-service 2FA enroll/disenroll
|
|
79
|
+
* (hub#473; QR + backup codes)
|
|
80
|
+
* /account/vault-token/<name> (POST) → friend mints a scoped
|
|
81
|
+
* vault:<name>:read|write bearer for
|
|
82
|
+
* an ASSIGNED vault (headless clients;
|
|
83
|
+
* session + assignment + scope-capped)
|
|
75
84
|
* /admin/config* → 301 → /admin/vaults (legacy
|
|
76
85
|
* portal retired post-SPA-rework)
|
|
77
86
|
*
|
|
@@ -108,11 +117,13 @@ import { existsSync } from "node:fs";
|
|
|
108
117
|
import { dirname, join, resolve } from "node:path";
|
|
109
118
|
import { fileURLToPath } from "node:url";
|
|
110
119
|
import pkg from "../package.json" with { type: "json" };
|
|
120
|
+
import { handleAccountVaultTokenPost } from "./account-vault-token.ts";
|
|
111
121
|
import { handleApproveClient, handleGetClient } from "./admin-clients.ts";
|
|
112
122
|
import { handleListGrants, handleRevokeGrant } from "./admin-grants.ts";
|
|
113
123
|
import {
|
|
114
124
|
handleAdminLoginGet,
|
|
115
125
|
handleAdminLoginPost,
|
|
126
|
+
handleAdminLoginTotpPost,
|
|
116
127
|
handleAdminLogoutPost,
|
|
117
128
|
} from "./admin-handlers.ts";
|
|
118
129
|
import { handleHostAdminToken } from "./admin-host-admin-token.ts";
|
|
@@ -199,6 +210,7 @@ import {
|
|
|
199
210
|
} from "./setup-wizard.ts";
|
|
200
211
|
import { getAllPublicKeys } from "./signing-keys.ts";
|
|
201
212
|
import type { Supervisor } from "./supervisor.ts";
|
|
213
|
+
import { handleTwoFactorGet, handleTwoFactorPost } from "./two-factor-handlers.ts";
|
|
202
214
|
import { getUserById, userCount } from "./users.ts";
|
|
203
215
|
import {
|
|
204
216
|
WELL_KNOWN_DIR,
|
|
@@ -2029,6 +2041,17 @@ export function hubFetch(
|
|
|
2029
2041
|
return new Response("method not allowed", { status: 405 });
|
|
2030
2042
|
}
|
|
2031
2043
|
|
|
2044
|
+
// /login/2fa — second-factor step (hub#473). POST-only: reached only
|
|
2045
|
+
// after a correct password POST for a 2FA-enrolled user handed back a
|
|
2046
|
+
// pending-login cookie + rendered the challenge page. A bare GET (e.g.
|
|
2047
|
+
// browser back button) has no form to render usefully, so 405 → the
|
|
2048
|
+
// operator restarts at /login.
|
|
2049
|
+
if (pathname === "/login/2fa") {
|
|
2050
|
+
if (!getDb) return dbNotConfigured();
|
|
2051
|
+
if (req.method === "POST") return handleAdminLoginTotpPost(getDb(), req);
|
|
2052
|
+
return new Response("method not allowed", { status: 405 });
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2032
2055
|
if (pathname === "/logout") {
|
|
2033
2056
|
if (!getDb) return dbNotConfigured();
|
|
2034
2057
|
if (req.method !== "POST") return new Response("method not allowed", { status: 405 });
|
|
@@ -2056,6 +2079,35 @@ export function hubFetch(
|
|
|
2056
2079
|
return new Response("method not allowed", { status: 405 });
|
|
2057
2080
|
}
|
|
2058
2081
|
|
|
2082
|
+
// /account/2fa — user self-service TOTP 2FA enroll / disenroll (hub#473).
|
|
2083
|
+
// Both GET (render state) and POST (start/confirm/disable) require an
|
|
2084
|
+
// active session; the handler does the session check + 302 to /login when
|
|
2085
|
+
// missing, same posture as /account/change-password.
|
|
2086
|
+
if (pathname === "/account/2fa") {
|
|
2087
|
+
if (!getDb) return dbNotConfigured();
|
|
2088
|
+
const twoFactorDeps = { db: getDb() };
|
|
2089
|
+
if (req.method === "GET") return handleTwoFactorGet(req, twoFactorDeps);
|
|
2090
|
+
if (req.method === "POST") return handleTwoFactorPost(req, twoFactorDeps);
|
|
2091
|
+
return new Response("method not allowed", { status: 405 });
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
// /account/vault-token/<name> — friend-facing scoped vault token mint.
|
|
2095
|
+
// POST-only, session-gated, assignment-capped: a non-admin friend mints a
|
|
2096
|
+
// `vault:<name>:read|write` bearer for a vault they're ASSIGNED to, for
|
|
2097
|
+
// scripts / headless clients that can't do browser OAuth. The handler
|
|
2098
|
+
// enforces session → assignment → scope-cap (never `:admin`, never a
|
|
2099
|
+
// vault outside the assignment, never a broader verb than the role
|
|
2100
|
+
// grants) + CSRF + per-user rate limit. Must precede the `/account/`
|
|
2101
|
+
// match below (more specific prefix). See `account-vault-token.ts`.
|
|
2102
|
+
if (pathname.startsWith("/account/vault-token/")) {
|
|
2103
|
+
if (!getDb) return dbNotConfigured();
|
|
2104
|
+
if (req.method !== "POST") return new Response("method not allowed", { status: 405 });
|
|
2105
|
+
const vaultName = decodeURIComponent(pathname.slice("/account/vault-token/".length));
|
|
2106
|
+
const db = getDb();
|
|
2107
|
+
const hubOrigin = resolveIssuer(req, db, configuredIssuer);
|
|
2108
|
+
return handleAccountVaultTokenPost(req, vaultName, { db, hubOrigin });
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2059
2111
|
// /account/ — friend-facing user home (multi-user Phase 1 follow-up).
|
|
2060
2112
|
// Companion to the first-admin gate on `/admin/host-admin-token`: a
|
|
2061
2113
|
// signed-in non-admin (friend) lands here instead of bouncing against
|
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,15 +84,34 @@ 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 {
|
|
91
103
|
// Inline POST form so sign-out works without JS. Submit button is
|
|
92
104
|
// styled as a text link via `.auth-signout` so the visual weight
|
|
93
105
|
// matches the surrounding "Signed in as <name>" text.
|
|
106
|
+
//
|
|
107
|
+
// The "Account" link is the single breadcrumb to `/account/` — the
|
|
108
|
+
// self-service home where any signed-in user (admin or invited
|
|
109
|
+
// member) can change their password, see their vault, and sign out.
|
|
110
|
+
// Without it, a friend who's been handed credentials has no way to
|
|
111
|
+
// discover the change-password surface after the first-login prompt.
|
|
94
112
|
return `<div class="auth-indicator">
|
|
95
113
|
<span class="muted">Signed in as <strong>${escapeHtml(displayName)}</strong></span>
|
|
114
|
+
<a href="/account/" class="auth-account">Account</a>
|
|
96
115
|
<form method="POST" action="/logout" class="auth-signout-form">
|
|
97
116
|
<input type="hidden" name="${CSRF_FIELD_NAME}" value="${escapeAttr(csrfToken)}" />
|
|
98
117
|
<button type="submit" class="auth-signout">Sign out</button>
|
|
@@ -203,7 +222,7 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
203
222
|
margin: 0;
|
|
204
223
|
display: inline;
|
|
205
224
|
}
|
|
206
|
-
.auth-signout, .auth-signin {
|
|
225
|
+
.auth-signout, .auth-signin, .auth-account {
|
|
207
226
|
background: none;
|
|
208
227
|
border: none;
|
|
209
228
|
padding: 0;
|
|
@@ -214,10 +233,10 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
214
233
|
text-decoration-thickness: 1px;
|
|
215
234
|
text-underline-offset: 2px;
|
|
216
235
|
}
|
|
217
|
-
.auth-signout:hover, .auth-signin:hover {
|
|
236
|
+
.auth-signout:hover, .auth-signin:hover, .auth-account:hover {
|
|
218
237
|
color: var(--accent-hover);
|
|
219
238
|
}
|
|
220
|
-
a.auth-signin {
|
|
239
|
+
a.auth-signin, a.auth-account {
|
|
221
240
|
/* Anchor needs explicit reset since the a element has its own
|
|
222
241
|
color/decoration. */
|
|
223
242
|
border-bottom: none;
|
|
@@ -262,6 +281,34 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
262
281
|
font-size: 0.92rem;
|
|
263
282
|
margin: 0 0 1.25rem;
|
|
264
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
|
+
}
|
|
265
312
|
.grid {
|
|
266
313
|
display: grid;
|
|
267
314
|
gap: 1.25rem;
|
|
@@ -371,7 +418,22 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
371
418
|
<h1>${WORDMARK_TEXT}</h1>
|
|
372
419
|
<p class="tagline">${CANONICAL_TAGLINE}</p>
|
|
373
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
|
+
`;
|
|
374
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 = `
|
|
375
437
|
<section class="section" id="get-started-section" hidden>
|
|
376
438
|
<h2>Get started</h2>
|
|
377
439
|
<p class="section-sub">Jump straight into what you came here for.</p>
|
|
@@ -391,12 +453,21 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
391
453
|
<p class="section-sub">Manage this hub — vaults, permissions, tokens.</p>
|
|
392
454
|
<div class="grid" id="admin-grid"></div>
|
|
393
455
|
</section>
|
|
456
|
+
`;
|
|
394
457
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
<
|
|
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>
|
|
400
471
|
(async () => {
|
|
401
472
|
const servicesGrid = document.getElementById('services-grid');
|
|
402
473
|
const adminGrid = document.getElementById('admin-grid');
|
|
@@ -607,7 +678,4 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
607
678
|
|
|
608
679
|
void loadServices();
|
|
609
680
|
})();
|
|
610
|
-
</script
|
|
611
|
-
</body>
|
|
612
|
-
</html>
|
|
613
|
-
`;
|
|
681
|
+
</script>`;
|