@openparachute/hub 0.6.4-rc.4 → 0.6.4-rc.5
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 +182 -109
- package/src/__tests__/account-mirror.test.ts +156 -0
- package/src/__tests__/api-account.test.ts +51 -1
- package/src/account-home-ui.ts +176 -254
- package/src/account-mirror.ts +126 -0
- package/src/api-account.ts +49 -0
package/src/account-home-ui.ts
CHANGED
|
@@ -117,15 +117,16 @@ export interface RenderAccountHomeOpts {
|
|
|
117
117
|
*/
|
|
118
118
|
twoFactorEnabled: boolean;
|
|
119
119
|
/**
|
|
120
|
-
* Per-vault
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
120
|
+
* Per-vault verbs the user's assignment role permits. Maps `vaultName` → the
|
|
121
|
+
* verbs (today `["read", "write", "admin"]` since every `user_vaults.role` is
|
|
122
|
+
* `'write'`, which grants admin). Now used solely to GATE the per-tile
|
|
123
|
+
* "Advanced vault settings ↗" deep-link (and the "Back up to GitHub ↗" action)
|
|
124
|
+
* on the `admin` verb — the deep-link mints a `vault:<name>:admin` token, so
|
|
125
|
+
* the button never offers authority the POST handler would 403. (The old
|
|
126
|
+
* token-mint affordance this map also drove was dropped from `/account/`
|
|
127
|
+
* 2026-06-04 — OAuth-first; minting header-auth tokens is an advanced concern
|
|
128
|
+
* that lives in the vault config SPA.) Omitted (or empty) for the admin /
|
|
129
|
+
* no-vault branches, where no vault tile is shown.
|
|
129
130
|
*/
|
|
130
131
|
mintableVerbs?: Record<string, VaultVerb[]>;
|
|
131
132
|
/**
|
|
@@ -137,18 +138,46 @@ export interface RenderAccountHomeOpts {
|
|
|
137
138
|
* that don't resolve). Omitted entirely on the admin / no-vault branches.
|
|
138
139
|
*/
|
|
139
140
|
usageStats?: Record<string, string>;
|
|
141
|
+
/**
|
|
142
|
+
* Per-vault backup (mirror) line for each assigned vault tile. Maps
|
|
143
|
+
* `vaultName` → the warm, pre-formatted line ("Backed up — full version
|
|
144
|
+
* history", or "… + GitHub" when a push remote is configured). A vault absent
|
|
145
|
+
* from this map renders no backup line — the page tolerates a vault whose
|
|
146
|
+
* mirror endpoint failed / is unreachable / is backup-off (the `/account/` GET
|
|
147
|
+
* handler builds this map by fetching `/.parachute/mirror` per admin-held
|
|
148
|
+
* vault and omitting any that don't resolve or read backup-off). Omitted
|
|
149
|
+
* entirely on the admin / no-vault branches.
|
|
150
|
+
*/
|
|
151
|
+
mirrorLines?: Record<string, string>;
|
|
152
|
+
/**
|
|
153
|
+
* Per-vault "is backup already pushing to a remote?" flag (the vault's
|
|
154
|
+
* `config.auto_push`, threaded from `VaultMirrorStat.backedUpToRemote`). Maps
|
|
155
|
+
* `vaultName` → `true` when an auto-push remote is configured. Drives whether
|
|
156
|
+
* the tile suppresses the "Back up to GitHub ↗" action — gated on this proper
|
|
157
|
+
* boolean, NOT re-derived from the `mirrorLines` display string. A vault absent
|
|
158
|
+
* defaults to `false` (offer the action). Built alongside `mirrorLines` by the
|
|
159
|
+
* GET handler; omitted on the admin / no-vault branches.
|
|
160
|
+
*/
|
|
161
|
+
mirrorPushing?: Record<string, boolean>;
|
|
140
162
|
/**
|
|
141
163
|
* Set after a successful `POST /account/vault-token/<name>` to show the
|
|
142
164
|
* freshly-minted token ONCE (the only time it's ever shown — the hub keeps
|
|
143
165
|
* no plaintext copy). Drives the show-once banner at the top of the page.
|
|
144
166
|
* Absent on the normal GET render.
|
|
167
|
+
*
|
|
168
|
+
* NOT vestigial after the 2026-06-04 token-mint-UI removal: the page no
|
|
169
|
+
* longer renders the mint *form*, but the `POST /account/vault-token/<name>`
|
|
170
|
+
* route still exists (a script/advanced path) and on success re-renders THIS
|
|
171
|
+
* page with `mintedToken` set, so the show-once banner still fires for that
|
|
172
|
+
* flow. The renderer keeps the prop + banner for it.
|
|
145
173
|
*/
|
|
146
174
|
mintedToken?: MintedTokenView;
|
|
147
175
|
/**
|
|
148
176
|
* Set after a `POST /account/vault-token/<name>` that failed authorization
|
|
149
177
|
* or validation, to surface an inline error banner on the re-rendered page
|
|
150
178
|
* (e.g. unassigned vault, capped verb, rate-limited). Absent on success and
|
|
151
|
-
* on the normal GET render.
|
|
179
|
+
* on the normal GET render. Same non-vestigial note as `mintedToken`: the
|
|
180
|
+
* mint route still re-renders this page, so the error banner stays live.
|
|
152
181
|
*/
|
|
153
182
|
mintError?: string;
|
|
154
183
|
/**
|
|
@@ -210,6 +239,11 @@ export function renderAccountHome(opts: RenderAccountHomeOpts): string {
|
|
|
210
239
|
// condenses to a quiet "you're connected" line so it stops nagging. Shown
|
|
211
240
|
// only on the assigned-vault branch — the admin + no-vault branches have no
|
|
212
241
|
// single "your vault" to connect, so the checklist would be misleading there.
|
|
242
|
+
// TODO(multi-vault): `connectedVault` is true if ANY of the user's vaults has
|
|
243
|
+
// a grant (handler uses `.some(...)`), but the checklist shows the connect step
|
|
244
|
+
// for the FIRST vault only. With multiple vaults, connecting vault B condenses
|
|
245
|
+
// the checklist even though the displayed primary vault A isn't connected. Fine
|
|
246
|
+
// today — single-vault is the live case; revisit if multi-vault ships.
|
|
213
247
|
const checklist =
|
|
214
248
|
assignedVaults.length > 0
|
|
215
249
|
? renderOnboardingChecklist({
|
|
@@ -226,6 +260,8 @@ export function renderAccountHome(opts: RenderAccountHomeOpts): string {
|
|
|
226
260
|
csrfToken,
|
|
227
261
|
mintableVerbs: opts.mintableVerbs ?? {},
|
|
228
262
|
usageStats: opts.usageStats ?? {},
|
|
263
|
+
mirrorLines: opts.mirrorLines ?? {},
|
|
264
|
+
mirrorPushing: opts.mirrorPushing ?? {},
|
|
229
265
|
});
|
|
230
266
|
|
|
231
267
|
const accountCard = renderAccountCard({
|
|
@@ -352,7 +388,7 @@ function renderOnboardingChecklist(opts: OnboardingChecklistOpts): string {
|
|
|
352
388
|
</li>
|
|
353
389
|
</ol>
|
|
354
390
|
<p class="onboarding-foot" data-testid="onboarding-foot">Your vault is
|
|
355
|
-
<code>${safeVault}</code>.
|
|
391
|
+
<code>${safeVault}</code>. Its size, backup state, Notes, and advanced settings are
|
|
356
392
|
just below.</p>
|
|
357
393
|
</section>`;
|
|
358
394
|
}
|
|
@@ -403,6 +439,10 @@ interface VaultCardOpts {
|
|
|
403
439
|
mintableVerbs: Record<string, VaultVerb[]>;
|
|
404
440
|
/** `vaultName` → pre-formatted usage stat ("X notes · Y MB"). */
|
|
405
441
|
usageStats: Record<string, string>;
|
|
442
|
+
/** `vaultName` → pre-formatted backup line ("Backed up — full version history"). */
|
|
443
|
+
mirrorLines: Record<string, string>;
|
|
444
|
+
/** `vaultName` → is backup already pushing to a remote (gates the GitHub action). */
|
|
445
|
+
mirrorPushing: Record<string, boolean>;
|
|
406
446
|
}
|
|
407
447
|
|
|
408
448
|
/**
|
|
@@ -434,84 +474,56 @@ export function accountClaudeMcpAddCommand(trimmedOrigin: string, vaultName: str
|
|
|
434
474
|
function renderVaultCard(opts: VaultCardOpts): string {
|
|
435
475
|
const { assignedVaults, trimmedOrigin, isFirstAdmin, csrfToken, mintableVerbs, usageStats } =
|
|
436
476
|
opts;
|
|
477
|
+
const { mirrorLines, mirrorPushing } = opts;
|
|
437
478
|
|
|
438
479
|
if (assignedVaults.length > 0) {
|
|
439
|
-
// One vault tile per assignment (multi-user Phase 2 PR 2).
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
// operator docs and the friend's account page stay consistent.
|
|
449
|
-
//
|
|
450
|
-
// This closes the multi-user gap where the friend tile read as MCP
|
|
451
|
-
// jargon ("Connect an MCP client") rather than "here's how to connect
|
|
452
|
-
// this to your AI" — and where the web (Claude.ai) path was entirely
|
|
453
|
-
// missing, only the Claude Code CLI command was offered.
|
|
480
|
+
// One vault tile per assignment (multi-user Phase 2 PR 2). The tile is the
|
|
481
|
+
// everyday "here's your vault" detail card — name, size, backup state, a
|
|
482
|
+
// browser-UI on-ramp (Notes + "build your own"), and a single deep-link
|
|
483
|
+
// into the advanced vault settings SPA. It deliberately does NOT repeat the
|
|
484
|
+
// "Connect your AI" instructions: the onboarding checklist above owns that
|
|
485
|
+
// step (collapsing to "✓ You're connected" once a grant lands), so the
|
|
486
|
+
// page never shows the connect endpoint + both methods twice. Token minting
|
|
487
|
+
// + raw mirror config are advanced concerns that live in the vault config
|
|
488
|
+
// SPA, reached via "Advanced vault settings ↗" — not duplicated here.
|
|
454
489
|
const heading = assignedVaults.length === 1 ? "<h2>Your vault</h2>" : "<h2>Your vaults</h2>";
|
|
455
490
|
const tiles = assignedVaults
|
|
456
491
|
.map((vaultName) => {
|
|
457
492
|
const safeVault = escapeHtml(vaultName);
|
|
458
493
|
const vaultUrlForAdd = encodeURIComponent(`${trimmedOrigin}/vault/${vaultName}`);
|
|
459
|
-
const endpoint = accountMcpEndpoint(trimmedOrigin, vaultName);
|
|
460
|
-
const addCmd = accountClaudeMcpAddCommand(trimmedOrigin, vaultName);
|
|
461
|
-
const safeEndpoint = escapeHtml(endpoint);
|
|
462
|
-
const safeAddCmd = escapeHtml(addCmd);
|
|
463
494
|
const verbsForVault = mintableVerbs[vaultName] ?? [];
|
|
464
|
-
const
|
|
465
|
-
// "
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
// authority the POST handler would 403.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
: "";
|
|
495
|
+
const holdsAdmin = verbsForVault.includes("admin");
|
|
496
|
+
// "Advanced vault settings ↗" — only for users whose assignment grants
|
|
497
|
+
// `admin` (the verb the deep-link mints). Today every assigned user
|
|
498
|
+
// holds admin, but gate on the verb so the button never offers
|
|
499
|
+
// authority the POST handler would 403. The single advanced entry point:
|
|
500
|
+
// schema, tokens, retention, raw mirror config all live in the SPA.
|
|
501
|
+
const manageBlock = holdsAdmin ? renderVaultAdminLink(vaultName, csrfToken) : "";
|
|
472
502
|
// Compact usage stat ("X notes · Y MB"), when the vault's usage endpoint
|
|
473
503
|
// resolved. Omitted gracefully otherwise.
|
|
474
504
|
const usageStat = usageStats[vaultName];
|
|
475
505
|
const usageLine = usageStat
|
|
476
506
|
? `<p class="vault-usage" data-testid="vault-usage">${escapeHtml(usageStat)}</p>`
|
|
477
507
|
: "";
|
|
508
|
+
// Backup state line + a "Back up to GitHub ↗" deep-link when not already
|
|
509
|
+
// pushing. Both gated on admin: the backup line is only fetched for
|
|
510
|
+
// admin-held vaults (the mirror endpoint is admin-scoped), and the
|
|
511
|
+
// GitHub action reuses the same `/account/vault-admin-token/<name>`
|
|
512
|
+
// deep-link that opens the vault config SPA. Omitted silently when the
|
|
513
|
+
// mirror fetch failed / backup is off (the renderer just gets no entry).
|
|
514
|
+
const mirrorLine = mirrorLines[vaultName];
|
|
515
|
+
const backupBlock = renderBackupBlock(
|
|
516
|
+
vaultName,
|
|
517
|
+
mirrorLine,
|
|
518
|
+
mirrorPushing[vaultName] ?? false,
|
|
519
|
+
holdsAdmin,
|
|
520
|
+
csrfToken,
|
|
521
|
+
);
|
|
478
522
|
return `
|
|
479
523
|
<div class="vault-tile" data-testid="vault-tile" data-vault-name="${safeVault}">
|
|
480
524
|
<p class="vault-name"><strong>${safeVault}</strong></p>
|
|
481
525
|
${usageLine}
|
|
482
|
-
|
|
483
|
-
<p class="mcp-connect-label" data-testid="connect-ai-heading">Connect your AI
|
|
484
|
-
assistant to this vault</p>
|
|
485
|
-
<p class="mcp-connect-intro">Two common ways. Both sign you in to this hub over
|
|
486
|
-
HTTPS and ask you to approve access the first time — no token to copy.</p>
|
|
487
|
-
|
|
488
|
-
<div class="mcp-method" data-testid="connect-method-claude-code">
|
|
489
|
-
<p class="mcp-method-title">Claude Code (terminal)</p>
|
|
490
|
-
<p class="mcp-method-sub">Run this in your terminal:</p>
|
|
491
|
-
<div class="copy-row">
|
|
492
|
-
<code data-testid="mcp-add-command">${safeAddCmd}</code>
|
|
493
|
-
<button type="button" class="btn btn-copy" data-copy="${safeAddCmd}"
|
|
494
|
-
data-testid="copy-mcp-add-command">Copy</button>
|
|
495
|
-
</div>
|
|
496
|
-
</div>
|
|
497
|
-
|
|
498
|
-
<div class="mcp-method" data-testid="connect-method-claude-ai">
|
|
499
|
-
<p class="mcp-method-title">Claude.ai (web)</p>
|
|
500
|
-
<p class="mcp-method-sub">In Claude.ai, open <strong>Settings → Connectors</strong>,
|
|
501
|
-
choose <strong>Add custom connector</strong>, and paste this endpoint:</p>
|
|
502
|
-
<div class="copy-row">
|
|
503
|
-
<code data-testid="mcp-endpoint">${safeEndpoint}</code>
|
|
504
|
-
<button type="button" class="btn btn-copy" data-copy="${safeEndpoint}"
|
|
505
|
-
data-testid="copy-mcp-endpoint">Copy</button>
|
|
506
|
-
</div>
|
|
507
|
-
<p class="mcp-method-note">Claude.ai then redirects you here to sign in and
|
|
508
|
-
approve. (Your hub must be reachable from the web for this.)</p>
|
|
509
|
-
</div>
|
|
510
|
-
|
|
511
|
-
<p class="mcp-connect-hint" data-testid="connect-any-client-hint">Using something
|
|
512
|
-
else? Point any MCP client at the same endpoint above. (ChatGPT and some other
|
|
513
|
-
web UIs call these "connectors.")</p>
|
|
514
|
-
</div>
|
|
526
|
+
${backupBlock}
|
|
515
527
|
<p class="vault-notes-cta">
|
|
516
528
|
<a class="btn btn-primary" href="https://notes.parachute.computer/add?url=${vaultUrlForAdd}"
|
|
517
529
|
target="_blank" rel="noopener" data-testid="open-notes-cta">Open Notes ↗</a>
|
|
@@ -521,19 +533,21 @@ function renderVaultCard(opts: VaultCardOpts): string {
|
|
|
521
533
|
capture in this vault — or jump straight to bulk-importing Markdown/Obsidian
|
|
522
534
|
notes into it.</span>
|
|
523
535
|
</p>
|
|
536
|
+
<p class="vault-build-ui" data-testid="build-your-own-ui">Notes is just one way to see
|
|
537
|
+
your vault — when you're ready, your AI can build you a custom UI for it in a few
|
|
538
|
+
minutes. <a href="https://parachute.computer/onboarding/surface-build/"
|
|
539
|
+
target="_blank" rel="noopener" data-testid="build-your-own-ui-link">Build your own ↗</a></p>
|
|
524
540
|
${manageBlock}
|
|
525
|
-
${tokenMintBlock}
|
|
526
541
|
</div>`;
|
|
527
542
|
})
|
|
528
543
|
.join("");
|
|
529
544
|
return `
|
|
530
545
|
<section class="section" data-testid="vault-card">
|
|
531
546
|
${heading}
|
|
532
|
-
<p>
|
|
547
|
+
<p>Your vault${
|
|
533
548
|
assignedVaults.length === 1 ? "" : "s"
|
|
534
|
-
} —
|
|
535
|
-
|
|
536
|
-
to your hub over HTTPS and asks you to approve access.</p>
|
|
549
|
+
} at a glance — size, backup, and a browser UI. You connect your AI from the
|
|
550
|
+
steps above; the deeper settings live one click away.</p>
|
|
537
551
|
<div class="vault-tiles">${tiles}
|
|
538
552
|
</div>
|
|
539
553
|
</section>`;
|
|
@@ -563,83 +577,62 @@ function renderVaultCard(opts: VaultCardOpts): string {
|
|
|
563
577
|
}
|
|
564
578
|
|
|
565
579
|
/**
|
|
566
|
-
* The
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
* path for clients that can't do an interactive browser sign-in (cron jobs,
|
|
570
|
-
* headless agents, a `curl` script).
|
|
580
|
+
* The backup-state block on a vault tile: a warm, plain-language line telling
|
|
581
|
+
* the owner their vault is backed up (local version history, optionally pushed
|
|
582
|
+
* to GitHub), plus a "Back up to GitHub ↗" action when no push remote is set.
|
|
571
583
|
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
584
|
+
* `mirrorLine` is the pre-formatted backup line ("Backed up — full version
|
|
585
|
+
* history" / "… + GitHub") the GET handler built from the vault's mirror status,
|
|
586
|
+
* or `undefined` when the mirror fetch failed / backup is off / the user doesn't
|
|
587
|
+
* hold admin. When absent, the whole block is omitted silently — the everyday
|
|
588
|
+
* home never nags with a "not backed up" warning.
|
|
577
589
|
*
|
|
578
|
-
* The
|
|
579
|
-
* `/account/vault-token/<name>`
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
590
|
+
* The "Back up to GitHub ↗" action reuses the EXISTING
|
|
591
|
+
* `/account/vault-admin-token/<name>` deep-link (the same POST `renderVaultAdminLink`
|
|
592
|
+
* uses) — it mints a `vault:<name>:admin` token and opens the vault config SPA,
|
|
593
|
+
* where the GitHub push is configured. We do NOT invent a new auth path. It's
|
|
594
|
+
* shown only when the user holds admin AND `pushing` is false (not already
|
|
595
|
+
* pushing to a remote) — `pushing` is a proper boolean threaded from the mirror
|
|
596
|
+
* status (`VaultMirrorStat.backedUpToRemote`), never re-derived from the
|
|
597
|
+
* display string.
|
|
583
598
|
*/
|
|
584
|
-
function
|
|
599
|
+
function renderBackupBlock(
|
|
585
600
|
vaultName: string,
|
|
586
|
-
|
|
587
|
-
|
|
601
|
+
mirrorLine: string | undefined,
|
|
602
|
+
pushing: boolean,
|
|
603
|
+
holdsAdmin: boolean,
|
|
588
604
|
csrfToken: string,
|
|
589
605
|
): string {
|
|
590
|
-
if (
|
|
591
|
-
//
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
<span><strong>${verb}</strong> — ${label} access to <code>${safeVault}</code></span>
|
|
607
|
-
</label>`;
|
|
608
|
-
})
|
|
609
|
-
.join("");
|
|
606
|
+
if (!mirrorLine) return "";
|
|
607
|
+
// "Back up to GitHub ↗" — only when admin (the deep-link mints admin) and not
|
|
608
|
+
// already pushing. Reuses the vault-admin-token deep-link to open the SPA's
|
|
609
|
+
// backup page; no new auth path.
|
|
610
|
+
const action = escapeHtml(`/account/vault-admin-token/${encodeURIComponent(vaultName)}`);
|
|
611
|
+
const githubAction =
|
|
612
|
+
holdsAdmin && !pushing
|
|
613
|
+
? `
|
|
614
|
+
<form method="POST" action="${action}" class="vault-backup-github"
|
|
615
|
+
data-testid="backup-github-form">
|
|
616
|
+
${renderCsrfHiddenInput(csrfToken)}
|
|
617
|
+
<button type="submit" class="btn btn-secondary" data-testid="backup-github-button">
|
|
618
|
+
Back up to GitHub ↗
|
|
619
|
+
</button>
|
|
620
|
+
</form>`
|
|
621
|
+
: "";
|
|
610
622
|
return `
|
|
611
|
-
<
|
|
612
|
-
<
|
|
613
|
-
<span class="
|
|
614
|
-
|
|
615
|
-
<p class="token-mint-intro">Most clients should use the no-token
|
|
616
|
-
connect options above — they sign you in over HTTPS and never
|
|
617
|
-
ask you to paste a secret. Mint a token only for a script or
|
|
618
|
-
headless client that can't open a browser to sign in. It's a
|
|
619
|
-
bearer for <code>vault:${safeVault}:<verb></code>, scoped to
|
|
620
|
-
<strong>this vault only</strong>, and you'll see it once.</p>
|
|
621
|
-
<form method="POST" action="${action}" class="mint-form"
|
|
622
|
-
data-testid="mint-form">
|
|
623
|
-
${renderCsrfHiddenInput(csrfToken)}
|
|
624
|
-
<fieldset class="mint-verbs">
|
|
625
|
-
<legend>Access level</legend>${radios}
|
|
626
|
-
</fieldset>
|
|
627
|
-
<button type="submit" class="btn btn-secondary"
|
|
628
|
-
data-testid="mint-token-button">Mint token</button>
|
|
629
|
-
</form>
|
|
630
|
-
</div>
|
|
631
|
-
</details>`;
|
|
623
|
+
<div class="vault-backup" data-testid="vault-backup">
|
|
624
|
+
<p class="vault-backup-line" data-testid="backup-state-line">
|
|
625
|
+
<span class="vault-backup-check" aria-hidden="true">✓</span>${escapeHtml(mirrorLine)}</p>${githubAction}
|
|
626
|
+
</div>`;
|
|
632
627
|
}
|
|
633
628
|
|
|
634
629
|
/**
|
|
635
|
-
* The "
|
|
636
|
-
* POST form to `/account/vault-admin-token/<name>`
|
|
637
|
-
* `vault:<name>:admin` deep-link token and redirects into the
|
|
638
|
-
* SPA — where the assigned user can
|
|
639
|
-
*
|
|
640
|
-
* the
|
|
641
|
-
* action for an individual user, more prominent than the headless-token mint
|
|
642
|
-
* below it.
|
|
630
|
+
* The "Advanced vault settings ↗" affordance on a vault tile — the single
|
|
631
|
+
* advanced entry point. A small POST form to `/account/vault-admin-token/<name>`
|
|
632
|
+
* that mints a `vault:<name>:admin` deep-link token and redirects into the
|
|
633
|
+
* vault's own config SPA — where the assigned user can manage schema, rotate
|
|
634
|
+
* access tokens, set retention, and edit raw mirror/backup config. Shown only
|
|
635
|
+
* when the user's assignment grants `admin` (gated by the caller).
|
|
643
636
|
*
|
|
644
637
|
* No-JS posture: a same-origin form POST that 303-redirects on success, same
|
|
645
638
|
* shape as the other `/account/*` forms. CSRF-gated via the hidden field.
|
|
@@ -652,10 +645,10 @@ function renderVaultAdminLink(vaultName: string, csrfToken: string): string {
|
|
|
652
645
|
data-testid="vault-admin-form">
|
|
653
646
|
${renderCsrfHiddenInput(csrfToken)}
|
|
654
647
|
<button type="submit" class="btn btn-secondary" data-testid="vault-admin-button">
|
|
655
|
-
|
|
648
|
+
Advanced vault settings ↗
|
|
656
649
|
</button>
|
|
657
|
-
<span class="vault-admin-sub">Open this vault's
|
|
658
|
-
and
|
|
650
|
+
<span class="vault-admin-sub">Open this vault's config — schema, access tokens,
|
|
651
|
+
retention, and raw backup settings.</span>
|
|
659
652
|
</form>`;
|
|
660
653
|
}
|
|
661
654
|
|
|
@@ -766,8 +759,8 @@ const COPY_SCRIPT = `
|
|
|
766
759
|
//
|
|
767
760
|
// Same brand palette + font stack as account-change-password-ui.ts so the
|
|
768
761
|
// `/account/*` family is visually cohesive. Extra rules (.section, .kv,
|
|
769
|
-
// .vault-name, .
|
|
770
|
-
//
|
|
762
|
+
// .vault-name, .vault-backup, .vault-build-ui, .copy-row) describe the card +
|
|
763
|
+
// backup-state + onboarding-checklist shapes this page introduces.
|
|
771
764
|
|
|
772
765
|
const STYLES = `
|
|
773
766
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -989,6 +982,36 @@ const STYLES = `
|
|
|
989
982
|
color: ${PALETTE.fgMuted};
|
|
990
983
|
margin: 0 0 0.5rem;
|
|
991
984
|
}
|
|
985
|
+
.vault-backup { margin: 0 0 0.5rem; }
|
|
986
|
+
.vault-backup-line {
|
|
987
|
+
display: flex;
|
|
988
|
+
align-items: center;
|
|
989
|
+
gap: 0.4rem;
|
|
990
|
+
font-size: 0.85rem;
|
|
991
|
+
color: ${PALETTE.success};
|
|
992
|
+
margin: 0;
|
|
993
|
+
}
|
|
994
|
+
.vault-backup-check {
|
|
995
|
+
flex: 0 0 auto;
|
|
996
|
+
width: 1.1rem;
|
|
997
|
+
height: 1.1rem;
|
|
998
|
+
border-radius: 999px;
|
|
999
|
+
background: ${PALETTE.successSoft};
|
|
1000
|
+
color: ${PALETTE.success};
|
|
1001
|
+
border: 1px solid ${PALETTE.success};
|
|
1002
|
+
font-size: 0.7rem;
|
|
1003
|
+
display: inline-flex;
|
|
1004
|
+
align-items: center;
|
|
1005
|
+
justify-content: center;
|
|
1006
|
+
}
|
|
1007
|
+
.vault-backup-github { margin: 0.4rem 0 0; }
|
|
1008
|
+
.vault-build-ui {
|
|
1009
|
+
font-size: 0.82rem;
|
|
1010
|
+
color: ${PALETTE.fgMuted};
|
|
1011
|
+
margin: 0.6rem 0 0;
|
|
1012
|
+
padding-top: 0.6rem;
|
|
1013
|
+
border-top: 1px solid ${PALETTE.borderLight};
|
|
1014
|
+
}
|
|
992
1015
|
.vault-tiles {
|
|
993
1016
|
display: flex;
|
|
994
1017
|
flex-direction: column;
|
|
@@ -1004,52 +1027,6 @@ const STYLES = `
|
|
|
1004
1027
|
.vault-tile p { margin: 0.2rem 0; }
|
|
1005
1028
|
.vault-tile p:last-child { margin-top: 0.5rem; }
|
|
1006
1029
|
|
|
1007
|
-
.mcp-connect {
|
|
1008
|
-
margin-bottom: 0.75rem;
|
|
1009
|
-
}
|
|
1010
|
-
.mcp-connect-label {
|
|
1011
|
-
font-family: ${FONT_SERIF};
|
|
1012
|
-
font-size: 1.05rem;
|
|
1013
|
-
font-weight: 400;
|
|
1014
|
-
color: ${PALETTE.fg};
|
|
1015
|
-
margin: 0 0 0.3rem;
|
|
1016
|
-
}
|
|
1017
|
-
.mcp-connect-intro {
|
|
1018
|
-
font-size: 0.85rem;
|
|
1019
|
-
color: ${PALETTE.fgMuted};
|
|
1020
|
-
margin: 0 0 0.75rem;
|
|
1021
|
-
}
|
|
1022
|
-
.mcp-method {
|
|
1023
|
-
margin: 0.75rem 0;
|
|
1024
|
-
padding-top: 0.6rem;
|
|
1025
|
-
border-top: 1px solid ${PALETTE.borderLight};
|
|
1026
|
-
}
|
|
1027
|
-
.mcp-method-title {
|
|
1028
|
-
font-size: 0.9rem;
|
|
1029
|
-
font-weight: 600;
|
|
1030
|
-
color: ${PALETTE.fg};
|
|
1031
|
-
margin: 0 0 0.15rem;
|
|
1032
|
-
}
|
|
1033
|
-
.mcp-method-sub {
|
|
1034
|
-
font-size: 0.82rem;
|
|
1035
|
-
color: ${PALETTE.fgMuted};
|
|
1036
|
-
margin: 0 0 0.4rem;
|
|
1037
|
-
}
|
|
1038
|
-
.mcp-method-note {
|
|
1039
|
-
font-size: 0.78rem;
|
|
1040
|
-
color: ${PALETTE.fgMuted};
|
|
1041
|
-
margin: 0.35rem 0 0;
|
|
1042
|
-
}
|
|
1043
|
-
.mcp-field { margin: 0.5rem 0; }
|
|
1044
|
-
.mcp-field-label {
|
|
1045
|
-
display: block;
|
|
1046
|
-
font-size: 0.7rem;
|
|
1047
|
-
text-transform: uppercase;
|
|
1048
|
-
letter-spacing: 0.06em;
|
|
1049
|
-
color: ${PALETTE.fgMuted};
|
|
1050
|
-
font-family: ${FONT_MONO};
|
|
1051
|
-
margin-bottom: 0.2rem;
|
|
1052
|
-
}
|
|
1053
1030
|
.vault-notes-cta {
|
|
1054
1031
|
margin: 0.9rem 0 0;
|
|
1055
1032
|
padding-top: 0.6rem;
|
|
@@ -1090,11 +1067,6 @@ const STYLES = `
|
|
|
1090
1067
|
border-color: ${PALETTE.border};
|
|
1091
1068
|
}
|
|
1092
1069
|
.btn-copy:hover { background: ${PALETTE.bgSoft}; border-color: ${PALETTE.accent}; }
|
|
1093
|
-
.mcp-connect-hint {
|
|
1094
|
-
font-size: 0.82rem;
|
|
1095
|
-
color: ${PALETTE.fgMuted};
|
|
1096
|
-
margin: 0.4rem 0 0;
|
|
1097
|
-
}
|
|
1098
1070
|
|
|
1099
1071
|
.vault-admin-link {
|
|
1100
1072
|
margin: 0.9rem 0 0;
|
|
@@ -1111,53 +1083,6 @@ const STYLES = `
|
|
|
1111
1083
|
flex: 1 1 12rem;
|
|
1112
1084
|
}
|
|
1113
1085
|
|
|
1114
|
-
.token-mint {
|
|
1115
|
-
margin: 0.9rem 0 0;
|
|
1116
|
-
padding-top: 0.6rem;
|
|
1117
|
-
border-top: 1px solid ${PALETTE.borderLight};
|
|
1118
|
-
}
|
|
1119
|
-
.token-mint > summary {
|
|
1120
|
-
cursor: pointer;
|
|
1121
|
-
font-size: 0.88rem;
|
|
1122
|
-
font-weight: 600;
|
|
1123
|
-
color: ${PALETTE.fg};
|
|
1124
|
-
list-style: revert;
|
|
1125
|
-
}
|
|
1126
|
-
.token-mint-sub {
|
|
1127
|
-
font-weight: 400;
|
|
1128
|
-
font-size: 0.8rem;
|
|
1129
|
-
color: ${PALETTE.fgMuted};
|
|
1130
|
-
}
|
|
1131
|
-
.token-mint-body { margin-top: 0.6rem; }
|
|
1132
|
-
.token-mint-intro {
|
|
1133
|
-
font-size: 0.8rem;
|
|
1134
|
-
color: ${PALETTE.fgMuted};
|
|
1135
|
-
margin: 0 0 0.6rem;
|
|
1136
|
-
}
|
|
1137
|
-
.mint-verbs {
|
|
1138
|
-
border: 1px solid ${PALETTE.borderLight};
|
|
1139
|
-
border-radius: 6px;
|
|
1140
|
-
padding: 0.5rem 0.7rem;
|
|
1141
|
-
margin: 0 0 0.6rem;
|
|
1142
|
-
}
|
|
1143
|
-
.mint-verbs legend {
|
|
1144
|
-
font-size: 0.7rem;
|
|
1145
|
-
text-transform: uppercase;
|
|
1146
|
-
letter-spacing: 0.06em;
|
|
1147
|
-
color: ${PALETTE.fgMuted};
|
|
1148
|
-
font-family: ${FONT_MONO};
|
|
1149
|
-
padding: 0 0.3rem;
|
|
1150
|
-
}
|
|
1151
|
-
.mint-verb-option {
|
|
1152
|
-
display: flex;
|
|
1153
|
-
align-items: baseline;
|
|
1154
|
-
gap: 0.5rem;
|
|
1155
|
-
font-size: 0.85rem;
|
|
1156
|
-
margin: 0.3rem 0;
|
|
1157
|
-
}
|
|
1158
|
-
.mint-verb-option input { margin: 0; }
|
|
1159
|
-
.mint-form .btn { margin-top: 0.2rem; }
|
|
1160
|
-
|
|
1161
1086
|
.minted-banner {
|
|
1162
1087
|
border: 1px solid ${PALETTE.accent};
|
|
1163
1088
|
background: ${PALETTE.accentSoft};
|
|
@@ -1266,19 +1191,18 @@ const STYLES = `
|
|
|
1266
1191
|
body { background: #1a1815; color: #e8e4dc; }
|
|
1267
1192
|
.card { background: #25221d; border-color: #3a362f; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); }
|
|
1268
1193
|
h1, h2 { color: #f0ece4; }
|
|
1269
|
-
.subtitle, .kv dt,
|
|
1270
|
-
.
|
|
1271
|
-
.vault-notes-cta-sub, .vault-usage,
|
|
1194
|
+
.subtitle, .kv dt,
|
|
1195
|
+
.vault-notes-cta-sub, .vault-usage, .vault-build-ui,
|
|
1272
1196
|
.onboarding-intro, .onboarding-step-sub, .onboarding-method,
|
|
1273
1197
|
.onboarding-foot { color: #a8a29a; }
|
|
1274
|
-
.vault-name strong,
|
|
1198
|
+
.vault-name strong,
|
|
1275
1199
|
.onboarding-step-title, .onboarding-method strong,
|
|
1276
1200
|
.onboarding-done-line { color: #f0ece4; }
|
|
1277
1201
|
.onboarding-step-done .onboarding-step-title { color: #a8a29a; }
|
|
1278
1202
|
code { background: #1f1c18; color: #e8e4dc; }
|
|
1279
1203
|
.copy-row code { background: transparent; }
|
|
1280
1204
|
.section { border-top-color: #3a362f; }
|
|
1281
|
-
.
|
|
1205
|
+
.vault-notes-cta, .vault-build-ui,
|
|
1282
1206
|
.vault-admin-link, .account-security { border-top-color: #3a362f; }
|
|
1283
1207
|
.vault-admin-sub { color: #a8a29a; }
|
|
1284
1208
|
.get-started h3 { color: #f0ece4; }
|
|
@@ -1291,9 +1215,7 @@ const STYLES = `
|
|
|
1291
1215
|
.copy-row { background: #1f1c18; border-color: #3a362f; }
|
|
1292
1216
|
.btn-secondary, .btn-copy { color: #e8e4dc; border-color: #3a362f; }
|
|
1293
1217
|
.btn-secondary:hover, .btn-copy:hover { background: #1f1c18; border-color: ${PALETTE.accent}; }
|
|
1294
|
-
.
|
|
1295
|
-
.token-mint-sub, .token-mint-intro, .mint-verbs legend, .minted-hint { color: #a8a29a; }
|
|
1296
|
-
.mint-verbs { border-color: #3a362f; }
|
|
1218
|
+
.minted-hint { color: #a8a29a; }
|
|
1297
1219
|
.minted-title, .minted-warn { color: #f0ece4; }
|
|
1298
1220
|
}
|
|
1299
1221
|
`;
|