@openparachute/hub 0.6.3-rc.4 → 0.6.3
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
CHANGED
|
@@ -175,6 +175,103 @@ describe("renderAccountHome", () => {
|
|
|
175
175
|
expect(html).not.toContain('data-testid="mcp-connect"');
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
+
test("get-started card — links to the two onboarding prompts, placed before the vault card", () => {
|
|
179
|
+
const html = renderAccountHome({
|
|
180
|
+
username: "alice",
|
|
181
|
+
assignedVaults: ["alice"],
|
|
182
|
+
passwordChanged: true,
|
|
183
|
+
hubOrigin: HUB_ORIGIN,
|
|
184
|
+
isFirstAdmin: false,
|
|
185
|
+
csrfToken: CSRF,
|
|
186
|
+
twoFactorEnabled: false,
|
|
187
|
+
});
|
|
188
|
+
// The card renders with both onboarding-prompt links (mirrors the
|
|
189
|
+
// operator setup-wizard's starter-prompts section).
|
|
190
|
+
expect(html).toContain('data-testid="get-started-card"');
|
|
191
|
+
expect(html).toContain("Get started with your AI");
|
|
192
|
+
expect(html).toContain("https://parachute.computer/onboarding/vault-setup/");
|
|
193
|
+
expect(html).toContain("https://parachute.computer/onboarding/surface-build/");
|
|
194
|
+
expect(html).toContain('data-testid="starter-vault-setup"');
|
|
195
|
+
expect(html).toContain('data-testid="starter-surface-build"');
|
|
196
|
+
// External links open safely.
|
|
197
|
+
expect(html).toContain('rel="noopener"');
|
|
198
|
+
// Placed prominently — before the vault card in document order.
|
|
199
|
+
expect(html.indexOf('data-testid="get-started-card"')).toBeLessThan(
|
|
200
|
+
html.indexOf('data-testid="vault-card"'),
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("get-started card — present on the admin branch, hidden on the no-vault branch", () => {
|
|
205
|
+
// The on-ramp belongs on branches that have a vault to act against (admin +
|
|
206
|
+
// assigned-vault). It's suppressed on the no-vault branch, where the page
|
|
207
|
+
// says "You don't have a vault yet" — a do-the-thing card there would
|
|
208
|
+
// contradict the you-lack-the-prerequisite message.
|
|
209
|
+
const admin = renderAccountHome({
|
|
210
|
+
username: "admin",
|
|
211
|
+
assignedVaults: [],
|
|
212
|
+
passwordChanged: true,
|
|
213
|
+
hubOrigin: HUB_ORIGIN,
|
|
214
|
+
isFirstAdmin: true,
|
|
215
|
+
csrfToken: CSRF,
|
|
216
|
+
twoFactorEnabled: false,
|
|
217
|
+
});
|
|
218
|
+
expect(admin).toContain('data-testid="get-started-card"');
|
|
219
|
+
|
|
220
|
+
const noVault = renderAccountHome({
|
|
221
|
+
username: "ghost",
|
|
222
|
+
assignedVaults: [],
|
|
223
|
+
passwordChanged: true,
|
|
224
|
+
hubOrigin: HUB_ORIGIN,
|
|
225
|
+
isFirstAdmin: false,
|
|
226
|
+
csrfToken: CSRF,
|
|
227
|
+
twoFactorEnabled: false,
|
|
228
|
+
});
|
|
229
|
+
// No-vault branch: card suppressed, and the no-vault message stands alone.
|
|
230
|
+
expect(noVault).not.toContain('data-testid="get-started-card"');
|
|
231
|
+
expect(noVault).toContain('data-testid="no-vault-card"');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("connect-any-client hint bridges MCP ↔ ChatGPT 'connector' terminology", () => {
|
|
235
|
+
const html = renderAccountHome({
|
|
236
|
+
username: "alice",
|
|
237
|
+
assignedVaults: ["alice"],
|
|
238
|
+
passwordChanged: true,
|
|
239
|
+
hubOrigin: HUB_ORIGIN,
|
|
240
|
+
isFirstAdmin: false,
|
|
241
|
+
csrfToken: CSRF,
|
|
242
|
+
twoFactorEnabled: false,
|
|
243
|
+
});
|
|
244
|
+
// The "any other client" hint now names the ChatGPT "connector" term so
|
|
245
|
+
// a friend who only knows that word can find the right place to paste.
|
|
246
|
+
// Assert the NEW hint string specifically — a bare toContain("connector")
|
|
247
|
+
// was already satisfied pre-PR by the Claude.ai "Connectors" block, so it
|
|
248
|
+
// wouldn't catch a regression that drops this bridging copy.
|
|
249
|
+
expect(html).toContain('data-testid="connect-any-client-hint"');
|
|
250
|
+
expect(html).toContain('call these "connectors."');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("account card — security actions collapse into a secondary <details>", () => {
|
|
254
|
+
const html = renderAccountHome({
|
|
255
|
+
username: "alice",
|
|
256
|
+
assignedVaults: ["alice"],
|
|
257
|
+
passwordChanged: true,
|
|
258
|
+
hubOrigin: HUB_ORIGIN,
|
|
259
|
+
isFirstAdmin: false,
|
|
260
|
+
csrfToken: CSRF,
|
|
261
|
+
twoFactorEnabled: false,
|
|
262
|
+
});
|
|
263
|
+
// Username + sign-out stay prominent; change-password + 2FA tuck into a
|
|
264
|
+
// collapsed "Security & password" details so the card reads calmer.
|
|
265
|
+
expect(html).toContain('data-testid="account-security"');
|
|
266
|
+
expect(html).toContain("Security & password");
|
|
267
|
+
// The security actions live inside the details block (after its summary).
|
|
268
|
+
const securityIdx = html.indexOf('data-testid="account-security"');
|
|
269
|
+
expect(securityIdx).toBeGreaterThan(-1);
|
|
270
|
+
expect(html.indexOf('data-testid="change-password-link"')).toBeGreaterThan(securityIdx);
|
|
271
|
+
// Sign-out form comes BEFORE the security details — it stays prominent.
|
|
272
|
+
expect(html.indexOf('data-testid="signout-form"')).toBeLessThan(securityIdx);
|
|
273
|
+
});
|
|
274
|
+
|
|
178
275
|
test("account card — change-password link and sign-out form are present", () => {
|
|
179
276
|
const html = renderAccountHome({
|
|
180
277
|
username: "alice",
|
package/src/account-home-ui.ts
CHANGED
|
@@ -174,6 +174,15 @@ export function renderAccountHome(opts: RenderAccountHomeOpts): string {
|
|
|
174
174
|
)}</div>`
|
|
175
175
|
: "";
|
|
176
176
|
|
|
177
|
+
// Suppress the "Get started with your AI" card on the no-vault branch:
|
|
178
|
+
// that branch tells the user "You don't have a vault yet" + "ask the operator
|
|
179
|
+
// to assign you one," so a do-the-thing card alongside reads as contradictory
|
|
180
|
+
// (do-this vs you-lack-the-prerequisite). The admin (isFirstAdmin) and
|
|
181
|
+
// assigned-vault branches both have a vault to act against, so the card
|
|
182
|
+
// belongs there.
|
|
183
|
+
const hasNoVault = !isFirstAdmin && assignedVaults.length === 0;
|
|
184
|
+
const startedCard = hasNoVault ? "" : renderGetStartedCard();
|
|
185
|
+
|
|
177
186
|
const vaultCard = renderVaultCard({
|
|
178
187
|
assignedVaults,
|
|
179
188
|
trimmedOrigin,
|
|
@@ -197,12 +206,51 @@ export function renderAccountHome(opts: RenderAccountHomeOpts): string {
|
|
|
197
206
|
</div>
|
|
198
207
|
${mintedBanner}
|
|
199
208
|
${mintErrorBanner}
|
|
209
|
+
${startedCard}
|
|
200
210
|
${vaultCard}
|
|
201
211
|
${accountCard}
|
|
202
212
|
</div>${COPY_SCRIPT}`;
|
|
203
213
|
return baseDocument(`${username} — Parachute`, body);
|
|
204
214
|
}
|
|
205
215
|
|
|
216
|
+
/**
|
|
217
|
+
* The "Get started with your AI" card — the real first stop for a friend
|
|
218
|
+
* landing on `/account/`. Mirrors the operator setup-wizard's
|
|
219
|
+
* `renderStarterPromptsSection` (same two parachute.computer/onboarding/*
|
|
220
|
+
* links + copy) so friends and operators get the same on-ramp. The prompts
|
|
221
|
+
* live on parachute.computer rather than embedded here so they iterate
|
|
222
|
+
* without a hub release; this card just links.
|
|
223
|
+
*
|
|
224
|
+
* Placed near the top of the page (after any banners, before the vault card)
|
|
225
|
+
* because "what do I actually do with this?" is the friend's first question —
|
|
226
|
+
* the connect details below answer "how", this answers "what next".
|
|
227
|
+
*/
|
|
228
|
+
function renderGetStartedCard(): string {
|
|
229
|
+
return `
|
|
230
|
+
<section class="section get-started" data-testid="get-started-card">
|
|
231
|
+
<h2>Get started with your AI</h2>
|
|
232
|
+
<p>Two ready-made prompts to paste into Claude (or another AI assistant)
|
|
233
|
+
once your vault is connected — they walk you through it, no setup
|
|
234
|
+
knowledge needed.</p>
|
|
235
|
+
<div class="starter-grid">
|
|
236
|
+
<a class="starter-tile" href="https://parachute.computer/onboarding/vault-setup/"
|
|
237
|
+
target="_blank" rel="noopener" data-testid="starter-vault-setup">
|
|
238
|
+
<h3>Set up your vault</h3>
|
|
239
|
+
<p>Your AI interviews you about where your notes live now and suggests
|
|
240
|
+
a structure that fits how you think.</p>
|
|
241
|
+
<span class="starter-cta">Open prompt ↗</span>
|
|
242
|
+
</a>
|
|
243
|
+
<a class="starter-tile" href="https://parachute.computer/onboarding/surface-build/"
|
|
244
|
+
target="_blank" rel="noopener" data-testid="starter-surface-build">
|
|
245
|
+
<h3>Build a custom UI</h3>
|
|
246
|
+
<p>Your AI builds you a little web app for your vault — your own way to
|
|
247
|
+
see and add to it.</p>
|
|
248
|
+
<span class="starter-cta">Open prompt ↗</span>
|
|
249
|
+
</a>
|
|
250
|
+
</div>
|
|
251
|
+
</section>`;
|
|
252
|
+
}
|
|
253
|
+
|
|
206
254
|
interface VaultCardOpts {
|
|
207
255
|
assignedVaults: string[];
|
|
208
256
|
trimmedOrigin: string;
|
|
@@ -303,9 +351,9 @@ function renderVaultCard(opts: VaultCardOpts): string {
|
|
|
303
351
|
approve. (Your hub must be reachable from the web for this.)</p>
|
|
304
352
|
</div>
|
|
305
353
|
|
|
306
|
-
<p class="mcp-connect-hint" data-testid="connect-any-client-hint">
|
|
307
|
-
|
|
308
|
-
|
|
354
|
+
<p class="mcp-connect-hint" data-testid="connect-any-client-hint">Using something
|
|
355
|
+
else? Point any MCP client at the same endpoint above. (ChatGPT and some other
|
|
356
|
+
web UIs call these "connectors.")</p>
|
|
309
357
|
</div>
|
|
310
358
|
<p class="vault-notes-cta">
|
|
311
359
|
<a class="btn btn-primary" href="https://notes.parachute.computer/add?url=${vaultUrlForAdd}"
|
|
@@ -478,19 +526,24 @@ function renderAccountCard(opts: AccountCardOpts): string {
|
|
|
478
526
|
<dl class="kv">
|
|
479
527
|
<dt>Username</dt>
|
|
480
528
|
<dd><code>${username}</code></dd>
|
|
481
|
-
<dt>Two-factor authentication</dt>
|
|
482
|
-
${twoFactorStatus}
|
|
483
529
|
</dl>
|
|
484
|
-
<p>
|
|
485
|
-
<a class="account-action" href="/account/change-password" data-testid="change-password-link">Change password →</a>
|
|
486
|
-
</p>
|
|
487
|
-
<p>
|
|
488
|
-
${twoFactorLink}
|
|
489
|
-
</p>
|
|
490
530
|
<form method="POST" action="/logout" class="signout-form" data-testid="signout-form">
|
|
491
531
|
${renderCsrfHiddenInput(csrfToken)}
|
|
492
532
|
<button type="submit" class="btn btn-secondary">Sign out</button>
|
|
493
533
|
</form>
|
|
534
|
+
<details class="account-security" data-testid="account-security">
|
|
535
|
+
<summary>Security & password</summary>
|
|
536
|
+
<dl class="kv">
|
|
537
|
+
<dt>Two-factor authentication</dt>
|
|
538
|
+
${twoFactorStatus}
|
|
539
|
+
</dl>
|
|
540
|
+
<p>
|
|
541
|
+
<a class="account-action" href="/account/change-password" data-testid="change-password-link">Change password →</a>
|
|
542
|
+
</p>
|
|
543
|
+
<p>
|
|
544
|
+
${twoFactorLink}
|
|
545
|
+
</p>
|
|
546
|
+
</details>
|
|
494
547
|
</section>`;
|
|
495
548
|
}
|
|
496
549
|
|
|
@@ -604,6 +657,60 @@ const STYLES = `
|
|
|
604
657
|
margin-top: 1.25rem;
|
|
605
658
|
}
|
|
606
659
|
.section p { margin: 0.4rem 0; }
|
|
660
|
+
|
|
661
|
+
.get-started h3 {
|
|
662
|
+
font-family: ${FONT_SERIF};
|
|
663
|
+
font-weight: 400;
|
|
664
|
+
font-size: 1rem;
|
|
665
|
+
margin: 0 0 0.3rem;
|
|
666
|
+
color: ${PALETTE.fg};
|
|
667
|
+
}
|
|
668
|
+
.starter-grid {
|
|
669
|
+
display: grid;
|
|
670
|
+
grid-template-columns: 1fr 1fr;
|
|
671
|
+
gap: 0.75rem;
|
|
672
|
+
margin: 0.75rem 0 0.2rem;
|
|
673
|
+
}
|
|
674
|
+
.starter-tile {
|
|
675
|
+
display: block;
|
|
676
|
+
border: 1px solid ${PALETTE.borderLight};
|
|
677
|
+
border-radius: 8px;
|
|
678
|
+
padding: 0.8rem 0.9rem;
|
|
679
|
+
background: ${PALETTE.bgSoft};
|
|
680
|
+
text-decoration: none;
|
|
681
|
+
color: inherit;
|
|
682
|
+
transition: border-color 0.15s ease, background 0.15s ease;
|
|
683
|
+
}
|
|
684
|
+
.starter-tile:hover { border-color: ${PALETTE.accent}; background: ${PALETTE.accentSoft}; }
|
|
685
|
+
.starter-tile p {
|
|
686
|
+
font-size: 0.84rem;
|
|
687
|
+
color: ${PALETTE.fgMuted};
|
|
688
|
+
margin: 0 0 0.5rem;
|
|
689
|
+
}
|
|
690
|
+
.starter-cta {
|
|
691
|
+
font-size: 0.85rem;
|
|
692
|
+
font-weight: 500;
|
|
693
|
+
color: ${PALETTE.accent};
|
|
694
|
+
}
|
|
695
|
+
@media (max-width: 480px) {
|
|
696
|
+
.starter-grid { grid-template-columns: 1fr; }
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.account-security {
|
|
700
|
+
margin: 0.9rem 0 0;
|
|
701
|
+
padding-top: 0.6rem;
|
|
702
|
+
border-top: 1px solid ${PALETTE.borderLight};
|
|
703
|
+
}
|
|
704
|
+
.account-security > summary {
|
|
705
|
+
cursor: pointer;
|
|
706
|
+
font-size: 0.88rem;
|
|
707
|
+
font-weight: 600;
|
|
708
|
+
color: ${PALETTE.fgMuted};
|
|
709
|
+
list-style: revert;
|
|
710
|
+
}
|
|
711
|
+
.account-security > summary:hover { color: ${PALETTE.fg}; }
|
|
712
|
+
.account-security .kv { margin-top: 0.6rem; }
|
|
713
|
+
|
|
607
714
|
.vault-name {
|
|
608
715
|
font-family: ${FONT_MONO};
|
|
609
716
|
font-size: 1rem;
|
|
@@ -879,7 +986,14 @@ const STYLES = `
|
|
|
879
986
|
code { background: #1f1c18; color: #e8e4dc; }
|
|
880
987
|
.copy-row code { background: transparent; }
|
|
881
988
|
.section { border-top-color: #3a362f; }
|
|
882
|
-
.mcp-method, .vault-notes-cta, .token-mint
|
|
989
|
+
.mcp-method, .vault-notes-cta, .token-mint,
|
|
990
|
+
.account-security { border-top-color: #3a362f; }
|
|
991
|
+
.get-started h3 { color: #f0ece4; }
|
|
992
|
+
.starter-tile { border-color: #3a362f; background: #1f1c18; }
|
|
993
|
+
.starter-tile:hover { border-color: ${PALETTE.accent}; }
|
|
994
|
+
.starter-tile p { color: #a8a29a; }
|
|
995
|
+
.account-security > summary { color: #a8a29a; }
|
|
996
|
+
.account-security > summary:hover { color: #f0ece4; }
|
|
883
997
|
.brand-tag { border-color: #3a362f; color: #a8a29a; }
|
|
884
998
|
.copy-row { background: #1f1c18; border-color: #3a362f; }
|
|
885
999
|
.btn-secondary, .btn-copy { color: #e8e4dc; border-color: #3a362f; }
|
|
@@ -58,4 +58,4 @@ Error generating stack: `+n.message+`
|
|
|
58
58
|
*/var wh="popstate";function Th(i){return typeof i=="object"&&i!=null&&"pathname"in i&&"search"in i&&"hash"in i&&"state"in i&&"key"in i}function I1(i={}){function c(o,f){var p;let h=(p=f.state)==null?void 0:p.masked,{pathname:b,search:j,hash:v}=h||o.location;return Pc("",{pathname:b,search:j,hash:v},f.state&&f.state.usr||null,f.state&&f.state.key||"default",h?{pathname:o.location.pathname,search:o.location.search,hash:o.location.hash}:void 0)}function r(o,f){return typeof f=="string"?f:Gl(f)}return ev(c,r,null,i)}function De(i,c){if(i===!1||i===null||typeof i>"u")throw new Error(c)}function qt(i,c){if(!i){typeof console<"u"&&console.warn(c);try{throw new Error(c)}catch{}}}function P1(){return Math.random().toString(36).substring(2,10)}function _h(i,c){return{usr:i.state,key:i.key,idx:c,masked:i.unstable_mask?{pathname:i.pathname,search:i.search,hash:i.hash}:void 0}}function Pc(i,c,r=null,o,f){return{pathname:typeof i=="string"?i:i.pathname,search:"",hash:"",...typeof c=="string"?Vn(c):c,state:r,key:c&&c.key||o||P1(),unstable_mask:f}}function Gl({pathname:i="/",search:c="",hash:r=""}){return c&&c!=="?"&&(i+=c.charAt(0)==="?"?c:"?"+c),r&&r!=="#"&&(i+=r.charAt(0)==="#"?r:"#"+r),i}function Vn(i){let c={};if(i){let r=i.indexOf("#");r>=0&&(c.hash=i.substring(r),i=i.substring(0,r));let o=i.indexOf("?");o>=0&&(c.search=i.substring(o),i=i.substring(0,o)),i&&(c.pathname=i)}return c}function ev(i,c,r,o={}){let{window:f=document.defaultView,v5Compat:h=!1}=o,b=f.history,j="POP",v=null,p=g();p==null&&(p=0,b.replaceState({...b.state,idx:p},""));function g(){return(b.state||{idx:null}).idx}function y(){j="POP";let Z=g(),V=Z==null?null:Z-p;p=Z,v&&v({action:j,location:q.location,delta:V})}function A(Z,V){j="PUSH";let ee=Th(Z)?Z:Pc(q.location,Z,V);p=g()+1;let I=_h(ee,p),ue=q.createHref(ee.unstable_mask||ee);try{b.pushState(I,"",ue)}catch(K){if(K instanceof DOMException&&K.name==="DataCloneError")throw K;f.location.assign(ue)}h&&v&&v({action:j,location:q.location,delta:1})}function T(Z,V){j="REPLACE";let ee=Th(Z)?Z:Pc(q.location,Z,V);p=g();let I=_h(ee,p),ue=q.createHref(ee.unstable_mask||ee);b.replaceState(I,"",ue),h&&v&&v({action:j,location:q.location,delta:0})}function D(Z){return tv(Z)}let q={get action(){return j},get location(){return i(f,b)},listen(Z){if(v)throw new Error("A history only accepts one active listener");return f.addEventListener(wh,y),v=Z,()=>{f.removeEventListener(wh,y),v=null}},createHref(Z){return c(f,Z)},createURL:D,encodeLocation(Z){let V=D(Z);return{pathname:V.pathname,search:V.search,hash:V.hash}},push:A,replace:T,go(Z){return b.go(Z)}};return q}function tv(i,c=!1){let r="http://localhost";typeof window<"u"&&(r=window.location.origin!=="null"?window.location.origin:window.location.href),De(r,"No window.location.(origin|href) available to create URL");let o=typeof i=="string"?i:Gl(i);return o=o.replace(/ $/,"%20"),!c&&o.startsWith("//")&&(o=r+o),new URL(o,r)}function Lh(i,c,r="/"){return av(i,c,r,!1)}function av(i,c,r,o){let f=typeof c=="string"?Vn(c):c,h=oa(f.pathname||"/",r);if(h==null)return null;let b=qh(i);nv(b);let j=null;for(let v=0;j==null&&v<b.length;++v){let p=mv(h);j=fv(b[v],p,o)}return j}function qh(i,c=[],r=[],o="",f=!1){let h=(b,j,v=f,p)=>{let g={relativePath:p===void 0?b.path||"":p,caseSensitive:b.caseSensitive===!0,childrenIndex:j,route:b};if(g.relativePath.startsWith("/")){if(!g.relativePath.startsWith(o)&&v)return;De(g.relativePath.startsWith(o),`Absolute route path "${g.relativePath}" nested under path "${o}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),g.relativePath=g.relativePath.slice(o.length)}let y=Lt([o,g.relativePath]),A=r.concat(g);b.children&&b.children.length>0&&(De(b.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${y}".`),qh(b.children,c,A,y,v)),!(b.path==null&&!b.index)&&c.push({path:y,score:ov(y,b.index),routesMeta:A})};return i.forEach((b,j)=>{var v;if(b.path===""||!((v=b.path)!=null&&v.includes("?")))h(b,j);else for(let p of Yh(b.path))h(b,j,!0,p)}),c}function Yh(i){let c=i.split("/");if(c.length===0)return[];let[r,...o]=c,f=r.endsWith("?"),h=r.replace(/\?$/,"");if(o.length===0)return f?[h,""]:[h];let b=Yh(o.join("/")),j=[];return j.push(...b.map(v=>v===""?h:[h,v].join("/"))),f&&j.push(...b),j.map(v=>i.startsWith("/")&&v===""?"/":v)}function nv(i){i.sort((c,r)=>c.score!==r.score?r.score-c.score:dv(c.routesMeta.map(o=>o.childrenIndex),r.routesMeta.map(o=>o.childrenIndex)))}var lv=/^:[\w-]+$/,iv=3,uv=2,sv=1,cv=10,rv=-2,Rh=i=>i==="*";function ov(i,c){let r=i.split("/"),o=r.length;return r.some(Rh)&&(o+=rv),c&&(o+=uv),r.filter(f=>!Rh(f)).reduce((f,h)=>f+(lv.test(h)?iv:h===""?sv:cv),o)}function dv(i,c){return i.length===c.length&&i.slice(0,-1).every((o,f)=>o===c[f])?i[i.length-1]-c[c.length-1]:0}function fv(i,c,r=!1){let{routesMeta:o}=i,f={},h="/",b=[];for(let j=0;j<o.length;++j){let v=o[j],p=j===o.length-1,g=h==="/"?c:c.slice(h.length)||"/",y=pu({path:v.relativePath,caseSensitive:v.caseSensitive,end:p},g),A=v.route;if(!y&&p&&r&&!o[o.length-1].route.index&&(y=pu({path:v.relativePath,caseSensitive:v.caseSensitive,end:!1},g)),!y)return null;Object.assign(f,y.params),b.push({params:f,pathname:Lt([h,y.pathname]),pathnameBase:yv(Lt([h,y.pathnameBase])),route:A}),y.pathnameBase!=="/"&&(h=Lt([h,y.pathnameBase]))}return b}function pu(i,c){typeof i=="string"&&(i={path:i,caseSensitive:!1,end:!0});let[r,o]=hv(i.path,i.caseSensitive,i.end),f=c.match(r);if(!f)return null;let h=f[0],b=h.replace(/(.)\/+$/,"$1"),j=f.slice(1);return{params:o.reduce((p,{paramName:g,isOptional:y},A)=>{if(g==="*"){let D=j[A]||"";b=h.slice(0,h.length-D.length).replace(/(.)\/+$/,"$1")}const T=j[A];return y&&!T?p[g]=void 0:p[g]=(T||"").replace(/%2F/g,"/"),p},{}),pathname:h,pathnameBase:b,pattern:i}}function hv(i,c=!1,r=!0){qt(i==="*"||!i.endsWith("*")||i.endsWith("/*"),`Route path "${i}" will be treated as if it were "${i.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${i.replace(/\*$/,"/*")}".`);let o=[],f="^"+i.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(b,j,v,p,g)=>{if(o.push({paramName:j,isOptional:v!=null}),v){let y=g.charAt(p+b.length);return y&&y!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return i.endsWith("*")?(o.push({paramName:"*"}),f+=i==="*"||i==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):r?f+="\\/*$":i!==""&&i!=="/"&&(f+="(?:(?=\\/|$))"),[new RegExp(f,c?void 0:"i"),o]}function mv(i){try{return i.split("/").map(c=>decodeURIComponent(c).replace(/\//g,"%2F")).join("/")}catch(c){return qt(!1,`The URL path "${i}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${c}).`),i}}function oa(i,c){if(c==="/")return i;if(!i.toLowerCase().startsWith(c.toLowerCase()))return null;let r=c.endsWith("/")?c.length-1:c.length,o=i.charAt(r);return o&&o!=="/"?null:i.slice(r)||"/"}var pv=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function vv(i,c="/"){let{pathname:r,search:o="",hash:f=""}=typeof i=="string"?Vn(i):i,h;return r?(r=Vh(r),r.startsWith("/")?h=Ah(r.substring(1),"/"):h=Ah(r,c)):h=c,{pathname:h,search:bv(o),hash:xv(f)}}function Ah(i,c){let r=vu(c).split("/");return i.split("/").forEach(f=>{f===".."?r.length>1&&r.pop():f!=="."&&r.push(f)}),r.length>1?r.join("/"):"/"}function Qc(i,c,r,o){return`Cannot include a '${i}' character in a manually specified \`to.${c}\` field [${JSON.stringify(o)}]. Please separate it out to the \`to.${r}\` field. Alternatively you may provide the full path as a string in <Link to="..."> and the router will parse it for you.`}function gv(i){return i.filter((c,r)=>r===0||c.route.path&&c.route.path.length>0)}function Gh(i){let c=gv(i);return c.map((r,o)=>o===c.length-1?r.pathname:r.pathnameBase)}function lr(i,c,r,o=!1){let f;typeof i=="string"?f=Vn(i):(f={...i},De(!f.pathname||!f.pathname.includes("?"),Qc("?","pathname","search",f)),De(!f.pathname||!f.pathname.includes("#"),Qc("#","pathname","hash",f)),De(!f.search||!f.search.includes("#"),Qc("#","search","hash",f)));let h=i===""||f.pathname==="",b=h?"/":f.pathname,j;if(b==null)j=r;else{let y=c.length-1;if(!o&&b.startsWith("..")){let A=b.split("/");for(;A[0]==="..";)A.shift(),y-=1;f.pathname=A.join("/")}j=y>=0?c[y]:"/"}let v=vv(f,j),p=b&&b!=="/"&&b.endsWith("/"),g=(h||b===".")&&r.endsWith("/");return!v.pathname.endsWith("/")&&(p||g)&&(v.pathname+="/"),v}var Vh=i=>i.replace(/\/\/+/g,"/"),Lt=i=>Vh(i.join("/")),vu=i=>i.replace(/\/+$/,""),yv=i=>vu(i).replace(/^\/*/,"/"),bv=i=>!i||i==="?"?"":i.startsWith("?")?i:"?"+i,xv=i=>!i||i==="#"?"":i.startsWith("#")?i:"#"+i,jv=class{constructor(i,c,r,o=!1){this.status=i,this.statusText=c||"",this.internal=o,r instanceof Error?(this.data=r.toString(),this.error=r):this.data=r}};function Sv(i){return i!=null&&typeof i.status=="number"&&typeof i.statusText=="string"&&typeof i.internal=="boolean"&&"data"in i}function Cv(i){let c=i.map(r=>r.route.path).filter(Boolean);return Lt(c)||"/"}var Zh=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Xh(i,c){let r=i;if(typeof r!="string"||!pv.test(r))return{absoluteURL:void 0,isExternal:!1,to:r};let o=r,f=!1;if(Zh)try{let h=new URL(window.location.href),b=r.startsWith("//")?new URL(h.protocol+r):new URL(r),j=oa(b.pathname,c);b.origin===h.origin&&j!=null?r=j+b.search+b.hash:f=!0}catch{qt(!1,`<Link to="${r}"> contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:o,isExternal:f,to:r}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var Qh=["POST","PUT","PATCH","DELETE"];new Set(Qh);var Ev=["GET",...Qh];new Set(Ev);var Zn=S.createContext(null);Zn.displayName="DataRouter";var bu=S.createContext(null);bu.displayName="DataRouterState";var Jh=S.createContext(!1);function Nv(){return S.useContext(Jh)}var Kh=S.createContext({isTransitioning:!1});Kh.displayName="ViewTransition";var wv=S.createContext(new Map);wv.displayName="Fetchers";var Tv=S.createContext(null);Tv.displayName="Await";var Ut=S.createContext(null);Ut.displayName="Navigation";var Zl=S.createContext(null);Zl.displayName="Location";var Xt=S.createContext({outlet:null,matches:[],isDataRoute:!1});Xt.displayName="Route";var ir=S.createContext(null);ir.displayName="RouteError";var $h="REACT_ROUTER_ERROR",_v="REDIRECT",Rv="ROUTE_ERROR_RESPONSE";function Av(i){if(i.startsWith(`${$h}:${_v}:{`))try{let c=JSON.parse(i.slice(28));if(typeof c=="object"&&c&&typeof c.status=="number"&&typeof c.statusText=="string"&&typeof c.location=="string"&&typeof c.reloadDocument=="boolean"&&typeof c.replace=="boolean")return c}catch{}}function zv(i){if(i.startsWith(`${$h}:${Rv}:{`))try{let c=JSON.parse(i.slice(40));if(typeof c=="object"&&c&&typeof c.status=="number"&&typeof c.statusText=="string")return new jv(c.status,c.statusText,c.data)}catch{}}function Mv(i,{relative:c}={}){De(Xl(),"useHref() may be used only in the context of a <Router> component.");let{basename:r,navigator:o}=S.useContext(Ut),{hash:f,pathname:h,search:b}=Ql(i,{relative:c}),j=h;return r!=="/"&&(j=h==="/"?r:Lt([r,h])),o.createHref({pathname:j,search:b,hash:f})}function Xl(){return S.useContext(Zl)!=null}function Dt(){return De(Xl(),"useLocation() may be used only in the context of a <Router> component."),S.useContext(Zl).location}var Fh="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function Wh(i){S.useContext(Ut).static||S.useLayoutEffect(i)}function ur(){let{isDataRoute:i}=S.useContext(Xt);return i?Xv():Ov()}function Ov(){De(Xl(),"useNavigate() may be used only in the context of a <Router> component.");let i=S.useContext(Zn),{basename:c,navigator:r}=S.useContext(Ut),{matches:o}=S.useContext(Xt),{pathname:f}=Dt(),h=JSON.stringify(Gh(o)),b=S.useRef(!1);return Wh(()=>{b.current=!0}),S.useCallback((v,p={})=>{if(qt(b.current,Fh),!b.current)return;if(typeof v=="number"){r.go(v);return}let g=lr(v,JSON.parse(h),f,p.relative==="path");i==null&&c!=="/"&&(g.pathname=g.pathname==="/"?c:Lt([c,g.pathname])),(p.replace?r.replace:r.push)(g,p.state,p)},[c,r,h,f,i])}S.createContext(null);function Ih(){let{matches:i}=S.useContext(Xt),c=i[i.length-1];return(c==null?void 0:c.params)??{}}function Ql(i,{relative:c}={}){let{matches:r}=S.useContext(Xt),{pathname:o}=Dt(),f=JSON.stringify(Gh(r));return S.useMemo(()=>lr(i,JSON.parse(f),o,c==="path"),[i,f,o,c])}function Uv(i,c){return Ph(i,c)}function Ph(i,c,r){var Z;De(Xl(),"useRoutes() may be used only in the context of a <Router> component.");let{navigator:o}=S.useContext(Ut),{matches:f}=S.useContext(Xt),h=f[f.length-1],b=h?h.params:{},j=h?h.pathname:"/",v=h?h.pathnameBase:"/",p=h&&h.route;{let V=p&&p.path||"";tm(j,!p||V.endsWith("*")||V.endsWith("*?"),`You rendered descendant <Routes> (or called \`useRoutes()\`) at "${j}" (under <Route path="${V}">) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render.
|
|
59
59
|
|
|
60
60
|
Please change the parent <Route path="${V}"> to <Route path="${V==="/"?"*":`${V}/*`}">.`)}let g=Dt(),y;if(c){let V=typeof c=="string"?Vn(c):c;De(v==="/"||((Z=V.pathname)==null?void 0:Z.startsWith(v)),`When overriding the location using \`<Routes location>\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${v}" but pathname "${V.pathname}" was given in the \`location\` prop.`),y=V}else y=g;let A=y.pathname||"/",T=A;if(v!=="/"){let V=v.replace(/^\//,"").split("/");T="/"+A.replace(/^\//,"").split("/").slice(V.length).join("/")}let D=Lh(i,{pathname:T});qt(p||D!=null,`No routes matched location "${y.pathname}${y.search}${y.hash}" `),qt(D==null||D[D.length-1].route.element!==void 0||D[D.length-1].route.Component!==void 0||D[D.length-1].route.lazy!==void 0,`Matched leaf route at location "${y.pathname}${y.search}${y.hash}" does not have an element or Component. This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.`);let q=Lv(D&&D.map(V=>Object.assign({},V,{params:Object.assign({},b,V.params),pathname:Lt([v,o.encodeLocation?o.encodeLocation(V.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:V.pathname]),pathnameBase:V.pathnameBase==="/"?v:Lt([v,o.encodeLocation?o.encodeLocation(V.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:V.pathnameBase])})),f,r);return c&&q?S.createElement(Zl.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",unstable_mask:void 0,...y},navigationType:"POP"}},q):q}function Dv(){let i=Zv(),c=Sv(i)?`${i.status} ${i.statusText}`:i instanceof Error?i.message:JSON.stringify(i),r=i instanceof Error?i.stack:null,o="rgba(200,200,200, 0.5)",f={padding:"0.5rem",backgroundColor:o},h={padding:"2px 4px",backgroundColor:o},b=null;return console.error("Error handled by React Router default ErrorBoundary:",i),b=S.createElement(S.Fragment,null,S.createElement("p",null,"💿 Hey developer 👋"),S.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",S.createElement("code",{style:h},"ErrorBoundary")," or"," ",S.createElement("code",{style:h},"errorElement")," prop on your route.")),S.createElement(S.Fragment,null,S.createElement("h2",null,"Unexpected Application Error!"),S.createElement("h3",{style:{fontStyle:"italic"}},c),r?S.createElement("pre",{style:f},r):null,b)}var kv=S.createElement(Dv,null),em=class extends S.Component{constructor(i){super(i),this.state={location:i.location,revalidation:i.revalidation,error:i.error}}static getDerivedStateFromError(i){return{error:i}}static getDerivedStateFromProps(i,c){return c.location!==i.location||c.revalidation!=="idle"&&i.revalidation==="idle"?{error:i.error,location:i.location,revalidation:i.revalidation}:{error:i.error!==void 0?i.error:c.error,location:c.location,revalidation:i.revalidation||c.revalidation}}componentDidCatch(i,c){this.props.onError?this.props.onError(i,c):console.error("React Router caught the following error during render",i)}render(){let i=this.state.error;if(this.context&&typeof i=="object"&&i&&"digest"in i&&typeof i.digest=="string"){const r=zv(i.digest);r&&(i=r)}let c=i!==void 0?S.createElement(Xt.Provider,{value:this.props.routeContext},S.createElement(ir.Provider,{value:i,children:this.props.component})):this.props.children;return this.context?S.createElement(Hv,{error:i},c):c}};em.contextType=Jh;var Jc=new WeakMap;function Hv({children:i,error:c}){let{basename:r}=S.useContext(Ut);if(typeof c=="object"&&c&&"digest"in c&&typeof c.digest=="string"){let o=Av(c.digest);if(o){let f=Jc.get(c);if(f)throw f;let h=Xh(o.location,r);if(Zh&&!Jc.get(c))if(h.isExternal||o.reloadDocument)window.location.href=h.absoluteURL||h.to;else{const b=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(h.to,{replace:o.replace}));throw Jc.set(c,b),b}return S.createElement("meta",{httpEquiv:"refresh",content:`0;url=${h.absoluteURL||h.to}`})}}return i}function Bv({routeContext:i,match:c,children:r}){let o=S.useContext(Zn);return o&&o.static&&o.staticContext&&(c.route.errorElement||c.route.ErrorBoundary)&&(o.staticContext._deepestRenderedBoundaryId=c.route.id),S.createElement(Xt.Provider,{value:i},r)}function Lv(i,c=[],r){let o=r==null?void 0:r.state;if(i==null){if(!o)return null;if(o.errors)i=o.matches;else if(c.length===0&&!o.initialized&&o.matches.length>0)i=o.matches;else return null}let f=i,h=o==null?void 0:o.errors;if(h!=null){let g=f.findIndex(y=>y.route.id&&(h==null?void 0:h[y.route.id])!==void 0);De(g>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(h).join(",")}`),f=f.slice(0,Math.min(f.length,g+1))}let b=!1,j=-1;if(r&&o){b=o.renderFallback;for(let g=0;g<f.length;g++){let y=f[g];if((y.route.HydrateFallback||y.route.hydrateFallbackElement)&&(j=g),y.route.id){let{loaderData:A,errors:T}=o,D=y.route.loader&&!A.hasOwnProperty(y.route.id)&&(!T||T[y.route.id]===void 0);if(y.route.lazy||D){r.isStatic&&(b=!0),j>=0?f=f.slice(0,j+1):f=[f[0]];break}}}}let v=r==null?void 0:r.onError,p=o&&v?(g,y)=>{var A,T;v(g,{location:o.location,params:((T=(A=o.matches)==null?void 0:A[0])==null?void 0:T.params)??{},unstable_pattern:Cv(o.matches),errorInfo:y})}:void 0;return f.reduceRight((g,y,A)=>{let T,D=!1,q=null,Z=null;o&&(T=h&&y.route.id?h[y.route.id]:void 0,q=y.route.errorElement||kv,b&&(j<0&&A===0?(tm("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),D=!0,Z=null):j===A&&(D=!0,Z=y.route.hydrateFallbackElement||null)));let V=c.concat(f.slice(0,A+1)),ee=()=>{let I;return T?I=q:D?I=Z:y.route.Component?I=S.createElement(y.route.Component,null):y.route.element?I=y.route.element:I=g,S.createElement(Bv,{match:y,routeContext:{outlet:g,matches:V,isDataRoute:o!=null},children:I})};return o&&(y.route.ErrorBoundary||y.route.errorElement||A===0)?S.createElement(em,{location:o.location,revalidation:o.revalidation,component:q,error:T,children:ee(),routeContext:{outlet:null,matches:V,isDataRoute:!0},onError:p}):ee()},null)}function sr(i){return`${i} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function qv(i){let c=S.useContext(Zn);return De(c,sr(i)),c}function Yv(i){let c=S.useContext(bu);return De(c,sr(i)),c}function Gv(i){let c=S.useContext(Xt);return De(c,sr(i)),c}function cr(i){let c=Gv(i),r=c.matches[c.matches.length-1];return De(r.route.id,`${i} can only be used on routes that contain a unique "id"`),r.route.id}function Vv(){return cr("useRouteId")}function Zv(){var o;let i=S.useContext(ir),c=Yv("useRouteError"),r=cr("useRouteError");return i!==void 0?i:(o=c.errors)==null?void 0:o[r]}function Xv(){let{router:i}=qv("useNavigate"),c=cr("useNavigate"),r=S.useRef(!1);return Wh(()=>{r.current=!0}),S.useCallback(async(f,h={})=>{qt(r.current,Fh),r.current&&(typeof f=="number"?await i.navigate(f):await i.navigate(f,{fromRouteId:c,...h}))},[i,c])}var zh={};function tm(i,c,r){!c&&!zh[i]&&(zh[i]=!0,qt(!1,r))}S.memo(Qv);function Qv({routes:i,future:c,state:r,isStatic:o,onError:f}){return Ph(i,void 0,{state:r,isStatic:o,onError:f})}function Ot(i){De(!1,"A <Route> is only ever to be used as the child of <Routes> element, never rendered directly. Please wrap your <Route> in a <Routes>.")}function Jv({basename:i="/",children:c=null,location:r,navigationType:o="POP",navigator:f,static:h=!1,unstable_useTransitions:b}){De(!Xl(),"You cannot render a <Router> inside another <Router>. You should never have more than one in your app.");let j=i.replace(/^\/*/,"/"),v=S.useMemo(()=>({basename:j,navigator:f,static:h,unstable_useTransitions:b,future:{}}),[j,f,h,b]);typeof r=="string"&&(r=Vn(r));let{pathname:p="/",search:g="",hash:y="",state:A=null,key:T="default",unstable_mask:D}=r,q=S.useMemo(()=>{let Z=oa(p,j);return Z==null?null:{location:{pathname:Z,search:g,hash:y,state:A,key:T,unstable_mask:D},navigationType:o}},[j,p,g,y,A,T,o,D]);return qt(q!=null,`<Router basename="${j}"> is not able to match the URL "${p}${g}${y}" because it does not start with the basename, so the <Router> won't render anything.`),q==null?null:S.createElement(Ut.Provider,{value:v},S.createElement(Zl.Provider,{children:c,value:q}))}function Kv({children:i,location:c}){return Uv(er(i),c)}function er(i,c=[]){let r=[];return S.Children.forEach(i,(o,f)=>{if(!S.isValidElement(o))return;let h=[...c,f];if(o.type===S.Fragment){r.push.apply(r,er(o.props.children,h));return}De(o.type===Ot,`[${typeof o.type=="string"?o.type:o.type.name}] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>`),De(!o.props.index||!o.props.children,"An index route cannot have child routes.");let b={id:o.props.id||h.join("-"),caseSensitive:o.props.caseSensitive,element:o.props.element,Component:o.props.Component,index:o.props.index,path:o.props.path,middleware:o.props.middleware,loader:o.props.loader,action:o.props.action,hydrateFallbackElement:o.props.hydrateFallbackElement,HydrateFallback:o.props.HydrateFallback,errorElement:o.props.errorElement,ErrorBoundary:o.props.ErrorBoundary,hasErrorBoundary:o.props.hasErrorBoundary===!0||o.props.ErrorBoundary!=null||o.props.errorElement!=null,shouldRevalidate:o.props.shouldRevalidate,handle:o.props.handle,lazy:o.props.lazy};o.props.children&&(b.children=er(o.props.children,h)),r.push(b)}),r}var hu="get",mu="application/x-www-form-urlencoded";function xu(i){return typeof HTMLElement<"u"&&i instanceof HTMLElement}function $v(i){return xu(i)&&i.tagName.toLowerCase()==="button"}function Fv(i){return xu(i)&&i.tagName.toLowerCase()==="form"}function Wv(i){return xu(i)&&i.tagName.toLowerCase()==="input"}function Iv(i){return!!(i.metaKey||i.altKey||i.ctrlKey||i.shiftKey)}function Pv(i,c){return i.button===0&&(!c||c==="_self")&&!Iv(i)}function tr(i=""){return new URLSearchParams(typeof i=="string"||Array.isArray(i)||i instanceof URLSearchParams?i:Object.keys(i).reduce((c,r)=>{let o=i[r];return c.concat(Array.isArray(o)?o.map(f=>[r,f]):[[r,o]])},[]))}function e0(i,c){let r=tr(i);return c&&c.forEach((o,f)=>{r.has(f)||c.getAll(f).forEach(h=>{r.append(f,h)})}),r}var du=null;function t0(){if(du===null)try{new FormData(document.createElement("form"),0),du=!1}catch{du=!0}return du}var a0=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function Kc(i){return i!=null&&!a0.has(i)?(qt(!1,`"${i}" is not a valid \`encType\` for \`<Form>\`/\`<fetcher.Form>\` and will default to "${mu}"`),null):i}function n0(i,c){let r,o,f,h,b;if(Fv(i)){let j=i.getAttribute("action");o=j?oa(j,c):null,r=i.getAttribute("method")||hu,f=Kc(i.getAttribute("enctype"))||mu,h=new FormData(i)}else if($v(i)||Wv(i)&&(i.type==="submit"||i.type==="image")){let j=i.form;if(j==null)throw new Error('Cannot submit a <button> or <input type="submit"> without a <form>');let v=i.getAttribute("formaction")||j.getAttribute("action");if(o=v?oa(v,c):null,r=i.getAttribute("formmethod")||j.getAttribute("method")||hu,f=Kc(i.getAttribute("formenctype"))||Kc(j.getAttribute("enctype"))||mu,h=new FormData(j,i),!t0()){let{name:p,type:g,value:y}=i;if(g==="image"){let A=p?`${p}.`:"";h.append(`${A}x`,"0"),h.append(`${A}y`,"0")}else p&&h.append(p,y)}}else{if(xu(i))throw new Error('Cannot submit element that is not <form>, <button>, or <input type="submit|image">');r=hu,o=null,f=mu,b=i}return h&&f==="text/plain"&&(b=h,h=void 0),{action:o,method:r.toLowerCase(),encType:f,formData:h,body:b}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");function rr(i,c){if(i===!1||i===null||typeof i>"u")throw new Error(c)}function am(i,c,r,o){let f=typeof i=="string"?new URL(i,typeof window>"u"?"server://singlefetch/":window.location.origin):i;return r?f.pathname.endsWith("/")?f.pathname=`${f.pathname}_.${o}`:f.pathname=`${f.pathname}.${o}`:f.pathname==="/"?f.pathname=`_root.${o}`:c&&oa(f.pathname,c)==="/"?f.pathname=`${vu(c)}/_root.${o}`:f.pathname=`${vu(f.pathname)}.${o}`,f}async function l0(i,c){if(i.id in c)return c[i.id];try{let r=await import(i.module);return c[i.id]=r,r}catch(r){return console.error(`Error loading route module \`${i.module}\`, reloading page...`),console.error(r),window.__reactRouterContext&&window.__reactRouterContext.isSpaMode,window.location.reload(),new Promise(()=>{})}}function i0(i){return i==null?!1:i.href==null?i.rel==="preload"&&typeof i.imageSrcSet=="string"&&typeof i.imageSizes=="string":typeof i.rel=="string"&&typeof i.href=="string"}async function u0(i,c,r){let o=await Promise.all(i.map(async f=>{let h=c.routes[f.route.id];if(h){let b=await l0(h,r);return b.links?b.links():[]}return[]}));return o0(o.flat(1).filter(i0).filter(f=>f.rel==="stylesheet"||f.rel==="preload").map(f=>f.rel==="stylesheet"?{...f,rel:"prefetch",as:"style"}:{...f,rel:"prefetch"}))}function Mh(i,c,r,o,f,h){let b=(v,p)=>r[p]?v.route.id!==r[p].route.id:!0,j=(v,p)=>{var g;return r[p].pathname!==v.pathname||((g=r[p].route.path)==null?void 0:g.endsWith("*"))&&r[p].params["*"]!==v.params["*"]};return h==="assets"?c.filter((v,p)=>b(v,p)||j(v,p)):h==="data"?c.filter((v,p)=>{var y;let g=o.routes[v.route.id];if(!g||!g.hasLoader)return!1;if(b(v,p)||j(v,p))return!0;if(v.route.shouldRevalidate){let A=v.route.shouldRevalidate({currentUrl:new URL(f.pathname+f.search+f.hash,window.origin),currentParams:((y=r[0])==null?void 0:y.params)||{},nextUrl:new URL(i,window.origin),nextParams:v.params,defaultShouldRevalidate:!0});if(typeof A=="boolean")return A}return!0}):[]}function s0(i,c,{includeHydrateFallback:r}={}){return c0(i.map(o=>{let f=c.routes[o.route.id];if(!f)return[];let h=[f.module];return f.clientActionModule&&(h=h.concat(f.clientActionModule)),f.clientLoaderModule&&(h=h.concat(f.clientLoaderModule)),r&&f.hydrateFallbackModule&&(h=h.concat(f.hydrateFallbackModule)),f.imports&&(h=h.concat(f.imports)),h}).flat(1))}function c0(i){return[...new Set(i)]}function r0(i){let c={},r=Object.keys(i).sort();for(let o of r)c[o]=i[o];return c}function o0(i,c){let r=new Set;return new Set(c),i.reduce((o,f)=>{let h=JSON.stringify(r0(f));return r.has(h)||(r.add(h),o.push({key:h,link:f})),o},[])}function or(){let i=S.useContext(Zn);return rr(i,"You must render this element inside a <DataRouterContext.Provider> element"),i}function d0(){let i=S.useContext(bu);return rr(i,"You must render this element inside a <DataRouterStateContext.Provider> element"),i}var dr=S.createContext(void 0);dr.displayName="FrameworkContext";function fr(){let i=S.useContext(dr);return rr(i,"You must render this element inside a <HydratedRouter> element"),i}function f0(i,c){let r=S.useContext(dr),[o,f]=S.useState(!1),[h,b]=S.useState(!1),{onFocus:j,onBlur:v,onMouseEnter:p,onMouseLeave:g,onTouchStart:y}=c,A=S.useRef(null);S.useEffect(()=>{if(i==="render"&&b(!0),i==="viewport"){let q=V=>{V.forEach(ee=>{b(ee.isIntersecting)})},Z=new IntersectionObserver(q,{threshold:.5});return A.current&&Z.observe(A.current),()=>{Z.disconnect()}}},[i]),S.useEffect(()=>{if(o){let q=setTimeout(()=>{b(!0)},100);return()=>{clearTimeout(q)}}},[o]);let T=()=>{f(!0)},D=()=>{f(!1),b(!1)};return r?i!=="intent"?[h,A,{}]:[h,A,{onFocus:ql(j,T),onBlur:ql(v,D),onMouseEnter:ql(p,T),onMouseLeave:ql(g,D),onTouchStart:ql(y,T)}]:[!1,A,{}]}function ql(i,c){return r=>{i&&i(r),r.defaultPrevented||c(r)}}function h0({page:i,...c}){let r=Nv(),{router:o}=or(),f=S.useMemo(()=>Lh(o.routes,i,o.basename),[o.routes,i,o.basename]);return f?r?S.createElement(p0,{page:i,matches:f,...c}):S.createElement(v0,{page:i,matches:f,...c}):null}function m0(i){let{manifest:c,routeModules:r}=fr(),[o,f]=S.useState([]);return S.useEffect(()=>{let h=!1;return u0(i,c,r).then(b=>{h||f(b)}),()=>{h=!0}},[i,c,r]),o}function p0({page:i,matches:c,...r}){let o=Dt(),{future:f}=fr(),{basename:h}=or(),b=S.useMemo(()=>{if(i===o.pathname+o.search+o.hash)return[];let j=am(i,h,f.unstable_trailingSlashAwareDataRequests,"rsc"),v=!1,p=[];for(let g of c)typeof g.route.shouldRevalidate=="function"?v=!0:p.push(g.route.id);return v&&p.length>0&&j.searchParams.set("_routes",p.join(",")),[j.pathname+j.search]},[h,f.unstable_trailingSlashAwareDataRequests,i,o,c]);return S.createElement(S.Fragment,null,b.map(j=>S.createElement("link",{key:j,rel:"prefetch",as:"fetch",href:j,...r})))}function v0({page:i,matches:c,...r}){let o=Dt(),{future:f,manifest:h,routeModules:b}=fr(),{basename:j}=or(),{loaderData:v,matches:p}=d0(),g=S.useMemo(()=>Mh(i,c,p,h,o,"data"),[i,c,p,h,o]),y=S.useMemo(()=>Mh(i,c,p,h,o,"assets"),[i,c,p,h,o]),A=S.useMemo(()=>{if(i===o.pathname+o.search+o.hash)return[];let q=new Set,Z=!1;if(c.forEach(ee=>{var ue;let I=h.routes[ee.route.id];!I||!I.hasLoader||(!g.some(K=>K.route.id===ee.route.id)&&ee.route.id in v&&((ue=b[ee.route.id])!=null&&ue.shouldRevalidate)||I.hasClientLoader?Z=!0:q.add(ee.route.id))}),q.size===0)return[];let V=am(i,j,f.unstable_trailingSlashAwareDataRequests,"data");return Z&&q.size>0&&V.searchParams.set("_routes",c.filter(ee=>q.has(ee.route.id)).map(ee=>ee.route.id).join(",")),[V.pathname+V.search]},[j,f.unstable_trailingSlashAwareDataRequests,v,o,h,g,c,i,b]),T=S.useMemo(()=>s0(y,h),[y,h]),D=m0(y);return S.createElement(S.Fragment,null,A.map(q=>S.createElement("link",{key:q,rel:"prefetch",as:"fetch",href:q,...r})),T.map(q=>S.createElement("link",{key:q,rel:"modulepreload",href:q,...r})),D.map(({key:q,link:Z})=>S.createElement("link",{key:q,nonce:r.nonce,...Z,crossOrigin:Z.crossOrigin??r.crossOrigin})))}function g0(...i){return c=>{i.forEach(r=>{typeof r=="function"?r(c):r!=null&&(r.current=c)})}}var y0=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";try{y0&&(window.__reactRouterVersion="7.14.2")}catch{}function b0({basename:i,children:c,unstable_useTransitions:r,window:o}){let f=S.useRef();f.current==null&&(f.current=I1({window:o,v5Compat:!0}));let h=f.current,[b,j]=S.useState({action:h.action,location:h.location}),v=S.useCallback(p=>{r===!1?j(p):S.startTransition(()=>j(p))},[r]);return S.useLayoutEffect(()=>h.listen(v),[h,v]),S.createElement(Jv,{basename:i,children:c,location:b.location,navigationType:b.action,navigator:h,unstable_useTransitions:r})}var nm=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,Ke=S.forwardRef(function({onClick:c,discover:r="render",prefetch:o="none",relative:f,reloadDocument:h,replace:b,unstable_mask:j,state:v,target:p,to:g,preventScrollReset:y,viewTransition:A,unstable_defaultShouldRevalidate:T,...D},q){let{basename:Z,navigator:V,unstable_useTransitions:ee}=S.useContext(Ut),I=typeof g=="string"&&nm.test(g),ue=Xh(g,Z);g=ue.to;let K=Mv(g,{relative:f}),Y=Dt(),C=null;if(j){let Ye=lr(j,[],Y.unstable_mask?Y.unstable_mask.pathname:"/",!0);Z!=="/"&&(Ye.pathname=Ye.pathname==="/"?Z:Lt([Z,Ye.pathname])),C=V.createHref(Ye)}let[O,G,ve]=f0(o,D),te=C0(g,{replace:b,unstable_mask:j,state:v,target:p,preventScrollReset:y,relative:f,viewTransition:A,unstable_defaultShouldRevalidate:T,unstable_useTransitions:ee});function he(Ye){c&&c(Ye),Ye.defaultPrevented||te(Ye)}let Qe=!(ue.isExternal||h),St=S.createElement("a",{...D,...ve,href:(Qe?C:void 0)||ue.absoluteURL||K,onClick:Qe?he:c,ref:g0(q,G),target:p,"data-discover":!I&&r==="render"?"true":void 0});return O&&!I?S.createElement(S.Fragment,null,St,S.createElement(h0,{page:K})):St});Ke.displayName="Link";var x0=S.forwardRef(function({"aria-current":c="page",caseSensitive:r=!1,className:o="",end:f=!1,style:h,to:b,viewTransition:j,children:v,...p},g){let y=Ql(b,{relative:p.relative}),A=Dt(),T=S.useContext(bu),{navigator:D,basename:q}=S.useContext(Ut),Z=T!=null&&_0(y)&&j===!0,V=D.encodeLocation?D.encodeLocation(y).pathname:y.pathname,ee=A.pathname,I=T&&T.navigation&&T.navigation.location?T.navigation.location.pathname:null;r||(ee=ee.toLowerCase(),I=I?I.toLowerCase():null,V=V.toLowerCase()),I&&q&&(I=oa(I,q)||I);const ue=V!=="/"&&V.endsWith("/")?V.length-1:V.length;let K=ee===V||!f&&ee.startsWith(V)&&ee.charAt(ue)==="/",Y=I!=null&&(I===V||!f&&I.startsWith(V)&&I.charAt(V.length)==="/"),C={isActive:K,isPending:Y,isTransitioning:Z},O=K?c:void 0,G;typeof o=="function"?G=o(C):G=[o,K?"active":null,Y?"pending":null,Z?"transitioning":null].filter(Boolean).join(" ");let ve=typeof h=="function"?h(C):h;return S.createElement(Ke,{...p,"aria-current":O,className:G,ref:g,style:ve,to:b,viewTransition:j},typeof v=="function"?v(C):v)});x0.displayName="NavLink";var j0=S.forwardRef(({discover:i="render",fetcherKey:c,navigate:r,reloadDocument:o,replace:f,state:h,method:b=hu,action:j,onSubmit:v,relative:p,preventScrollReset:g,viewTransition:y,unstable_defaultShouldRevalidate:A,...T},D)=>{let{unstable_useTransitions:q}=S.useContext(Ut),Z=w0(),V=T0(j,{relative:p}),ee=b.toLowerCase()==="get"?"get":"post",I=typeof j=="string"&&nm.test(j),ue=K=>{if(v&&v(K),K.defaultPrevented)return;K.preventDefault();let Y=K.nativeEvent.submitter,C=(Y==null?void 0:Y.getAttribute("formmethod"))||b,O=()=>Z(Y||K.currentTarget,{fetcherKey:c,method:C,navigate:r,replace:f,state:h,relative:p,preventScrollReset:g,viewTransition:y,unstable_defaultShouldRevalidate:A});q&&r!==!1?S.startTransition(()=>O()):O()};return S.createElement("form",{ref:D,method:ee,action:V,onSubmit:o?v:ue,...T,"data-discover":!I&&i==="render"?"true":void 0})});j0.displayName="Form";function S0(i){return`${i} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function lm(i){let c=S.useContext(Zn);return De(c,S0(i)),c}function C0(i,{target:c,replace:r,unstable_mask:o,state:f,preventScrollReset:h,relative:b,viewTransition:j,unstable_defaultShouldRevalidate:v,unstable_useTransitions:p}={}){let g=ur(),y=Dt(),A=Ql(i,{relative:b});return S.useCallback(T=>{if(Pv(T,c)){T.preventDefault();let D=r!==void 0?r:Gl(y)===Gl(A),q=()=>g(i,{replace:D,unstable_mask:o,state:f,preventScrollReset:h,relative:b,viewTransition:j,unstable_defaultShouldRevalidate:v});p?S.startTransition(()=>q()):q()}},[y,g,A,r,o,f,c,i,h,b,j,v,p])}function im(i){qt(typeof URLSearchParams<"u","You cannot use the `useSearchParams` hook in a browser that does not support the URLSearchParams API. If you need to support Internet Explorer 11, we recommend you load a polyfill such as https://github.com/ungap/url-search-params.");let c=S.useRef(tr(i)),r=S.useRef(!1),o=Dt(),f=S.useMemo(()=>e0(o.search,r.current?null:c.current),[o.search]),h=ur(),b=S.useCallback((j,v)=>{const p=tr(typeof j=="function"?j(new URLSearchParams(f)):j);r.current=!0,h("?"+p,v)},[h,f]);return[f,b]}var E0=0,N0=()=>`__${String(++E0)}__`;function w0(){let{router:i}=lm("useSubmit"),{basename:c}=S.useContext(Ut),r=Vv(),o=i.fetch,f=i.navigate;return S.useCallback(async(h,b={})=>{let{action:j,method:v,encType:p,formData:g,body:y}=n0(h,c);if(b.navigate===!1){let A=b.fetcherKey||N0();await o(A,r,b.action||j,{unstable_defaultShouldRevalidate:b.unstable_defaultShouldRevalidate,preventScrollReset:b.preventScrollReset,formData:g,body:y,formMethod:b.method||v,formEncType:b.encType||p,flushSync:b.flushSync})}else await f(b.action||j,{unstable_defaultShouldRevalidate:b.unstable_defaultShouldRevalidate,preventScrollReset:b.preventScrollReset,formData:g,body:y,formMethod:b.method||v,formEncType:b.encType||p,replace:b.replace,state:b.state,fromRouteId:r,flushSync:b.flushSync,viewTransition:b.viewTransition})},[o,f,c,r])}function T0(i,{relative:c}={}){let{basename:r}=S.useContext(Ut),o=S.useContext(Xt);De(o,"useFormAction must be used inside a RouteContext");let[f]=o.matches.slice(-1),h={...Ql(i||".",{relative:c})},b=Dt();if(i==null){h.search=b.search;let j=new URLSearchParams(h.search),v=j.getAll("index");if(v.some(g=>g==="")){j.delete("index"),v.filter(y=>y).forEach(y=>j.append("index",y));let g=j.toString();h.search=g?`?${g}`:""}}return(!i||i===".")&&f.route.index&&(h.search=h.search?h.search.replace(/^\?/,"?index&"):"?index"),r!=="/"&&(h.pathname=h.pathname==="/"?r:Lt([r,h.pathname])),Gl(h)}function _0(i,{relative:c}={}){let r=S.useContext(Kh);De(r!=null,"`useViewTransitionState` must be used within `react-router-dom`'s `RouterProvider`. Did you accidentally import `RouterProvider` from `react-router`?");let{basename:o}=lm("useViewTransitionState"),f=Ql(i,{relative:c});if(!r.isTransitioning)return!1;let h=oa(r.currentLocation.pathname,o)||r.currentLocation.pathname,b=oa(r.nextLocation.pathname,o)||r.nextLocation.pathname;return pu(f.pathname,b)!=null||pu(f.pathname,h)!=null}const R0='<path d="M23.1599 14.9453C22.7429 14.9429 22.3775 15.2985 22.375 15.7204C22.3726 16.1374 22.7282 16.5028 23.1501 16.5053C23.567 16.5077 23.9325 16.1521 23.935 15.7302C23.9374 15.3108 23.5793 14.9478 23.1599 14.9453Z" fill="currentColor"/><path d="M15.758 22.3758C15.3435 22.3562 14.9657 22.702 14.9461 23.1214C14.9265 23.5359 15.2723 23.9137 15.6917 23.9333C16.1063 23.9529 16.484 23.6071 16.5036 23.1877C16.5232 22.7731 16.1774 22.3954 15.758 22.3758Z" fill="currentColor"/><path d="M23.1208 9.08552C23.5721 9.10024 23.9375 8.76176 23.9473 8.31291C23.9571 7.86161 23.6137 7.50351 23.1649 7.49615C22.7308 7.49124 22.3825 7.81746 22.3604 8.24668C22.3383 8.70044 22.6744 9.06835 23.1208 9.08307V9.08552Z" fill="currentColor"/><path d="M8.32678 22.3598C7.87547 22.3451 7.51002 22.6836 7.50021 23.1324C7.49039 23.5837 7.83378 23.9418 8.28263 23.9492C8.73393 23.9541 9.08712 23.6058 9.08712 23.1545C9.08712 22.7032 8.75601 22.3746 8.32678 22.3598Z" fill="currentColor"/><path d="M23.1502 12.8994C23.6113 12.9019 24.0135 12.4947 24.0013 12.0361C23.9914 11.5897 23.6039 11.2095 23.16 11.207C22.6989 11.2046 22.2966 11.6117 22.3089 12.0704C22.3187 12.5143 22.7062 12.897 23.1502 12.8994Z" fill="currentColor"/><path d="M12.9002 23.1849C12.9198 22.7459 12.5568 22.3436 12.1079 22.3068C11.6542 22.2725 11.2299 22.6551 11.2078 23.1162C11.1882 23.5553 11.5512 23.9575 12 23.9943C12.4538 24.0287 12.8781 23.646 12.9002 23.1849Z" fill="currentColor"/><path d="M19.4899 20.3568C19.9829 20.3544 20.368 19.9595 20.3582 19.464C20.3508 18.9882 19.9755 18.6129 19.4997 18.6056C19.0067 18.5982 18.6118 18.9833 18.6094 19.4763C18.6094 19.9693 18.9969 20.3593 19.4899 20.3544V20.3568Z" fill="currentColor"/><path d="M0.946568 14.8555C0.483002 14.8555 0.0881117 15.243 0.0783008 15.7066C0.0684898 16.1873 0.470738 16.5994 0.951474 16.5969C1.41504 16.5969 1.80993 16.2094 1.81974 15.7458C1.82955 15.2651 1.4273 14.853 0.946568 14.8555Z" fill="currentColor"/><path d="M15.6895 1.82027C16.1678 1.83989 16.5872 1.445 16.597 0.964263C16.6044 0.500696 16.2267 0.0984479 15.7631 0.0788261C15.2848 0.0592042 14.8654 0.454094 14.8556 0.93483C14.8482 1.3984 15.2259 1.80065 15.6895 1.82027Z" fill="currentColor"/><path d="M0.928315 9.18321C1.44829 9.19302 1.84073 8.81285 1.84073 8.29532C1.84073 7.79742 1.47037 7.41479 0.974917 7.40253C0.454937 7.39272 0.0625 7.77289 0.0625 8.29042C0.0625 8.79078 0.432863 9.17095 0.928315 9.18321Z" fill="currentColor"/><path d="M8.33104 0.0630625C7.81106 0.0458934 7.41126 0.423614 7.40636 0.938689C7.399 1.43905 7.76691 1.82658 8.25991 1.84129C8.76272 1.85601 9.15761 1.50036 9.18459 1.00982C9.21157 0.489838 8.84121 0.0777789 8.33349 0.0630625H8.33104Z" fill="currentColor"/><path d="M19.483 3.67042C18.9728 3.67042 18.5362 4.1021 18.5313 4.61227C18.524 5.11999 18.9532 5.56148 19.4634 5.57374C19.9858 5.58846 20.4445 5.1347 20.4371 4.60982C20.4298 4.09965 19.9932 3.66797 19.483 3.66797V3.67042Z" fill="currentColor"/><path d="M0.976227 11.102C0.456247 11.0849 -0.00486668 11.5411 3.87869e-05 12.0611C0.00494425 12.5663 0.441531 13.0029 0.946794 13.0029C1.45206 13.0029 1.8911 12.5737 1.90091 12.066C1.91072 11.5631 1.48394 11.1192 0.976227 11.102Z" fill="currentColor"/><path d="M12.0584 4.16361e-05C11.5531 -0.00486383 11.1116 0.424365 11.1018 0.93208C11.0895 1.45206 11.5457 1.91072 12.0657 1.90091C12.571 1.8911 13.0051 1.45206 13.0002 0.946797C12.9978 0.441534 12.5636 0.0049471 12.0584 4.16361e-05Z" fill="currentColor"/><path d="M4.65891 18.5322C4.13894 18.5077 3.67046 18.9516 3.66801 19.479C3.6631 19.9867 4.09233 20.4257 4.6025 20.438C5.11022 20.4478 5.55416 20.0259 5.57133 19.5133C5.59095 19.0081 5.16908 18.5567 4.65891 18.5322Z" fill="currentColor"/><path d="M4.58641 5.65236C5.13337 5.67443 5.62637 5.21332 5.64845 4.65654C5.67052 4.10959 5.20941 3.61659 4.65264 3.59451C4.10568 3.57244 3.61268 4.03355 3.5906 4.59032C3.56853 5.13728 4.02964 5.63028 4.58641 5.65236Z" fill="currentColor"/><path d="M19.5008 16.8099C20.1017 16.8 20.5726 16.3169 20.5677 15.7159C20.5628 15.115 20.087 14.6392 19.4836 14.6367C18.8803 14.6343 18.402 15.1077 18.3946 15.7086C18.3873 16.3267 18.8803 16.8197 19.5008 16.8074V16.8099Z" fill="currentColor"/><path d="M15.7209 20.5694C16.3218 20.5694 16.8025 20.0985 16.8099 19.4976C16.8172 18.8967 16.3488 18.411 15.7478 18.3988C15.1298 18.384 14.6318 18.8746 14.6368 19.4927C14.6417 20.0936 15.1199 20.5694 15.7209 20.5719V20.5694Z" fill="currentColor"/><path d="M9.42652 19.4702C9.41916 18.8644 8.9188 18.364 8.31298 18.3518C7.69243 18.3395 7.1651 18.8546 7.16019 19.4751C7.15529 20.0981 7.67281 20.6157 8.29581 20.6157C8.9188 20.6157 9.43388 20.0908 9.42652 19.4702Z" fill="currentColor"/><path d="M19.4553 7.16016C18.8495 7.17487 18.354 7.68259 18.3516 8.28841C18.3491 8.91141 18.8666 9.42893 19.4896 9.42403C20.1126 9.41912 20.6253 8.89669 20.6154 8.27615C20.6056 7.65316 20.0734 7.14544 19.4553 7.16261V7.16016Z" fill="currentColor"/><path d="M15.7219 5.79748C16.3817 5.79748 16.9115 5.26034 16.8993 4.60055C16.887 3.95793 16.3695 3.44531 15.7244 3.44531C15.0793 3.44531 14.5348 3.98246 14.5471 4.64225C14.5593 5.28732 15.0793 5.79748 15.7219 5.79748Z" fill="currentColor"/><path d="M4.63052 16.9006C5.27559 16.8957 5.78821 16.3806 5.79557 15.738C5.80292 15.0782 5.27068 14.5435 4.6109 14.5509C3.94866 14.5582 3.42623 15.0978 3.44585 15.7576C3.46302 16.4002 3.9879 16.9055 4.63052 16.9006Z" fill="currentColor"/><path d="M12.0637 20.6756C12.7088 20.6683 13.2533 20.1115 13.246 19.4714C13.2386 18.8263 12.6818 18.2818 12.0417 18.2891C11.3966 18.2965 10.8521 18.8533 10.8594 19.4934C10.8668 20.1385 11.4211 20.683 12.0637 20.6756Z" fill="currentColor"/><path d="M19.4762 10.8594C18.8312 10.8618 18.2842 11.4137 18.2891 12.0563C18.2915 12.7014 18.8434 13.2483 19.486 13.2434C20.1311 13.241 20.6781 12.6891 20.6732 12.0465C20.6682 11.4039 20.1188 10.8569 19.4762 10.8594Z" fill="currentColor"/><path d="M8.31147 5.84627C8.98106 5.83645 9.52067 5.28459 9.51576 4.61499C9.51085 3.9454 8.9639 3.40089 8.29675 3.39844C7.62716 3.39844 7.07774 3.93804 7.07038 4.60764C7.06303 5.2944 7.6247 5.85362 8.31147 5.84627Z" fill="currentColor"/><path d="M4.64934 7.0706C3.96257 7.05588 3.39599 7.6102 3.39845 8.29942C3.39845 8.96902 3.94541 9.51597 4.615 9.51843C5.2846 9.52088 5.83646 8.98128 5.84382 8.31168C5.85118 7.64209 5.31648 7.08532 4.64689 7.0706H4.64934Z" fill="currentColor"/><path d="M12.0484 5.91679C12.7376 5.92169 13.3312 5.34285 13.3508 4.64873C13.3704 3.94479 12.7671 3.32916 12.0607 3.32425C11.3715 3.31934 10.7779 3.89819 10.7583 4.59231C10.7387 5.29625 11.3396 5.91434 12.0484 5.91679Z" fill="currentColor"/><path d="M4.58021 13.3473C5.28169 13.3743 5.90469 12.7783 5.91695 12.0695C5.92921 11.3827 5.35528 10.7818 4.66115 10.7548C3.95967 10.7278 3.33668 11.3238 3.32441 12.0327C3.31215 12.7194 3.88609 13.3203 4.58021 13.3473Z" fill="currentColor"/><path d="M15.7193 14.3359C14.9687 14.3359 14.3335 14.9761 14.3359 15.7266C14.3359 16.4772 14.9761 17.1124 15.7266 17.11C16.4772 17.11 17.1124 16.4698 17.11 15.7193C17.1075 14.9687 16.4698 14.3335 15.7193 14.3359Z" fill="currentColor"/><path d="M15.7407 9.73609C16.5428 9.72628 17.1756 9.0763 17.1658 8.27671C17.156 7.47712 16.506 6.84186 15.7064 6.85167C14.9068 6.86149 14.2716 7.51146 14.2814 8.31105C14.2912 9.11064 14.9411 9.7459 15.7407 9.73609Z" fill="currentColor"/><path d="M8.2987 14.2813C7.50156 14.2764 6.8565 14.9165 6.85159 15.7161C6.84669 16.5133 7.48685 17.1583 8.28644 17.1632C9.08358 17.1681 9.72865 16.528 9.73355 15.7284C9.73601 14.9313 9.09584 14.2862 8.2987 14.2813Z" fill="currentColor"/><path d="M8.2854 9.79467C9.12669 9.79712 9.78647 9.15696 9.79874 8.32057C9.811 7.45967 9.15857 6.79007 8.30257 6.78516C7.46128 6.78271 6.8015 7.42533 6.78923 8.25926C6.77697 9.12017 7.4294 9.78976 8.2854 9.79467Z" fill="currentColor"/><path d="M15.7268 10.5156C14.8757 10.5156 14.1644 11.2343 14.184 12.0829C14.2036 12.9242 14.9075 13.6061 15.7415 13.5914C16.5803 13.5766 17.2671 12.8801 17.2622 12.0461C17.2573 11.2097 16.5631 10.5181 15.7268 10.5156Z" fill="currentColor"/><path d="M12.0588 14.1836C11.2077 14.1787 10.4964 14.8998 10.516 15.7485C10.5356 16.5897 11.2371 17.2716 12.0686 17.2593C12.9074 17.2471 13.5942 16.553 13.5917 15.7166C13.5893 14.8802 12.8976 14.1885 12.0612 14.1836H12.0588Z" fill="currentColor"/><path d="M12.0397 6.66802C11.1568 6.67538 10.4356 7.39894 10.4258 8.28192C10.4185 9.17717 11.1666 9.92525 12.0618 9.91789C12.9448 9.91054 13.6659 9.18698 13.6757 8.304C13.6831 7.40875 12.935 6.66066 12.0397 6.66802Z" fill="currentColor"/><path d="M8.29197 13.6757C9.1725 13.6757 9.90096 12.9619 9.91813 12.074C9.9353 11.1812 9.19212 10.4282 8.29442 10.4258C7.41389 10.4258 6.68543 11.1395 6.66826 12.0274C6.65109 12.9202 7.39427 13.6732 8.29197 13.6757Z" fill="currentColor"/><path d="M12.0638 10.2891C11.068 10.2842 10.2905 11.0568 10.293 12.0526C10.293 13.0288 11.0533 13.8014 12.0222 13.8137C13.0204 13.8259 13.8077 13.0631 13.8151 12.0722C13.8225 11.074 13.0548 10.294 12.0638 10.2891Z" fill="currentColor"/>';function A0({size:i=20,idSuffix:c,className:r}){const o=`pc-brand-mark-clip-${c}`;return s.jsx("svg",{width:i,height:i,viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",className:r,dangerouslySetInnerHTML:{__html:`<g clip-path="url(#${o})">${R0}</g><defs><clipPath id="${o}"><rect width="24" height="24" fill="white"/></clipPath></defs>`}})}const um="Parachute";let an=null,Yl=null;const z0=3e4;function M0(){return"/admin/host-admin-token"}function O0(){const i=`${window.location.pathname}${window.location.search}`;return`/login?next=${encodeURIComponent(i)}`}function sm(){return window.location.replace(O0()),new Promise(()=>{})}function U0(){return window.location.replace("/account/"),new Promise(()=>{})}async function ze(){const i=Date.now();return an&&an.expiresAt-i>z0?an.token:Yl||(Yl=D0().finally(()=>{Yl=null}),Yl)}async function D0(){const i=await fetch(M0(),{method:"GET",headers:{accept:"application/json"},credentials:"same-origin"});if(i.status===401)return an=null,sm();if(i.status===403)return an=null,U0();if(!i.ok)throw new Error(`/admin/host-admin-token failed: ${i.status} ${await i.text()}`);const c=await i.json();if(!c.token||!c.expires_at)throw new Error("/admin/host-admin-token returned malformed body");return an={token:c.token,expiresAt:new Date(c.expires_at).getTime()},c.token}function Ee(){an=null}class X extends Error{constructor(c,r){super(r),this.status=c,this.name="HttpError"}}async function k0(){const i=await fetch("/.well-known/parachute.json",{headers:{accept:"application/json"}});if(!i.ok)throw new X(i.status,`well-known fetch failed: ${i.status}`);const c=await i.json(),r=c.vaults??[],o=c.services??[];return{moduleInstalled:o.some(h=>h.name==="parachute-vault"||h.name.startsWith("parachute-vault-")),vaults:r.map(h=>{const b={name:h.name,url:h.url,version:h.version,path:H0(h.name,h.url,o)};return h.managementUrl&&(b.managementUrl=h.managementUrl),b})}}function H0(i,c,r){const o=r.find(f=>f.name===`parachute-vault-${i}`||f.path===`/vault/${i}`);if(o)return o.path;try{return new URL(c).pathname}catch{return`/vault/${i}`}}async function B0(i){const c=await ze(),r=await fetch("/vaults",{method:"POST",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${c}`},body:JSON.stringify(i)});if(r.status===401||r.status===403)throw Ee(),new X(r.status,await W(r));if(!r.ok)throw new X(r.status,await W(r));const o=r.status===201,f=await r.json(),h={name:f.name,url:f.url,version:f.version,created:o};return f.token&&(h.token=f.token),f.token_guidance&&(h.tokenGuidance=f.token_guidance),f.paths&&(h.paths=f.paths),h}async function L0(i){const c=await fetch(`/admin/vault-admin-token/${encodeURIComponent(i)}`,{method:"GET",headers:{accept:"application/json"},credentials:"same-origin"});if(c.status===401)return sm();if(!c.ok)throw new X(c.status,await W(c));const r=await c.json();if(!r.token||!r.expires_at)throw new X(500,"/admin/vault-admin-token returned malformed body");return{token:r.token,expiresAt:r.expires_at,scopes:r.scopes??[]}}async function q0(i={}){const c=await ze(),r=i.vault?`?vault=${encodeURIComponent(i.vault)}`:"",o=await fetch(`/api/grants${r}`,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${c}`}});if(o.status===401||o.status===403)throw Ee(),new X(o.status,await W(o));if(!o.ok)throw new X(o.status,await W(o));return(await o.json()).grants??[]}async function Y0(i){const c=await ze(),r=await fetch(`/api/grants/${encodeURIComponent(i)}`,{method:"DELETE",headers:{authorization:`Bearer ${c}`}});if(r.status===401||r.status===403)throw Ee(),new X(r.status,await W(r));if(!r.ok)throw new X(r.status,await W(r))}async function G0(){const i=await fetch("/api/me",{method:"GET",headers:{accept:"application/json"},credentials:"same-origin"});if(!i.ok)throw new X(i.status,await W(i));return await i.json()}async function V0(i){const c=new URLSearchParams({__csrf:i}),r=await fetch("/logout",{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded",accept:"text/html, application/json"},credentials:"same-origin",body:c,redirect:"manual"});if(!(r.status===0||r.ok||r.status===302))throw new X(r.status,await W(r))}function Z0(i,c){if(/^https?:\/\//i.test(c))return c;const r=i.replace(/\/+$/,""),o=c.startsWith("/")?c:`/${c}`;return`${r}${o}`}async function Oh(i={}){const c=new URLSearchParams;i.revoked&&c.set("revoked",i.revoked),i.subject&&c.set("subject",i.subject),i.createdVia&&c.set("created_via",i.createdVia),i.cursor&&c.set("cursor",i.cursor);const r=c.toString(),o=r?`/api/auth/tokens?${r}`:"/api/auth/tokens",f=await ze(),h=await fetch(o,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${f}`}});if(h.status===401||h.status===403)throw Ee(),new X(h.status,await W(h));if(!h.ok)throw new X(h.status,await W(h));return await h.json()}async function cm(i){const c=await ze(),r=await fetch("/api/auth/mint-token",{method:"POST",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${c}`},body:JSON.stringify(i)});if(r.status===401||r.status===403)throw Ee(),new X(r.status,await W(r));if(!r.ok)throw new X(r.status,await W(r));return await r.json()}async function X0(i){const c=await ze(),r=await fetch("/api/auth/revoke-token",{method:"POST",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${c}`},body:JSON.stringify({jti:i})});if(r.status===401||r.status===403)throw Ee(),new X(r.status,await W(r));if(!r.ok)throw new X(r.status,await W(r));return await r.json()}async function Q0(i){const c=await ze(),r=await fetch(`/api/oauth/clients/${encodeURIComponent(i)}`,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${c}`}});if(r.status===401||r.status===403)throw Ee(),new X(r.status,await W(r));if(!r.ok)throw new X(r.status,await W(r));return await r.json()}async function J0(i,c){const o={method:"POST",headers:{accept:"application/json",authorization:`Bearer ${await ze()}`}};c!==void 0&&(o.headers={...o.headers,"content-type":"application/json"},o.body=JSON.stringify({return_to:c}));const f=await fetch(`/api/oauth/clients/${encodeURIComponent(i)}/approve`,o);if(f.status===401||f.status===403)throw Ee(),new X(f.status,await W(f));if(!f.ok)throw new X(f.status,await W(f));return await f.json()}async function rm(){const i=await ze(),c=await fetch("/api/modules",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${i}`}});if(c.status===401||c.status===403)throw Ee(),new X(c.status,await W(c));if(!c.ok)throw new X(c.status,await W(c));return await c.json()}async function ju(i,c){const r=await ze();return await fetch(`/api/modules/${encodeURIComponent(i)}/${c}`,{method:"POST",headers:{accept:"application/json",authorization:`Bearer ${r}`}})}async function K0(i){const c=await ju(i,"install");if(c.status===401||c.status===403)throw Ee(),new X(c.status,await W(c));if(!c.ok)throw new X(c.status,await W(c));return(await c.json()).operation_id}async function $0(i){const c=await ju(i,"upgrade");if(c.status===401||c.status===403)throw Ee(),new X(c.status,await W(c));if(!c.ok)throw new X(c.status,await W(c));return(await c.json()).operation_id}async function F0(i){const c=await ju(i,"restart");if(c.status===401||c.status===403)throw Ee(),new X(c.status,await W(c));if(!c.ok)throw new X(c.status,await W(c));return await c.json()}async function W0(i){const c=await ju(i,"uninstall");if(c.status===401||c.status===403)throw Ee(),new X(c.status,await W(c));if(!c.ok)throw new X(c.status,await W(c));return await c.json()}async function I0(i){const c=await ze(),r=await fetch("/api/modules/channel",{method:"PUT",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${c}`},body:JSON.stringify({channel:i})});if(r.status===401||r.status===403)throw Ee(),new X(r.status,await W(r));if(!r.ok)throw new X(r.status,await W(r));return(await r.json()).channel}async function om(){const i=await ze(),c=await fetch("/api/settings/hub-origin",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${i}`}});if(c.status===401||c.status===403)throw Ee(),new X(c.status,await W(c));if(!c.ok)throw new X(c.status,await W(c));return await c.json()}async function Uh(i){const c=await ze(),r=await fetch("/api/settings/hub-origin",{method:"PUT",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${c}`},body:JSON.stringify({hub_origin:i})});if(r.status===401||r.status===403)throw Ee(),new X(r.status,await W(r));if(!r.ok)throw new X(r.status,await W(r));return(await r.json()).hub_origin}async function P0(i){const c=await ze(),r=await fetch(`/api/modules/${encodeURIComponent(i)}/config/schema`,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${c}`}});if(r.status===401||r.status===403)throw Ee(),new X(r.status,await W(r));if(r.status===404){let o;try{o=(await r.clone().json()).error}catch{}if(o==="no_config_schema")return null;throw new X(404,await W(r))}if(!r.ok)throw new X(r.status,await W(r));return await r.json()}async function eg(i){const c=await ze(),r=await fetch(`/api/modules/${encodeURIComponent(i)}/config`,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${c}`}});if(r.status===401||r.status===403)throw Ee(),new X(r.status,await W(r));if(r.status===404)return{};if(!r.ok)throw new X(r.status,await W(r));return await r.json()}async function tg(i,c){const r=await ze(),o=await fetch(`/api/modules/${encodeURIComponent(i)}/config`,{method:"PUT",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${r}`},body:JSON.stringify(c)});if(o.status===401||o.status===403)throw Ee(),new X(o.status,await W(o));if(!o.ok)throw new X(o.status,await W(o));return await o.json()}async function ag(i){const c=await ze(),r=await fetch(`/api/modules/operations/${encodeURIComponent(i)}`,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${c}`}});if(r.status===401||r.status===403)throw Ee(),new X(r.status,await W(r));if(!r.ok)throw new X(r.status,await W(r));return await r.json()}async function ng(){const i=await ze(),c=await fetch("/api/users",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${i}`}});if(c.status===401||c.status===403)throw Ee(),new X(c.status,await W(c));if(!c.ok)throw new X(c.status,await W(c));return(await c.json()).users??[]}async function lg(i){const c=await ze(),r=await fetch("/api/users",{method:"POST",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${c}`},body:JSON.stringify(i)});if(r.status===401||r.status===403)throw Ee(),new X(r.status,await W(r));if(!r.ok)throw new X(r.status,await W(r));return(await r.json()).user}async function ig(i){const c=await ze(),r=await fetch(`/api/users/${encodeURIComponent(i)}`,{method:"DELETE",headers:{authorization:`Bearer ${c}`}});if(r.status===401||r.status===403)throw r.status===401&&Ee(),new X(r.status,await W(r));if(!r.ok)throw new X(r.status,await W(r));const o=await r.json().catch(()=>({}));return{revocationLagSeconds:typeof o.revocation_lag_seconds=="number"?o.revocation_lag_seconds:60}}async function ug(i,c){const r=await ze(),o=await fetch(`/api/users/${encodeURIComponent(i)}/reset-password`,{method:"POST",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${r}`},body:JSON.stringify({new_password:c})});if(o.status===401||o.status===403)throw o.status===401&&Ee(),new X(o.status,await W(o));if(!o.ok)throw new X(o.status,await W(o));const f=await o.json().catch(()=>({}));return{revocationLagSeconds:typeof f.revocation_lag_seconds=="number"?f.revocation_lag_seconds:60}}async function sg(i,c){const r=await ze(),o=await fetch(`/api/users/${encodeURIComponent(i)}/vaults`,{method:"PATCH",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${r}`},body:JSON.stringify({assigned_vaults:c})});if(o.status===401||o.status===403)throw o.status===401&&Ee(),new X(o.status,await W(o));if(!o.ok)throw new X(o.status,await W(o))}async function cg(){const i=await ze(),c=await fetch("/api/users/vaults",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${i}`}});if(c.status===401||c.status===403)throw Ee(),new X(c.status,await W(c));if(!c.ok)throw new X(c.status,await W(c));return(await c.json()).vaults??[]}async function ar(){const i=await ze(),c=await fetch("/api/hub",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${i}`}});if(c.status===401||c.status===403)throw Ee(),new X(c.status,await W(c));if(!c.ok)throw new X(c.status,await W(c));return await c.json()}async function rg(i){const c=await ze(),r=await fetch("/api/hub/upgrade",{method:"POST",headers:{accept:"application/json",authorization:`Bearer ${c}`}});if(r.status===401||r.status===403)throw Ee(),new X(r.status,await W(r));if(!r.ok)throw new X(r.status,await W(r));return await r.json()}async function og(){const i=await ze(),c=await fetch("/api/hub/upgrade/status",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${i}`}});if(c.status===404)return null;if(c.status===401||c.status===403)throw Ee(),new X(c.status,await W(c));if(!c.ok)throw new X(c.status,await W(c));return await c.json()}async function W(i){try{const c=await i.text(),r=JSON.parse(c);if(r.error_description)return r.error_description;if(r.error)return r.error;if(c)return c}catch{}return`${i.status} ${i.statusText}`}const dg=3e4;function Dh(i){const c=Math.max(0,i),r=Math.floor(c/1e3);if(r<60)return`${r}s`;const o=Math.floor(r/60);if(o<60)return`${o}m`;const f=Math.floor(o/60);return f<24?`${f}h ${o%60}m`:`${Math.floor(f/24)}d ${f%24}h`}function $c(i){const c=new Date(i);if(Number.isNaN(c.getTime()))return i;const r=o=>String(o).padStart(2,"0");return`${c.getUTCFullYear()}-${r(c.getUTCMonth()+1)}-${r(c.getUTCDate())} ${r(c.getUTCHours())}:${r(c.getUTCMinutes())}:${r(c.getUTCSeconds())} UTC`}function fg(i){var c;if(i.source==="container")return"container";if(i.source==="npm")return"npm";if(i.source==="bun-linked"){const r=(c=i.bun_linked_path)==null?void 0:c.split("/").filter(Boolean).pop();return r&&i.git_head?`bun-linked → ${r} @ ${i.git_head}`:r?`bun-linked → ${r}`:"bun-linked"}return"unknown"}function hg(){const[i,c]=S.useState(null),[r,o]=S.useState(!1),[f,h]=S.useState(!1),[b,j]=S.useState(null),v=S.useCallback(async()=>{h(!0);try{const p=await ar();c(p),j(new Date)}catch{}finally{h(!1)}},[]);return S.useEffect(()=>{v()},[v]),S.useEffect(()=>{const p=()=>{v()};window.addEventListener("focus",p);const g=window.setInterval(()=>{v()},dg);return()=>{window.removeEventListener("focus",p),window.clearInterval(g)}},[v]),i?s.jsxs("footer",{className:"hub-version-badge","data-testid":"hub-version-badge",children:[s.jsxs("button",{type:"button",className:"hub-version-badge-summary",onClick:()=>o(p=>!p),"aria-expanded":r,"data-testid":"hub-version-badge-summary",children:["Hub ",s.jsx("strong",{children:i.version})," · running"," ",s.jsx("span",{"data-testid":"hub-version-badge-uptime",children:Dh(i.uptime_ms)})," ·"," ",s.jsx("span",{className:"hub-version-badge-source","data-testid":"hub-version-badge-source",children:i.source})]}),r?s.jsxs("div",{className:"hub-version-badge-panel","data-testid":"hub-version-badge-panel",children:[s.jsxs("dl",{children:[s.jsx("dt",{children:"Hub"}),s.jsx("dd",{children:s.jsxs("code",{children:["@openparachute/hub ",i.version]})}),s.jsx("dt",{children:"Source"}),s.jsx("dd",{children:fg(i)}),s.jsx("dt",{children:"Started"}),s.jsxs("dd",{children:[$c(i.started_at)," (",Dh(i.uptime_ms)," ago)"]}),i.container_build_time?s.jsxs(s.Fragment,{children:[s.jsx("dt",{children:"Built"}),s.jsx("dd",{children:$c(i.container_build_time)})]}):null,i.render_commit?s.jsxs(s.Fragment,{children:[s.jsx("dt",{children:"Commit"}),s.jsxs("dd",{children:[s.jsx("code",{children:i.render_commit.slice(0,8)}),i.render_branch?s.jsxs(s.Fragment,{children:[" on ",s.jsx("code",{children:i.render_branch})]}):null]})]}):null,b?s.jsxs(s.Fragment,{children:[s.jsx("dt",{children:"Last checked"}),s.jsx("dd",{children:$c(b.toISOString())})]}):null]}),s.jsx("button",{type:"button",className:"hub-version-badge-refresh secondary",onClick:()=>void v(),disabled:f,"data-testid":"hub-version-badge-refresh",children:f?"Refreshing…":"Refresh"})]}):null]}):null}function mg(){const{clientId:i}=Ih(),c=i??"",[r]=im(),o=pg(r.get("return_to")),[f,h]=S.useState({kind:"loading"}),[b,j]=S.useState({kind:"idle"}),[v,p]=S.useState(0);S.useEffect(()=>{if(!c){h({kind:"error",message:"missing client id in URL"});return}let y=!1;return h({kind:"loading"}),Q0(c).then(A=>{y||(h({kind:"ok",client:A}),A.status==="approved"&&(j({kind:"approved",alreadyApproved:!0}),o&&window.location.assign(o)))}).catch(A=>{if(y)return;if(A instanceof X&&A.status===404){h({kind:"not_found"});return}const T=A instanceof Error?A.message:String(A);h({kind:"error",message:T})}),()=>{y=!0}},[c,v,o]);async function g(){j({kind:"approving"});try{const y=await J0(c,o||void 0);if(y.redirect_to&&dm(y.redirect_to)){window.location.assign(y.redirect_to);return}j({kind:"approved",alreadyApproved:y.already_approved})}catch(y){const A=y instanceof X?`approve failed (${y.status}): ${y.message}`:y instanceof Error?y.message:String(y);j({kind:"error",message:A})}}return s.jsxs("div",{children:[s.jsx("div",{className:"list-header",children:s.jsx("h2",{children:"Approve app"})}),vg({loadState:f,action:b,onApprove:g,onRetry:()=>p(y=>y+1)})]})}function dm(i){return!(!i||!i.startsWith("/")||i.startsWith("//"))}function pg(i){return i===null?null:dm(i)?i:null}function vg({loadState:i,action:c,onApprove:r,onRetry:o}){if(i.kind==="loading")return s.jsx("p",{className:"muted","data-loading":"true",children:"Loading…"});if(i.kind==="not_found")return s.jsxs("div",{className:"empty empty-rich",children:[s.jsx("p",{className:"empty-headline",children:"Unknown client."}),s.jsxs("p",{className:"muted",children:["This client_id isn't registered with this hub. The deep link may be stale, or the requesting app may have been registered against a different hub."," ",s.jsx(Ke,{to:"/permissions",children:"Back to permissions"}),"."]})]});if(i.kind==="error")return s.jsxs(s.Fragment,{children:[s.jsxs("div",{className:"error-banner",children:["Couldn't load the client: ",s.jsx("code",{children:i.message})]}),s.jsx("button",{type:"button",onClick:o,className:"secondary",children:"Retry"})]});const{client:f}=i,h=f.client_name??f.client_id;if(c.kind==="approved")return s.jsxs("div",{className:"empty empty-rich",children:[s.jsx("p",{className:"empty-headline",children:c.alreadyApproved?"Already approved.":"Approved."}),s.jsxs("p",{className:"muted",children:[s.jsx("code",{children:h})," ",c.alreadyApproved?"was already on this hub's approved list.":"can now run an OAuth flow with this hub."," ","Return to the app that sent you here and retry the action — the request will go through now."]}),s.jsx("p",{className:"muted",style:{marginTop:"1rem"},children:s.jsx(Ke,{to:"/permissions",children:"View permissions"})})]});const b=c.kind==="approving",j=c.kind==="error"?c.message:null;return s.jsxs("div",{children:[s.jsx("p",{className:"muted",children:"An app is asking this hub to issue OAuth tokens. Review the details below and approve only if you recognize the app."}),s.jsx("div",{className:"vault-row",style:{marginTop:"1rem"},children:s.jsxs("div",{className:"body",children:[s.jsx("div",{className:"name",children:s.jsx("code",{children:h})}),s.jsxs("div",{className:"dim",children:[s.jsx("span",{className:"muted",children:"client_id: "}),s.jsx("code",{children:f.client_id})]}),s.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[s.jsx("span",{className:"muted",children:"redirect_uris: "}),f.redirect_uris.map((v,p)=>s.jsxs("span",{children:[s.jsx("code",{children:v}),p<f.redirect_uris.length-1?" ":null]},v))]}),f.scopes.length>0?s.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[s.jsx("span",{className:"muted",children:"requested scopes: "}),f.scopes.map((v,p)=>s.jsxs("span",{children:[s.jsx("code",{children:gg(v)}),p<f.scopes.length-1?" ":null]},v)),f.scopes.some(fm)?s.jsxs("p",{className:"muted",style:{marginTop:"0.4rem",fontStyle:"italic",fontSize:"0.85rem"},children:[s.jsx("code",{children:"*"})," — a specific vault is selected during sign-in via the consent picker (or the user's assigned vault for multi-user setups)."]}):null]}):null,s.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[s.jsx("span",{className:"muted",children:"registered: "}),s.jsx("code",{title:f.registered_at,children:f.registered_at})]}),j?s.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:s.jsx("code",{children:j})}):null]})}),s.jsxs("div",{style:{marginTop:"1rem",display:"flex",gap:"0.5rem"},children:[s.jsx("button",{type:"button",onClick:()=>{r()},disabled:b,children:b?"Approving…":`Approve ${h}`}),s.jsx(Ke,{to:"/permissions",className:"secondary",style:{alignSelf:"center"},children:"Cancel"})]})]})}function gg(i){return fm(i)?`vault:*:${i.split(":")[1]}`:i}function fm(i){const c=i.split(":");if(c.length!==2||c[0]!=="vault")return!1;const r=c[1];return r==="read"||r==="write"}function yg(i,c){return Vl(i,i,new Set)}function Vl(i,c,r){if(Array.isArray(i))return i.map(h=>Vl(h,c,r));if(i===null||typeof i!="object")return i;const o=i;if(typeof o.$ref=="string")return bg(o,c,r);const f={};for(const[h,b]of Object.entries(o))f[h]=Vl(b,c,r);return f}function bg(i,c,r){const o=i.$ref;if(typeof o!="string")throw new Error(`Invalid $ref: expected string, got ${typeof o}`);if(!o.startsWith("#"))throw new Error(`Unsupported $ref "${o}": external refs (URLs / file paths) are not supported`);if(r.has(o))throw new Error(`Circular $ref detected: "${o}" already on resolution path`);const f=xg(o,c);if(f===void 0)throw new Error(`Unknown $ref "${o}": no definition at that path`);if(f===null||typeof f!="object"||Array.isArray(f))throw new Error(`Invalid $ref target at "${o}": expected object, got ${typeof f}`);const h=new Set(r);h.add(o);const b=Vl(f,c,h),j={};for(const[v,p]of Object.entries(i))v!=="$ref"&&(j[v]=Vl(p,c,h));return{...b,...j}}function xg(i,c){const r=i.slice(1);if(r==="")return c;if(!r.startsWith("/"))throw new Error(`Unsupported $ref "${i}": fragment must start with "/"`);const o=r.slice(1).split("/").map(h=>decodeURIComponent(h).replace(/~1/g,"/").replace(/~0/g,"~"));let f=c;for(const h of o)if(f===null||typeof f!="object"||(f=f[h],f===void 0))return;return f}function jg(){const{short:i=""}=Ih(),[c,r]=S.useState({kind:"loading"}),[o,f]=S.useState({}),[h,b]=S.useState({}),[j,v]=S.useState({}),[p,g]=S.useState(!1),[y,A]=S.useState(null),[T,D]=S.useState({}),q=S.useCallback(async()=>{r({kind:"loading"}),f({}),b({}),v({}),A(null),D({});try{const Y=await P0(i);if(Y===null){r({kind:"no_schema"});return}let C;try{C=yg(Y)}catch(G){const ve=G instanceof Error?G.message:String(G);r({kind:"error",message:`Schema $ref resolution failed — ${ve}`});return}const O=await eg(i);r({kind:"ok",schema:C,values:O}),f({...O}),v({...O})}catch(Y){if(Y instanceof X&&Y.status===404){r({kind:"not_installed"});return}const C=Y instanceof Error?Y.message:String(Y);r({kind:"error",message:C})}},[i]);S.useEffect(()=>{q()},[q]);function Z(Y,C){var ve;f(te=>({...te,[Y]:C}));const O=c.kind==="ok"?(ve=c.schema.properties)==null?void 0:ve[Y]:void 0;if((O==null?void 0:O.writeOnly)===!0){const te=C!==""&&C!==void 0&&C!==null;b(he=>({...he,[Y]:te}))}else b(te=>({...te,[Y]:C!==j[Y]}));D(te=>{if(!(Y in te))return te;const he={...te};return delete he[Y],he})}async function V(Y){if(Y.preventDefault(),p||c.kind!=="ok")return;g(!0),A(null),D({});const C={};for(const[O,G]of Object.entries(h))G&&(C[O]=o[O]);if(Object.keys(C).length===0){g(!1),A({kind:"error",message:"No changes to save. Edit a field first."});return}try{const O=await tg(i,C);A({kind:"success",message:"Configuration saved.",restartRequired:Array.isArray(O.restart_required)?O.restart_required:[]}),b({})}catch(O){if(O instanceof X&&O.status===400)try{const ve=JSON.parse(O.message),te=Array.isArray(ve.errors)?ve.errors:[];if(te.length>0){const he={};for(const Qe of te)typeof(Qe==null?void 0:Qe.path)=="string"&&typeof(Qe==null?void 0:Qe.message)=="string"&&(he[Qe.path]=Qe.message);D(he),A({kind:"error",message:ve.message??ve.error_description??"Validation failed."});return}A({kind:"error",message:ve.message??ve.error_description??"Validation failed."});return}catch{}const G=O instanceof Error?O.message:String(O);A({kind:"error",message:`Save failed — ${G}`})}finally{g(!1)}}if(c.kind==="loading")return s.jsx("div",{className:"empty",children:"Loading configuration…"});if(c.kind==="not_installed")return s.jsxs("section",{className:"module-config",children:[s.jsxs("h1",{children:["Configure ",i]}),s.jsxs("div",{className:"empty",children:[s.jsxs("p",{children:[s.jsx("code",{children:i})," is not installed on this hub."]}),s.jsxs("p",{children:["Visit ",s.jsx(Ke,{to:"/modules",children:"Modules"})," to install it first."]})]})]});if(c.kind==="no_schema")return s.jsxs("section",{className:"module-config",children:[s.jsxs("h1",{children:["Configure ",i]}),s.jsxs("div",{className:"empty",children:[s.jsxs("p",{children:[s.jsx("code",{children:i})," does not expose an operator-editable configuration schema."]}),s.jsxs("p",{children:["Some modules are configured entirely through environment variables or files on disk — check the module's documentation, or visit ",s.jsx(Ke,{to:"/modules",children:"Modules"}),"."]})]})]});if(c.kind==="error")return s.jsxs("section",{className:"module-config",children:[s.jsxs("h1",{children:["Configure ",i]}),s.jsxs("div",{className:"empty",children:["Failed to load configuration: ",c.message,"."," ",s.jsx("button",{type:"button",onClick:()=>void q(),children:"Retry"})]})]});const{schema:ee}=c,I=ee.properties??{},ue=new Set(ee.required??[]),K=Object.entries(I);return s.jsxs("section",{className:"module-config",children:[s.jsxs("header",{className:"module-config-header",children:[s.jsxs("h1",{children:["Configure ",i]}),s.jsxs("p",{className:"muted",children:["Edit values and save. Some changes apply immediately; others (provider, port) require restarting the module to take effect. ",s.jsx(Ke,{to:"/modules",children:"Back to modules"}),"."]})]}),K.length===0&&s.jsx("div",{className:"empty",children:s.jsx("p",{children:"This module's schema defines no editable properties."})}),s.jsxs("form",{onSubmit:Y=>void V(Y),className:"module-config-form","data-testid":"config-form",children:[s.jsx("fieldset",{children:K.map(([Y,C])=>s.jsx(Sg,{name:Y,property:C,value:o[Y],required:ue.has(Y),writeOnlyStored:C.writeOnly===!0&&!(Y in c.values),error:T[Y],dirty:!!h[Y],onChange:O=>Z(Y,O)},Y))}),s.jsxs("div",{className:"actions",children:[s.jsx("button",{type:"submit",disabled:p||K.length===0,children:p?"Saving…":"Save"}),s.jsx("button",{type:"button",className:"destructive",disabled:p,onClick:()=>void q(),children:"Discard changes"})]}),(y==null?void 0:y.kind)==="success"&&s.jsxs("div",{className:"banner banner-success","data-testid":"save-success",children:[s.jsx("strong",{children:y.message}),y.restartRequired.length>0&&s.jsxs(s.Fragment,{children:[s.jsxs("p",{className:"muted",children:["Restart ",i," to apply these field changes:"]}),s.jsx("ul",{children:y.restartRequired.map(Y=>s.jsx("li",{children:s.jsx("code",{children:Y})},Y))})]})]}),(y==null?void 0:y.kind)==="error"&&s.jsx("div",{className:"banner banner-error error","data-testid":"save-error",children:y.message})]})]})}function Sg({name:i,property:c,value:r,required:o,writeOnlyStored:f,error:h,dirty:b,onChange:j}){const v=`config-field-${i}`,p=c.title??i,g=c.description,y=Array.isArray(c.enum)&&c.enum.length>0;if(c.type==="boolean")return s.jsxs("div",{className:`field field-inline ${h?"field-invalid":""}`,"data-field":i,children:[s.jsxs("label",{htmlFor:v,children:[s.jsx("input",{id:v,type:"checkbox",checked:!!r,onChange:A=>j(A.target.checked)}),s.jsx("span",{className:"field-label-inline",children:p})]}),g&&s.jsx("p",{className:"muted field-hint",children:g}),h&&s.jsx("p",{className:"error field-error",children:h})]});if(y){const A=c.enum;return s.jsxs("div",{className:`field ${h?"field-invalid":""}`,"data-field":i,children:[s.jsxs("label",{htmlFor:v,children:[s.jsxs("span",{className:"field-label",children:[p,o&&s.jsx("span",{className:"muted",children:" (required)"})]}),s.jsxs("select",{id:v,value:r==null?"":String(r),onChange:T=>{const D=T.target.value;c.type==="number"||c.type==="integer"?j(D===""?void 0:Number(D)):j(D===""?void 0:D)},children:[!o&&s.jsx("option",{value:"",children:"(unset)"}),A.map(T=>s.jsx("option",{value:String(T),children:String(T)},String(T)))]})]}),g&&s.jsx("p",{className:"muted field-hint",children:g}),h&&s.jsx("p",{className:"error field-error",children:h})]})}return c.type==="string"&&c.writeOnly===!0?s.jsxs("div",{className:`field ${h?"field-invalid":""}`,"data-field":i,children:[s.jsxs("label",{htmlFor:v,children:[s.jsxs("span",{className:"field-label",children:[p,o&&s.jsx("span",{className:"muted",children:" (required)"})]}),s.jsx("input",{id:v,type:"password",autoComplete:"new-password",placeholder:f&&!b?"••••••••":"",value:typeof r=="string"?r:"",onChange:A=>j(A.target.value)})]}),s.jsxs("p",{className:"muted field-hint",children:[g&&s.jsxs(s.Fragment,{children:[g," "]}),f?s.jsx("em",{children:"Leave blank to keep the current value."}):s.jsx("em",{children:"Secret — sent only when saved."})]}),h&&s.jsx("p",{className:"error field-error",children:h})]}):c.type==="string"?s.jsxs("div",{className:`field ${h?"field-invalid":""}`,"data-field":i,children:[s.jsxs("label",{htmlFor:v,children:[s.jsxs("span",{className:"field-label",children:[p,o&&s.jsx("span",{className:"muted",children:" (required)"})]}),s.jsx("input",{id:v,type:"text",value:typeof r=="string"?r:"",onChange:A=>j(A.target.value)})]}),g&&s.jsx("p",{className:"muted field-hint",children:g}),h&&s.jsx("p",{className:"error field-error",children:h})]}):c.type==="number"||c.type==="integer"?s.jsxs("div",{className:`field ${h?"field-invalid":""}`,"data-field":i,children:[s.jsxs("label",{htmlFor:v,children:[s.jsxs("span",{className:"field-label",children:[p,o&&s.jsx("span",{className:"muted",children:" (required)"})]}),s.jsx("input",{id:v,type:"number",step:c.type==="integer"?1:"any",min:c.minimum,max:c.maximum,value:typeof r=="number"?r:"",onChange:A=>{const T=A.target.value;j(T===""?void 0:Number(T))}})]}),g&&s.jsx("p",{className:"muted field-hint",children:g}),h&&s.jsx("p",{className:"error field-error",children:h})]}):s.jsxs("div",{className:"field","data-field":i,children:[s.jsxs("span",{className:"field-label",children:[p," ",s.jsx("span",{className:"muted",children:"(unsupported type)"})]}),s.jsx("pre",{className:"muted",children:JSON.stringify(c,null,2)}),g&&s.jsx("p",{className:"muted field-hint",children:g})]})}const Cg=2e3,Eg=12e4;function Ng(){const[i,c]=S.useState(null),[r,o]=S.useState({kind:"idle"}),f=S.useRef(null),h=S.useRef(0),b=S.useCallback(async()=>{try{c(await ar())}catch{}},[]);S.useEffect(()=>{b()},[b]),S.useEffect(()=>()=>{f.current&&clearInterval(f.current)},[]);const j=S.useCallback(()=>{f.current&&(clearInterval(f.current),f.current=null)},[]),v=S.useCallback((g,y)=>{h.current=Date.now()+Eg,j(),f.current=setInterval(()=>{(async()=>{let A=null,T=null;try{A=await ar()}catch{}try{T=await og()}catch{}if(A){const D=A.version!==g,q=y!==null&&A.version===y;if(D||q){j(),c(A),o({kind:"succeeded",newVersion:A.version});return}}if((T==null?void 0:T.phase)==="failed"){j(),o({kind:"error",message:T.error??"the hub upgrade failed — check the platform log"});return}if(Date.now()>=h.current){j();const D=(T==null?void 0:T.log.at(-1))??null;o({kind:"timeout",lastLog:D})}})()},Cg)},[j]);async function p(){if(!i)return;const g=i.version;o({kind:"starting"});try{const y=await rg();if(y.mode==="redeploy-required"){o({kind:"redeploy-required",targetVersion:y.target_version});return}o({kind:"upgrading",previousVersion:g,targetVersion:y.target_version}),v(g,y.target_version)}catch(y){o({kind:"error",message:y instanceof Error?y.message:String(y)})}}return i?s.jsxs("section",{className:"hub-upgrade-card install-card","data-testid":"hub-upgrade-card",children:[s.jsxs("div",{className:"install-card-body",children:[s.jsxs("h3",{children:["Hub ",s.jsx("span",{className:"muted",children:"(@openparachute/hub)"})]}),s.jsxs("p",{className:"install-card-meta muted",children:["Installed ",s.jsxs("code",{"data-testid":"hub-current-version",children:["v",i.version]})," · source"," ",s.jsx("code",{children:i.source})]}),s.jsx(Tg,{state:r})]}),s.jsx("div",{className:"install-card-actions",children:s.jsx(wg,{state:r,onUpgrade:()=>void p()})})]}):null}function wg({state:i,onUpgrade:c}){if(i.kind==="redeploy-required")return s.jsx("span",{className:"muted","data-testid":"hub-redeploy-hint",children:"Redeploy from your platform dashboard"});const r=i.kind==="starting"||i.kind==="upgrading";return s.jsx("button",{type:"button",onClick:c,disabled:r,"data-testid":"hub-upgrade-button",children:r?"Upgrading…":"Upgrade hub"})}function Tg({state:i}){switch(i.kind){case"idle":return null;case"starting":return s.jsx("p",{className:"muted","data-testid":"hub-upgrade-state-starting",children:"Starting the hub upgrade…"});case"upgrading":return s.jsxs("p",{className:"muted","data-testid":"hub-upgrade-state-upgrading",children:["Upgrading",i.targetVersion?` to v${i.targetVersion}`:"","… the hub will restart; this page reconnects automatically."]});case"succeeded":return s.jsxs("p",{className:"muted","data-testid":"hub-upgrade-state-success",children:["Upgraded — the hub is now running ",s.jsxs("code",{children:["v",i.newVersion]}),"."]});case"timeout":return s.jsxs("p",{className:"warn-banner","data-testid":"hub-upgrade-state-timeout",children:["The hub may still be coming up — refresh shortly.",i.lastLog?s.jsxs(s.Fragment,{children:[" ",s.jsxs("span",{className:"dim",children:["Last status: ",i.lastLog]})]}):null]});case"redeploy-required":return s.jsxs("p",{className:"warn-banner","data-testid":"hub-upgrade-state-redeploy",children:["This hub is baked into its container image, so an in-place upgrade wouldn't survive a restart."," ",i.targetVersion?s.jsxs(s.Fragment,{children:["Redeploy from your platform dashboard to move to ",s.jsxs("code",{children:["v",i.targetVersion]}),"."]}):s.jsx(s.Fragment,{children:"Redeploy from your platform dashboard to pick up the latest hub."})]});case"error":return s.jsxs("p",{className:"error-banner","data-testid":"hub-upgrade-state-error",children:["Upgrade failed: ",i.message]})}}function _g(){if(typeof navigator>"u")return"other";const i=(navigator.platform||"").toLowerCase();return i.includes("mac")?"darwin":i.includes("linux")?"linux":"other"}function Rg(i,c){const r=[];if(i.darwin&&r.push({label:"macOS",command:i.darwin,preferred:c==="darwin"}),i.linux&&r.push({label:"Linux",command:i.linux,preferred:c==="linux"}),i.generic){const o=r.some(f=>f.preferred);r.push({label:"Any platform",command:i.generic,preferred:!o})}return r.sort((o,f)=>Number(f.preferred)-Number(o.preferred))}function Ag({command:i}){const[c,r]=S.useState(!1);async function o(){try{await navigator.clipboard.writeText(i),r(!0),setTimeout(()=>r(!1),1500)}catch{}}return s.jsxs("div",{className:"depcard-cmd",children:[s.jsx("pre",{className:"depcard-cmd-text",children:i}),s.jsx("button",{type:"button",className:"btn btn-secondary depcard-copy",onClick:()=>void o(),"aria-label":"Copy install command",children:c?"Copied":"Copy"})]})}function zg({wire:i}){const c=_g(),r=Rg(i.install,c);return s.jsxs("div",{className:"depcard","data-testid":"missing-dependency-card",children:[s.jsxs("h3",{className:"depcard-heading",children:[i.binary," isn't installed"]}),i.why?s.jsxs("p",{className:"depcard-why muted",children:["It's needed to ",i.why,"."]}):null,r.length>0?s.jsxs("div",{className:"depcard-installs",children:[s.jsx("p",{className:"depcard-installs-label",children:"Install it:"}),r.map(o=>s.jsxs("div",{className:o.preferred?"depcard-install preferred":"depcard-install",children:[s.jsx("span",{className:"depcard-os",children:o.label}),s.jsx(Ag,{command:o.command})]},o.label))]}):null,i.docs_url?s.jsx("p",{className:"depcard-docs",children:s.jsx("a",{href:i.docs_url,target:"_blank",rel:"noreferrer noopener",children:"Documentation"})}):null,i.sysadmin_hint?s.jsx("p",{className:"depcard-hint muted",children:i.sysadmin_hint}):null]})}function Mg(i){const{error:c,errorDetail:r}=i;return(r==null?void 0:r.error_type)==="missing_dependency"?s.jsx(zg,{wire:r}):r!=null&&r.error_description?s.jsx("span",{className:"depcard-fallback",children:r.error_description}):c?s.jsx("span",{className:"depcard-fallback",children:c}):null}function Og(i){switch(i){case"running":return"active";case"starting":case"restarting":return"pending";case"crashed":return"failing";case"stopped":case null:case void 0:return"inactive";default:return"inactive"}}function Ug(i){if(i==null)return"active";switch(i){case"active":case"pending":case"inactive":case"failing":return i;case"pending-oauth":return"pending";case"disabled":return"inactive";default:return"active"}}const Dg=1e3;function kg(){const[i,c]=S.useState({kind:"loading"}),[r,o]=S.useState([]),[f,h]=S.useState({}),[b,j]=S.useState({}),v=S.useCallback(async()=>{try{const C=await rm();c({kind:"ok",catalog:C})}catch(C){const O=C instanceof Error?C.message:String(C);c({kind:"error",message:O})}},[]);S.useEffect(()=>{v()},[v]);const p=S.useRef(null);S.useEffect(()=>{if(r.length===0){p.current&&(clearInterval(p.current),p.current=null);return}if(!p.current)return p.current=setInterval(()=>{Promise.all(r.map(async C=>{var O;try{const G=await ag(C.operationId);o(ve=>ve.map(te=>te.operationId===G.id?{...te,log:G.log,status:G.status,error:G.error,errorDetail:G.error_detail}:te)),(G.status==="succeeded"||G.status==="failed")&&(G.status==="failed"&&((O=G.error_detail)==null?void 0:O.error_type)==="missing_dependency"?v():setTimeout(()=>{o(te=>te.filter(he=>he.operationId!==G.id)),v()},1500))}catch(G){const ve=G instanceof Error?G.message:String(G);o(te=>te.map(he=>he.operationId===C.operationId?{...he,status:"failed",error:ve}:he))}}))},Dg),()=>{p.current&&(clearInterval(p.current),p.current=null)}},[r,v]);async function g(C){j(O=>{const G={...O};return delete G[C],G});try{const O=await K0(C);o(G=>[...G,{kind:"install",operationId:O,short:C,log:[],status:"pending"}])}catch(O){j(G=>({...G,[C]:O instanceof Error?O.message:String(O)}))}}async function y(C){j(O=>{const G={...O};return delete G[C],G});try{const O=await $0(C);o(G=>[...G,{kind:"upgrade",operationId:O,short:C,log:[],status:"pending"}])}catch(O){j(G=>({...G,[C]:O instanceof Error?O.message:String(O)}))}}async function A(C){if(!f[C]){h(O=>({...O,[C]:!0})),j(O=>{const G={...O};return delete G[C],G});try{await F0(C),await v()}catch(O){j(G=>({...G,[C]:O instanceof Error?O.message:String(O)}))}finally{h(O=>({...O,[C]:!1}))}}}async function T(C){if(!f[C]&&window.confirm(`Uninstall ${C}? The container will stop the service, remove the package, and drop its services.json entry. The persistent disk's data dir is left intact.`)){h(O=>({...O,[C]:!0})),j(O=>{const G={...O};return delete G[C],G});try{await W0(C),await v()}catch(O){j(G=>({...G,[C]:O instanceof Error?O.message:String(O)}))}finally{h(O=>({...O,[C]:!1}))}}}if(i.kind==="loading")return s.jsx("div",{className:"empty",children:"Loading modules…"});if(i.kind==="error")return i.message.includes("setup_required")?s.jsxs("div",{className:"empty",children:["Hub not yet configured. ",s.jsx("a",{href:"/admin/setup",children:"Finish first-boot setup"})," first."]}):s.jsxs("div",{className:"empty",children:["Failed to load modules: ",i.message,"."," ",s.jsx("button",{type:"button",onClick:()=>void v(),children:"Retry"})]});const D=i.catalog,{modules:q,supervisor_available:Z,module_install_channel:V}=D,ee=q.filter(C=>C.installed),I=q.filter(C=>!C.installed&&C.available),ue=new Set(r.filter(C=>C.kind==="install").map(C=>C.short)),K=I.filter(C=>!ue.has(C.short));async function Y(C){if(C!==V){c({kind:"ok",catalog:{...D,module_install_channel:C}});try{const O=await I0(C);c(G=>G.kind==="ok"?{kind:"ok",catalog:{...G.catalog,module_install_channel:O}}:G)}catch(O){const G=O instanceof Error?O.message:String(O);c({kind:"error",message:`Failed to update channel — ${G}`})}}}return s.jsxs("section",{className:"modules",children:[s.jsx("h1",{children:"Modules"}),s.jsx("p",{className:"muted",children:"Install, upgrade, and manage Parachute modules. Available modules are pinned for the v0.6 release; the marketplace is on the roadmap."}),s.jsx(Ng,{}),s.jsx(Gg,{channel:V,disabled:!Z,onChange:C=>void Y(C)}),!Z&&s.jsxs("div",{className:"banner banner-info",children:["This hub is running outside container mode (no supervisor wired). Module install / restart / upgrade is available only under ",s.jsx("code",{children:"parachute serve"}),". On-box installs use"," ",s.jsx("code",{children:"parachute install vault"})," etc. from a shell instead."]}),r.length>0&&s.jsxs("div",{className:"banner","data-testid":"pending-ops-banner",children:[s.jsx("strong",{children:"In progress:"}),s.jsx("ul",{children:r.map(C=>s.jsxs("li",{children:[s.jsx("code",{children:C.short})," · ",C.kind," · status: ",C.status,C.status==="failed"&&(C.errorDetail||C.error)?s.jsx("div",{className:"depcard-wrap",children:Mg({error:C.error,errorDetail:C.errorDetail})}):null,C.log.length>0&&s.jsxs("details",{children:[s.jsxs("summary",{children:[C.log.length," log line(s)"]}),s.jsx("pre",{children:C.log.join(`
|
|
61
|
-
`)})]})]},C.operationId))})]}),s.jsxs("section",{className:"modules-installed","data-testid":"installed-section",children:[s.jsx("h2",{children:"Installed modules"}),ee.length===0?s.jsxs("p",{className:"muted","data-testid":"installed-empty",children:["No modules installed yet. Pick one from ",s.jsx("strong",{children:"Install a module"})," below to get started."]}):s.jsx("ul",{className:"module-list",children:ee.map(C=>s.jsx(Bg,{module:C,supervisorAvailable:Z,syncBusy:!!f[C.short],errorMessage:b[C.short],onUpgrade:()=>void y(C.short),onRestart:()=>void A(C.short),onUninstall:()=>void T(C.short)},C.short))})]}),s.jsxs("section",{className:"modules-installable","data-testid":"installable-section",children:[s.jsx("h2",{children:"Install a module"}),K.length===0?s.jsx("p",{className:"muted","data-testid":"installable-empty",children:ue.size>0?"Install in progress — see In progress above.":"All available modules are installed."}):s.jsx("ul",{className:"install-list",children:K.map(C=>s.jsx(Lg,{module:C,supervisorAvailable:Z,installing:ue.has(C.short),errorMessage:b[C.short],onInstall:()=>void g(C.short)},C.short))})]})]})}const Hg={scribe:"Scribe admin SPA tracked at scribe#53",runner:"Runner admin SPA tracked at runner#8"};function Bg({module:i,supervisorAvailable:c,syncBusy:r,errorMessage:o,onUpgrade:f,onRestart:h,onUninstall:b}){const j=c&&!r,v=i.installed_version!==i.latest_version&&i.latest_version!==null,p=i.management_url,g=Hg[i.short];return s.jsxs("li",{className:"module-row","data-short":i.short,children:[s.jsxs("header",{children:[s.jsxs("h2",{children:[i.display_name," ",s.jsxs("span",{className:"muted",children:["(",i.short,")"]})]}),(()=>{const y=qg(i);return s.jsx("span",{className:`status status-${y.cssState}`,"data-state":y.cssState,"data-testid":`module-status-${i.short}`,children:y.label})})()]}),i.tagline?s.jsx("p",{className:"tagline",children:i.tagline}):null,s.jsxs("dl",{className:"meta",children:[s.jsx("dt",{children:"Package"}),s.jsx("dd",{children:s.jsx("code",{children:i.package})}),s.jsx("dt",{children:"Installed"}),s.jsxs("dd",{children:["v",i.installed_version??"unknown",v&&s.jsxs("span",{className:"badge",children:["v",i.latest_version," available"]})]}),i.pid&&s.jsxs(s.Fragment,{children:[s.jsx("dt",{children:"PID"}),s.jsx("dd",{children:i.pid})]})]}),i.uis.length>0&&s.jsx(Yg,{uis:i.uis}),s.jsxs("div",{className:"actions",children:[p?s.jsx("a",{className:"btn",href:p,"data-testid":`open-${i.short}`,children:"Open"}):s.jsx("button",{type:"button",className:"btn",disabled:!0,title:g??"This module hasn't shipped an admin UI yet.","data-testid":`open-${i.short}`,children:"Open"}),s.jsx("button",{type:"button",disabled:!j,onClick:h,children:"Restart"}),s.jsx("button",{type:"button",disabled:!j||!v,onClick:f,children:v?`Upgrade to v${i.latest_version}`:"Up to date"}),s.jsx("button",{type:"button",className:"destructive",disabled:!j,onClick:b,children:"Uninstall"})]}),o&&s.jsx("div",{className:"error",children:o})]})}function Lg({module:i,supervisorAvailable:c,installing:r,errorMessage:o,onInstall:f}){const h=c&&!r;return s.jsxs("li",{className:"install-card","data-short":i.short,children:[s.jsxs("div",{className:"install-card-body",children:[s.jsxs("h3",{children:[i.display_name," ",s.jsxs("span",{className:"muted",children:["(",i.short,")"]})]}),i.tagline?s.jsx("p",{className:"tagline",children:i.tagline}):null,s.jsxs("p",{className:"muted install-card-meta",children:[s.jsx("code",{children:i.package}),i.latest_version?s.jsxs(s.Fragment,{children:[" · ","latest ",s.jsxs("code",{children:["v",i.latest_version]})]}):null]})]}),s.jsx("div",{className:"install-card-actions",children:s.jsx("button",{type:"button",disabled:!h,onClick:f,children:r?"Installing…":"Install"})}),o&&s.jsx("div",{className:"error",children:o})]})}function qg(i){if(!i.installed)return{cssState:"absent",label:"not installed"};if(!i.supervisor_status)return{cssState:"absent",label:"not supervised"};const c=Og(i.supervisor_status);return{cssState:c,label:c}}function Yg({uis:i}){return s.jsxs("details",{className:"module-uis","data-testid":"module-uis",open:!0,children:[s.jsxs("summary",{children:["Hosted UIs ",s.jsxs("span",{className:"muted",children:["(",i.length,")"]})]}),s.jsx("ul",{className:"ui-sub-units",children:i.map(c=>{const r=Ug(c.status);return s.jsxs("li",{className:"ui-sub-unit","data-name":c.name,children:[c.icon_url&&s.jsx("img",{src:c.icon_url,alt:"",className:"ui-icon",width:20,height:20,loading:"lazy"}),s.jsxs("div",{className:"ui-sub-unit-body",children:[s.jsxs("a",{href:c.path,className:"ui-sub-unit-link",children:[s.jsx("strong",{children:c.display_name}),s.jsxs("span",{className:"muted",children:[" · ",c.path]})]}),c.tagline?s.jsx("p",{className:"tagline",children:c.tagline}):null]}),s.jsx("span",{className:`status status-${r}`,"data-state":r,"data-testid":`ui-status-${c.name}`,children:r})]},c.name)})})]})}function Gg({channel:i,disabled:c,onChange:r}){return s.jsxs("fieldset",{className:"channel-toggle","data-testid":"channel-toggle",children:[s.jsx("legend",{children:"Install channel"}),s.jsxs("label",{children:[s.jsx("input",{type:"radio",name:"module-install-channel",value:"latest",checked:i==="latest",disabled:c,onChange:()=>r("latest")}),"Stable (",s.jsx("code",{children:"latest"}),")"]}),s.jsxs("label",{children:[s.jsx("input",{type:"radio",name:"module-install-channel",value:"rc",checked:i==="rc",disabled:c,onChange:()=>r("rc")}),"Release candidates (",s.jsx("code",{children:"rc"}),")"]}),s.jsxs("p",{className:"muted",children:["All future module installs and upgrades use this channel. Existing installed modules are unaffected — use ",s.jsx("strong",{children:"Upgrade"})," to pull a newer version."]}),s.jsxs("p",{className:"muted",children:["More hub settings (canonical URL, etc.) at ",s.jsx(Ke,{to:"/settings",children:"Settings"}),"."]})]})}const Vg="https://parachute.computer/install#connect-mcp-clients";function hm(i){return`${i.replace(/\/+$/,"")}/mcp`}function mm(i,c){return`claude mcp add --transport http parachute-${i} ${hm(c)}`}function Zg(i,c,r){return`${mm(i,c)} --header "Authorization: Bearer ${r}"`}function Fc({value:i,label:c="Copy"}){const[r,o]=S.useState(!1);return s.jsx("button",{type:"button",className:"secondary",onClick:()=>{typeof navigator>"u"||!navigator.clipboard||navigator.clipboard.writeText(i).then(()=>{o(!0),setTimeout(()=>o(!1),2e3)})},children:r?"Copied ✓":c})}function pm({vaultName:i,vaultUrl:c,embedded:r=!1}){const o=hm(c),f=mm(i,c),[h,b]=S.useState(!1),[j,v]=S.useState({kind:"idle"});async function p(){if(j.kind!=="submitting"){v({kind:"submitting"});try{const A=await cm({scope:`vault:${i}:read vault:${i}:write`});v({kind:"minted",token:A})}catch(A){const T=A instanceof X?`mint failed (${A.status}): ${A.message}`:A instanceof Error?A.message:String(A);v({kind:"error",message:T})}}}const g=j.kind==="minted"?Zg(i,c,j.token.token):null,y=s.jsxs(s.Fragment,{children:[s.jsx("h3",{children:"Connect an MCP client"}),s.jsxs("p",{className:"muted",children:["Connect an MCP client — Claude Code, Claude.ai, etc. — to the ",s.jsx("code",{children:i})," ","vault. The client signs in to this hub and reads/writes vault data over MCP."]}),s.jsxs("div",{className:"mcp-field",children:[s.jsx("span",{className:"mcp-field-label",children:"Endpoint"}),s.jsxs("div",{className:"token-box",children:[s.jsx("code",{"data-testid":"mcp-endpoint",children:o}),s.jsx(Fc,{value:o})]})]}),s.jsxs("div",{className:"mcp-field",children:[s.jsx("span",{className:"mcp-field-label",children:"Claude Code"}),s.jsxs("div",{className:"token-box",children:[s.jsx("code",{"data-testid":"mcp-add-command",children:f}),s.jsx(Fc,{value:f})]}),s.jsx("p",{className:"dim",children:"No token needed — the command triggers browser OAuth on first use (you sign in to this hub and approve access). For other clients, point them at the endpoint above."})]}),s.jsxs("details",{className:"mcp-token-path",open:h,onToggle:A=>b(A.currentTarget.open),children:[s.jsx("summary",{children:"Use a token instead (headless / CI clients)"}),s.jsxs("p",{className:"dim",children:["For clients that can't do browser OAuth, mint a scope-narrow hub token (",s.jsxs("code",{children:["vault:",i,":read vault:",i,":write"]})," ","— not admin) and pass it as a header. Revealed once; copy it now."]}),j.kind==="minted"&&g?s.jsxs("div",{className:"mint-banner","data-testid":"mcp-header-banner",children:[s.jsx("h3",{children:"Token minted"}),s.jsxs("p",{className:"muted",children:["This is the only time the hub shows this token. Copy the command below — it embeds the live token in the ",s.jsx("code",{children:"Authorization"})," header."]}),s.jsxs("div",{className:"token-box",children:[s.jsx("code",{"data-testid":"mcp-header-command",children:g}),s.jsx(Fc,{value:g})]}),s.jsxs("p",{className:"warn",children:["⚠ Manage and revoke this token at ",s.jsx("code",{children:"/admin/tokens"}),"."]})]}):s.jsx("div",{className:"actions",children:s.jsx("button",{type:"button",className:"secondary",onClick:()=>{p()},disabled:j.kind==="submitting",children:j.kind==="submitting"?"Minting…":"Mint a token"})}),j.kind==="error"?s.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:s.jsx("code",{children:j.message})}):null]}),s.jsx("p",{className:"dim mcp-docs-link",children:s.jsx("a",{href:Vg,target:"_blank",rel:"noreferrer",children:"Full connect docs →"})})]});return r?s.jsx("div",{className:"mcp-connect-card mcp-connect-card-embedded",children:y}):s.jsx("div",{className:"mcp-connect-card",children:y})}const Xg=/^[a-zA-Z0-9_-]+$/,Qg=new Set(["list"]);function Jg(){const i=ur(),[c,r]=S.useState(""),[o,f]=S.useState({kind:"idle"}),h=o.kind==="created"&&!!o.result.token;S.useEffect(()=>{if(!h)return;const v=p=>{p.preventDefault(),p.returnValue=""};return window.addEventListener("beforeunload",v),()=>window.removeEventListener("beforeunload",v)},[h]);const b=$g(c),j=async v=>{if(v.preventDefault(),!b){f({kind:"submitting"});try{const p=await B0({name:c});f({kind:"created",result:p})}catch(p){const g=p instanceof X?`${p.status}: ${p.message}`:p instanceof Error?p.message:String(p);f({kind:"error",message:g})}}};return o.kind==="created"?s.jsx(Kg,{result:o.result,onDone:()=>i("/vaults")}):s.jsxs("div",{children:[s.jsx("h2",{children:"Create a vault"}),s.jsxs("p",{className:"muted",children:["A vault stores tokens, secrets, and notes scoped to this hub. The hub provisions it via"," ",s.jsx("code",{children:"parachute-vault create"})," on the host filesystem and registers it in"," ",s.jsx("code",{children:"services.json"}),"."]}),o.kind==="error"&&s.jsxs("div",{className:"error-banner",children:["Couldn't create vault: ",s.jsx("code",{children:o.message})]}),s.jsxs("form",{onSubmit:j,className:"section",children:[s.jsxs("div",{className:"row",children:[s.jsx("label",{htmlFor:"vault-name",children:"Vault name"}),s.jsx("input",{id:"vault-name",type:"text",value:c,onChange:v=>r(v.target.value),placeholder:"e.g. work, home, scratch",disabled:o.kind==="submitting"}),b?s.jsx("div",{className:"field-error",children:b}):s.jsxs("div",{className:"field-hint",children:["Letters, numbers, hyphens, and underscores. Becomes the path under"," ",s.jsx("code",{children:"/vault/<name>"}),"."]})]}),s.jsxs("div",{className:"actions",children:[s.jsx("button",{type:"submit",disabled:c.length===0||!!b||o.kind==="submitting",children:o.kind==="submitting"?"Creating…":"Create vault"}),s.jsx(Ke,{to:"/vaults",className:"muted",children:"Cancel"})]})]})]})}function Kg({result:i,onDone:c}){const[r,o]=S.useState(!1),f=async()=>{if(i.token)try{await navigator.clipboard.writeText(i.token),o(!0),setTimeout(()=>o(!1),2e3)}catch{}};return s.jsxs("div",{children:[s.jsx("h2",{children:"Vault created"}),i.token?s.jsxs("div",{className:"mint-banner",children:[s.jsx("h3",{children:"Your access token (shown once)"}),s.jsxs("p",{className:"muted",children:["This is a hub-issued access token (a JWT scoped ",s.jsxs("code",{children:["vault:",i.name,":admin"]}),") — not a vault password. It's the only time the hub will show it. Copy it and store it somewhere safe — a password manager, the operator's notes. If you lose it, you don't need it for the OAuth connect path below; for a header-auth token, mint a fresh scope-narrow one with"," ",s.jsxs("code",{children:["parachute auth mint-token --scope vault:",i.name,":read"]}),` (or the connect card's "Use a token instead" option).`]}),s.jsxs("div",{className:"token-box",children:[s.jsx("code",{children:i.token}),s.jsx("button",{type:"button",onClick:f,className:"secondary",children:r?"Copied":"Copy"})]}),s.jsx("p",{className:"warn",children:"⚠ Don't navigate away until you've saved this token."}),s.jsx("div",{className:"actions",children:s.jsx("button",{type:"button",onClick:c,children:"Done — I've saved the token"})})]}):i.created?s.jsxs("div",{className:"section",children:[s.jsxs("p",{children:["Vault ",s.jsx("code",{children:i.name})," was created, but no access token was minted",i.tokenGuidance?s.jsxs(s.Fragment,{children:[" ","— ",s.jsx("span",{className:"muted",children:i.tokenGuidance})]}):s.jsxs("span",{className:"muted",children:[" ","(the hub couldn't mint one at create time — common on a loopback origin)"]}),". You don't need a token for the OAuth connect command below. For a header-auth token, mint a scope-narrow one with"," ",s.jsxs("code",{children:["parachute auth mint-token --scope vault:",i.name,":read"]}),"."]}),s.jsx("div",{className:"actions",children:s.jsx("button",{type:"button",onClick:c,children:"Done"})})]}):s.jsxs("div",{className:"section",children:[s.jsxs("p",{children:["Vault ",s.jsx("code",{children:i.name})," already existed; nothing new was created. Connect to it with the command below (OAuth, no token needed), or mint a scope-narrow header-auth token with ",s.jsxs("code",{children:["parachute auth mint-token --scope vault:",i.name,":read"]}),"."]}),s.jsx("div",{className:"actions",children:s.jsx(Ke,{to:"/vaults",children:s.jsx("button",{type:"button",children:"Continue"})})})]}),s.jsx("div",{className:"section",children:s.jsx(pm,{vaultName:i.name,vaultUrl:i.url})}),s.jsxs("div",{className:"kv section",children:[s.jsx("div",{children:"Name"}),s.jsx("div",{children:s.jsx("code",{children:i.name})}),s.jsx("div",{children:"URL"}),s.jsx("div",{children:s.jsx("code",{children:i.url})}),s.jsx("div",{children:"Version"}),s.jsx("div",{children:s.jsx("code",{children:i.version})}),i.paths&&s.jsxs(s.Fragment,{children:[s.jsx("div",{children:"Vault dir"}),s.jsx("div",{children:s.jsx("code",{children:i.paths.vault_dir})}),s.jsx("div",{children:"Database"}),s.jsx("div",{children:s.jsx("code",{children:i.paths.vault_db})}),s.jsx("div",{children:"Config"}),s.jsx("div",{children:s.jsx("code",{children:i.paths.vault_config})})]})]})]})}function $g(i){return i.length===0?null:Xg.test(i)?Qg.has(i)?`"${i}" is a reserved name.`:null:"Letters, numbers, hyphens, and underscores only."}function Fg(){const[i,c]=S.useState({kind:"loading"}),[r,o]=S.useState(""),[f,h]=S.useState(""),[b,j]=S.useState(0),[v,p]=S.useState({kind:"idle"});S.useEffect(()=>{let T=!1;return c({kind:"loading"}),q0(f?{vault:f}:{}).then(q=>{T||c({kind:"ok",grants:q})}).catch(q=>{if(T)return;const Z=q instanceof Error?q.message:String(q);c({kind:"error",message:Z})}),()=>{T=!0}},[b,f]);function g(T){T.preventDefault(),h(r.trim())}function y(){o(""),h("")}async function A(T){p({kind:"revoking",clientId:T.client_id});try{await Y0(T.client_id),p({kind:"idle"}),j(D=>D+1)}catch(D){const q=D instanceof X?`revoke failed (${D.status}): ${D.message}`:D instanceof Error?D.message:String(D);p({kind:"error",clientId:T.client_id,message:q})}}return s.jsxs("div",{"data-route-content":"true",children:[s.jsx("div",{className:"list-header",children:s.jsx("h1",{children:"Permissions"})}),s.jsxs("p",{className:"muted",children:["Apps you've granted OAuth scopes to. Revoking a grant forces the consent screen on the next authorize flow — it does ",s.jsx("em",{children:"not"})," invalidate tokens already issued."]}),s.jsxs("form",{onSubmit:g,style:{marginTop:"1rem",marginBottom:"1rem"},children:[s.jsx("label",{htmlFor:"vault-filter",className:"muted",style:{marginRight:"0.5rem"},children:"Filter by vault:"}),s.jsx("input",{id:"vault-filter",type:"text",value:r,onChange:T=>o(T.target.value),placeholder:"e.g. work",style:{marginRight:"0.5rem"}}),s.jsx("button",{type:"submit",children:"Apply"}),f?s.jsx("button",{type:"button",onClick:y,className:"secondary",style:{marginLeft:"0.5rem"},children:"Clear"}):null]}),Wg({state:i,revoke:v,setRevoke:p,onConfirm:A,onRetry:()=>j(T=>T+1)})]})}function Wg({state:i,revoke:c,setRevoke:r,onConfirm:o,onRetry:f}){return i.kind==="loading"?s.jsx("p",{className:"muted","data-loading":"true",children:"Loading…"}):i.kind==="error"?s.jsxs(s.Fragment,{children:[s.jsxs("div",{className:"error-banner",children:["Couldn't load grants: ",s.jsx("code",{children:i.message})]}),s.jsx("button",{type:"button",onClick:f,className:"secondary",children:"Retry"})]}):i.grants.length===0?s.jsxs("div",{className:"empty empty-rich",children:[s.jsx("p",{className:"empty-headline",children:"No grants."}),s.jsx("p",{className:"muted",children:"When an app asks for OAuth scopes and you approve them, the grant lands here. Revoking it forces the consent screen on the next authorize flow."})]}):s.jsx("div",{style:{marginTop:"1rem"},children:i.grants.map(h=>{const b=c.kind==="revoking"&&c.clientId===h.client_id,j=c.kind==="error"&&c.clientId===h.client_id?c:null,v=c.kind==="confirming"&&c.grant.client_id===h.client_id;return s.jsxs("div",{className:"vault-row",children:[s.jsxs("div",{className:"body",children:[s.jsx("div",{className:"name",children:s.jsx("code",{children:h.client_name??h.client_id})}),s.jsxs("div",{className:"dim",children:[s.jsx("span",{className:"muted",children:"granted "}),s.jsx("code",{title:h.granted_at,children:Ig(h.granted_at)})]}),s.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[s.jsx("span",{className:"muted",children:"scopes: "}),h.scopes.map((p,g)=>s.jsxs("span",{children:[s.jsx("code",{children:p}),g<h.scopes.length-1?" ":null]},p))]}),j?s.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:s.jsx("code",{children:j.message})}):null,v?s.jsxs("dialog",{open:!0,className:"error-banner",style:{marginTop:"0.5rem",background:"var(--bg-warn, #fffbe6)"},"aria-label":`Confirm revoke ${h.client_name??h.client_id}`,children:[s.jsxs("p",{children:["Revoke ",s.jsx("code",{children:h.client_name??h.client_id}),"? Next OAuth flow for this app will prompt you to consent again."]}),s.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[s.jsx("button",{type:"button",onClick:()=>{o(h)},disabled:b,children:b?"Revoking…":"Revoke"}),s.jsx("button",{type:"button",className:"secondary",onClick:()=>r({kind:"idle"}),disabled:b,children:"Cancel"})]})]}):null]}),v?null:s.jsx("button",{type:"button",className:"secondary",onClick:()=>r({kind:"confirming",grant:h}),"aria-label":`Revoke ${h.client_name??h.client_id}`,children:"Revoke"})]},h.client_id)})})}function Ig(i){const c=new Date(i);return Number.isNaN(c.getTime())?i:c.toLocaleString()}function Pg(){const[i,c]=S.useState({kind:"loading"}),[r,o]=S.useState(""),[f,h]=S.useState(!1),[b,j]=S.useState(null),v=S.useCallback(async()=>{try{const D=await om();c({kind:"ok",setting:D}),o(D.hub_origin??""),j(null)}catch(D){const q=D instanceof Error?D.message:String(D);c({kind:"error",message:q})}},[]);S.useEffect(()=>{v()},[v]);async function p(D){if(D.preventDefault(),f||i.kind!=="ok")return;const q=r.trim(),Z=q.length===0?null:q;h(!0),j(null);try{await Uh(Z),await v()}catch(V){const ee=V instanceof Error?V.message:String(V);j(ee)}finally{h(!1)}}async function g(){if(!f&&i.kind==="ok"){h(!0),j(null);try{await Uh(null),await v()}catch(D){const q=D instanceof Error?D.message:String(D);j(q)}finally{h(!1)}}}if(i.kind==="loading")return s.jsx("div",{className:"empty",children:"Loading settings…"});if(i.kind==="error")return s.jsxs("div",{className:"empty",children:["Failed to load settings: ",i.message,"."," ",s.jsx("button",{type:"button",onClick:()=>void v(),children:"Retry"})]});const{setting:y}=i,A=y.hub_origin!==null,T=r.trim()!==(y.hub_origin??"");return s.jsxs("section",{className:"settings",children:[s.jsx("h1",{children:"Hub settings"}),s.jsx("p",{className:"muted",children:"Hub-wide operator controls. Settings here apply to every module + every minted token."}),s.jsxs("section",{className:"settings-block","aria-labelledby":"canonical-hub-url-heading",children:[s.jsx("h2",{id:"canonical-hub-url-heading",children:"Canonical hub URL"}),s.jsxs("dl",{className:"meta","data-testid":"hub-origin-current",children:[s.jsx("dt",{children:"Current value"}),s.jsxs("dd",{children:[s.jsx("code",{children:y.resolved_issuer})," ",s.jsx(ey,{source:y.source,hasStored:A})]})]}),s.jsxs("form",{onSubmit:D=>void p(D),className:"settings-form","data-testid":"hub-origin-form",children:[s.jsxs("label",{htmlFor:"hub-origin-input",children:[s.jsx("span",{children:"Canonical URL"}),s.jsx("input",{id:"hub-origin-input",type:"url",inputMode:"url",placeholder:"https://hub.example.com",value:r,disabled:f,onChange:D=>o(D.target.value),autoComplete:"off",spellCheck:!1})]}),s.jsxs("p",{className:"muted",children:["Set this when you've attached a custom domain. Tokens are minted against this URL — changing it invalidates any tokens already in circulation (the ",s.jsx("code",{children:"iss"})," claim won't match the new issuer on verification). Leave blank to use the request origin (default for Render-assigned URLs)."]}),s.jsxs("div",{className:"actions",children:[s.jsx("button",{type:"submit",disabled:f||!T,children:f?"Saving…":"Save"}),s.jsx("button",{type:"button",className:"destructive",disabled:f||!A,onClick:()=>void g(),children:"Reset to default"})]}),b&&s.jsx("div",{className:"error","data-testid":"hub-origin-save-error",children:b})]})]})]})}function ey({source:i,hasStored:c}){return i==="settings"?s.jsx("span",{className:"badge badge-info","data-testid":"hub-origin-source",children:"from settings"}):i==="env"?s.jsxs("span",{className:"badge badge-info","data-testid":"hub-origin-source",children:["from env var ",s.jsx("code",{children:"PARACHUTE_HUB_ORIGIN"})]}):s.jsx("span",{className:"badge badge-info","data-testid":"hub-origin-source",children:"from request origin"})}const Wc={scope:"",audience:"",expiresIn:"",subject:"",permissions:""};function ty(){const[i,c]=im(),r=ly(i.get("status")),o=iy(i.get("source")),[f,h]=S.useState({kind:"loading"}),[b,j]=S.useState(0),[v,p]=S.useState({kind:"idle"}),[g,y]=S.useState(Wc),[A,T]=S.useState({kind:"idle"}),[D,q]=S.useState(!1),[Z,V]=S.useState(!1);function ee(C,O){c(G=>{const ve=new URLSearchParams(G);return O==="all"?ve.delete(C):ve.set(C,O),ve},{replace:!0})}S.useEffect(()=>{let C=!1;return h({kind:"loading"}),Oh(kh(r,o)).then(O=>{C||h({kind:"ok",tokens:O.tokens,nextCursor:O.next_cursor})}).catch(O=>{if(C)return;const G=O instanceof Error?O.message:String(O);h({kind:"error",message:G})}),()=>{C=!0}},[b,r,o]);async function I(){if(f.kind!=="ok"||!f.nextCursor||Z)return;const C={...kh(r,o),cursor:f.nextCursor};V(!0);try{const O=await Oh(C);h({kind:"ok",tokens:[...f.tokens,...O.tokens],nextCursor:O.next_cursor})}catch(O){const G=O instanceof Error?O.message:String(O);h({kind:"error",message:G})}finally{V(!1)}}async function ue(C){C.preventDefault();const O=g.scope.trim();if(O.length===0){p({kind:"error",message:"scope is required"});return}let G;if(g.permissions.trim().length>0)try{const te=JSON.parse(g.permissions);if(te===null||typeof te!="object"||Array.isArray(te)){p({kind:"error",message:'permissions must be a JSON object (e.g. {"vault":{"default":...}})'});return}G=te}catch(te){const he=te instanceof Error?te.message:String(te);p({kind:"error",message:`permissions is not valid JSON — ${he}`});return}let ve;if(g.expiresIn.trim().length>0){const te=Number(g.expiresIn);if(!Number.isInteger(te)||te<=0){p({kind:"error",message:"expires_in must be a positive integer (seconds)"});return}ve=te}p({kind:"submitting"});try{const te=await cm({scope:O,...g.audience.trim()?{audience:g.audience.trim()}:{},...ve!==void 0?{expires_in:ve}:{},...g.subject.trim()?{subject:g.subject.trim()}:{},...G?{permissions:G}:{}});p({kind:"minted",token:te}),y(Wc),q(!1),j(he=>he+1)}catch(te){const he=te instanceof X?`mint failed (${te.status}): ${te.message}`:te instanceof Error?te.message:String(te);p({kind:"error",message:he})}}async function K(C){T({kind:"revoking",jti:C});try{await X0(C),T({kind:"idle"}),j(O=>O+1)}catch(O){const G=O instanceof X?`revoke failed (${O.status}): ${O.message}`:O instanceof Error?O.message:String(O);T({kind:"error",jti:C,message:G})}}function Y(C){typeof navigator<"u"&&navigator.clipboard&&navigator.clipboard.writeText(C)}return s.jsxs("div",{children:[s.jsxs("div",{className:"list-header",children:[s.jsx("h1",{children:"Tokens"}),s.jsx("button",{type:"button",onClick:()=>q(C=>!C),children:D?"Hide form":"Mint new token"})]}),s.jsxs("p",{className:"muted",children:["The hub's token registry. Every CLI / OAuth / operator-mint writes a row here. Revoking flips ",s.jsx("code",{children:"revoked_at"}),"; resource servers on"," ",s.jsx("code",{children:"@openparachute/scope-guard@^0.2.0"})," reject within ~60s of the next poll."]}),v.kind==="minted"?s.jsxs("div",{className:"mint-banner",children:[s.jsx("h3",{children:"Minted"}),s.jsxs("p",{children:["Your new access token (jti: ",s.jsx("code",{children:v.token.jti}),"):"]}),s.jsx("div",{className:"token-box",children:s.jsx("code",{children:v.token.token})}),s.jsx("p",{className:"warn",children:"This is the only time the JWT is shown. Copy it now — there is no DB-side recovery."}),s.jsxs("div",{className:"actions",children:[s.jsx("button",{type:"button",onClick:()=>Y(v.token.token),children:"Copy"}),s.jsx("button",{type:"button",className:"secondary",onClick:()=>p({kind:"idle"}),children:"Dismiss"})]})]}):null,D?s.jsx("div",{className:"section",children:s.jsxs("form",{onSubmit:ue,children:[s.jsxs("div",{className:"row",children:[s.jsx("label",{htmlFor:"mint-scope",children:"Scope (space-separated)"}),s.jsx("input",{id:"mint-scope",type:"text",value:g.scope,onChange:C=>y({...g,scope:C.target.value}),placeholder:"e.g. scribe:transcribe vault:default:read",required:!0}),s.jsxs("div",{className:"field-hint",children:["Space-separated ",s.jsx("code",{children:"resource:verb"})," or ",s.jsx("code",{children:"resource:name:verb"})," ","tuples. The hub rejects non-requestable scopes (admin, host:*) per the privilege-diffusion guard."]})]}),s.jsxs("div",{className:"row",children:[s.jsx("label",{htmlFor:"mint-audience",children:"Audience (optional)"}),s.jsx("input",{id:"mint-audience",type:"text",value:g.audience,onChange:C=>y({...g,audience:C.target.value}),placeholder:"inferred from scope when blank"}),s.jsxs("div",{className:"field-hint",children:["Inferred from scope if omitted. ",s.jsx("code",{children:"vault:<name>:<verb>"})," →"," ",s.jsx("code",{children:"vault.<name>"}),"; otherwise the first colon-prefixed scope's namespace; fallback ",s.jsx("code",{children:"hub"}),"."]})]}),s.jsxs("div",{className:"row",children:[s.jsx("label",{htmlFor:"mint-expires-in",children:"Expires in (seconds, optional)"}),s.jsx("input",{id:"mint-expires-in",type:"text",inputMode:"numeric",value:g.expiresIn,onChange:C=>y({...g,expiresIn:C.target.value}),placeholder:"default 90d (7776000)"})]}),s.jsxs("div",{className:"row",children:[s.jsx("label",{htmlFor:"mint-subject",children:"Subject (optional)"}),s.jsx("input",{id:"mint-subject",type:"text",value:g.subject,onChange:C=>y({...g,subject:C.target.value}),placeholder:"defaults to operator's sub"})]}),s.jsxs("div",{className:"row",children:[s.jsx("label",{htmlFor:"mint-permissions",children:"Permissions (JSON object, optional)"}),s.jsx("textarea",{id:"mint-permissions",value:g.permissions,onChange:C=>y({...g,permissions:C.target.value}),placeholder:'e.g. {"vault":{"default":{"write_tags":["health"]}}}',rows:3,style:{width:"100%",fontFamily:"monospace",fontSize:"0.9rem"}})]}),v.kind==="error"?s.jsx("div",{className:"field-error",children:s.jsx("code",{children:v.message})}):null,s.jsxs("div",{className:"actions",children:[s.jsx("button",{type:"submit",disabled:v.kind==="submitting",children:v.kind==="submitting"?"Minting…":"Mint"}),s.jsx("button",{type:"button",className:"secondary",onClick:()=>{q(!1),y(Wc),p({kind:"idle"})},children:"Cancel"})]})]})}):null,s.jsxs("div",{style:{marginTop:"1rem",marginBottom:"0.5rem",display:"flex",flexWrap:"wrap",gap:"0.5rem",alignItems:"center"},children:[s.jsx("span",{className:"muted",style:{marginRight:"0.5rem",minWidth:"4rem"},children:"Status:"}),[{value:"all",label:"Show all"},{value:"live",label:"Live only"},{value:"revoked",label:"Revoked only"}].map(C=>s.jsx("button",{type:"button",onClick:()=>ee("status",C.value),className:r===C.value?void 0:"secondary","aria-pressed":r===C.value,children:C.label},C.value))]}),s.jsxs("div",{style:{marginBottom:"1rem",display:"flex",flexWrap:"wrap",gap:"0.5rem",alignItems:"center"},children:[s.jsx("span",{className:"muted",style:{marginRight:"0.5rem",minWidth:"4rem"},children:"Source:"}),[{value:"all",label:"All sources"},{value:"oauth_refresh",label:"OAuth"},{value:"operator_mint",label:"Operator"},{value:"cli_mint",label:"CLI mint"}].map(C=>s.jsx("button",{type:"button",onClick:()=>ee("source",C.value),className:o===C.value?void 0:"secondary","aria-pressed":o===C.value,children:C.label},C.value))]}),ay({list:f,revoke:A,setRevoke:T,onConfirm:K,onLoadMore:I,onRetry:()=>j(C=>C+1),loadingMore:Z,filtersActive:r!=="all"||o!=="all"})]})}function ay({list:i,revoke:c,setRevoke:r,onConfirm:o,onLoadMore:f,onRetry:h,loadingMore:b,filtersActive:j}){return i.kind==="loading"?s.jsx("p",{className:"muted","data-loading":"true",children:"Loading…"}):i.kind==="error"?s.jsxs(s.Fragment,{children:[s.jsxs("div",{className:"error-banner",children:["Couldn't load tokens: ",s.jsx("code",{children:i.message})]}),s.jsx("button",{type:"button",onClick:h,className:"secondary",children:"Retry"})]}):i.tokens.length===0?j?s.jsxs("div",{className:"empty empty-rich",children:[s.jsx("p",{className:"empty-headline",children:"No tokens match the current filter."}),s.jsx("p",{className:"muted",children:'Try widening the Status or Source pills above. The default "Show all / All sources" view shows every registry row.'})]}):s.jsxs("div",{className:"empty empty-rich",children:[s.jsx("p",{className:"empty-headline",children:"No tokens."}),s.jsxs("p",{className:"muted",children:["Every CLI mint, OAuth grant, and operator-token rotation lands here. Mint one with the form above, or via ",s.jsx("code",{children:"parachute auth mint-token"}),"."]})]}):s.jsxs("div",{"data-route-content":"true",children:[i.tokens.map(v=>{const p=ny(v),g=c.kind==="revoking"&&c.jti===v.jti,y=c.kind==="confirming"&&c.jti===v.jti,A=c.kind==="error"&&c.jti===v.jti?c:null,T=v.user_id??v.subject??"(unknown)";return s.jsxs("div",{className:"vault-row",children:[s.jsxs("div",{className:"body",children:[s.jsxs("div",{className:"name",children:[s.jsx("code",{title:v.jti,children:fu(v.jti)}),s.jsx("span",{className:`tag${p==="live"?"":" muted"}`,children:p}),s.jsx("span",{className:`tag source-${sy(v.created_via)}`,title:`created_via: ${v.created_via}`,children:uy(v.created_via)})]}),s.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[s.jsx("span",{className:"muted",children:"identity: "}),s.jsx("code",{children:T}),v.client_id?s.jsxs(s.Fragment,{children:[s.jsx("span",{className:"muted",children:" · client: "}),s.jsx("code",{children:v.client_id})]}):null]}),s.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[s.jsx("span",{className:"muted",children:"scope: "}),v.scopes.map((D,q)=>s.jsxs("span",{children:[s.jsx("code",{children:D}),q<v.scopes.length-1?" ":null]},D))]}),s.jsxs("div",{className:"dim",style:{marginTop:"0.25rem",fontSize:"0.82rem"},children:[s.jsx("span",{className:"muted",children:"created "}),s.jsx("code",{title:v.created_at,children:Ic(v.created_at)}),s.jsx("span",{className:"muted",children:" · expires "}),s.jsx("code",{title:v.expires_at,children:Ic(v.expires_at)}),v.revoked_at?s.jsxs(s.Fragment,{children:[s.jsx("span",{className:"muted",children:" · revoked "}),s.jsx("code",{title:v.revoked_at,children:Ic(v.revoked_at)})]}):null]}),v.permissions?s.jsxs("details",{style:{marginTop:"0.35rem"},children:[s.jsx("summary",{className:"muted",style:{cursor:"pointer",fontSize:"0.85rem"},children:"permissions"}),s.jsx("pre",{style:{fontSize:"0.82rem",marginTop:"0.25rem"},children:JSON.stringify(v.permissions,null,2)})]}):null,A?s.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:s.jsx("code",{children:A.message})}):null,y?s.jsxs("dialog",{open:!0,className:"error-banner",style:{marginTop:"0.5rem",background:"var(--bg-warn, #fffbe6)"},"aria-label":`Confirm revoke ${fu(v.jti)}`,children:[s.jsxs("p",{children:["Revoke ",s.jsx("code",{children:fu(v.jti)}),"? Resource servers reject within ~60s of the next revocation-list poll. This cannot be undone."]}),s.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[s.jsx("button",{type:"button",onClick:()=>{o(v.jti)},disabled:g,children:g?"Revoking…":"Revoke"}),s.jsx("button",{type:"button",className:"secondary",onClick:()=>r({kind:"idle"}),disabled:g,children:"Cancel"})]})]}):null]}),!y&&p==="live"?s.jsx("button",{type:"button",className:"secondary",onClick:()=>r({kind:"confirming",jti:v.jti}),"aria-label":`Revoke ${fu(v.jti)}`,children:"Revoke"}):null]},v.jti)}),i.nextCursor?s.jsx("div",{style:{marginTop:"1rem"},children:s.jsx("button",{type:"button",className:"secondary",disabled:b,onClick:()=>{f()},children:b?"Loading…":"Load more"})}):null]})}function ny(i){if(i.revoked_at)return"revoked";const c=new Date(i.expires_at).getTime();return!Number.isNaN(c)&&c<Date.now()?"expired":"live"}function ly(i){return i==="live"||i==="revoked"||i==="all"?i:"all"}function iy(i){return i==="oauth_refresh"||i==="operator_mint"||i==="cli_mint"||i==="all"?i:"all"}function kh(i,c){const r={};return i==="live"?r.revoked="false":i==="revoked"?r.revoked="true":r.revoked="all",c!=="all"&&(r.createdVia=c),r}function uy(i){switch(i){case"oauth_refresh":return"OAuth";case"operator_mint":return"Operator";case"cli_mint":return"CLI";default:return i}}function sy(i){switch(i){case"oauth_refresh":return"oauth";case"operator_mint":return"operator";case"cli_mint":return"cli";default:return"unknown"}}function fu(i){return i.length<=14?i:`${i.slice(0,8)}…${i.slice(-4)}`}function Ic(i){const c=new Date(i);return Number.isNaN(c.getTime())?i:c.toLocaleString()}const ka=12,cy=/^[a-z0-9_-]+$/,gu=2,yu=32,Hh={username:"",password:"",assignedVaults:[]};function vm({username:i,hubOrigin:c}){const r=`${c||""}/login`;return s.jsxs("div",{className:"muted",style:{marginTop:"0.5rem",fontSize:"0.9em"},children:["Send them to ",s.jsx("code",{children:r})," with username ",s.jsx("code",{children:i})," and this password — they'll be prompted to set a new one on first sign-in."]})}function ry(){const[i,c]=S.useState({kind:"loading"}),[r,o]=S.useState(0),[f,h]=S.useState(!1),[b,j]=S.useState(Hh),[v,p]=S.useState({kind:"idle"}),[g,y]=S.useState({kind:"idle"}),[A,T]=S.useState({kind:"idle"}),[D,q]=S.useState({kind:"idle"});S.useEffect(()=>{let K=!1;return c({kind:"loading"}),Promise.all([ng(),cg(),om()]).then(([Y,C,O])=>{if(K)return;const G=O.resolved_issuer.replace(/\/+$/,"");c({kind:"ok",data:{users:Y,vaults:C,hubOrigin:G}})}).catch(Y=>{if(K)return;const C=Y instanceof Error?Y.message:String(Y);c({kind:"error",message:C})}),()=>{K=!0}},[r]);function Z(K){return K.username.length<gu||K.username.length>yu?`Username must be ${gu}-${yu} characters.`:cy.test(K.username)?K.password.length<ka?`Password must be at least ${ka} characters.`:null:"Username may only contain lowercase letters, digits, hyphens, and underscores."}async function V(K){K.preventDefault();const Y=Z(b);if(Y){p({kind:"error",message:Y});return}p({kind:"submitting"});const C={username:b.username,password:b.password,assignedVaults:b.assignedVaults};try{const O=await lg(C);j(Hh),p({kind:"created",username:O.username}),o(G=>G+1)}catch(O){const G=O instanceof X?`Create failed (${O.status}): ${O.message}`:O instanceof Error?O.message:String(O);p({kind:"error",message:G})}}async function ee(K){y({kind:"deleting",userId:K.id});try{const{revocationLagSeconds:Y}=await ig(K.id);y({kind:"done",username:K.username,revocationLagSeconds:Y}),o(C=>C+1)}catch(Y){const C=Y instanceof X?`Delete failed (${Y.status}): ${Y.message}`:Y instanceof Error?Y.message:String(Y);y({kind:"error",userId:K.id,message:C})}}async function I(K,Y){q({kind:"submitting",userId:K.id,selected:Y});try{await sg(K.id,Y),q({kind:"done",userId:K.id,username:K.username}),o(C=>C+1)}catch(C){const O=C instanceof X?`Edit vaults failed (${C.status}): ${C.message}`:C instanceof Error?C.message:String(C);q({kind:"error",userId:K.id,selected:Y,message:O})}}async function ue(K,Y){if(Y.length<ka){T({kind:"error",userId:K.id,password:Y,message:`Password must be at least ${ka} characters.`});return}T({kind:"submitting",userId:K.id,password:Y});try{const{revocationLagSeconds:C}=await ug(K.id,Y);T({kind:"done",userId:K.id,username:K.username,revocationLagSeconds:C}),o(O=>O+1)}catch(C){const O=C instanceof X?`Reset failed (${C.status}): ${C.message}`:C instanceof Error?C.message:String(C);T({kind:"error",userId:K.id,password:Y,message:O})}}return s.jsxs("div",{"data-route-content":"true",children:[s.jsx("div",{className:"list-header",children:s.jsx("h1",{children:"Users"})}),s.jsxs("p",{className:"muted",children:["Hub user accounts. Each user can be a member of one or more vaults — the OAuth issuer narrows their tokens to ",s.jsx("code",{children:"vault:<assigned>:*"})," scopes for any vault in their list. Users with no assignments can't authorize any vault yet — assign at least one above. The first admin is unrestricted (admin posture). Admin-created users land with a default password and are prompted to change it on first sign-in",i.kind==="ok"?s.jsxs(s.Fragment,{children:[" ","at ",s.jsxs("code",{children:[i.data.hubOrigin,"/login"]})]}):null,"."]}),g.kind==="done"&&s.jsxs("output",{className:"success-banner",style:{display:"block",marginBottom:"0.75rem"},"data-testid":"delete-done-banner",children:["Deleted ",s.jsx("code",{children:g.username}),". Their tokens are revoked. Resource servers (vault, scribe, etc.) cache the revocation list for up to ",g.revocationLagSeconds," ","seconds — if you're deleting because of a suspected compromise, also restart the affected services (e.g. ",s.jsx("code",{children:"parachute restart vault"}),") to flush their cache immediately.",s.jsx("div",{style:{marginTop:"0.5rem"},children:s.jsx("button",{type:"button",className:"secondary",onClick:()=>y({kind:"idle"}),children:"Dismiss"})})]}),oy(i,g,y,ee,A,T,ue,D,q,I,i.kind==="ok"?i.data.vaults:[],i.kind==="ok"?i.data.hubOrigin:"",()=>o(K=>K+1)),i.kind==="ok"&&s.jsx(my,{show:f,setShow:h,form:b,setForm:j,vaults:i.data.vaults,hubOrigin:i.data.hubOrigin,createState:v,setCreateState:p,onSubmit:V})]})}function oy(i,c,r,o,f,h,b,j,v,p,g,y,A){var q;if(i.kind==="loading")return s.jsx("p",{className:"muted",children:"Loading users…"});if(i.kind==="error")return s.jsxs(s.Fragment,{children:[s.jsxs("div",{className:"error-banner",children:["Couldn't load users: ",s.jsx("code",{children:i.message})]}),s.jsx("button",{type:"button",onClick:A,className:"secondary",children:"Retry"})]});const{users:T}=i.data;if(T.length===0)return s.jsxs("div",{className:"empty empty-rich",children:[s.jsx("p",{className:"empty-headline",children:"No users yet."}),s.jsx("p",{className:"muted",children:"Click Create User below to invite someone."})]});const D=(q=T[0])==null?void 0:q.id;return s.jsx(dy,{users:T,firstAdminId:D,deleteSt:c,setDeleteSt:r,onConfirmDelete:o,resetSt:f,setResetSt:h,onSubmitReset:b,editVaultsSt:j,setEditVaultsSt:v,onSubmitEditVaults:p,availableVaults:g,hubOrigin:y})}function dy({users:i,firstAdminId:c,deleteSt:r,setDeleteSt:o,onConfirmDelete:f,resetSt:h,setResetSt:b,onSubmitReset:j,editVaultsSt:v,setEditVaultsSt:p,onSubmitEditVaults:g,availableVaults:y,hubOrigin:A}){return s.jsx("div",{className:"user-list",style:{marginTop:"1rem"},children:s.jsx("div",{className:"table-scroll",children:s.jsxs("table",{className:"user-table",children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{scope:"col",children:"Username"}),s.jsx("th",{scope:"col",children:"Assigned vaults"}),s.jsx("th",{scope:"col",children:"Password set"}),s.jsx("th",{scope:"col",children:"Created"}),s.jsx("th",{scope:"col",children:"Actions"})]})}),s.jsx("tbody",{children:i.map(T=>{const D=T.id===c,q=r.kind==="deleting"&&r.userId===T.id,Z=r.kind==="confirming"&&r.user.id===T.id,V=r.kind==="error"&&r.userId===T.id?r:null,ee=(h.kind==="open"||h.kind==="submitting"||h.kind==="error")&&h.userId===T.id?h:null,I=h.kind==="done"&&h.userId===T.id?h:null,ue=(v.kind==="open"||v.kind==="submitting"||v.kind==="error")&&v.userId===T.id?v:null,K=v.kind==="done"&&v.userId===T.id?v:null;return s.jsxs("tr",{"data-user-id":T.id,children:[s.jsxs("td",{children:[s.jsx("code",{children:T.username}),D&&s.jsx("span",{className:"badge",style:{marginLeft:"0.5rem"},children:"first admin"})]}),s.jsx("td",{children:T.assigned_vaults.length>0?s.jsx("span",{style:{display:"inline-flex",flexWrap:"wrap",gap:"0.25rem"},children:T.assigned_vaults.map(Y=>s.jsx("code",{children:Y},Y))}):s.jsx("span",{className:"muted",title:D?"First admin is unrestricted (admin posture)":"No vaults assigned — user can't authorize any vault yet",children:"—"})}),s.jsx("td",{children:T.password_changed?s.jsx("span",{"aria-label":"changed",children:"✓"}):s.jsx("span",{className:"status status-pending",title:"User hasn't completed first-sign-in change-password yet",children:"pending first login"})}),s.jsx("td",{children:s.jsx("span",{title:T.created_at,children:py(T.created_at)})}),s.jsxs("td",{children:[Z?null:s.jsxs("div",{style:{display:"flex",flexWrap:"wrap",gap:"0.5rem",alignItems:"center"},children:[D&&s.jsxs(s.Fragment,{children:[s.jsx("span",{id:`first-admin-tooltip-${T.id}`,className:"sr-only",children:"First admin can't be deleted (would self-lock the hub)"}),s.jsx("span",{id:`first-admin-reset-tooltip-${T.id}`,className:"sr-only",children:"First admin uses /account/change-password directly"}),s.jsx("span",{id:`first-admin-vaults-tooltip-${T.id}`,className:"sr-only",children:"First admin's vault membership is unrestricted by design"})]}),s.jsx("button",{type:"button",className:"secondary",disabled:D||ue!==null||v.kind==="submitting"&&v.userId===T.id,title:D?"First admin's vault membership is unrestricted by design":void 0,"aria-describedby":D?`first-admin-vaults-tooltip-${T.id}`:void 0,onClick:()=>p({kind:"open",userId:T.id,selected:[...T.assigned_vaults]}),"aria-label":`Edit vaults for ${T.username}`,children:"Edit vaults"}),s.jsx("button",{type:"button",className:"secondary",disabled:D||ee!==null||h.kind==="submitting"&&h.userId===T.id,title:D?"First admin uses /account/change-password directly":void 0,"aria-describedby":D?`first-admin-reset-tooltip-${T.id}`:void 0,onClick:()=>b({kind:"open",userId:T.id,password:""}),"aria-label":`Reset password for ${T.username}`,children:"Reset password"}),s.jsx("button",{type:"button",className:"secondary",disabled:D||q,title:D?"First admin can't be deleted (would self-lock the hub)":void 0,"aria-describedby":D?`first-admin-tooltip-${T.id}`:void 0,onClick:()=>o({kind:"confirming",user:T}),"aria-label":`Delete ${T.username}`,children:q?"Deleting…":"Delete"})]}),Z&&s.jsxs("dialog",{open:!0,className:"error-banner",style:{marginTop:"0.25rem",background:"var(--bg-warn, #fffbe6)"},"aria-label":`Confirm delete ${T.username}`,children:[s.jsxs("p",{children:["Delete ",s.jsx("code",{children:T.username}),"? This revokes their tokens, drops their sessions and grants, and removes the account. The audit trail is preserved — tokens stay with ",s.jsx("code",{children:"revoked_at"})," set, anonymised."]}),s.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[s.jsx("button",{type:"button",className:"destructive",onClick:()=>{f(T)},disabled:q,children:q?"Deleting…":"Delete"}),s.jsx("button",{type:"button",className:"secondary",onClick:()=>o({kind:"idle"}),disabled:q,children:"Cancel"})]})]}),V&&s.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:s.jsx("code",{children:V.message})}),ee&&s.jsx(fy,{user:T,state:ee,onCancel:()=>b({kind:"idle"}),onPasswordChange:Y=>b({kind:"open",userId:T.id,password:Y}),onSubmit:Y=>{j(T,Y)}}),I&&s.jsxs("output",{className:"success-banner",style:{marginTop:"0.25rem",display:"block"},children:["Password reset for ",s.jsx("code",{children:I.username}),". Hand them the new password and tell them they'll be prompted to change it on first sign-in.",s.jsx(vm,{username:I.username,hubOrigin:A}),s.jsxs("div",{className:"muted",style:{marginTop:"0.5rem",fontSize:"0.85em"},children:["Their existing tokens are revoked. Resource servers (vault, scribe, etc.) cache the revocation list for up to ",I.revocationLagSeconds," ","seconds — if you're resetting because of a suspected compromise, also restart the affected services (e.g. ",s.jsx("code",{children:"parachute restart vault"}),") to flush their cache immediately."]}),s.jsx("div",{style:{marginTop:"0.5rem"},children:s.jsx("button",{type:"button",className:"secondary",onClick:()=>b({kind:"idle"}),children:"Dismiss"})})]}),ue&&s.jsx(hy,{user:T,state:ue,availableVaults:y,onCancel:()=>p({kind:"idle"}),onSelectedChange:Y=>p({kind:"open",userId:T.id,selected:Y}),onSubmit:Y=>{g(T,Y)}}),K&&s.jsxs("output",{className:"success-banner",style:{marginTop:"0.25rem",display:"block"},children:["Vault assignments updated for ",s.jsx("code",{children:K.username}),".",s.jsx("div",{style:{marginTop:"0.5rem"},children:s.jsx("button",{type:"button",className:"secondary",onClick:()=>p({kind:"idle"}),children:"Dismiss"})})]})]})]},T.id)})})]})})})}function fy({user:i,state:c,onCancel:r,onPasswordChange:o,onSubmit:f}){const h=c.kind==="submitting",b=c.kind==="error"?c.message:null,j=`reset-password-input-${i.id}`;return s.jsxs("form",{onSubmit:v=>{v.preventDefault(),f(c.password)},"aria-label":`Reset password for ${i.username}`,style:{marginTop:"0.5rem",padding:"0.5rem",background:"var(--bg-soft, #f5f5f5)",borderRadius:"4px"},children:[s.jsxs("p",{style:{margin:0},children:[s.jsxs("label",{htmlFor:j,children:["New temporary password for ",s.jsx("code",{children:i.username})," ",s.jsxs("span",{className:"muted",children:["(min ",ka," chars)"]})]}),s.jsx("br",{}),s.jsx("input",{id:j,type:"password",required:!0,autoComplete:"new-password",minLength:ka,value:c.password,disabled:h,onChange:v=>o(v.target.value)})]}),b&&s.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:s.jsx("code",{children:b})}),s.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[s.jsx("button",{type:"submit",disabled:h,children:h?"Setting…":"Set new password"}),s.jsx("button",{type:"button",className:"secondary",onClick:r,disabled:h,children:"Cancel"})]})]})}function hy({user:i,state:c,availableVaults:r,onCancel:o,onSelectedChange:f,onSubmit:h}){const b=c.kind==="submitting",j=c.kind==="error"?c.message:null,v=`edit-vaults-select-${i.id}`;function p(g){const y=Array.from(g.target.selectedOptions).map(A=>A.value);f(y)}return s.jsxs("form",{onSubmit:g=>{g.preventDefault(),h(c.selected)},"aria-label":`Edit vaults for ${i.username}`,style:{marginTop:"0.5rem",padding:"0.5rem",background:"var(--bg-soft, #f5f5f5)",borderRadius:"4px"},children:[s.jsx("p",{style:{margin:0},children:s.jsxs("label",{htmlFor:v,children:["Vault assignments for ",s.jsx("code",{children:i.username})," ",s.jsx("span",{className:"muted",children:"(empty = no narrowing; shift-click to multi-select)"})]})}),c.selected.length>0&&s.jsx("div",{"data-testid":`edit-vaults-chips-${i.id}`,style:{marginTop:"0.4rem",display:"flex",flexWrap:"wrap",gap:"0.25rem"},children:c.selected.map(g=>s.jsx("code",{style:{padding:"0.1rem 0.4rem",borderRadius:"4px"},children:g},g))}),s.jsx("select",{id:v,multiple:!0,value:c.selected,onChange:p,disabled:b,size:Math.min(Math.max(r.length,3),8),style:{marginTop:"0.4rem",minWidth:"12rem"},children:r.map(g=>s.jsx("option",{value:g,children:g},g))}),r.length===0&&s.jsx("p",{className:"muted",style:{marginTop:"0.25rem"},children:"No vaults registered on this hub yet."}),j&&s.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:s.jsx("code",{children:j})}),s.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[s.jsx("button",{type:"submit",disabled:b,children:b?"Saving…":"Save vault assignments"}),s.jsx("button",{type:"button",className:"secondary",onClick:o,disabled:b,children:"Cancel"})]})]})}function my({show:i,setShow:c,form:r,setForm:o,vaults:f,hubOrigin:h,createState:b,setCreateState:j,onSubmit:v}){const p=b.kind==="submitting";return s.jsx("section",{style:{marginTop:"1.5rem"},children:i?s.jsxs("form",{onSubmit:g=>void v(g),"aria-label":"Create user",children:[s.jsx("h3",{children:"Create user"}),s.jsxs("p",{children:[s.jsxs("label",{htmlFor:"new-user-username",children:["Username"," ",s.jsxs("span",{className:"muted",children:["(",gu,"-",yu," chars, lowercase letters/digits/hyphens/ underscores)"]})]}),s.jsx("br",{}),s.jsx("input",{id:"new-user-username",type:"text",required:!0,autoComplete:"off",value:r.username,minLength:gu,maxLength:yu,pattern:"[a-z0-9_\\-]+",onChange:g=>o({...r,username:g.target.value})})]}),s.jsxs("p",{children:[s.jsxs("label",{htmlFor:"new-user-password",children:["Password ",s.jsxs("span",{className:"muted",children:["(min ",ka," chars)"]})]}),s.jsx("br",{}),s.jsx("input",{id:"new-user-password",type:"password",required:!0,autoComplete:"new-password",value:r.password,minLength:ka,onChange:g=>o({...r,password:g.target.value})})]}),s.jsxs("p",{children:[s.jsxs("label",{htmlFor:"new-user-vaults",children:["Assigned vaults"," ",s.jsx("span",{className:"muted",children:"(empty = no restriction; shift-click to select multiple)"})]}),s.jsx("br",{}),r.assignedVaults.length>0&&s.jsx("span",{"data-testid":"new-user-vault-chips",style:{display:"inline-flex",flexWrap:"wrap",gap:"0.25rem",marginBottom:"0.25rem"},children:r.assignedVaults.map(g=>s.jsx("code",{children:g},g))}),s.jsx("br",{}),s.jsx("select",{id:"new-user-vaults",multiple:!0,value:r.assignedVaults,onChange:g=>o({...r,assignedVaults:Array.from(g.target.selectedOptions).map(y=>y.value)}),size:Math.min(Math.max(f.length,3),8),style:{minWidth:"12rem"},children:f.map(g=>s.jsx("option",{value:g,children:g},g))}),f.length===0&&s.jsx("span",{className:"muted",style:{marginLeft:"0.5rem"},children:"No vaults registered on this hub yet."})]}),b.kind==="error"&&s.jsx("div",{className:"error-banner",children:s.jsx("code",{children:b.message})}),b.kind==="created"&&s.jsxs("output",{className:"success-banner",children:["User ",s.jsx("code",{children:b.username})," created. They'll be prompted to change their password on first sign-in.",s.jsx(vm,{username:b.username,hubOrigin:h})]}),s.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[s.jsx("button",{type:"submit",disabled:p,children:p?"Creating…":"Create user"}),s.jsx("button",{type:"button",className:"secondary",disabled:p,onClick:()=>{c(!1),j({kind:"idle"})},children:"Close"})]})]}):s.jsx("button",{type:"button",onClick:()=>c(!0),children:"Create User"})})}function py(i){const c=new Date(i);return Number.isNaN(c.getTime())?i:c.toLocaleString()}function Bh(){const[i,c]=S.useState({kind:"loading"}),[r,o]=S.useState(0),[f,h]=S.useState({kind:"idle"}),[b,j]=S.useState(null);async function v(g){if(g.managementUrl){h({kind:"minting",name:g.name});try{const y=await L0(g.name),A=Z0(g.url,g.managementUrl),T=A.includes("#")?"&":"#";window.location.assign(`${A}${T}token=${y.token}`)}catch(y){const A=y instanceof X?`mint failed (${y.status}): ${y.message}`:y instanceof Error?y.message:String(y);h({kind:"error",name:g.name,message:A})}}}if(S.useEffect(()=>{let g=!1;return k0().then(y=>{g||c({kind:"ok",vaults:y.vaults,moduleInstalled:y.moduleInstalled})}).catch(y=>{if(g)return;const A=y instanceof Error?y.message:String(y);c({kind:"error",message:A})}),()=>{g=!0}},[r]),i.kind==="loading")return s.jsxs("div",{children:[s.jsx("div",{className:"list-header",children:s.jsx("h1",{children:"Vaults"})}),s.jsx("p",{className:"muted",children:"Loading…"})]});if(i.kind==="error")return s.jsxs("div",{children:[s.jsxs("div",{className:"list-header",children:[s.jsx("h1",{children:"Vaults"}),s.jsx(Ke,{to:"/vaults/new",children:s.jsx("button",{type:"button",children:"New vault"})})]}),s.jsxs("div",{className:"error-banner",children:["Couldn't load vaults: ",s.jsx("code",{children:i.message})]}),s.jsx("button",{type:"button",onClick:()=>o(g=>g+1),className:"secondary",children:"Retry"})]});const p=i.vaults.length===0&&!i.moduleInstalled;return s.jsxs("div",{"data-route-content":"true",children:[s.jsxs("div",{className:"list-header",children:[s.jsxs("h1",{children:["Vaults (",i.vaults.length,")"]}),p?s.jsx(Ke,{to:"/modules",children:s.jsx("button",{type:"button",children:"Install vault module"})}):s.jsx(Ke,{to:"/vaults/new",children:s.jsx("button",{type:"button",children:"New vault"})})]}),s.jsxs("p",{className:"muted",children:["Vaults registered with this hub at ",s.jsx("code",{children:"/.well-known/parachute.json"}),"."]}),i.vaults.length===0?p?s.jsxs("div",{className:"empty empty-rich",children:[s.jsx("p",{className:"empty-headline",children:"No vault module installed."}),s.jsx("p",{className:"muted",children:"The vault backend isn't installed on this hub yet, so there's nothing to provision a vault against. Install it from the Modules page first, then come back here to create your first vault."}),s.jsx("p",{style:{marginTop:"0.75rem"},children:s.jsx(Ke,{to:"/modules",children:"Install vault module →"})})]}):s.jsxs("div",{className:"empty empty-rich",children:[s.jsx("p",{className:"empty-headline",children:"No vaults yet."}),s.jsx("p",{className:"muted",children:"Create your first vault to start storing tokens, secrets, and notes scoped to this hub."}),s.jsx("p",{style:{marginTop:"0.75rem"},children:s.jsx(Ke,{to:"/vaults/new",children:"Create a vault →"})})]}):s.jsx("div",{style:{marginTop:"1rem"},children:i.vaults.map(g=>{const y=f.kind==="minting"&&f.name===g.name,A=f.kind==="error"&&f.name===g.name?f:null,T=b===g.name;return s.jsxs("div",{className:"vault-row-group",children:[s.jsxs("div",{className:"vault-row",children:[s.jsxs("div",{className:"body",children:[s.jsxs("div",{className:"name",children:[s.jsx("code",{children:g.name}),s.jsxs("span",{className:"tag muted",title:"Vault version",children:["v",g.version]})]}),s.jsx("div",{className:"dim url",children:s.jsx("code",{children:g.url})}),A?s.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:s.jsx("code",{children:A.message})}):null]}),s.jsxs("div",{className:"vault-row-actions",children:[s.jsx("button",{type:"button",className:T?void 0:"secondary","aria-expanded":T,"aria-label":`Connect an MCP client to vault ${g.name}`,onClick:()=>j(D=>D===g.name?null:g.name),children:T?"Hide connect":"Connect"}),g.managementUrl?s.jsx("button",{type:"button",onClick:()=>{v(g)},disabled:y,"aria-label":`Manage vault ${g.name}`,children:y?"Opening…":"Manage"}):s.jsx("span",{className:"muted",title:"This vault has no admin SPA — manage with parachute-vault on the host.",children:"CLI only"})]})]}),T?s.jsx(pm,{vaultName:g.name,vaultUrl:g.url}):null]},g.name)})})]})}function gm(i){return i==="/permissions"||i.startsWith("/permissions/")?"permissions":i==="/tokens"||i.startsWith("/tokens/")?"tokens":i==="/modules"||i.startsWith("/modules/")?/^\/modules\/[a-z][a-z0-9-]*\/config$/.test(i)?"module config":"modules":i==="/users"||i.startsWith("/users/")?"users":i==="/settings"||i.startsWith("/settings/")?"settings":i.startsWith("/approve-client/")?"approve app":"vaults"}function vy(i){return`${gm(i).split(" ").map(r=>r.length>0?r[0].toUpperCase()+r.slice(1):r).join(" ")} · ${um}`}function gy(){const{pathname:i}=Dt(),c=gm(i),[r,o]=S.useState(null);S.useEffect(()=>{document.title=vy(i)},[i]);const[f,h]=S.useState(!1),[b,j]=S.useState([]);S.useEffect(()=>{let p=!1;return G0().then(g=>{p||o(g)}).catch(()=>{p||o({hasSession:!1})}),()=>{p=!0}},[]),S.useEffect(()=>{if(!r||!r.hasSession)return;let p=!1;return rm().then(g=>{p||j(g.modules.filter(y=>y.installed))}).catch(()=>{p||j([])}),()=>{p=!0}},[r]);async function v(p){h(!0);try{await V0(p),window.location.href="/"}catch{h(!1)}}return s.jsxs("div",{className:"page",children:[s.jsxs("nav",{className:"nav",children:[s.jsxs(Ke,{to:"/vaults",className:"brand",children:[s.jsx(A0,{size:18,idSuffix:"spa-nav",className:"brand-mark-icon"}),s.jsx("span",{className:"brand-wordmark",children:um}),s.jsx("span",{className:"sub",children:c})]}),s.jsx(by,{me:r,signingOut:f,onSignOut:v}),s.jsx(Gn,{to:"/vaults",label:"Vaults",alsoActiveAt:"/"}),s.jsx(Gn,{to:"/modules",label:"Modules"}),s.jsx(yy,{services:b}),s.jsx(Gn,{to:"/users",label:"Users"}),s.jsx(Gn,{to:"/permissions",label:"Permissions"}),s.jsx(Gn,{to:"/tokens",label:"Tokens"}),s.jsx(Gn,{to:"/settings",label:"Settings"}),s.jsx("span",{className:"nav-divider","aria-hidden":"true"}),s.jsx("a",{href:"/",title:"Hub discovery page (top-level)",children:"Discovery"})]}),s.jsxs(Kv,{children:[s.jsx(Ot,{path:"/",element:s.jsx(Bh,{})}),s.jsx(Ot,{path:"/vaults",element:s.jsx(Bh,{})}),s.jsx(Ot,{path:"/vaults/new",element:s.jsx(Jg,{})}),s.jsx(Ot,{path:"/modules",element:s.jsx(kg,{})}),s.jsx(Ot,{path:"/modules/:short/config",element:s.jsx(jg,{})}),s.jsx(Ot,{path:"/users",element:s.jsx(ry,{})}),s.jsx(Ot,{path:"/permissions",element:s.jsx(Fg,{})}),s.jsx(Ot,{path:"/tokens",element:s.jsx(ty,{})}),s.jsx(Ot,{path:"/settings",element:s.jsx(Pg,{})}),s.jsx(Ot,{path:"/approve-client/:clientId",element:s.jsx(mg,{})}),s.jsx(Ot,{path:"*",element:s.jsxs("div",{className:"empty",children:["404 — back to ",s.jsx(Ke,{to:"/vaults",children:"vaults"}),"."]})})]}),r!=null&&r.hasSession?s.jsx(hg,{}):null]})}function Gn({to:i,label:c,alsoActiveAt:r}){const{pathname:o}=Dt(),b=o===i||o.startsWith(`${i}/`)||r!==void 0&&o===r;return s.jsx(Ke,{to:i,className:b?"nav-link nav-link-active":"nav-link","aria-current":b?"page":void 0,children:c})}function yy({services:i}){return i.length===0?null:s.jsxs("details",{className:"nav-dropdown","data-testid":"installed-services-dropdown",children:[s.jsx("summary",{className:"nav-dropdown-summary",children:"Services"}),s.jsx("div",{className:"nav-dropdown-panel",role:"menu",children:i.map(c=>{const r=c.management_url;return r?s.jsx("a",{className:"nav-dropdown-item",href:r,role:"menuitem","data-testid":`nav-service-${c.short}`,children:c.display_name},c.short):s.jsx("span",{className:"nav-dropdown-item nav-dropdown-item-disabled",role:"menuitem","aria-disabled":"true",title:"This module hasn't shipped an admin UI yet.","data-testid":`nav-service-${c.short}`,children:c.display_name},c.short)})})]})}function by({me:i,signingOut:c,onSignOut:r}){return i===null?null:i.hasSession?s.jsxs("span",{className:"auth-spa",children:[s.jsxs("span",{className:"muted",children:["Signed in as ",s.jsx("strong",{children:i.user.displayName})]})," ","·"," ",s.jsx("button",{type:"button",className:"auth-spa-signout",disabled:c,onClick:()=>{r(i.csrf)},children:c?"Signing out…":"Sign out"})]}):s.jsx("a",{href:`/login?next=${encodeURIComponent(window.location.pathname)}`,className:"auth-spa",children:"Sign in"})}const ym=document.getElementById("root");if(!ym)throw new Error("#root not found");function xy(){const i=window.location.pathname;return i==="/admin"||i.startsWith("/admin/")?"/admin":""}W1.createRoot(ym).render(s.jsx(S.StrictMode,{children:s.jsx(b0,{basename:xy(),children:s.jsx(gy,{})})}));
|
|
61
|
+
`)})]})]},C.operationId))})]}),s.jsxs("section",{className:"modules-installed","data-testid":"installed-section",children:[s.jsx("h2",{children:"Installed modules"}),ee.length===0?s.jsxs("p",{className:"muted","data-testid":"installed-empty",children:["No modules installed yet. Pick one from ",s.jsx("strong",{children:"Install a module"})," below to get started."]}):s.jsx("ul",{className:"module-list",children:ee.map(C=>s.jsx(Bg,{module:C,supervisorAvailable:Z,syncBusy:!!f[C.short],errorMessage:b[C.short],onUpgrade:()=>void y(C.short),onRestart:()=>void A(C.short),onUninstall:()=>void T(C.short)},C.short))})]}),s.jsxs("section",{className:"modules-installable","data-testid":"installable-section",children:[s.jsx("h2",{children:"Install a module"}),K.length===0?s.jsx("p",{className:"muted","data-testid":"installable-empty",children:ue.size>0?"Install in progress — see In progress above.":"All available modules are installed."}):s.jsx("ul",{className:"install-list",children:K.map(C=>s.jsx(Lg,{module:C,supervisorAvailable:Z,installing:ue.has(C.short),errorMessage:b[C.short],onInstall:()=>void g(C.short)},C.short))})]})]})}const Hg={scribe:"Scribe admin SPA tracked at scribe#53",runner:"Runner admin SPA tracked at runner#8"};function Bg({module:i,supervisorAvailable:c,syncBusy:r,errorMessage:o,onUpgrade:f,onRestart:h,onUninstall:b}){const j=c&&!r,v=i.installed_version!==i.latest_version&&i.latest_version!==null,p=i.management_url,g=Hg[i.short];return s.jsxs("li",{className:"module-row","data-short":i.short,children:[s.jsxs("header",{children:[s.jsxs("h2",{children:[i.display_name," ",s.jsxs("span",{className:"muted",children:["(",i.short,")"]})]}),(()=>{const y=qg(i);return s.jsx("span",{className:`status status-${y.cssState}`,"data-state":y.cssState,"data-testid":`module-status-${i.short}`,children:y.label})})()]}),i.tagline?s.jsx("p",{className:"tagline",children:i.tagline}):null,s.jsxs("dl",{className:"meta",children:[s.jsx("dt",{children:"Package"}),s.jsx("dd",{children:s.jsx("code",{children:i.package})}),s.jsx("dt",{children:"Installed"}),s.jsxs("dd",{children:["v",i.installed_version??"unknown",v&&s.jsxs("span",{className:"badge",children:["v",i.latest_version," available"]})]}),i.pid&&s.jsxs(s.Fragment,{children:[s.jsx("dt",{children:"PID"}),s.jsx("dd",{children:i.pid})]})]}),i.uis.length>0&&s.jsx(Yg,{uis:i.uis}),s.jsxs("div",{className:"actions",children:[p?s.jsx("a",{className:"btn",href:p,"data-testid":`open-${i.short}`,children:"Open"}):s.jsx("button",{type:"button",className:"btn",disabled:!0,title:g??"This module hasn't shipped an admin UI yet.","data-testid":`open-${i.short}`,children:"Open"}),s.jsx("button",{type:"button",disabled:!j,onClick:h,children:"Restart"}),s.jsx("button",{type:"button",disabled:!j||!v,onClick:f,children:v?`Upgrade to v${i.latest_version}`:"Up to date"}),s.jsx("button",{type:"button",className:"destructive",disabled:!j,onClick:b,children:"Uninstall"})]}),o&&s.jsx("div",{className:"error",children:o})]})}function Lg({module:i,supervisorAvailable:c,installing:r,errorMessage:o,onInstall:f}){const h=c&&!r;return s.jsxs("li",{className:"install-card","data-short":i.short,children:[s.jsxs("div",{className:"install-card-body",children:[s.jsxs("h3",{children:[i.display_name," ",s.jsxs("span",{className:"muted",children:["(",i.short,")"]})]}),i.tagline?s.jsx("p",{className:"tagline",children:i.tagline}):null,s.jsxs("p",{className:"muted install-card-meta",children:[s.jsx("code",{children:i.package}),i.latest_version?s.jsxs(s.Fragment,{children:[" · ","latest ",s.jsxs("code",{children:["v",i.latest_version]})]}):null]})]}),s.jsx("div",{className:"install-card-actions",children:s.jsx("button",{type:"button",disabled:!h,onClick:f,children:r?"Installing…":"Install"})}),o&&s.jsx("div",{className:"error",children:o})]})}function qg(i){if(!i.installed)return{cssState:"absent",label:"not installed"};if(!i.supervisor_status)return{cssState:"absent",label:"not supervised"};const c=Og(i.supervisor_status);return{cssState:c,label:c}}function Yg({uis:i}){return s.jsxs("details",{className:"module-uis","data-testid":"module-uis",open:!0,children:[s.jsxs("summary",{children:["Hosted UIs ",s.jsxs("span",{className:"muted",children:["(",i.length,")"]})]}),s.jsx("ul",{className:"ui-sub-units",children:i.map(c=>{const r=Ug(c.status);return s.jsxs("li",{className:"ui-sub-unit","data-name":c.name,children:[c.icon_url&&s.jsx("img",{src:c.icon_url,alt:"",className:"ui-icon",width:20,height:20,loading:"lazy"}),s.jsxs("div",{className:"ui-sub-unit-body",children:[s.jsxs("a",{href:c.path,className:"ui-sub-unit-link",children:[s.jsx("strong",{children:c.display_name}),s.jsxs("span",{className:"muted",children:[" · ",c.path]})]}),c.tagline?s.jsx("p",{className:"tagline",children:c.tagline}):null]}),s.jsx("span",{className:`status status-${r}`,"data-state":r,"data-testid":`ui-status-${c.name}`,children:r})]},c.name)})})]})}function Gg({channel:i,disabled:c,onChange:r}){return s.jsxs("fieldset",{className:"channel-toggle","data-testid":"channel-toggle",children:[s.jsx("legend",{children:"Install channel"}),s.jsxs("label",{children:[s.jsx("input",{type:"radio",name:"module-install-channel",value:"latest",checked:i==="latest",disabled:c,onChange:()=>r("latest")}),"Stable (",s.jsx("code",{children:"latest"}),")"]}),s.jsxs("label",{children:[s.jsx("input",{type:"radio",name:"module-install-channel",value:"rc",checked:i==="rc",disabled:c,onChange:()=>r("rc")}),"Release candidates (",s.jsx("code",{children:"rc"}),")"]}),s.jsxs("p",{className:"muted",children:["All future module installs and upgrades use this channel. Existing installed modules are unaffected — use ",s.jsx("strong",{children:"Upgrade"})," to pull a newer version."]}),s.jsxs("p",{className:"muted",children:["More hub settings (canonical URL, etc.) at ",s.jsx(Ke,{to:"/settings",children:"Settings"}),"."]})]})}const Vg="https://parachute.computer/install#connect-mcp-clients";function hm(i){return`${i.replace(/\/+$/,"")}/mcp`}function mm(i,c){return`claude mcp add --transport http parachute-${i} ${hm(c)}`}function Zg(i,c,r){return`${mm(i,c)} --header "Authorization: Bearer ${r}"`}function Fc({value:i,label:c="Copy"}){const[r,o]=S.useState(!1);return s.jsx("button",{type:"button",className:"secondary",onClick:()=>{typeof navigator>"u"||!navigator.clipboard||navigator.clipboard.writeText(i).then(()=>{o(!0),setTimeout(()=>o(!1),2e3)})},children:r?"Copied ✓":c})}function pm({vaultName:i,vaultUrl:c,embedded:r=!1}){const o=hm(c),f=mm(i,c),[h,b]=S.useState(!1),[j,v]=S.useState({kind:"idle"});async function p(){if(j.kind!=="submitting"){v({kind:"submitting"});try{const A=await cm({scope:`vault:${i}:read vault:${i}:write`});v({kind:"minted",token:A})}catch(A){const T=A instanceof X?`mint failed (${A.status}): ${A.message}`:A instanceof Error?A.message:String(A);v({kind:"error",message:T})}}}const g=j.kind==="minted"?Zg(i,c,j.token.token):null,y=s.jsxs(s.Fragment,{children:[s.jsx("h3",{children:"Connect an MCP client (or connector)"}),s.jsxs("p",{className:"muted",children:["Connect an MCP client — Claude Code, Claude.ai, etc. (sometimes called a connector in ChatGPT and other web UIs) — to the ",s.jsx("code",{children:i})," vault. The client signs in to this hub and reads/writes vault data over MCP."]}),s.jsxs("div",{className:"mcp-field",children:[s.jsx("span",{className:"mcp-field-label",children:"Endpoint"}),s.jsxs("div",{className:"token-box",children:[s.jsx("code",{"data-testid":"mcp-endpoint",children:o}),s.jsx(Fc,{value:o})]})]}),s.jsxs("div",{className:"mcp-field",children:[s.jsx("span",{className:"mcp-field-label",children:"Claude Code"}),s.jsxs("div",{className:"token-box",children:[s.jsx("code",{"data-testid":"mcp-add-command",children:f}),s.jsx(Fc,{value:f})]}),s.jsx("p",{className:"dim",children:"No token needed — the command triggers browser OAuth on first use (you sign in to this hub and approve access). For other clients, point them at the endpoint above."})]}),s.jsxs("details",{className:"mcp-token-path",open:h,onToggle:A=>b(A.currentTarget.open),children:[s.jsx("summary",{children:"Use a token instead (headless / CI clients)"}),s.jsxs("p",{className:"dim",children:["For clients that can't do browser OAuth, mint a scope-narrow hub token (",s.jsxs("code",{children:["vault:",i,":read vault:",i,":write"]})," ","— not admin) and pass it as a header. Revealed once; copy it now."]}),j.kind==="minted"&&g?s.jsxs("div",{className:"mint-banner","data-testid":"mcp-header-banner",children:[s.jsx("h3",{children:"Token minted"}),s.jsxs("p",{className:"muted",children:["This is the only time the hub shows this token. Copy the command below — it embeds the live token in the ",s.jsx("code",{children:"Authorization"})," header."]}),s.jsxs("div",{className:"token-box",children:[s.jsx("code",{"data-testid":"mcp-header-command",children:g}),s.jsx(Fc,{value:g})]}),s.jsxs("p",{className:"warn",children:["⚠ Manage and revoke this token at ",s.jsx("code",{children:"/admin/tokens"}),"."]})]}):s.jsx("div",{className:"actions",children:s.jsx("button",{type:"button",className:"secondary",onClick:()=>{p()},disabled:j.kind==="submitting",children:j.kind==="submitting"?"Minting…":"Mint a token"})}),j.kind==="error"?s.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:s.jsx("code",{children:j.message})}):null]}),s.jsx("p",{className:"dim mcp-docs-link",children:s.jsx("a",{href:Vg,target:"_blank",rel:"noreferrer",children:"Full connect docs →"})})]});return r?s.jsx("div",{className:"mcp-connect-card mcp-connect-card-embedded",children:y}):s.jsx("div",{className:"mcp-connect-card",children:y})}const Xg=/^[a-zA-Z0-9_-]+$/,Qg=new Set(["list"]);function Jg(){const i=ur(),[c,r]=S.useState(""),[o,f]=S.useState({kind:"idle"}),h=o.kind==="created"&&!!o.result.token;S.useEffect(()=>{if(!h)return;const v=p=>{p.preventDefault(),p.returnValue=""};return window.addEventListener("beforeunload",v),()=>window.removeEventListener("beforeunload",v)},[h]);const b=$g(c),j=async v=>{if(v.preventDefault(),!b){f({kind:"submitting"});try{const p=await B0({name:c});f({kind:"created",result:p})}catch(p){const g=p instanceof X?`${p.status}: ${p.message}`:p instanceof Error?p.message:String(p);f({kind:"error",message:g})}}};return o.kind==="created"?s.jsx(Kg,{result:o.result,onDone:()=>i("/vaults")}):s.jsxs("div",{children:[s.jsx("h2",{children:"Create a vault"}),s.jsxs("p",{className:"muted",children:["A vault stores tokens, secrets, and notes scoped to this hub. The hub provisions it via"," ",s.jsx("code",{children:"parachute-vault create"})," on the host filesystem and registers it in"," ",s.jsx("code",{children:"services.json"}),"."]}),o.kind==="error"&&s.jsxs("div",{className:"error-banner",children:["Couldn't create vault: ",s.jsx("code",{children:o.message})]}),s.jsxs("form",{onSubmit:j,className:"section",children:[s.jsxs("div",{className:"row",children:[s.jsx("label",{htmlFor:"vault-name",children:"Vault name"}),s.jsx("input",{id:"vault-name",type:"text",value:c,onChange:v=>r(v.target.value),placeholder:"e.g. work, home, scratch",disabled:o.kind==="submitting"}),b?s.jsx("div",{className:"field-error",children:b}):s.jsxs("div",{className:"field-hint",children:["Letters, numbers, hyphens, and underscores. Becomes the path under"," ",s.jsx("code",{children:"/vault/<name>"}),"."]})]}),s.jsxs("div",{className:"actions",children:[s.jsx("button",{type:"submit",disabled:c.length===0||!!b||o.kind==="submitting",children:o.kind==="submitting"?"Creating…":"Create vault"}),s.jsx(Ke,{to:"/vaults",className:"muted",children:"Cancel"})]})]})]})}function Kg({result:i,onDone:c}){const[r,o]=S.useState(!1),f=async()=>{if(i.token)try{await navigator.clipboard.writeText(i.token),o(!0),setTimeout(()=>o(!1),2e3)}catch{}};return s.jsxs("div",{children:[s.jsx("h2",{children:"Vault created"}),i.token?s.jsxs("div",{className:"mint-banner",children:[s.jsx("h3",{children:"Your access token (shown once)"}),s.jsxs("p",{className:"muted",children:["This is a hub-issued access token (a JWT scoped ",s.jsxs("code",{children:["vault:",i.name,":admin"]}),") — not a vault password. It's the only time the hub will show it. Copy it and store it somewhere safe — a password manager, the operator's notes. If you lose it, you don't need it for the OAuth connect path below; for a header-auth token, mint a fresh scope-narrow one with"," ",s.jsxs("code",{children:["parachute auth mint-token --scope vault:",i.name,":read"]}),` (or the connect card's "Use a token instead" option).`]}),s.jsxs("div",{className:"token-box",children:[s.jsx("code",{children:i.token}),s.jsx("button",{type:"button",onClick:f,className:"secondary",children:r?"Copied":"Copy"})]}),s.jsx("p",{className:"warn",children:"⚠ Don't navigate away until you've saved this token."}),s.jsx("div",{className:"actions",children:s.jsx("button",{type:"button",onClick:c,children:"Done — I've saved the token"})})]}):i.created?s.jsxs("div",{className:"section",children:[s.jsxs("p",{children:["Vault ",s.jsx("code",{children:i.name})," was created, but no access token was minted",i.tokenGuidance?s.jsxs(s.Fragment,{children:[" ","— ",s.jsx("span",{className:"muted",children:i.tokenGuidance})]}):s.jsxs("span",{className:"muted",children:[" ","(the hub couldn't mint one at create time — common on a loopback origin)"]}),". You don't need a token for the OAuth connect command below. For a header-auth token, mint a scope-narrow one with"," ",s.jsxs("code",{children:["parachute auth mint-token --scope vault:",i.name,":read"]}),"."]}),s.jsx("div",{className:"actions",children:s.jsx("button",{type:"button",onClick:c,children:"Done"})})]}):s.jsxs("div",{className:"section",children:[s.jsxs("p",{children:["Vault ",s.jsx("code",{children:i.name})," already existed; nothing new was created. Connect to it with the command below (OAuth, no token needed), or mint a scope-narrow header-auth token with ",s.jsxs("code",{children:["parachute auth mint-token --scope vault:",i.name,":read"]}),"."]}),s.jsx("div",{className:"actions",children:s.jsx(Ke,{to:"/vaults",children:s.jsx("button",{type:"button",children:"Continue"})})})]}),s.jsx("div",{className:"section",children:s.jsx(pm,{vaultName:i.name,vaultUrl:i.url})}),s.jsxs("div",{className:"kv section",children:[s.jsx("div",{children:"Name"}),s.jsx("div",{children:s.jsx("code",{children:i.name})}),s.jsx("div",{children:"URL"}),s.jsx("div",{children:s.jsx("code",{children:i.url})}),s.jsx("div",{children:"Version"}),s.jsx("div",{children:s.jsx("code",{children:i.version})}),i.paths&&s.jsxs(s.Fragment,{children:[s.jsx("div",{children:"Vault dir"}),s.jsx("div",{children:s.jsx("code",{children:i.paths.vault_dir})}),s.jsx("div",{children:"Database"}),s.jsx("div",{children:s.jsx("code",{children:i.paths.vault_db})}),s.jsx("div",{children:"Config"}),s.jsx("div",{children:s.jsx("code",{children:i.paths.vault_config})})]})]})]})}function $g(i){return i.length===0?null:Xg.test(i)?Qg.has(i)?`"${i}" is a reserved name.`:null:"Letters, numbers, hyphens, and underscores only."}function Fg(){const[i,c]=S.useState({kind:"loading"}),[r,o]=S.useState(""),[f,h]=S.useState(""),[b,j]=S.useState(0),[v,p]=S.useState({kind:"idle"});S.useEffect(()=>{let T=!1;return c({kind:"loading"}),q0(f?{vault:f}:{}).then(q=>{T||c({kind:"ok",grants:q})}).catch(q=>{if(T)return;const Z=q instanceof Error?q.message:String(q);c({kind:"error",message:Z})}),()=>{T=!0}},[b,f]);function g(T){T.preventDefault(),h(r.trim())}function y(){o(""),h("")}async function A(T){p({kind:"revoking",clientId:T.client_id});try{await Y0(T.client_id),p({kind:"idle"}),j(D=>D+1)}catch(D){const q=D instanceof X?`revoke failed (${D.status}): ${D.message}`:D instanceof Error?D.message:String(D);p({kind:"error",clientId:T.client_id,message:q})}}return s.jsxs("div",{"data-route-content":"true",children:[s.jsx("div",{className:"list-header",children:s.jsx("h1",{children:"Permissions"})}),s.jsxs("p",{className:"muted",children:["Apps you've granted OAuth scopes to. Revoking a grant forces the consent screen on the next authorize flow — it does ",s.jsx("em",{children:"not"})," invalidate tokens already issued."]}),s.jsxs("form",{onSubmit:g,style:{marginTop:"1rem",marginBottom:"1rem"},children:[s.jsx("label",{htmlFor:"vault-filter",className:"muted",style:{marginRight:"0.5rem"},children:"Filter by vault:"}),s.jsx("input",{id:"vault-filter",type:"text",value:r,onChange:T=>o(T.target.value),placeholder:"e.g. work",style:{marginRight:"0.5rem"}}),s.jsx("button",{type:"submit",children:"Apply"}),f?s.jsx("button",{type:"button",onClick:y,className:"secondary",style:{marginLeft:"0.5rem"},children:"Clear"}):null]}),Wg({state:i,revoke:v,setRevoke:p,onConfirm:A,onRetry:()=>j(T=>T+1)})]})}function Wg({state:i,revoke:c,setRevoke:r,onConfirm:o,onRetry:f}){return i.kind==="loading"?s.jsx("p",{className:"muted","data-loading":"true",children:"Loading…"}):i.kind==="error"?s.jsxs(s.Fragment,{children:[s.jsxs("div",{className:"error-banner",children:["Couldn't load grants: ",s.jsx("code",{children:i.message})]}),s.jsx("button",{type:"button",onClick:f,className:"secondary",children:"Retry"})]}):i.grants.length===0?s.jsxs("div",{className:"empty empty-rich",children:[s.jsx("p",{className:"empty-headline",children:"No grants."}),s.jsx("p",{className:"muted",children:"When an app asks for OAuth scopes and you approve them, the grant lands here. Revoking it forces the consent screen on the next authorize flow."})]}):s.jsx("div",{style:{marginTop:"1rem"},children:i.grants.map(h=>{const b=c.kind==="revoking"&&c.clientId===h.client_id,j=c.kind==="error"&&c.clientId===h.client_id?c:null,v=c.kind==="confirming"&&c.grant.client_id===h.client_id;return s.jsxs("div",{className:"vault-row",children:[s.jsxs("div",{className:"body",children:[s.jsx("div",{className:"name",children:s.jsx("code",{children:h.client_name??h.client_id})}),s.jsxs("div",{className:"dim",children:[s.jsx("span",{className:"muted",children:"granted "}),s.jsx("code",{title:h.granted_at,children:Ig(h.granted_at)})]}),s.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[s.jsx("span",{className:"muted",children:"scopes: "}),h.scopes.map((p,g)=>s.jsxs("span",{children:[s.jsx("code",{children:p}),g<h.scopes.length-1?" ":null]},p))]}),j?s.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:s.jsx("code",{children:j.message})}):null,v?s.jsxs("dialog",{open:!0,className:"error-banner",style:{marginTop:"0.5rem",background:"var(--bg-warn, #fffbe6)"},"aria-label":`Confirm revoke ${h.client_name??h.client_id}`,children:[s.jsxs("p",{children:["Revoke ",s.jsx("code",{children:h.client_name??h.client_id}),"? Next OAuth flow for this app will prompt you to consent again."]}),s.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[s.jsx("button",{type:"button",onClick:()=>{o(h)},disabled:b,children:b?"Revoking…":"Revoke"}),s.jsx("button",{type:"button",className:"secondary",onClick:()=>r({kind:"idle"}),disabled:b,children:"Cancel"})]})]}):null]}),v?null:s.jsx("button",{type:"button",className:"secondary",onClick:()=>r({kind:"confirming",grant:h}),"aria-label":`Revoke ${h.client_name??h.client_id}`,children:"Revoke"})]},h.client_id)})})}function Ig(i){const c=new Date(i);return Number.isNaN(c.getTime())?i:c.toLocaleString()}function Pg(){const[i,c]=S.useState({kind:"loading"}),[r,o]=S.useState(""),[f,h]=S.useState(!1),[b,j]=S.useState(null),v=S.useCallback(async()=>{try{const D=await om();c({kind:"ok",setting:D}),o(D.hub_origin??""),j(null)}catch(D){const q=D instanceof Error?D.message:String(D);c({kind:"error",message:q})}},[]);S.useEffect(()=>{v()},[v]);async function p(D){if(D.preventDefault(),f||i.kind!=="ok")return;const q=r.trim(),Z=q.length===0?null:q;h(!0),j(null);try{await Uh(Z),await v()}catch(V){const ee=V instanceof Error?V.message:String(V);j(ee)}finally{h(!1)}}async function g(){if(!f&&i.kind==="ok"){h(!0),j(null);try{await Uh(null),await v()}catch(D){const q=D instanceof Error?D.message:String(D);j(q)}finally{h(!1)}}}if(i.kind==="loading")return s.jsx("div",{className:"empty",children:"Loading settings…"});if(i.kind==="error")return s.jsxs("div",{className:"empty",children:["Failed to load settings: ",i.message,"."," ",s.jsx("button",{type:"button",onClick:()=>void v(),children:"Retry"})]});const{setting:y}=i,A=y.hub_origin!==null,T=r.trim()!==(y.hub_origin??"");return s.jsxs("section",{className:"settings",children:[s.jsx("h1",{children:"Hub settings"}),s.jsx("p",{className:"muted",children:"Hub-wide operator controls. Settings here apply to every module + every minted token."}),s.jsxs("section",{className:"settings-block","aria-labelledby":"canonical-hub-url-heading",children:[s.jsx("h2",{id:"canonical-hub-url-heading",children:"Canonical hub URL"}),s.jsxs("dl",{className:"meta","data-testid":"hub-origin-current",children:[s.jsx("dt",{children:"Current value"}),s.jsxs("dd",{children:[s.jsx("code",{children:y.resolved_issuer})," ",s.jsx(ey,{source:y.source,hasStored:A})]})]}),s.jsxs("form",{onSubmit:D=>void p(D),className:"settings-form","data-testid":"hub-origin-form",children:[s.jsxs("label",{htmlFor:"hub-origin-input",children:[s.jsx("span",{children:"Canonical URL"}),s.jsx("input",{id:"hub-origin-input",type:"url",inputMode:"url",placeholder:"https://hub.example.com",value:r,disabled:f,onChange:D=>o(D.target.value),autoComplete:"off",spellCheck:!1})]}),s.jsxs("p",{className:"muted",children:["Set this when you've attached a custom domain. Tokens are minted against this URL — changing it invalidates any tokens already in circulation (the ",s.jsx("code",{children:"iss"})," claim won't match the new issuer on verification). Leave blank to use the request origin (default for Render-assigned URLs)."]}),s.jsxs("div",{className:"actions",children:[s.jsx("button",{type:"submit",disabled:f||!T,children:f?"Saving…":"Save"}),s.jsx("button",{type:"button",className:"destructive",disabled:f||!A,onClick:()=>void g(),children:"Reset to default"})]}),b&&s.jsx("div",{className:"error","data-testid":"hub-origin-save-error",children:b})]})]})]})}function ey({source:i,hasStored:c}){return i==="settings"?s.jsx("span",{className:"badge badge-info","data-testid":"hub-origin-source",children:"from settings"}):i==="env"?s.jsxs("span",{className:"badge badge-info","data-testid":"hub-origin-source",children:["from env var ",s.jsx("code",{children:"PARACHUTE_HUB_ORIGIN"})]}):s.jsx("span",{className:"badge badge-info","data-testid":"hub-origin-source",children:"from request origin"})}const Wc={scope:"",audience:"",expiresIn:"",subject:"",permissions:""};function ty(){const[i,c]=im(),r=ly(i.get("status")),o=iy(i.get("source")),[f,h]=S.useState({kind:"loading"}),[b,j]=S.useState(0),[v,p]=S.useState({kind:"idle"}),[g,y]=S.useState(Wc),[A,T]=S.useState({kind:"idle"}),[D,q]=S.useState(!1),[Z,V]=S.useState(!1);function ee(C,O){c(G=>{const ve=new URLSearchParams(G);return O==="all"?ve.delete(C):ve.set(C,O),ve},{replace:!0})}S.useEffect(()=>{let C=!1;return h({kind:"loading"}),Oh(kh(r,o)).then(O=>{C||h({kind:"ok",tokens:O.tokens,nextCursor:O.next_cursor})}).catch(O=>{if(C)return;const G=O instanceof Error?O.message:String(O);h({kind:"error",message:G})}),()=>{C=!0}},[b,r,o]);async function I(){if(f.kind!=="ok"||!f.nextCursor||Z)return;const C={...kh(r,o),cursor:f.nextCursor};V(!0);try{const O=await Oh(C);h({kind:"ok",tokens:[...f.tokens,...O.tokens],nextCursor:O.next_cursor})}catch(O){const G=O instanceof Error?O.message:String(O);h({kind:"error",message:G})}finally{V(!1)}}async function ue(C){C.preventDefault();const O=g.scope.trim();if(O.length===0){p({kind:"error",message:"scope is required"});return}let G;if(g.permissions.trim().length>0)try{const te=JSON.parse(g.permissions);if(te===null||typeof te!="object"||Array.isArray(te)){p({kind:"error",message:'permissions must be a JSON object (e.g. {"vault":{"default":...}})'});return}G=te}catch(te){const he=te instanceof Error?te.message:String(te);p({kind:"error",message:`permissions is not valid JSON — ${he}`});return}let ve;if(g.expiresIn.trim().length>0){const te=Number(g.expiresIn);if(!Number.isInteger(te)||te<=0){p({kind:"error",message:"expires_in must be a positive integer (seconds)"});return}ve=te}p({kind:"submitting"});try{const te=await cm({scope:O,...g.audience.trim()?{audience:g.audience.trim()}:{},...ve!==void 0?{expires_in:ve}:{},...g.subject.trim()?{subject:g.subject.trim()}:{},...G?{permissions:G}:{}});p({kind:"minted",token:te}),y(Wc),q(!1),j(he=>he+1)}catch(te){const he=te instanceof X?`mint failed (${te.status}): ${te.message}`:te instanceof Error?te.message:String(te);p({kind:"error",message:he})}}async function K(C){T({kind:"revoking",jti:C});try{await X0(C),T({kind:"idle"}),j(O=>O+1)}catch(O){const G=O instanceof X?`revoke failed (${O.status}): ${O.message}`:O instanceof Error?O.message:String(O);T({kind:"error",jti:C,message:G})}}function Y(C){typeof navigator<"u"&&navigator.clipboard&&navigator.clipboard.writeText(C)}return s.jsxs("div",{children:[s.jsxs("div",{className:"list-header",children:[s.jsx("h1",{children:"Tokens"}),s.jsx("button",{type:"button",onClick:()=>q(C=>!C),children:D?"Hide form":"Mint new token"})]}),s.jsxs("p",{className:"muted",children:["The hub's token registry. Every CLI / OAuth / operator-mint writes a row here. Revoking flips ",s.jsx("code",{children:"revoked_at"}),"; resource servers on"," ",s.jsx("code",{children:"@openparachute/scope-guard@^0.2.0"})," reject within ~60s of the next poll."]}),v.kind==="minted"?s.jsxs("div",{className:"mint-banner",children:[s.jsx("h3",{children:"Minted"}),s.jsxs("p",{children:["Your new access token (jti: ",s.jsx("code",{children:v.token.jti}),"):"]}),s.jsx("div",{className:"token-box",children:s.jsx("code",{children:v.token.token})}),s.jsx("p",{className:"warn",children:"This is the only time the JWT is shown. Copy it now — there is no DB-side recovery."}),s.jsxs("div",{className:"actions",children:[s.jsx("button",{type:"button",onClick:()=>Y(v.token.token),children:"Copy"}),s.jsx("button",{type:"button",className:"secondary",onClick:()=>p({kind:"idle"}),children:"Dismiss"})]})]}):null,D?s.jsx("div",{className:"section",children:s.jsxs("form",{onSubmit:ue,children:[s.jsxs("div",{className:"row",children:[s.jsx("label",{htmlFor:"mint-scope",children:"Scope (space-separated)"}),s.jsx("input",{id:"mint-scope",type:"text",value:g.scope,onChange:C=>y({...g,scope:C.target.value}),placeholder:"e.g. scribe:transcribe vault:default:read",required:!0}),s.jsxs("div",{className:"field-hint",children:["Space-separated ",s.jsx("code",{children:"resource:verb"})," or ",s.jsx("code",{children:"resource:name:verb"})," ","tuples. The hub rejects non-requestable scopes (admin, host:*) per the privilege-diffusion guard."]})]}),s.jsxs("div",{className:"row",children:[s.jsx("label",{htmlFor:"mint-audience",children:"Audience (optional)"}),s.jsx("input",{id:"mint-audience",type:"text",value:g.audience,onChange:C=>y({...g,audience:C.target.value}),placeholder:"inferred from scope when blank"}),s.jsxs("div",{className:"field-hint",children:["Inferred from scope if omitted. ",s.jsx("code",{children:"vault:<name>:<verb>"})," →"," ",s.jsx("code",{children:"vault.<name>"}),"; otherwise the first colon-prefixed scope's namespace; fallback ",s.jsx("code",{children:"hub"}),"."]})]}),s.jsxs("div",{className:"row",children:[s.jsx("label",{htmlFor:"mint-expires-in",children:"Expires in (seconds, optional)"}),s.jsx("input",{id:"mint-expires-in",type:"text",inputMode:"numeric",value:g.expiresIn,onChange:C=>y({...g,expiresIn:C.target.value}),placeholder:"default 90d (7776000)"})]}),s.jsxs("div",{className:"row",children:[s.jsx("label",{htmlFor:"mint-subject",children:"Subject (optional)"}),s.jsx("input",{id:"mint-subject",type:"text",value:g.subject,onChange:C=>y({...g,subject:C.target.value}),placeholder:"defaults to operator's sub"})]}),s.jsxs("div",{className:"row",children:[s.jsx("label",{htmlFor:"mint-permissions",children:"Permissions (JSON object, optional)"}),s.jsx("textarea",{id:"mint-permissions",value:g.permissions,onChange:C=>y({...g,permissions:C.target.value}),placeholder:'e.g. {"vault":{"default":{"write_tags":["health"]}}}',rows:3,style:{width:"100%",fontFamily:"monospace",fontSize:"0.9rem"}})]}),v.kind==="error"?s.jsx("div",{className:"field-error",children:s.jsx("code",{children:v.message})}):null,s.jsxs("div",{className:"actions",children:[s.jsx("button",{type:"submit",disabled:v.kind==="submitting",children:v.kind==="submitting"?"Minting…":"Mint"}),s.jsx("button",{type:"button",className:"secondary",onClick:()=>{q(!1),y(Wc),p({kind:"idle"})},children:"Cancel"})]})]})}):null,s.jsxs("div",{style:{marginTop:"1rem",marginBottom:"0.5rem",display:"flex",flexWrap:"wrap",gap:"0.5rem",alignItems:"center"},children:[s.jsx("span",{className:"muted",style:{marginRight:"0.5rem",minWidth:"4rem"},children:"Status:"}),[{value:"all",label:"Show all"},{value:"live",label:"Live only"},{value:"revoked",label:"Revoked only"}].map(C=>s.jsx("button",{type:"button",onClick:()=>ee("status",C.value),className:r===C.value?void 0:"secondary","aria-pressed":r===C.value,children:C.label},C.value))]}),s.jsxs("div",{style:{marginBottom:"1rem",display:"flex",flexWrap:"wrap",gap:"0.5rem",alignItems:"center"},children:[s.jsx("span",{className:"muted",style:{marginRight:"0.5rem",minWidth:"4rem"},children:"Source:"}),[{value:"all",label:"All sources"},{value:"oauth_refresh",label:"OAuth"},{value:"operator_mint",label:"Operator"},{value:"cli_mint",label:"CLI mint"}].map(C=>s.jsx("button",{type:"button",onClick:()=>ee("source",C.value),className:o===C.value?void 0:"secondary","aria-pressed":o===C.value,children:C.label},C.value))]}),ay({list:f,revoke:A,setRevoke:T,onConfirm:K,onLoadMore:I,onRetry:()=>j(C=>C+1),loadingMore:Z,filtersActive:r!=="all"||o!=="all"})]})}function ay({list:i,revoke:c,setRevoke:r,onConfirm:o,onLoadMore:f,onRetry:h,loadingMore:b,filtersActive:j}){return i.kind==="loading"?s.jsx("p",{className:"muted","data-loading":"true",children:"Loading…"}):i.kind==="error"?s.jsxs(s.Fragment,{children:[s.jsxs("div",{className:"error-banner",children:["Couldn't load tokens: ",s.jsx("code",{children:i.message})]}),s.jsx("button",{type:"button",onClick:h,className:"secondary",children:"Retry"})]}):i.tokens.length===0?j?s.jsxs("div",{className:"empty empty-rich",children:[s.jsx("p",{className:"empty-headline",children:"No tokens match the current filter."}),s.jsx("p",{className:"muted",children:'Try widening the Status or Source pills above. The default "Show all / All sources" view shows every registry row.'})]}):s.jsxs("div",{className:"empty empty-rich",children:[s.jsx("p",{className:"empty-headline",children:"No tokens."}),s.jsxs("p",{className:"muted",children:["Every CLI mint, OAuth grant, and operator-token rotation lands here. Mint one with the form above, or via ",s.jsx("code",{children:"parachute auth mint-token"}),"."]})]}):s.jsxs("div",{"data-route-content":"true",children:[i.tokens.map(v=>{const p=ny(v),g=c.kind==="revoking"&&c.jti===v.jti,y=c.kind==="confirming"&&c.jti===v.jti,A=c.kind==="error"&&c.jti===v.jti?c:null,T=v.user_id??v.subject??"(unknown)";return s.jsxs("div",{className:"vault-row",children:[s.jsxs("div",{className:"body",children:[s.jsxs("div",{className:"name",children:[s.jsx("code",{title:v.jti,children:fu(v.jti)}),s.jsx("span",{className:`tag${p==="live"?"":" muted"}`,children:p}),s.jsx("span",{className:`tag source-${sy(v.created_via)}`,title:`created_via: ${v.created_via}`,children:uy(v.created_via)})]}),s.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[s.jsx("span",{className:"muted",children:"identity: "}),s.jsx("code",{children:T}),v.client_id?s.jsxs(s.Fragment,{children:[s.jsx("span",{className:"muted",children:" · client: "}),s.jsx("code",{children:v.client_id})]}):null]}),s.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[s.jsx("span",{className:"muted",children:"scope: "}),v.scopes.map((D,q)=>s.jsxs("span",{children:[s.jsx("code",{children:D}),q<v.scopes.length-1?" ":null]},D))]}),s.jsxs("div",{className:"dim",style:{marginTop:"0.25rem",fontSize:"0.82rem"},children:[s.jsx("span",{className:"muted",children:"created "}),s.jsx("code",{title:v.created_at,children:Ic(v.created_at)}),s.jsx("span",{className:"muted",children:" · expires "}),s.jsx("code",{title:v.expires_at,children:Ic(v.expires_at)}),v.revoked_at?s.jsxs(s.Fragment,{children:[s.jsx("span",{className:"muted",children:" · revoked "}),s.jsx("code",{title:v.revoked_at,children:Ic(v.revoked_at)})]}):null]}),v.permissions?s.jsxs("details",{style:{marginTop:"0.35rem"},children:[s.jsx("summary",{className:"muted",style:{cursor:"pointer",fontSize:"0.85rem"},children:"permissions"}),s.jsx("pre",{style:{fontSize:"0.82rem",marginTop:"0.25rem"},children:JSON.stringify(v.permissions,null,2)})]}):null,A?s.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:s.jsx("code",{children:A.message})}):null,y?s.jsxs("dialog",{open:!0,className:"error-banner",style:{marginTop:"0.5rem",background:"var(--bg-warn, #fffbe6)"},"aria-label":`Confirm revoke ${fu(v.jti)}`,children:[s.jsxs("p",{children:["Revoke ",s.jsx("code",{children:fu(v.jti)}),"? Resource servers reject within ~60s of the next revocation-list poll. This cannot be undone."]}),s.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[s.jsx("button",{type:"button",onClick:()=>{o(v.jti)},disabled:g,children:g?"Revoking…":"Revoke"}),s.jsx("button",{type:"button",className:"secondary",onClick:()=>r({kind:"idle"}),disabled:g,children:"Cancel"})]})]}):null]}),!y&&p==="live"?s.jsx("button",{type:"button",className:"secondary",onClick:()=>r({kind:"confirming",jti:v.jti}),"aria-label":`Revoke ${fu(v.jti)}`,children:"Revoke"}):null]},v.jti)}),i.nextCursor?s.jsx("div",{style:{marginTop:"1rem"},children:s.jsx("button",{type:"button",className:"secondary",disabled:b,onClick:()=>{f()},children:b?"Loading…":"Load more"})}):null]})}function ny(i){if(i.revoked_at)return"revoked";const c=new Date(i.expires_at).getTime();return!Number.isNaN(c)&&c<Date.now()?"expired":"live"}function ly(i){return i==="live"||i==="revoked"||i==="all"?i:"all"}function iy(i){return i==="oauth_refresh"||i==="operator_mint"||i==="cli_mint"||i==="all"?i:"all"}function kh(i,c){const r={};return i==="live"?r.revoked="false":i==="revoked"?r.revoked="true":r.revoked="all",c!=="all"&&(r.createdVia=c),r}function uy(i){switch(i){case"oauth_refresh":return"OAuth";case"operator_mint":return"Operator";case"cli_mint":return"CLI";default:return i}}function sy(i){switch(i){case"oauth_refresh":return"oauth";case"operator_mint":return"operator";case"cli_mint":return"cli";default:return"unknown"}}function fu(i){return i.length<=14?i:`${i.slice(0,8)}…${i.slice(-4)}`}function Ic(i){const c=new Date(i);return Number.isNaN(c.getTime())?i:c.toLocaleString()}const ka=12,cy=/^[a-z0-9_-]+$/,gu=2,yu=32,Hh={username:"",password:"",assignedVaults:[]};function vm({username:i,hubOrigin:c}){const r=`${c||""}/login`;return s.jsxs("div",{className:"muted",style:{marginTop:"0.5rem",fontSize:"0.9em"},children:["Send them to ",s.jsx("code",{children:r})," with username ",s.jsx("code",{children:i})," and this password — they'll be prompted to set a new one on first sign-in."]})}function ry(){const[i,c]=S.useState({kind:"loading"}),[r,o]=S.useState(0),[f,h]=S.useState(!1),[b,j]=S.useState(Hh),[v,p]=S.useState({kind:"idle"}),[g,y]=S.useState({kind:"idle"}),[A,T]=S.useState({kind:"idle"}),[D,q]=S.useState({kind:"idle"});S.useEffect(()=>{let K=!1;return c({kind:"loading"}),Promise.all([ng(),cg(),om()]).then(([Y,C,O])=>{if(K)return;const G=O.resolved_issuer.replace(/\/+$/,"");c({kind:"ok",data:{users:Y,vaults:C,hubOrigin:G}})}).catch(Y=>{if(K)return;const C=Y instanceof Error?Y.message:String(Y);c({kind:"error",message:C})}),()=>{K=!0}},[r]);function Z(K){return K.username.length<gu||K.username.length>yu?`Username must be ${gu}-${yu} characters.`:cy.test(K.username)?K.password.length<ka?`Password must be at least ${ka} characters.`:null:"Username may only contain lowercase letters, digits, hyphens, and underscores."}async function V(K){K.preventDefault();const Y=Z(b);if(Y){p({kind:"error",message:Y});return}p({kind:"submitting"});const C={username:b.username,password:b.password,assignedVaults:b.assignedVaults};try{const O=await lg(C);j(Hh),p({kind:"created",username:O.username}),o(G=>G+1)}catch(O){const G=O instanceof X?`Create failed (${O.status}): ${O.message}`:O instanceof Error?O.message:String(O);p({kind:"error",message:G})}}async function ee(K){y({kind:"deleting",userId:K.id});try{const{revocationLagSeconds:Y}=await ig(K.id);y({kind:"done",username:K.username,revocationLagSeconds:Y}),o(C=>C+1)}catch(Y){const C=Y instanceof X?`Delete failed (${Y.status}): ${Y.message}`:Y instanceof Error?Y.message:String(Y);y({kind:"error",userId:K.id,message:C})}}async function I(K,Y){q({kind:"submitting",userId:K.id,selected:Y});try{await sg(K.id,Y),q({kind:"done",userId:K.id,username:K.username}),o(C=>C+1)}catch(C){const O=C instanceof X?`Edit vaults failed (${C.status}): ${C.message}`:C instanceof Error?C.message:String(C);q({kind:"error",userId:K.id,selected:Y,message:O})}}async function ue(K,Y){if(Y.length<ka){T({kind:"error",userId:K.id,password:Y,message:`Password must be at least ${ka} characters.`});return}T({kind:"submitting",userId:K.id,password:Y});try{const{revocationLagSeconds:C}=await ug(K.id,Y);T({kind:"done",userId:K.id,username:K.username,revocationLagSeconds:C}),o(O=>O+1)}catch(C){const O=C instanceof X?`Reset failed (${C.status}): ${C.message}`:C instanceof Error?C.message:String(C);T({kind:"error",userId:K.id,password:Y,message:O})}}return s.jsxs("div",{"data-route-content":"true",children:[s.jsx("div",{className:"list-header",children:s.jsx("h1",{children:"Users"})}),s.jsxs("p",{className:"muted",children:["Hub user accounts. Each user can be a member of one or more vaults — the OAuth issuer narrows their tokens to ",s.jsx("code",{children:"vault:<assigned>:*"})," scopes for any vault in their list. Users with no assignments can't authorize any vault yet — assign at least one above. The first admin is unrestricted (admin posture). Admin-created users land with a default password and are prompted to change it on first sign-in",i.kind==="ok"?s.jsxs(s.Fragment,{children:[" ","at ",s.jsxs("code",{children:[i.data.hubOrigin,"/login"]})]}):null,"."]}),g.kind==="done"&&s.jsxs("output",{className:"success-banner",style:{display:"block",marginBottom:"0.75rem"},"data-testid":"delete-done-banner",children:["Deleted ",s.jsx("code",{children:g.username}),". Their tokens are revoked. Resource servers (vault, scribe, etc.) cache the revocation list for up to ",g.revocationLagSeconds," ","seconds — if you're deleting because of a suspected compromise, also restart the affected services (e.g. ",s.jsx("code",{children:"parachute restart vault"}),") to flush their cache immediately.",s.jsx("div",{style:{marginTop:"0.5rem"},children:s.jsx("button",{type:"button",className:"secondary",onClick:()=>y({kind:"idle"}),children:"Dismiss"})})]}),oy(i,g,y,ee,A,T,ue,D,q,I,i.kind==="ok"?i.data.vaults:[],i.kind==="ok"?i.data.hubOrigin:"",()=>o(K=>K+1)),i.kind==="ok"&&s.jsx(my,{show:f,setShow:h,form:b,setForm:j,vaults:i.data.vaults,hubOrigin:i.data.hubOrigin,createState:v,setCreateState:p,onSubmit:V})]})}function oy(i,c,r,o,f,h,b,j,v,p,g,y,A){var q;if(i.kind==="loading")return s.jsx("p",{className:"muted",children:"Loading users…"});if(i.kind==="error")return s.jsxs(s.Fragment,{children:[s.jsxs("div",{className:"error-banner",children:["Couldn't load users: ",s.jsx("code",{children:i.message})]}),s.jsx("button",{type:"button",onClick:A,className:"secondary",children:"Retry"})]});const{users:T}=i.data;if(T.length===0)return s.jsxs("div",{className:"empty empty-rich",children:[s.jsx("p",{className:"empty-headline",children:"No users yet."}),s.jsx("p",{className:"muted",children:"Click Create User below to invite someone."})]});const D=(q=T[0])==null?void 0:q.id;return s.jsx(dy,{users:T,firstAdminId:D,deleteSt:c,setDeleteSt:r,onConfirmDelete:o,resetSt:f,setResetSt:h,onSubmitReset:b,editVaultsSt:j,setEditVaultsSt:v,onSubmitEditVaults:p,availableVaults:g,hubOrigin:y})}function dy({users:i,firstAdminId:c,deleteSt:r,setDeleteSt:o,onConfirmDelete:f,resetSt:h,setResetSt:b,onSubmitReset:j,editVaultsSt:v,setEditVaultsSt:p,onSubmitEditVaults:g,availableVaults:y,hubOrigin:A}){return s.jsx("div",{className:"user-list",style:{marginTop:"1rem"},children:s.jsx("div",{className:"table-scroll",children:s.jsxs("table",{className:"user-table",children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{scope:"col",children:"Username"}),s.jsx("th",{scope:"col",children:"Assigned vaults"}),s.jsx("th",{scope:"col",children:"Password set"}),s.jsx("th",{scope:"col",children:"Created"}),s.jsx("th",{scope:"col",children:"Actions"})]})}),s.jsx("tbody",{children:i.map(T=>{const D=T.id===c,q=r.kind==="deleting"&&r.userId===T.id,Z=r.kind==="confirming"&&r.user.id===T.id,V=r.kind==="error"&&r.userId===T.id?r:null,ee=(h.kind==="open"||h.kind==="submitting"||h.kind==="error")&&h.userId===T.id?h:null,I=h.kind==="done"&&h.userId===T.id?h:null,ue=(v.kind==="open"||v.kind==="submitting"||v.kind==="error")&&v.userId===T.id?v:null,K=v.kind==="done"&&v.userId===T.id?v:null;return s.jsxs("tr",{"data-user-id":T.id,children:[s.jsxs("td",{children:[s.jsx("code",{children:T.username}),D&&s.jsx("span",{className:"badge",style:{marginLeft:"0.5rem"},children:"first admin"})]}),s.jsx("td",{children:T.assigned_vaults.length>0?s.jsx("span",{style:{display:"inline-flex",flexWrap:"wrap",gap:"0.25rem"},children:T.assigned_vaults.map(Y=>s.jsx("code",{children:Y},Y))}):s.jsx("span",{className:"muted",title:D?"First admin is unrestricted (admin posture)":"No vaults assigned — user can't authorize any vault yet",children:"—"})}),s.jsx("td",{children:T.password_changed?s.jsx("span",{"aria-label":"changed",children:"✓"}):s.jsx("span",{className:"status status-pending",title:"User hasn't completed first-sign-in change-password yet",children:"pending first login"})}),s.jsx("td",{children:s.jsx("span",{title:T.created_at,children:py(T.created_at)})}),s.jsxs("td",{children:[Z?null:s.jsxs("div",{style:{display:"flex",flexWrap:"wrap",gap:"0.5rem",alignItems:"center"},children:[D&&s.jsxs(s.Fragment,{children:[s.jsx("span",{id:`first-admin-tooltip-${T.id}`,className:"sr-only",children:"First admin can't be deleted (would self-lock the hub)"}),s.jsx("span",{id:`first-admin-reset-tooltip-${T.id}`,className:"sr-only",children:"First admin uses /account/change-password directly"}),s.jsx("span",{id:`first-admin-vaults-tooltip-${T.id}`,className:"sr-only",children:"First admin's vault membership is unrestricted by design"})]}),s.jsx("button",{type:"button",className:"secondary",disabled:D||ue!==null||v.kind==="submitting"&&v.userId===T.id,title:D?"First admin's vault membership is unrestricted by design":void 0,"aria-describedby":D?`first-admin-vaults-tooltip-${T.id}`:void 0,onClick:()=>p({kind:"open",userId:T.id,selected:[...T.assigned_vaults]}),"aria-label":`Edit vaults for ${T.username}`,children:"Edit vaults"}),s.jsx("button",{type:"button",className:"secondary",disabled:D||ee!==null||h.kind==="submitting"&&h.userId===T.id,title:D?"First admin uses /account/change-password directly":void 0,"aria-describedby":D?`first-admin-reset-tooltip-${T.id}`:void 0,onClick:()=>b({kind:"open",userId:T.id,password:""}),"aria-label":`Reset password for ${T.username}`,children:"Reset password"}),s.jsx("button",{type:"button",className:"secondary",disabled:D||q,title:D?"First admin can't be deleted (would self-lock the hub)":void 0,"aria-describedby":D?`first-admin-tooltip-${T.id}`:void 0,onClick:()=>o({kind:"confirming",user:T}),"aria-label":`Delete ${T.username}`,children:q?"Deleting…":"Delete"})]}),Z&&s.jsxs("dialog",{open:!0,className:"error-banner",style:{marginTop:"0.25rem",background:"var(--bg-warn, #fffbe6)"},"aria-label":`Confirm delete ${T.username}`,children:[s.jsxs("p",{children:["Delete ",s.jsx("code",{children:T.username}),"? This revokes their tokens, drops their sessions and grants, and removes the account. The audit trail is preserved — tokens stay with ",s.jsx("code",{children:"revoked_at"})," set, anonymised."]}),s.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[s.jsx("button",{type:"button",className:"destructive",onClick:()=>{f(T)},disabled:q,children:q?"Deleting…":"Delete"}),s.jsx("button",{type:"button",className:"secondary",onClick:()=>o({kind:"idle"}),disabled:q,children:"Cancel"})]})]}),V&&s.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:s.jsx("code",{children:V.message})}),ee&&s.jsx(fy,{user:T,state:ee,onCancel:()=>b({kind:"idle"}),onPasswordChange:Y=>b({kind:"open",userId:T.id,password:Y}),onSubmit:Y=>{j(T,Y)}}),I&&s.jsxs("output",{className:"success-banner",style:{marginTop:"0.25rem",display:"block"},children:["Password reset for ",s.jsx("code",{children:I.username}),". Hand them the new password and tell them they'll be prompted to change it on first sign-in.",s.jsx(vm,{username:I.username,hubOrigin:A}),s.jsxs("div",{className:"muted",style:{marginTop:"0.5rem",fontSize:"0.85em"},children:["Their existing tokens are revoked. Resource servers (vault, scribe, etc.) cache the revocation list for up to ",I.revocationLagSeconds," ","seconds — if you're resetting because of a suspected compromise, also restart the affected services (e.g. ",s.jsx("code",{children:"parachute restart vault"}),") to flush their cache immediately."]}),s.jsx("div",{style:{marginTop:"0.5rem"},children:s.jsx("button",{type:"button",className:"secondary",onClick:()=>b({kind:"idle"}),children:"Dismiss"})})]}),ue&&s.jsx(hy,{user:T,state:ue,availableVaults:y,onCancel:()=>p({kind:"idle"}),onSelectedChange:Y=>p({kind:"open",userId:T.id,selected:Y}),onSubmit:Y=>{g(T,Y)}}),K&&s.jsxs("output",{className:"success-banner",style:{marginTop:"0.25rem",display:"block"},children:["Vault assignments updated for ",s.jsx("code",{children:K.username}),".",s.jsx("div",{style:{marginTop:"0.5rem"},children:s.jsx("button",{type:"button",className:"secondary",onClick:()=>p({kind:"idle"}),children:"Dismiss"})})]})]})]},T.id)})})]})})})}function fy({user:i,state:c,onCancel:r,onPasswordChange:o,onSubmit:f}){const h=c.kind==="submitting",b=c.kind==="error"?c.message:null,j=`reset-password-input-${i.id}`;return s.jsxs("form",{onSubmit:v=>{v.preventDefault(),f(c.password)},"aria-label":`Reset password for ${i.username}`,style:{marginTop:"0.5rem",padding:"0.5rem",background:"var(--bg-soft, #f5f5f5)",borderRadius:"4px"},children:[s.jsxs("p",{style:{margin:0},children:[s.jsxs("label",{htmlFor:j,children:["New temporary password for ",s.jsx("code",{children:i.username})," ",s.jsxs("span",{className:"muted",children:["(min ",ka," chars)"]})]}),s.jsx("br",{}),s.jsx("input",{id:j,type:"password",required:!0,autoComplete:"new-password",minLength:ka,value:c.password,disabled:h,onChange:v=>o(v.target.value)})]}),b&&s.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:s.jsx("code",{children:b})}),s.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[s.jsx("button",{type:"submit",disabled:h,children:h?"Setting…":"Set new password"}),s.jsx("button",{type:"button",className:"secondary",onClick:r,disabled:h,children:"Cancel"})]})]})}function hy({user:i,state:c,availableVaults:r,onCancel:o,onSelectedChange:f,onSubmit:h}){const b=c.kind==="submitting",j=c.kind==="error"?c.message:null,v=`edit-vaults-select-${i.id}`;function p(g){const y=Array.from(g.target.selectedOptions).map(A=>A.value);f(y)}return s.jsxs("form",{onSubmit:g=>{g.preventDefault(),h(c.selected)},"aria-label":`Edit vaults for ${i.username}`,style:{marginTop:"0.5rem",padding:"0.5rem",background:"var(--bg-soft, #f5f5f5)",borderRadius:"4px"},children:[s.jsx("p",{style:{margin:0},children:s.jsxs("label",{htmlFor:v,children:["Vault assignments for ",s.jsx("code",{children:i.username})," ",s.jsx("span",{className:"muted",children:"(empty = no narrowing; shift-click to multi-select)"})]})}),c.selected.length>0&&s.jsx("div",{"data-testid":`edit-vaults-chips-${i.id}`,style:{marginTop:"0.4rem",display:"flex",flexWrap:"wrap",gap:"0.25rem"},children:c.selected.map(g=>s.jsx("code",{style:{padding:"0.1rem 0.4rem",borderRadius:"4px"},children:g},g))}),s.jsx("select",{id:v,multiple:!0,value:c.selected,onChange:p,disabled:b,size:Math.min(Math.max(r.length,3),8),style:{marginTop:"0.4rem",minWidth:"12rem"},children:r.map(g=>s.jsx("option",{value:g,children:g},g))}),r.length===0&&s.jsx("p",{className:"muted",style:{marginTop:"0.25rem"},children:"No vaults registered on this hub yet."}),j&&s.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:s.jsx("code",{children:j})}),s.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[s.jsx("button",{type:"submit",disabled:b,children:b?"Saving…":"Save vault assignments"}),s.jsx("button",{type:"button",className:"secondary",onClick:o,disabled:b,children:"Cancel"})]})]})}function my({show:i,setShow:c,form:r,setForm:o,vaults:f,hubOrigin:h,createState:b,setCreateState:j,onSubmit:v}){const p=b.kind==="submitting";return s.jsx("section",{style:{marginTop:"1.5rem"},children:i?s.jsxs("form",{onSubmit:g=>void v(g),"aria-label":"Create user",children:[s.jsx("h3",{children:"Create user"}),s.jsxs("p",{children:[s.jsxs("label",{htmlFor:"new-user-username",children:["Username"," ",s.jsxs("span",{className:"muted",children:["(",gu,"-",yu," chars, lowercase letters/digits/hyphens/ underscores)"]})]}),s.jsx("br",{}),s.jsx("input",{id:"new-user-username",type:"text",required:!0,autoComplete:"off",value:r.username,minLength:gu,maxLength:yu,pattern:"[a-z0-9_\\-]+",onChange:g=>o({...r,username:g.target.value})})]}),s.jsxs("p",{children:[s.jsxs("label",{htmlFor:"new-user-password",children:["Password ",s.jsxs("span",{className:"muted",children:["(min ",ka," chars)"]})]}),s.jsx("br",{}),s.jsx("input",{id:"new-user-password",type:"password",required:!0,autoComplete:"new-password",value:r.password,minLength:ka,onChange:g=>o({...r,password:g.target.value})})]}),s.jsxs("p",{children:[s.jsxs("label",{htmlFor:"new-user-vaults",children:["Assigned vaults"," ",s.jsx("span",{className:"muted",children:"(empty = no restriction; shift-click to select multiple)"})]}),s.jsx("br",{}),r.assignedVaults.length>0&&s.jsx("span",{"data-testid":"new-user-vault-chips",style:{display:"inline-flex",flexWrap:"wrap",gap:"0.25rem",marginBottom:"0.25rem"},children:r.assignedVaults.map(g=>s.jsx("code",{children:g},g))}),s.jsx("br",{}),s.jsx("select",{id:"new-user-vaults",multiple:!0,value:r.assignedVaults,onChange:g=>o({...r,assignedVaults:Array.from(g.target.selectedOptions).map(y=>y.value)}),size:Math.min(Math.max(f.length,3),8),style:{minWidth:"12rem"},children:f.map(g=>s.jsx("option",{value:g,children:g},g))}),f.length===0&&s.jsx("span",{className:"muted",style:{marginLeft:"0.5rem"},children:"No vaults registered on this hub yet."})]}),b.kind==="error"&&s.jsx("div",{className:"error-banner",children:s.jsx("code",{children:b.message})}),b.kind==="created"&&s.jsxs("output",{className:"success-banner",children:["User ",s.jsx("code",{children:b.username})," created. They'll be prompted to change their password on first sign-in.",s.jsx(vm,{username:b.username,hubOrigin:h})]}),s.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[s.jsx("button",{type:"submit",disabled:p,children:p?"Creating…":"Create user"}),s.jsx("button",{type:"button",className:"secondary",disabled:p,onClick:()=>{c(!1),j({kind:"idle"})},children:"Close"})]})]}):s.jsx("button",{type:"button",onClick:()=>c(!0),children:"Create User"})})}function py(i){const c=new Date(i);return Number.isNaN(c.getTime())?i:c.toLocaleString()}function Bh(){const[i,c]=S.useState({kind:"loading"}),[r,o]=S.useState(0),[f,h]=S.useState({kind:"idle"}),[b,j]=S.useState(null);async function v(g){if(g.managementUrl){h({kind:"minting",name:g.name});try{const y=await L0(g.name),A=Z0(g.url,g.managementUrl),T=A.includes("#")?"&":"#";window.location.assign(`${A}${T}token=${y.token}`)}catch(y){const A=y instanceof X?`mint failed (${y.status}): ${y.message}`:y instanceof Error?y.message:String(y);h({kind:"error",name:g.name,message:A})}}}if(S.useEffect(()=>{let g=!1;return k0().then(y=>{g||c({kind:"ok",vaults:y.vaults,moduleInstalled:y.moduleInstalled})}).catch(y=>{if(g)return;const A=y instanceof Error?y.message:String(y);c({kind:"error",message:A})}),()=>{g=!0}},[r]),i.kind==="loading")return s.jsxs("div",{children:[s.jsx("div",{className:"list-header",children:s.jsx("h1",{children:"Vaults"})}),s.jsx("p",{className:"muted",children:"Loading…"})]});if(i.kind==="error")return s.jsxs("div",{children:[s.jsxs("div",{className:"list-header",children:[s.jsx("h1",{children:"Vaults"}),s.jsx(Ke,{to:"/vaults/new",children:s.jsx("button",{type:"button",children:"New vault"})})]}),s.jsxs("div",{className:"error-banner",children:["Couldn't load vaults: ",s.jsx("code",{children:i.message})]}),s.jsx("button",{type:"button",onClick:()=>o(g=>g+1),className:"secondary",children:"Retry"})]});const p=i.vaults.length===0&&!i.moduleInstalled;return s.jsxs("div",{"data-route-content":"true",children:[s.jsxs("div",{className:"list-header",children:[s.jsxs("h1",{children:["Vaults (",i.vaults.length,")"]}),p?s.jsx(Ke,{to:"/modules",children:s.jsx("button",{type:"button",children:"Install vault module"})}):s.jsx(Ke,{to:"/vaults/new",children:s.jsx("button",{type:"button",children:"New vault"})})]}),s.jsxs("p",{className:"muted",children:["Vaults registered with this hub at ",s.jsx("code",{children:"/.well-known/parachute.json"}),"."]}),i.vaults.length===0?p?s.jsxs("div",{className:"empty empty-rich",children:[s.jsx("p",{className:"empty-headline",children:"No vault module installed."}),s.jsx("p",{className:"muted",children:"The vault backend isn't installed on this hub yet, so there's nothing to provision a vault against. Install it from the Modules page first, then come back here to create your first vault."}),s.jsx("p",{style:{marginTop:"0.75rem"},children:s.jsx(Ke,{to:"/modules",children:"Install vault module →"})})]}):s.jsxs("div",{className:"empty empty-rich",children:[s.jsx("p",{className:"empty-headline",children:"No vaults yet."}),s.jsx("p",{className:"muted",children:"Create your first vault to start storing tokens, secrets, and notes scoped to this hub."}),s.jsx("p",{style:{marginTop:"0.75rem"},children:s.jsx(Ke,{to:"/vaults/new",children:"Create a vault →"})})]}):s.jsx("div",{style:{marginTop:"1rem"},children:i.vaults.map(g=>{const y=f.kind==="minting"&&f.name===g.name,A=f.kind==="error"&&f.name===g.name?f:null,T=b===g.name;return s.jsxs("div",{className:"vault-row-group",children:[s.jsxs("div",{className:"vault-row",children:[s.jsxs("div",{className:"body",children:[s.jsxs("div",{className:"name",children:[s.jsx("code",{children:g.name}),s.jsxs("span",{className:"tag muted",title:"Vault version",children:["v",g.version]})]}),s.jsx("div",{className:"dim url",children:s.jsx("code",{children:g.url})}),A?s.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:s.jsx("code",{children:A.message})}):null]}),s.jsxs("div",{className:"vault-row-actions",children:[s.jsx("button",{type:"button",className:T?void 0:"secondary","aria-expanded":T,"aria-label":`Connect an MCP client to vault ${g.name}`,onClick:()=>j(D=>D===g.name?null:g.name),children:T?"Hide connect":"Connect"}),g.managementUrl?s.jsx("button",{type:"button",onClick:()=>{v(g)},disabled:y,"aria-label":`Manage vault ${g.name}`,children:y?"Opening…":"Manage"}):s.jsx("span",{className:"muted",title:"This vault has no admin SPA — manage with parachute-vault on the host.",children:"CLI only"})]})]}),T?s.jsx(pm,{vaultName:g.name,vaultUrl:g.url}):null]},g.name)})})]})}function gm(i){return i==="/permissions"||i.startsWith("/permissions/")?"permissions":i==="/tokens"||i.startsWith("/tokens/")?"tokens":i==="/modules"||i.startsWith("/modules/")?/^\/modules\/[a-z][a-z0-9-]*\/config$/.test(i)?"module config":"modules":i==="/users"||i.startsWith("/users/")?"users":i==="/settings"||i.startsWith("/settings/")?"settings":i.startsWith("/approve-client/")?"approve app":"vaults"}function vy(i){return`${gm(i).split(" ").map(r=>r.length>0?r[0].toUpperCase()+r.slice(1):r).join(" ")} · ${um}`}function gy(){const{pathname:i}=Dt(),c=gm(i),[r,o]=S.useState(null);S.useEffect(()=>{document.title=vy(i)},[i]);const[f,h]=S.useState(!1),[b,j]=S.useState([]);S.useEffect(()=>{let p=!1;return G0().then(g=>{p||o(g)}).catch(()=>{p||o({hasSession:!1})}),()=>{p=!0}},[]),S.useEffect(()=>{if(!r||!r.hasSession)return;let p=!1;return rm().then(g=>{p||j(g.modules.filter(y=>y.installed))}).catch(()=>{p||j([])}),()=>{p=!0}},[r]);async function v(p){h(!0);try{await V0(p),window.location.href="/"}catch{h(!1)}}return s.jsxs("div",{className:"page",children:[s.jsxs("nav",{className:"nav",children:[s.jsxs(Ke,{to:"/vaults",className:"brand",children:[s.jsx(A0,{size:18,idSuffix:"spa-nav",className:"brand-mark-icon"}),s.jsx("span",{className:"brand-wordmark",children:um}),s.jsx("span",{className:"sub",children:c})]}),s.jsx(by,{me:r,signingOut:f,onSignOut:v}),s.jsx(Gn,{to:"/vaults",label:"Vaults",alsoActiveAt:"/"}),s.jsx(Gn,{to:"/modules",label:"Modules"}),s.jsx(yy,{services:b}),s.jsx(Gn,{to:"/users",label:"Users"}),s.jsx(Gn,{to:"/permissions",label:"Permissions"}),s.jsx(Gn,{to:"/tokens",label:"Tokens"}),s.jsx(Gn,{to:"/settings",label:"Settings"}),s.jsx("span",{className:"nav-divider","aria-hidden":"true"}),s.jsx("a",{href:"/",title:"Hub discovery page (top-level)",children:"Discovery"})]}),s.jsxs(Kv,{children:[s.jsx(Ot,{path:"/",element:s.jsx(Bh,{})}),s.jsx(Ot,{path:"/vaults",element:s.jsx(Bh,{})}),s.jsx(Ot,{path:"/vaults/new",element:s.jsx(Jg,{})}),s.jsx(Ot,{path:"/modules",element:s.jsx(kg,{})}),s.jsx(Ot,{path:"/modules/:short/config",element:s.jsx(jg,{})}),s.jsx(Ot,{path:"/users",element:s.jsx(ry,{})}),s.jsx(Ot,{path:"/permissions",element:s.jsx(Fg,{})}),s.jsx(Ot,{path:"/tokens",element:s.jsx(ty,{})}),s.jsx(Ot,{path:"/settings",element:s.jsx(Pg,{})}),s.jsx(Ot,{path:"/approve-client/:clientId",element:s.jsx(mg,{})}),s.jsx(Ot,{path:"*",element:s.jsxs("div",{className:"empty",children:["404 — back to ",s.jsx(Ke,{to:"/vaults",children:"vaults"}),"."]})})]}),r!=null&&r.hasSession?s.jsx(hg,{}):null]})}function Gn({to:i,label:c,alsoActiveAt:r}){const{pathname:o}=Dt(),b=o===i||o.startsWith(`${i}/`)||r!==void 0&&o===r;return s.jsx(Ke,{to:i,className:b?"nav-link nav-link-active":"nav-link","aria-current":b?"page":void 0,children:c})}function yy({services:i}){return i.length===0?null:s.jsxs("details",{className:"nav-dropdown","data-testid":"installed-services-dropdown",children:[s.jsx("summary",{className:"nav-dropdown-summary",children:"Services"}),s.jsx("div",{className:"nav-dropdown-panel",role:"menu",children:i.map(c=>{const r=c.management_url;return r?s.jsx("a",{className:"nav-dropdown-item",href:r,role:"menuitem","data-testid":`nav-service-${c.short}`,children:c.display_name},c.short):s.jsx("span",{className:"nav-dropdown-item nav-dropdown-item-disabled",role:"menuitem","aria-disabled":"true",title:"This module hasn't shipped an admin UI yet.","data-testid":`nav-service-${c.short}`,children:c.display_name},c.short)})})]})}function by({me:i,signingOut:c,onSignOut:r}){return i===null?null:i.hasSession?s.jsxs("span",{className:"auth-spa",children:[s.jsxs("span",{className:"muted",children:["Signed in as ",s.jsx("strong",{children:i.user.displayName})]})," ","·"," ",s.jsx("button",{type:"button",className:"auth-spa-signout",disabled:c,onClick:()=>{r(i.csrf)},children:c?"Signing out…":"Sign out"})]}):s.jsx("a",{href:`/login?next=${encodeURIComponent(window.location.pathname)}`,className:"auth-spa",children:"Sign in"})}const ym=document.getElementById("root");if(!ym)throw new Error("#root not found");function xy(){const i=window.location.pathname;return i==="/admin"||i.startsWith("/admin/")?"/admin":""}W1.createRoot(ym).render(s.jsx(S.StrictMode,{children:s.jsx(b0,{basename:xy(),children:s.jsx(gy,{})})}));
|
package/web/ui/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Parachute Hub</title>
|
|
7
7
|
<meta name="description" content="Manage vaults registered with this Parachute hub." />
|
|
8
|
-
<script type="module" crossorigin src="/admin/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/admin/assets/index-D_0TRjeo.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/admin/assets/index-mz8XcVPP.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|