@openparachute/hub 0.7.4-rc.18 → 0.7.4-rc.19

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/hub",
3
- "version": "0.7.4-rc.18",
3
+ "version": "0.7.4-rc.19",
4
4
  "description": "parachute — the local hub for the Parachute ecosystem (discovery, ports, lifecycle, soon OAuth).",
5
5
  "license": "AGPL-3.0",
6
6
  "publishConfig": {
@@ -6,8 +6,10 @@ import {
6
6
  API_MODULES_CHANNEL_REQUIRED_SCOPE,
7
7
  API_MODULES_REQUIRED_SCOPE,
8
8
  _clearLatestVersionCacheForTests,
9
+ defaultReadInstalledVersion,
9
10
  handleApiModules,
10
11
  handleApiModulesChannel,
12
+ isUpgradeAvailable,
11
13
  } from "../api-modules.ts";
12
14
  import { hubDbPath, openHubDb } from "../hub-db.ts";
13
15
  import { getSetting, setModuleInstallChannel } from "../hub-settings.ts";
@@ -491,6 +493,147 @@ describe("GET /api/modules", () => {
491
493
  expect(scribe?.installed_version).toBeNull();
492
494
  });
493
495
 
496
+ // ── hub#243: upgrade-offer must be semver-aware + installed-version must be live ──
497
+
498
+ type UpgradeWire = {
499
+ short: string;
500
+ installed_version: string | null;
501
+ latest_version: string | null;
502
+ upgrade_available: boolean;
503
+ };
504
+
505
+ async function modulesWith(opts: {
506
+ installedVersion: string;
507
+ latest: string | null;
508
+ readInstalledVersion?: (installDir: string) => string | null;
509
+ }): Promise<UpgradeWire[]> {
510
+ writeManifest(h.manifestPath, [
511
+ {
512
+ name: "parachute-vault",
513
+ port: 1940,
514
+ paths: ["/vault/default"],
515
+ health: "/vault/default/health",
516
+ version: opts.installedVersion,
517
+ installDir: "/install/dir/vault",
518
+ },
519
+ ]);
520
+ const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
521
+ const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
522
+ db: h.db,
523
+ issuer: ISSUER,
524
+ manifestPath: h.manifestPath,
525
+ fetchLatestVersion: async () => opts.latest,
526
+ // Default: no live read (synthetic install dir has no package.json), so
527
+ // the services.json cache is used — matching the prior behavior.
528
+ readInstalledVersion: opts.readInstalledVersion ?? (() => null),
529
+ });
530
+ const body = (await res.json()) as { modules: UpgradeWire[] };
531
+ return body.modules;
532
+ }
533
+
534
+ test("does NOT offer an upgrade when the channel target is OLDER than installed (the live downgrade bug)", async () => {
535
+ // The exact live shape: rc operator installed 0.6.4-rc.15; channel resolved
536
+ // latest_version to the OLDER @latest 0.6.3. Strings differ, but it's a
537
+ // downgrade — upgrade_available MUST be false.
538
+ const mods = await modulesWith({ installedVersion: "0.6.4-rc.15", latest: "0.6.3" });
539
+ const vault = mods.find((m) => m.short === "vault");
540
+ expect(vault?.installed_version).toBe("0.6.4-rc.15");
541
+ expect(vault?.latest_version).toBe("0.6.3");
542
+ expect(vault?.upgrade_available).toBe(false);
543
+ });
544
+
545
+ test("offers an upgrade for a real rc → newer-rc move", async () => {
546
+ const mods = await modulesWith({ installedVersion: "0.6.4-rc.15", latest: "0.6.4-rc.16" });
547
+ const vault = mods.find((m) => m.short === "vault");
548
+ expect(vault?.upgrade_available).toBe(true);
549
+ });
550
+
551
+ test("offers an upgrade for rc → its own stable (stable > its rc per semver)", async () => {
552
+ const mods = await modulesWith({ installedVersion: "0.6.4-rc.15", latest: "0.6.4" });
553
+ const vault = mods.find((m) => m.short === "vault");
554
+ expect(vault?.upgrade_available).toBe(true);
555
+ });
556
+
557
+ test("offers an upgrade for a plain stable → newer stable", async () => {
558
+ const mods = await modulesWith({ installedVersion: "0.4.5", latest: "0.5.0" });
559
+ const vault = mods.find((m) => m.short === "vault");
560
+ expect(vault?.upgrade_available).toBe(true);
561
+ });
562
+
563
+ test("no upgrade when installed === latest", async () => {
564
+ const mods = await modulesWith({ installedVersion: "0.5.0", latest: "0.5.0" });
565
+ const vault = mods.find((m) => m.short === "vault");
566
+ expect(vault?.upgrade_available).toBe(false);
567
+ });
568
+
569
+ test("no upgrade when the npm probe failed (latest_version null)", async () => {
570
+ const mods = await modulesWith({ installedVersion: "0.5.0", latest: null });
571
+ const vault = mods.find((m) => m.short === "vault");
572
+ expect(vault?.latest_version).toBeNull();
573
+ expect(vault?.upgrade_available).toBe(false);
574
+ });
575
+
576
+ test("installed_version reflects the LIVE on-disk version, not a stale services.json cache (hub#243)", async () => {
577
+ // services.json cache lags the bun-linked checkout: cache says 0.5.4-rc.15
578
+ // (the live symptom) while package.json on disk is already 0.6.4-rc.15.
579
+ // The admin view must show what's actually installed.
580
+ const mods = await modulesWith({
581
+ installedVersion: "0.5.4-rc.15",
582
+ latest: "0.6.3",
583
+ readInstalledVersion: (dir) => (dir === "/install/dir/vault" ? "0.6.4-rc.15" : null),
584
+ });
585
+ const vault = mods.find((m) => m.short === "vault");
586
+ expect(vault?.installed_version).toBe("0.6.4-rc.15");
587
+ // And with the corrected current, @latest 0.6.3 is still a downgrade → no offer.
588
+ expect(vault?.upgrade_available).toBe(false);
589
+ });
590
+
591
+ test("falls back to the services.json version when the live read returns null", async () => {
592
+ const mods = await modulesWith({
593
+ installedVersion: "0.6.4-rc.15",
594
+ latest: "0.6.4-rc.16",
595
+ readInstalledVersion: () => null,
596
+ });
597
+ const vault = mods.find((m) => m.short === "vault");
598
+ expect(vault?.installed_version).toBe("0.6.4-rc.15");
599
+ expect(vault?.upgrade_available).toBe(true);
600
+ });
601
+
602
+ test("isUpgradeAvailable: semver-aware, fail-closed on unparseable + nulls", () => {
603
+ // strictly-newer → true
604
+ expect(isUpgradeAvailable("0.4.5", "0.5.0")).toBe(true);
605
+ expect(isUpgradeAvailable("0.6.4-rc.15", "0.6.4-rc.16")).toBe(true);
606
+ expect(isUpgradeAvailable("0.6.4-rc.15", "0.6.4")).toBe(true); // stable > its rc
607
+ // same / older → false
608
+ expect(isUpgradeAvailable("0.5.0", "0.5.0")).toBe(false);
609
+ expect(isUpgradeAvailable("0.6.4-rc.15", "0.6.3")).toBe(false); // the live downgrade
610
+ expect(isUpgradeAvailable("0.6.4", "0.6.4-rc.15")).toBe(false); // stable → its rc
611
+ // nulls → false (not installed / probe failed)
612
+ expect(isUpgradeAvailable(null, "0.5.0")).toBe(false);
613
+ expect(isUpgradeAvailable("0.5.0", null)).toBe(false);
614
+ // unparseable → false (fail-closed: never offer a move we can't verify)
615
+ expect(isUpgradeAvailable("not-a-version", "0.5.0")).toBe(false);
616
+ expect(isUpgradeAvailable("0.5.0", "garbage")).toBe(false);
617
+ });
618
+
619
+ test("defaultReadInstalledVersion reads package.json version + tolerates missing/bad files", () => {
620
+ const tmp = mkdtempSync(join(tmpdir(), "phub-live-ver-"));
621
+ try {
622
+ writeFileSync(join(tmp, "package.json"), JSON.stringify({ version: "0.6.4-rc.15" }));
623
+ expect(defaultReadInstalledVersion(tmp)).toBe("0.6.4-rc.15");
624
+ // Missing dir / no package.json → null.
625
+ expect(defaultReadInstalledVersion(join(tmp, "does-not-exist"))).toBeNull();
626
+ // Malformed JSON → null (no throw).
627
+ writeFileSync(join(tmp, "package.json"), "{ not json");
628
+ expect(defaultReadInstalledVersion(tmp)).toBeNull();
629
+ // No version field → null.
630
+ writeFileSync(join(tmp, "package.json"), JSON.stringify({ name: "x" }));
631
+ expect(defaultReadInstalledVersion(tmp)).toBeNull();
632
+ } finally {
633
+ rmSync(tmp, { recursive: true, force: true });
634
+ }
635
+ });
636
+
494
637
  test("includes supervisor status + pid when a supervisor is injected", async () => {
495
638
  writeManifest(h.manifestPath, [
496
639
  {
@@ -42,6 +42,9 @@
42
42
  */
43
43
 
44
44
  import type { Database } from "bun:sqlite";
45
+ import { readFileSync } from "node:fs";
46
+ import { join } from "node:path";
47
+ import { compareVersions } from "./commands/upgrade.ts";
45
48
  import { validateHostAdminToken } from "./host-admin-token-validation.ts";
46
49
  import {
47
50
  type ModuleInstallChannel,
@@ -178,6 +181,40 @@ export interface ApiModulesDeps {
178
181
  * (hub#342 — drives the admin SPA Modules page's "Open" button).
179
182
  */
180
183
  readModuleManifest?: (installDir: string) => Promise<ModuleManifest | null>;
184
+ /**
185
+ * Read the LIVE installed version of a module from its install dir's
186
+ * `package.json` `version` (hub#243). The services.json `version` field is a
187
+ * CACHE the module writes on its own boot — on the bun-linked dev path it
188
+ * goes stale the moment the checkout is rebuilt to a newer version without a
189
+ * restart that re-stamps services.json. The admin Modules view reads
190
+ * `installed_version` straight from that cache, so it can show a stale version
191
+ * (the live symptom: services.json said 0.5.4-rc.15 while the linked checkout
192
+ * was already 0.6.4-rc.15). When this resolves a version, it WINS over the
193
+ * services.json cache for the `installed_version` field so the operator sees
194
+ * what's actually on disk. Returns null on any failure (no install dir,
195
+ * missing/unreadable package.json, no version) → fall back to the
196
+ * services.json cache, which is the only source for a not-yet-booted module.
197
+ *
198
+ * Production reads `<installDir>/package.json`; tests inject a fake.
199
+ */
200
+ readInstalledVersion?: (installDir: string) => string | null;
201
+ }
202
+
203
+ /**
204
+ * Default `readInstalledVersion`. Reads `<installDir>/package.json`'s `version`
205
+ * — the authoritative live version of a module's code on disk, which the
206
+ * bun-linked dev path keeps current while the services.json cache can lag
207
+ * (hub#243). Returns null on any failure so the caller falls back to the
208
+ * services.json `version`.
209
+ */
210
+ export function defaultReadInstalledVersion(installDir: string): string | null {
211
+ try {
212
+ const raw = readFileSync(join(installDir, "package.json"), "utf8");
213
+ const parsed = JSON.parse(raw) as { version?: unknown };
214
+ return typeof parsed.version === "string" && parsed.version.length > 0 ? parsed.version : null;
215
+ } catch {
216
+ return null;
217
+ }
181
218
  }
182
219
 
183
220
  /**
@@ -239,6 +276,24 @@ interface ModuleWireShape {
239
276
  installed: boolean;
240
277
  installed_version: string | null;
241
278
  latest_version: string | null;
279
+ /**
280
+ * Whether the channel-resolved `latest_version` is a REAL upgrade — i.e.
281
+ * STRICTLY NEWER than `installed_version` under semver ordering (hub#243).
282
+ * Computed server-side via `compareVersions` so the rc/prerelease ordering is
283
+ * correct and there's ONE tested source of truth (the SPA used to derive this
284
+ * client-side with a string `!==`, which framed a *downgrade* as an upgrade:
285
+ * an rc operator on `0.6.4-rc.15` was offered an "upgrade" to the older
286
+ * `@latest` `0.6.3` purely because the two strings differed).
287
+ *
288
+ * `false` when: not installed · no `latest_version` (probe failed) · target ≤
289
+ * installed (same or older — incl. the rc→older-stable downgrade) · either
290
+ * version is unparseable (`compareVersions` returns null → fail-closed: don't
291
+ * offer a move we can't verify is forward). `true` ONLY when the target
292
+ * parses AND sorts strictly above installed — so `0.6.4-rc.15` → stable
293
+ * `0.6.4` IS an upgrade (stable > its own rc per semver §11.4.3), but
294
+ * `0.6.4-rc.15` → `0.6.3` is NOT.
295
+ */
296
+ upgrade_available: boolean;
242
297
  supervisor_status: ModuleState["status"] | null;
243
298
  pid: number | null;
244
299
  /**
@@ -451,6 +506,8 @@ export async function handleApiModules(req: Request, deps: ApiModulesDeps): Prom
451
506
  // Lenient read so a single bad row written by a buggy module install
452
507
  // (e.g. app@0.2.0-rc.4) doesn't take down /api/modules — see hub#406.
453
508
  const manifest = readManifestLenient(deps.manifestPath);
509
+ // Live on-disk version reader (hub#243) — see `defaultReadInstalledVersion`.
510
+ const readInstalledVersion = deps.readInstalledVersion ?? defaultReadInstalledVersion;
454
511
  const installedByShort = new Map<
455
512
  string,
456
513
  {
@@ -476,6 +533,18 @@ export async function handleApiModules(req: Request, deps: ApiModulesDeps): Prom
476
533
  uis?: Record<string, UiSubUnit>;
477
534
  mountPath?: string;
478
535
  } = { version: entry.version };
536
+ // Prefer the LIVE on-disk version over the services.json cache when we can
537
+ // read it (hub#243). `entry.version` is a cache the module stamps on its own
538
+ // boot; on the bun-linked dev path it lags after a rebuild-without-restart,
539
+ // so the admin view shows a stale "current" (the live symptom: cache said
540
+ // 0.5.4-rc.15 while the linked checkout was already 0.6.4-rc.15). The
541
+ // module's `<installDir>/package.json` `version` is authoritative for the
542
+ // code actually on disk. Falls back to the cache when there's no installDir
543
+ // or the package.json can't be read (e.g. a not-yet-booted seed row).
544
+ if (entry.installDir !== undefined) {
545
+ const live = readInstalledVersion(entry.installDir);
546
+ if (live !== null) value.version = live;
547
+ }
479
548
  if (entry.installDir !== undefined) value.installDir = entry.installDir;
480
549
  if (entry.uis !== undefined) value.uis = entry.uis;
481
550
  // First non-`.parachute` path is the module's user-facing mount
@@ -622,6 +691,10 @@ export async function handleApiModules(req: Request, deps: ApiModulesDeps): Prom
622
691
  installed: installed !== undefined,
623
692
  installed_version: installed?.version ?? null,
624
693
  latest_version: latestByShort.get(short) ?? null,
694
+ upgrade_available: isUpgradeAvailable(
695
+ installed?.version ?? null,
696
+ latestByShort.get(short) ?? null,
697
+ ),
625
698
  supervisor_status: state?.status ?? null,
626
699
  pid: state?.pid ?? null,
627
700
  supervisor_start_error: state?.startError ?? null,
@@ -644,6 +717,9 @@ export async function handleApiModules(req: Request, deps: ApiModulesDeps): Prom
644
717
 
645
718
  // Every supervised module's run-state — curated AND non-curated (hub#539).
646
719
  // Built from the same supervisor.list() snapshot already in `stateByShort`.
720
+ // `installed_version` here inherits the live-on-disk override already applied
721
+ // to `installedByShort[].version` in the manifest loop above (hub#243), so the
722
+ // `supervised` array reports the same corrected version as the `modules` rows.
647
723
  const supervised: SupervisedSnapshotWire[] = Array.from(stateByShort.values()).map((s) => ({
648
724
  short: s.short,
649
725
  installed: installedByShort.has(s.short),
@@ -842,6 +918,35 @@ function toUisWireShape(uis: Record<string, UiSubUnit> | undefined): UiSubUnitWi
842
918
  }));
843
919
  }
844
920
 
921
+ /**
922
+ * Whether `latest` is a REAL upgrade over `installed` — strictly newer under
923
+ * semver ordering (hub#243). The single tested source of truth for the
924
+ * "upgrade available?" decision, shared by the wire shape's `upgrade_available`
925
+ * field and (transitively) the admin SPA's Upgrade button.
926
+ *
927
+ * Returns `false` unless BOTH versions parse AND `latest` sorts strictly above
928
+ * `installed`:
929
+ * - no installed version (not installed) → false.
930
+ * - no latest version (probe failed / unknown package) → false.
931
+ * - `compareVersions` can't parse either side → false (FAIL-CLOSED: never
932
+ * offer a move we can't prove is forward).
933
+ * - target ≤ installed → false. This is the load-bearing downgrade guard:
934
+ * an rc operator on `0.6.4-rc.15` whose channel resolves `latest_version`
935
+ * to the OLDER `@latest` `0.6.3` is NOT offered an "upgrade" (the live bug
936
+ * — a downgrade framed as an upgrade by the old string `!==`).
937
+ *
938
+ * Reuses `commands/upgrade.ts:compareVersions`, the same comparator the CLI
939
+ * `parachute upgrade` downgrade guard uses, so the SPA offer and the CLI guard
940
+ * can't disagree about rc/prerelease ordering. Note `0.6.4-rc.15` → stable
941
+ * `0.6.4` IS strictly-newer (stable > its own rc per semver §11.4.3), so a real
942
+ * rc→its-stable promotion still surfaces as an upgrade.
943
+ */
944
+ export function isUpgradeAvailable(installed: string | null, latest: string | null): boolean {
945
+ if (installed === null || latest === null) return false;
946
+ const cmp = compareVersions(latest, installed);
947
+ return cmp !== null && cmp > 0;
948
+ }
949
+
845
950
  /**
846
951
  * Reset the in-memory `latest_version` cache. Tests call this between
847
952
  * runs to prevent state leakage across test cases; production never
@@ -58,4 +58,4 @@ Error generating stack: `+n.message+`
58
58
  */var Bh="popstate";function Lh(l){return typeof l=="object"&&l!=null&&"pathname"in l&&"search"in l&&"hash"in l&&"state"in l&&"key"in l}function pv(l={}){function r(o,f){var v;let h=(v=f.state)==null?void 0:v.masked,{pathname:g,search:y,hash:m}=h||o.location;return uc("",{pathname:g,search:y,hash:m},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 c(o,f){return typeof f=="string"?f:Ql(f)}return vv(r,c,null,l)}function Be(l,r){if(l===!1||l===null||typeof l>"u")throw new Error(r)}function Ht(l,r){if(!l){typeof console<"u"&&console.warn(r);try{throw new Error(r)}catch{}}}function gv(){return Math.random().toString(36).substring(2,10)}function qh(l,r){return{usr:l.state,key:l.key,idx:r,masked:l.unstable_mask?{pathname:l.pathname,search:l.search,hash:l.hash}:void 0}}function uc(l,r,c=null,o,f){return{pathname:typeof l=="string"?l:l.pathname,search:"",hash:"",...typeof r=="string"?Zn(r):r,state:c,key:r&&r.key||o||gv(),unstable_mask:f}}function Ql({pathname:l="/",search:r="",hash:c=""}){return r&&r!=="?"&&(l+=r.charAt(0)==="?"?r:"?"+r),c&&c!=="#"&&(l+=c.charAt(0)==="#"?c:"#"+c),l}function Zn(l){let r={};if(l){let c=l.indexOf("#");c>=0&&(r.hash=l.substring(c),l=l.substring(0,c));let o=l.indexOf("?");o>=0&&(r.search=l.substring(o),l=l.substring(0,o)),l&&(r.pathname=l)}return r}function vv(l,r,c,o={}){let{window:f=document.defaultView,v5Compat:h=!1}=o,g=f.history,y="POP",m=null,v=x();v==null&&(v=0,g.replaceState({...g.state,idx:v},""));function x(){return(g.state||{idx:null}).idx}function j(){y="POP";let L=x(),z=L==null?null:L-v;v=L,m&&m({action:y,location:O.location,delta:z})}function _(L,z){y="PUSH";let Z=Lh(L)?L:uc(O.location,L,z);v=x()+1;let X=qh(Z,v),P=O.createHref(Z.unstable_mask||Z);try{g.pushState(X,"",P)}catch(K){if(K instanceof DOMException&&K.name==="DataCloneError")throw K;f.location.assign(P)}h&&m&&m({action:y,location:O.location,delta:1})}function k(L,z){y="REPLACE";let Z=Lh(L)?L:uc(O.location,L,z);v=x();let X=qh(Z,v),P=O.createHref(Z.unstable_mask||Z);g.replaceState(X,"",P),h&&m&&m({action:y,location:O.location,delta:0})}function C(L){return yv(L)}let O={get action(){return y},get location(){return l(f,g)},listen(L){if(m)throw new Error("A history only accepts one active listener");return f.addEventListener(Bh,j),m=L,()=>{f.removeEventListener(Bh,j),m=null}},createHref(L){return r(f,L)},createURL:C,encodeLocation(L){let z=C(L);return{pathname:z.pathname,search:z.search,hash:z.hash}},push:_,replace:k,go(L){return g.go(L)}};return O}function yv(l,r=!1){let c="http://localhost";typeof window<"u"&&(c=window.location.origin!=="null"?window.location.origin:window.location.href),Be(c,"No window.location.(origin|href) available to create URL");let o=typeof l=="string"?l:Ql(l);return o=o.replace(/ $/,"%20"),!r&&o.startsWith("//")&&(o=c+o),new URL(o,c)}function tm(l,r,c="/"){return bv(l,r,c,!1)}function bv(l,r,c,o){let f=typeof r=="string"?Zn(r):r,h=fa(f.pathname||"/",c);if(h==null)return null;let g=am(l);xv(g);let y=null;for(let m=0;y==null&&m<g.length;++m){let v=Rv(h);y=_v(g[m],v,o)}return y}function am(l,r=[],c=[],o="",f=!1){let h=(g,y,m=f,v)=>{let x={relativePath:v===void 0?g.path||"":v,caseSensitive:g.caseSensitive===!0,childrenIndex:y,route:g};if(x.relativePath.startsWith("/")){if(!x.relativePath.startsWith(o)&&m)return;Be(x.relativePath.startsWith(o),`Absolute route path "${x.relativePath}" nested under path "${o}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),x.relativePath=x.relativePath.slice(o.length)}let j=Yt([o,x.relativePath]),_=c.concat(x);g.children&&g.children.length>0&&(Be(g.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${j}".`),am(g.children,r,_,j,m)),!(g.path==null&&!g.index)&&r.push({path:j,score:kv(j,g.index),routesMeta:_})};return l.forEach((g,y)=>{var m;if(g.path===""||!((m=g.path)!=null&&m.includes("?")))h(g,y);else for(let v of nm(g.path))h(g,y,!0,v)}),r}function nm(l){let r=l.split("/");if(r.length===0)return[];let[c,...o]=r,f=c.endsWith("?"),h=c.replace(/\?$/,"");if(o.length===0)return f?[h,""]:[h];let g=nm(o.join("/")),y=[];return y.push(...g.map(m=>m===""?h:[h,m].join("/"))),f&&y.push(...g),y.map(m=>l.startsWith("/")&&m===""?"/":m)}function xv(l){l.sort((r,c)=>r.score!==c.score?c.score-r.score:Tv(r.routesMeta.map(o=>o.childrenIndex),c.routesMeta.map(o=>o.childrenIndex)))}var jv=/^:[\w-]+$/,Sv=3,wv=2,Cv=1,Nv=10,Ev=-2,Yh=l=>l==="*";function kv(l,r){let c=l.split("/"),o=c.length;return c.some(Yh)&&(o+=Ev),r&&(o+=wv),c.filter(f=>!Yh(f)).reduce((f,h)=>f+(jv.test(h)?Sv:h===""?Cv:Nv),o)}function Tv(l,r){return l.length===r.length&&l.slice(0,-1).every((o,f)=>o===r[f])?l[l.length-1]-r[r.length-1]:0}function _v(l,r,c=!1){let{routesMeta:o}=l,f={},h="/",g=[];for(let y=0;y<o.length;++y){let m=o[y],v=y===o.length-1,x=h==="/"?r:r.slice(h.length)||"/",j=vs({path:m.relativePath,caseSensitive:m.caseSensitive,end:v},x),_=m.route;if(!j&&v&&c&&!o[o.length-1].route.index&&(j=vs({path:m.relativePath,caseSensitive:m.caseSensitive,end:!1},x)),!j)return null;Object.assign(f,j.params),g.push({params:f,pathname:Yt([h,j.pathname]),pathnameBase:Uv(Yt([h,j.pathnameBase])),route:_}),j.pathnameBase!=="/"&&(h=Yt([h,j.pathnameBase]))}return g}function vs(l,r){typeof l=="string"&&(l={path:l,caseSensitive:!1,end:!0});let[c,o]=Av(l.path,l.caseSensitive,l.end),f=r.match(c);if(!f)return null;let h=f[0],g=h.replace(/(.)\/+$/,"$1"),y=f.slice(1);return{params:o.reduce((v,{paramName:x,isOptional:j},_)=>{if(x==="*"){let C=y[_]||"";g=h.slice(0,h.length-C.length).replace(/(.)\/+$/,"$1")}const k=y[_];return j&&!k?v[x]=void 0:v[x]=(k||"").replace(/%2F/g,"/"),v},{}),pathname:h,pathnameBase:g,pattern:l}}function Av(l,r=!1,c=!0){Ht(l==="*"||!l.endsWith("*")||l.endsWith("/*"),`Route path "${l}" will be treated as if it were "${l.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${l.replace(/\*$/,"/*")}".`);let o=[],f="^"+l.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(g,y,m,v,x)=>{if(o.push({paramName:y,isOptional:m!=null}),m){let j=x.charAt(v+g.length);return j&&j!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return l.endsWith("*")?(o.push({paramName:"*"}),f+=l==="*"||l==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):c?f+="\\/*$":l!==""&&l!=="/"&&(f+="(?:(?=\\/|$))"),[new RegExp(f,r?void 0:"i"),o]}function Rv(l){try{return l.split("/").map(r=>decodeURIComponent(r).replace(/\//g,"%2F")).join("/")}catch(r){return Ht(!1,`The URL path "${l}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${r}).`),l}}function fa(l,r){if(r==="/")return l;if(!l.toLowerCase().startsWith(r.toLowerCase()))return null;let c=r.endsWith("/")?r.length-1:r.length,o=l.charAt(c);return o&&o!=="/"?null:l.slice(c)||"/"}var Mv=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function zv(l,r="/"){let{pathname:c,search:o="",hash:f=""}=typeof l=="string"?Zn(l):l,h;return c?(c=lm(c),c.startsWith("/")?h=Gh(c.substring(1),"/"):h=Gh(c,r)):h=r,{pathname:h,search:Dv(o),hash:Hv(f)}}function Gh(l,r){let c=ys(r).split("/");return l.split("/").forEach(f=>{f===".."?c.length>1&&c.pop():f!=="."&&c.push(f)}),c.length>1?c.join("/"):"/"}function Wr(l,r,c,o){return`Cannot include a '${l}' character in a manually specified \`to.${r}\` field [${JSON.stringify(o)}]. Please separate it out to the \`to.${c}\` field. Alternatively you may provide the full path as a string in <Link to="..."> and the router will parse it for you.`}function Ov(l){return l.filter((r,c)=>c===0||r.route.path&&r.route.path.length>0)}function fc(l){let r=Ov(l);return r.map((c,o)=>o===r.length-1?c.pathname:c.pathnameBase)}function js(l,r,c,o=!1){let f;typeof l=="string"?f=Zn(l):(f={...l},Be(!f.pathname||!f.pathname.includes("?"),Wr("?","pathname","search",f)),Be(!f.pathname||!f.pathname.includes("#"),Wr("#","pathname","hash",f)),Be(!f.search||!f.search.includes("#"),Wr("#","search","hash",f)));let h=l===""||f.pathname==="",g=h?"/":f.pathname,y;if(g==null)y=c;else{let j=r.length-1;if(!o&&g.startsWith("..")){let _=g.split("/");for(;_[0]==="..";)_.shift(),j-=1;f.pathname=_.join("/")}y=j>=0?r[j]:"/"}let m=zv(f,y),v=g&&g!=="/"&&g.endsWith("/"),x=(h||g===".")&&c.endsWith("/");return!m.pathname.endsWith("/")&&(v||x)&&(m.pathname+="/"),m}var lm=l=>l.replace(/\/\/+/g,"/"),Yt=l=>lm(l.join("/")),ys=l=>l.replace(/\/+$/,""),Uv=l=>ys(l).replace(/^\/*/,"/"),Dv=l=>!l||l==="?"?"":l.startsWith("?")?l:"?"+l,Hv=l=>!l||l==="#"?"":l.startsWith("#")?l:"#"+l,Bv=class{constructor(l,r,c,o=!1){this.status=l,this.statusText=r||"",this.internal=o,c instanceof Error?(this.data=c.toString(),this.error=c):this.data=c}};function Lv(l){return l!=null&&typeof l.status=="number"&&typeof l.statusText=="string"&&typeof l.internal=="boolean"&&"data"in l}function qv(l){let r=l.map(c=>c.route.path).filter(Boolean);return Yt(r)||"/"}var im=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function sm(l,r){let c=l;if(typeof c!="string"||!Mv.test(c))return{absoluteURL:void 0,isExternal:!1,to:c};let o=c,f=!1;if(im)try{let h=new URL(window.location.href),g=c.startsWith("//")?new URL(h.protocol+c):new URL(c),y=fa(g.pathname,r);g.origin===h.origin&&y!=null?c=y+g.search+g.hash:f=!0}catch{Ht(!1,`<Link to="${c}"> contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:o,isExternal:f,to:c}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var um=["POST","PUT","PATCH","DELETE"];new Set(um);var Yv=["GET",...um];new Set(Yv);var Xn=b.createContext(null);Xn.displayName="DataRouter";var Ss=b.createContext(null);Ss.displayName="DataRouterState";var rm=b.createContext(!1);function Gv(){return b.useContext(rm)}var cm=b.createContext({isTransitioning:!1});cm.displayName="ViewTransition";var Vv=b.createContext(new Map);Vv.displayName="Fetchers";var Zv=b.createContext(null);Zv.displayName="Await";var Ct=b.createContext(null);Ct.displayName="Navigation";var $l=b.createContext(null);$l.displayName="Location";var Gt=b.createContext({outlet:null,matches:[],isDataRoute:!1});Gt.displayName="Route";var hc=b.createContext(null);hc.displayName="RouteError";var om="REACT_ROUTER_ERROR",Xv="REDIRECT",Qv="ROUTE_ERROR_RESPONSE";function $v(l){if(l.startsWith(`${om}:${Xv}:{`))try{let r=JSON.parse(l.slice(28));if(typeof r=="object"&&r&&typeof r.status=="number"&&typeof r.statusText=="string"&&typeof r.location=="string"&&typeof r.reloadDocument=="boolean"&&typeof r.replace=="boolean")return r}catch{}}function Jv(l){if(l.startsWith(`${om}:${Qv}:{`))try{let r=JSON.parse(l.slice(40));if(typeof r=="object"&&r&&typeof r.status=="number"&&typeof r.statusText=="string")return new Bv(r.status,r.statusText,r.data)}catch{}}function Kv(l,{relative:r}={}){Be(Qn(),"useHref() may be used only in the context of a <Router> component.");let{basename:c,navigator:o}=b.useContext(Ct),{hash:f,pathname:h,search:g}=Jl(l,{relative:r}),y=h;return c!=="/"&&(y=h==="/"?c:Yt([c,h])),o.createHref({pathname:y,search:g,hash:f})}function Qn(){return b.useContext($l)!=null}function Nt(){return Be(Qn(),"useLocation() may be used only in the context of a <Router> component."),b.useContext($l).location}var dm="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function fm(l){b.useContext(Ct).static||b.useLayoutEffect(l)}function mc(){let{isDataRoute:l}=b.useContext(Gt);return l?cy():Fv()}function Fv(){Be(Qn(),"useNavigate() may be used only in the context of a <Router> component.");let l=b.useContext(Xn),{basename:r,navigator:c}=b.useContext(Ct),{matches:o}=b.useContext(Gt),{pathname:f}=Nt(),h=JSON.stringify(fc(o)),g=b.useRef(!1);return fm(()=>{g.current=!0}),b.useCallback((m,v={})=>{if(Ht(g.current,dm),!g.current)return;if(typeof m=="number"){c.go(m);return}let x=js(m,JSON.parse(h),f,v.relative==="path");l==null&&r!=="/"&&(x.pathname=x.pathname==="/"?r:Yt([r,x.pathname])),(v.replace?c.replace:c.push)(x,v.state,v)},[r,c,h,f,l])}b.createContext(null);function Wv(){let{matches:l}=b.useContext(Gt),r=l[l.length-1];return(r==null?void 0:r.params)??{}}function Jl(l,{relative:r}={}){let{matches:c}=b.useContext(Gt),{pathname:o}=Nt(),f=JSON.stringify(fc(c));return b.useMemo(()=>js(l,JSON.parse(f),o,r==="path"),[l,f,o,r])}function Iv(l,r){return hm(l,r)}function hm(l,r,c){var L;Be(Qn(),"useRoutes() may be used only in the context of a <Router> component.");let{navigator:o}=b.useContext(Ct),{matches:f}=b.useContext(Gt),h=f[f.length-1],g=h?h.params:{},y=h?h.pathname:"/",m=h?h.pathnameBase:"/",v=h&&h.route;{let z=v&&v.path||"";pm(y,!v||z.endsWith("*")||z.endsWith("*?"),`You rendered descendant <Routes> (or called \`useRoutes()\`) at "${y}" (under <Route path="${z}">) 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="${z}"> to <Route path="${z==="/"?"*":`${z}/*`}">.`)}let x=Nt(),j;if(r){let z=typeof r=="string"?Zn(r):r;Be(m==="/"||((L=z.pathname)==null?void 0:L.startsWith(m)),`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 "${m}" but pathname "${z.pathname}" was given in the \`location\` prop.`),j=z}else j=x;let _=j.pathname||"/",k=_;if(m!=="/"){let z=m.replace(/^\//,"").split("/");k="/"+_.replace(/^\//,"").split("/").slice(z.length).join("/")}let C=tm(l,{pathname:k});Ht(v||C!=null,`No routes matched location "${j.pathname}${j.search}${j.hash}" `),Ht(C==null||C[C.length-1].route.element!==void 0||C[C.length-1].route.Component!==void 0||C[C.length-1].route.lazy!==void 0,`Matched leaf route at location "${j.pathname}${j.search}${j.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 O=ny(C&&C.map(z=>Object.assign({},z,{params:Object.assign({},g,z.params),pathname:Yt([m,o.encodeLocation?o.encodeLocation(z.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:z.pathname]),pathnameBase:z.pathnameBase==="/"?m:Yt([m,o.encodeLocation?o.encodeLocation(z.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:z.pathnameBase])})),f,c);return r&&O?b.createElement($l.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",unstable_mask:void 0,...j},navigationType:"POP"}},O):O}function Pv(){let l=ry(),r=Lv(l)?`${l.status} ${l.statusText}`:l instanceof Error?l.message:JSON.stringify(l),c=l instanceof Error?l.stack:null,o="rgba(200,200,200, 0.5)",f={padding:"0.5rem",backgroundColor:o},h={padding:"2px 4px",backgroundColor:o},g=null;return console.error("Error handled by React Router default ErrorBoundary:",l),g=b.createElement(b.Fragment,null,b.createElement("p",null,"💿 Hey developer 👋"),b.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",b.createElement("code",{style:h},"ErrorBoundary")," or"," ",b.createElement("code",{style:h},"errorElement")," prop on your route.")),b.createElement(b.Fragment,null,b.createElement("h2",null,"Unexpected Application Error!"),b.createElement("h3",{style:{fontStyle:"italic"}},r),c?b.createElement("pre",{style:f},c):null,g)}var ey=b.createElement(Pv,null),mm=class extends b.Component{constructor(l){super(l),this.state={location:l.location,revalidation:l.revalidation,error:l.error}}static getDerivedStateFromError(l){return{error:l}}static getDerivedStateFromProps(l,r){return r.location!==l.location||r.revalidation!=="idle"&&l.revalidation==="idle"?{error:l.error,location:l.location,revalidation:l.revalidation}:{error:l.error!==void 0?l.error:r.error,location:r.location,revalidation:l.revalidation||r.revalidation}}componentDidCatch(l,r){this.props.onError?this.props.onError(l,r):console.error("React Router caught the following error during render",l)}render(){let l=this.state.error;if(this.context&&typeof l=="object"&&l&&"digest"in l&&typeof l.digest=="string"){const c=Jv(l.digest);c&&(l=c)}let r=l!==void 0?b.createElement(Gt.Provider,{value:this.props.routeContext},b.createElement(hc.Provider,{value:l,children:this.props.component})):this.props.children;return this.context?b.createElement(ty,{error:l},r):r}};mm.contextType=rm;var Ir=new WeakMap;function ty({children:l,error:r}){let{basename:c}=b.useContext(Ct);if(typeof r=="object"&&r&&"digest"in r&&typeof r.digest=="string"){let o=$v(r.digest);if(o){let f=Ir.get(r);if(f)throw f;let h=sm(o.location,c);if(im&&!Ir.get(r))if(h.isExternal||o.reloadDocument)window.location.href=h.absoluteURL||h.to;else{const g=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(h.to,{replace:o.replace}));throw Ir.set(r,g),g}return b.createElement("meta",{httpEquiv:"refresh",content:`0;url=${h.absoluteURL||h.to}`})}}return l}function ay({routeContext:l,match:r,children:c}){let o=b.useContext(Xn);return o&&o.static&&o.staticContext&&(r.route.errorElement||r.route.ErrorBoundary)&&(o.staticContext._deepestRenderedBoundaryId=r.route.id),b.createElement(Gt.Provider,{value:l},c)}function ny(l,r=[],c){let o=c==null?void 0:c.state;if(l==null){if(!o)return null;if(o.errors)l=o.matches;else if(r.length===0&&!o.initialized&&o.matches.length>0)l=o.matches;else return null}let f=l,h=o==null?void 0:o.errors;if(h!=null){let x=f.findIndex(j=>j.route.id&&(h==null?void 0:h[j.route.id])!==void 0);Be(x>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(h).join(",")}`),f=f.slice(0,Math.min(f.length,x+1))}let g=!1,y=-1;if(c&&o){g=o.renderFallback;for(let x=0;x<f.length;x++){let j=f[x];if((j.route.HydrateFallback||j.route.hydrateFallbackElement)&&(y=x),j.route.id){let{loaderData:_,errors:k}=o,C=j.route.loader&&!_.hasOwnProperty(j.route.id)&&(!k||k[j.route.id]===void 0);if(j.route.lazy||C){c.isStatic&&(g=!0),y>=0?f=f.slice(0,y+1):f=[f[0]];break}}}}let m=c==null?void 0:c.onError,v=o&&m?(x,j)=>{var _,k;m(x,{location:o.location,params:((k=(_=o.matches)==null?void 0:_[0])==null?void 0:k.params)??{},unstable_pattern:qv(o.matches),errorInfo:j})}:void 0;return f.reduceRight((x,j,_)=>{let k,C=!1,O=null,L=null;o&&(k=h&&j.route.id?h[j.route.id]:void 0,O=j.route.errorElement||ey,g&&(y<0&&_===0?(pm("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),C=!0,L=null):y===_&&(C=!0,L=j.route.hydrateFallbackElement||null)));let z=r.concat(f.slice(0,_+1)),Z=()=>{let X;return k?X=O:C?X=L:j.route.Component?X=b.createElement(j.route.Component,null):j.route.element?X=j.route.element:X=x,b.createElement(ay,{match:j,routeContext:{outlet:x,matches:z,isDataRoute:o!=null},children:X})};return o&&(j.route.ErrorBoundary||j.route.errorElement||_===0)?b.createElement(mm,{location:o.location,revalidation:o.revalidation,component:O,error:k,children:Z(),routeContext:{outlet:null,matches:z,isDataRoute:!0},onError:v}):Z()},null)}function pc(l){return`${l} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function ly(l){let r=b.useContext(Xn);return Be(r,pc(l)),r}function iy(l){let r=b.useContext(Ss);return Be(r,pc(l)),r}function sy(l){let r=b.useContext(Gt);return Be(r,pc(l)),r}function gc(l){let r=sy(l),c=r.matches[r.matches.length-1];return Be(c.route.id,`${l} can only be used on routes that contain a unique "id"`),c.route.id}function uy(){return gc("useRouteId")}function ry(){var o;let l=b.useContext(hc),r=iy("useRouteError"),c=gc("useRouteError");return l!==void 0?l:(o=r.errors)==null?void 0:o[c]}function cy(){let{router:l}=ly("useNavigate"),r=gc("useNavigate"),c=b.useRef(!1);return fm(()=>{c.current=!0}),b.useCallback(async(f,h={})=>{Ht(c.current,dm),c.current&&(typeof f=="number"?await l.navigate(f):await l.navigate(f,{fromRouteId:r,...h}))},[l,r])}var Vh={};function pm(l,r,c){!r&&!Vh[l]&&(Vh[l]=!0,Ht(!1,c))}b.memo(oy);function oy({routes:l,future:r,state:c,isStatic:o,onError:f}){return hm(l,void 0,{state:c,isStatic:o,onError:f})}function Pr({to:l,replace:r,state:c,relative:o}){Be(Qn(),"<Navigate> may be used only in the context of a <Router> component.");let{static:f}=b.useContext(Ct);Ht(!f,"<Navigate> must not be used on the initial render in a <StaticRouter>. This is a no-op, but you should modify your code so the <Navigate> is only ever rendered in response to some user interaction or state change.");let{matches:h}=b.useContext(Gt),{pathname:g}=Nt(),y=mc(),m=js(l,fc(h),g,o==="path"),v=JSON.stringify(m);return b.useEffect(()=>{y(JSON.parse(v),{replace:r,state:c,relative:o})},[y,v,o,r,c]),null}function nt(l){Be(!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 dy({basename:l="/",children:r=null,location:c,navigationType:o="POP",navigator:f,static:h=!1,unstable_useTransitions:g}){Be(!Qn(),"You cannot render a <Router> inside another <Router>. You should never have more than one in your app.");let y=l.replace(/^\/*/,"/"),m=b.useMemo(()=>({basename:y,navigator:f,static:h,unstable_useTransitions:g,future:{}}),[y,f,h,g]);typeof c=="string"&&(c=Zn(c));let{pathname:v="/",search:x="",hash:j="",state:_=null,key:k="default",unstable_mask:C}=c,O=b.useMemo(()=>{let L=fa(v,y);return L==null?null:{location:{pathname:L,search:x,hash:j,state:_,key:k,unstable_mask:C},navigationType:o}},[y,v,x,j,_,k,o,C]);return Ht(O!=null,`<Router basename="${y}"> is not able to match the URL "${v}${x}${j}" because it does not start with the basename, so the <Router> won't render anything.`),O==null?null:b.createElement(Ct.Provider,{value:m},b.createElement($l.Provider,{children:r,value:O}))}function fy({children:l,location:r}){return Iv(rc(l),r)}function rc(l,r=[]){let c=[];return b.Children.forEach(l,(o,f)=>{if(!b.isValidElement(o))return;let h=[...r,f];if(o.type===b.Fragment){c.push.apply(c,rc(o.props.children,h));return}Be(o.type===nt,`[${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>`),Be(!o.props.index||!o.props.children,"An index route cannot have child routes.");let g={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&&(g.children=rc(o.props.children,h)),c.push(g)}),c}var ps="get",gs="application/x-www-form-urlencoded";function ws(l){return typeof HTMLElement<"u"&&l instanceof HTMLElement}function hy(l){return ws(l)&&l.tagName.toLowerCase()==="button"}function my(l){return ws(l)&&l.tagName.toLowerCase()==="form"}function py(l){return ws(l)&&l.tagName.toLowerCase()==="input"}function gy(l){return!!(l.metaKey||l.altKey||l.ctrlKey||l.shiftKey)}function vy(l,r){return l.button===0&&(!r||r==="_self")&&!gy(l)}function cc(l=""){return new URLSearchParams(typeof l=="string"||Array.isArray(l)||l instanceof URLSearchParams?l:Object.keys(l).reduce((r,c)=>{let o=l[c];return r.concat(Array.isArray(o)?o.map(f=>[c,f]):[[c,o]])},[]))}function yy(l,r){let c=cc(l);return r&&r.forEach((o,f)=>{c.has(f)||r.getAll(f).forEach(h=>{c.append(f,h)})}),c}var hs=null;function by(){if(hs===null)try{new FormData(document.createElement("form"),0),hs=!1}catch{hs=!0}return hs}var xy=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function ec(l){return l!=null&&!xy.has(l)?(Ht(!1,`"${l}" is not a valid \`encType\` for \`<Form>\`/\`<fetcher.Form>\` and will default to "${gs}"`),null):l}function jy(l,r){let c,o,f,h,g;if(my(l)){let y=l.getAttribute("action");o=y?fa(y,r):null,c=l.getAttribute("method")||ps,f=ec(l.getAttribute("enctype"))||gs,h=new FormData(l)}else if(hy(l)||py(l)&&(l.type==="submit"||l.type==="image")){let y=l.form;if(y==null)throw new Error('Cannot submit a <button> or <input type="submit"> without a <form>');let m=l.getAttribute("formaction")||y.getAttribute("action");if(o=m?fa(m,r):null,c=l.getAttribute("formmethod")||y.getAttribute("method")||ps,f=ec(l.getAttribute("formenctype"))||ec(y.getAttribute("enctype"))||gs,h=new FormData(y,l),!by()){let{name:v,type:x,value:j}=l;if(x==="image"){let _=v?`${v}.`:"";h.append(`${_}x`,"0"),h.append(`${_}y`,"0")}else v&&h.append(v,j)}}else{if(ws(l))throw new Error('Cannot submit element that is not <form>, <button>, or <input type="submit|image">');c=ps,o=null,f=gs,g=l}return h&&f==="text/plain"&&(g=h,h=void 0),{action:o,method:c.toLowerCase(),encType:f,formData:h,body:g}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");function vc(l,r){if(l===!1||l===null||typeof l>"u")throw new Error(r)}function gm(l,r,c,o){let f=typeof l=="string"?new URL(l,typeof window>"u"?"server://singlefetch/":window.location.origin):l;return c?f.pathname.endsWith("/")?f.pathname=`${f.pathname}_.${o}`:f.pathname=`${f.pathname}.${o}`:f.pathname==="/"?f.pathname=`_root.${o}`:r&&fa(f.pathname,r)==="/"?f.pathname=`${ys(r)}/_root.${o}`:f.pathname=`${ys(f.pathname)}.${o}`,f}async function Sy(l,r){if(l.id in r)return r[l.id];try{let c=await import(l.module);return r[l.id]=c,c}catch(c){return console.error(`Error loading route module \`${l.module}\`, reloading page...`),console.error(c),window.__reactRouterContext&&window.__reactRouterContext.isSpaMode,window.location.reload(),new Promise(()=>{})}}function wy(l){return l==null?!1:l.href==null?l.rel==="preload"&&typeof l.imageSrcSet=="string"&&typeof l.imageSizes=="string":typeof l.rel=="string"&&typeof l.href=="string"}async function Cy(l,r,c){let o=await Promise.all(l.map(async f=>{let h=r.routes[f.route.id];if(h){let g=await Sy(h,c);return g.links?g.links():[]}return[]}));return Ty(o.flat(1).filter(wy).filter(f=>f.rel==="stylesheet"||f.rel==="preload").map(f=>f.rel==="stylesheet"?{...f,rel:"prefetch",as:"style"}:{...f,rel:"prefetch"}))}function Zh(l,r,c,o,f,h){let g=(m,v)=>c[v]?m.route.id!==c[v].route.id:!0,y=(m,v)=>{var x;return c[v].pathname!==m.pathname||((x=c[v].route.path)==null?void 0:x.endsWith("*"))&&c[v].params["*"]!==m.params["*"]};return h==="assets"?r.filter((m,v)=>g(m,v)||y(m,v)):h==="data"?r.filter((m,v)=>{var j;let x=o.routes[m.route.id];if(!x||!x.hasLoader)return!1;if(g(m,v)||y(m,v))return!0;if(m.route.shouldRevalidate){let _=m.route.shouldRevalidate({currentUrl:new URL(f.pathname+f.search+f.hash,window.origin),currentParams:((j=c[0])==null?void 0:j.params)||{},nextUrl:new URL(l,window.origin),nextParams:m.params,defaultShouldRevalidate:!0});if(typeof _=="boolean")return _}return!0}):[]}function Ny(l,r,{includeHydrateFallback:c}={}){return Ey(l.map(o=>{let f=r.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)),c&&f.hydrateFallbackModule&&(h=h.concat(f.hydrateFallbackModule)),f.imports&&(h=h.concat(f.imports)),h}).flat(1))}function Ey(l){return[...new Set(l)]}function ky(l){let r={},c=Object.keys(l).sort();for(let o of c)r[o]=l[o];return r}function Ty(l,r){let c=new Set;return new Set(r),l.reduce((o,f)=>{let h=JSON.stringify(ky(f));return c.has(h)||(c.add(h),o.push({key:h,link:f})),o},[])}function yc(){let l=b.useContext(Xn);return vc(l,"You must render this element inside a <DataRouterContext.Provider> element"),l}function _y(){let l=b.useContext(Ss);return vc(l,"You must render this element inside a <DataRouterStateContext.Provider> element"),l}var bc=b.createContext(void 0);bc.displayName="FrameworkContext";function xc(){let l=b.useContext(bc);return vc(l,"You must render this element inside a <HydratedRouter> element"),l}function Ay(l,r){let c=b.useContext(bc),[o,f]=b.useState(!1),[h,g]=b.useState(!1),{onFocus:y,onBlur:m,onMouseEnter:v,onMouseLeave:x,onTouchStart:j}=r,_=b.useRef(null);b.useEffect(()=>{if(l==="render"&&g(!0),l==="viewport"){let O=z=>{z.forEach(Z=>{g(Z.isIntersecting)})},L=new IntersectionObserver(O,{threshold:.5});return _.current&&L.observe(_.current),()=>{L.disconnect()}}},[l]),b.useEffect(()=>{if(o){let O=setTimeout(()=>{g(!0)},100);return()=>{clearTimeout(O)}}},[o]);let k=()=>{f(!0)},C=()=>{f(!1),g(!1)};return c?l!=="intent"?[h,_,{}]:[h,_,{onFocus:Vl(y,k),onBlur:Vl(m,C),onMouseEnter:Vl(v,k),onMouseLeave:Vl(x,C),onTouchStart:Vl(j,k)}]:[!1,_,{}]}function Vl(l,r){return c=>{l&&l(c),c.defaultPrevented||r(c)}}function Ry({page:l,...r}){let c=Gv(),{router:o}=yc(),f=b.useMemo(()=>tm(o.routes,l,o.basename),[o.routes,l,o.basename]);return f?c?b.createElement(zy,{page:l,matches:f,...r}):b.createElement(Oy,{page:l,matches:f,...r}):null}function My(l){let{manifest:r,routeModules:c}=xc(),[o,f]=b.useState([]);return b.useEffect(()=>{let h=!1;return Cy(l,r,c).then(g=>{h||f(g)}),()=>{h=!0}},[l,r,c]),o}function zy({page:l,matches:r,...c}){let o=Nt(),{future:f}=xc(),{basename:h}=yc(),g=b.useMemo(()=>{if(l===o.pathname+o.search+o.hash)return[];let y=gm(l,h,f.unstable_trailingSlashAwareDataRequests,"rsc"),m=!1,v=[];for(let x of r)typeof x.route.shouldRevalidate=="function"?m=!0:v.push(x.route.id);return m&&v.length>0&&y.searchParams.set("_routes",v.join(",")),[y.pathname+y.search]},[h,f.unstable_trailingSlashAwareDataRequests,l,o,r]);return b.createElement(b.Fragment,null,g.map(y=>b.createElement("link",{key:y,rel:"prefetch",as:"fetch",href:y,...c})))}function Oy({page:l,matches:r,...c}){let o=Nt(),{future:f,manifest:h,routeModules:g}=xc(),{basename:y}=yc(),{loaderData:m,matches:v}=_y(),x=b.useMemo(()=>Zh(l,r,v,h,o,"data"),[l,r,v,h,o]),j=b.useMemo(()=>Zh(l,r,v,h,o,"assets"),[l,r,v,h,o]),_=b.useMemo(()=>{if(l===o.pathname+o.search+o.hash)return[];let O=new Set,L=!1;if(r.forEach(Z=>{var P;let X=h.routes[Z.route.id];!X||!X.hasLoader||(!x.some(K=>K.route.id===Z.route.id)&&Z.route.id in m&&((P=g[Z.route.id])!=null&&P.shouldRevalidate)||X.hasClientLoader?L=!0:O.add(Z.route.id))}),O.size===0)return[];let z=gm(l,y,f.unstable_trailingSlashAwareDataRequests,"data");return L&&O.size>0&&z.searchParams.set("_routes",r.filter(Z=>O.has(Z.route.id)).map(Z=>Z.route.id).join(",")),[z.pathname+z.search]},[y,f.unstable_trailingSlashAwareDataRequests,m,o,h,x,r,l,g]),k=b.useMemo(()=>Ny(j,h),[j,h]),C=My(j);return b.createElement(b.Fragment,null,_.map(O=>b.createElement("link",{key:O,rel:"prefetch",as:"fetch",href:O,...c})),k.map(O=>b.createElement("link",{key:O,rel:"modulepreload",href:O,...c})),C.map(({key:O,link:L})=>b.createElement("link",{key:O,nonce:c.nonce,...L,crossOrigin:L.crossOrigin??c.crossOrigin})))}function Uy(...l){return r=>{l.forEach(c=>{typeof c=="function"?c(r):c!=null&&(c.current=r)})}}var Dy=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";try{Dy&&(window.__reactRouterVersion="7.14.2")}catch{}function Hy({basename:l,children:r,unstable_useTransitions:c,window:o}){let f=b.useRef();f.current==null&&(f.current=pv({window:o,v5Compat:!0}));let h=f.current,[g,y]=b.useState({action:h.action,location:h.location}),m=b.useCallback(v=>{c===!1?y(v):b.startTransition(()=>y(v))},[c]);return b.useLayoutEffect(()=>h.listen(m),[h,m]),b.createElement(dy,{basename:l,children:r,location:g.location,navigationType:g.action,navigator:h,unstable_useTransitions:c})}var vm=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,lt=b.forwardRef(function({onClick:r,discover:c="render",prefetch:o="none",relative:f,reloadDocument:h,replace:g,unstable_mask:y,state:m,target:v,to:x,preventScrollReset:j,viewTransition:_,unstable_defaultShouldRevalidate:k,...C},O){let{basename:L,navigator:z,unstable_useTransitions:Z}=b.useContext(Ct),X=typeof x=="string"&&vm.test(x),P=sm(x,L);x=P.to;let K=Kv(x,{relative:f}),J=Nt(),E=null;if(y){let be=js(y,[],J.unstable_mask?J.unstable_mask.pathname:"/",!0);L!=="/"&&(be.pathname=be.pathname==="/"?L:Yt([L,be.pathname])),E=z.createHref(be)}let[D,Q,te]=Ay(o,C),le=Yy(x,{replace:g,unstable_mask:y,state:m,target:v,preventScrollReset:j,relative:f,viewTransition:_,unstable_defaultShouldRevalidate:k,unstable_useTransitions:Z});function ie(be){r&&r(be),be.defaultPrevented||le(be)}let W=!(P.isExternal||h),ge=b.createElement("a",{...C,...te,href:(W?E:void 0)||P.absoluteURL||K,onClick:W?ie:r,ref:Uy(O,Q),target:v,"data-discover":!X&&c==="render"?"true":void 0});return D&&!X?b.createElement(b.Fragment,null,ge,b.createElement(Ry,{page:K})):ge});lt.displayName="Link";var By=b.forwardRef(function({"aria-current":r="page",caseSensitive:c=!1,className:o="",end:f=!1,style:h,to:g,viewTransition:y,children:m,...v},x){let j=Jl(g,{relative:v.relative}),_=Nt(),k=b.useContext(Ss),{navigator:C,basename:O}=b.useContext(Ct),L=k!=null&&Qy(j)&&y===!0,z=C.encodeLocation?C.encodeLocation(j).pathname:j.pathname,Z=_.pathname,X=k&&k.navigation&&k.navigation.location?k.navigation.location.pathname:null;c||(Z=Z.toLowerCase(),X=X?X.toLowerCase():null,z=z.toLowerCase()),X&&O&&(X=fa(X,O)||X);const P=z!=="/"&&z.endsWith("/")?z.length-1:z.length;let K=Z===z||!f&&Z.startsWith(z)&&Z.charAt(P)==="/",J=X!=null&&(X===z||!f&&X.startsWith(z)&&X.charAt(z.length)==="/"),E={isActive:K,isPending:J,isTransitioning:L},D=K?r:void 0,Q;typeof o=="function"?Q=o(E):Q=[o,K?"active":null,J?"pending":null,L?"transitioning":null].filter(Boolean).join(" ");let te=typeof h=="function"?h(E):h;return b.createElement(lt,{...v,"aria-current":D,className:Q,ref:x,style:te,to:g,viewTransition:y},typeof m=="function"?m(E):m)});By.displayName="NavLink";var Ly=b.forwardRef(({discover:l="render",fetcherKey:r,navigate:c,reloadDocument:o,replace:f,state:h,method:g=ps,action:y,onSubmit:m,relative:v,preventScrollReset:x,viewTransition:j,unstable_defaultShouldRevalidate:_,...k},C)=>{let{unstable_useTransitions:O}=b.useContext(Ct),L=Zy(),z=Xy(y,{relative:v}),Z=g.toLowerCase()==="get"?"get":"post",X=typeof y=="string"&&vm.test(y),P=K=>{if(m&&m(K),K.defaultPrevented)return;K.preventDefault();let J=K.nativeEvent.submitter,E=(J==null?void 0:J.getAttribute("formmethod"))||g,D=()=>L(J||K.currentTarget,{fetcherKey:r,method:E,navigate:c,replace:f,state:h,relative:v,preventScrollReset:x,viewTransition:j,unstable_defaultShouldRevalidate:_});O&&c!==!1?b.startTransition(()=>D()):D()};return b.createElement("form",{ref:C,method:Z,action:z,onSubmit:o?m:P,...k,"data-discover":!X&&l==="render"?"true":void 0})});Ly.displayName="Form";function qy(l){return`${l} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function ym(l){let r=b.useContext(Xn);return Be(r,qy(l)),r}function Yy(l,{target:r,replace:c,unstable_mask:o,state:f,preventScrollReset:h,relative:g,viewTransition:y,unstable_defaultShouldRevalidate:m,unstable_useTransitions:v}={}){let x=mc(),j=Nt(),_=Jl(l,{relative:g});return b.useCallback(k=>{if(vy(k,r)){k.preventDefault();let C=c!==void 0?c:Ql(j)===Ql(_),O=()=>x(l,{replace:C,unstable_mask:o,state:f,preventScrollReset:h,relative:g,viewTransition:y,unstable_defaultShouldRevalidate:m});v?b.startTransition(()=>O()):O()}},[j,x,_,c,o,f,r,l,h,g,y,m,v])}function bm(l){Ht(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 r=b.useRef(cc(l)),c=b.useRef(!1),o=Nt(),f=b.useMemo(()=>yy(o.search,c.current?null:r.current),[o.search]),h=mc(),g=b.useCallback((y,m)=>{const v=cc(typeof y=="function"?y(new URLSearchParams(f)):y);c.current=!0,h("?"+v,m)},[h,f]);return[f,g]}var Gy=0,Vy=()=>`__${String(++Gy)}__`;function Zy(){let{router:l}=ym("useSubmit"),{basename:r}=b.useContext(Ct),c=uy(),o=l.fetch,f=l.navigate;return b.useCallback(async(h,g={})=>{let{action:y,method:m,encType:v,formData:x,body:j}=jy(h,r);if(g.navigate===!1){let _=g.fetcherKey||Vy();await o(_,c,g.action||y,{unstable_defaultShouldRevalidate:g.unstable_defaultShouldRevalidate,preventScrollReset:g.preventScrollReset,formData:x,body:j,formMethod:g.method||m,formEncType:g.encType||v,flushSync:g.flushSync})}else await f(g.action||y,{unstable_defaultShouldRevalidate:g.unstable_defaultShouldRevalidate,preventScrollReset:g.preventScrollReset,formData:x,body:j,formMethod:g.method||m,formEncType:g.encType||v,replace:g.replace,state:g.state,fromRouteId:c,flushSync:g.flushSync,viewTransition:g.viewTransition})},[o,f,r,c])}function Xy(l,{relative:r}={}){let{basename:c}=b.useContext(Ct),o=b.useContext(Gt);Be(o,"useFormAction must be used inside a RouteContext");let[f]=o.matches.slice(-1),h={...Jl(l||".",{relative:r})},g=Nt();if(l==null){h.search=g.search;let y=new URLSearchParams(h.search),m=y.getAll("index");if(m.some(x=>x==="")){y.delete("index"),m.filter(j=>j).forEach(j=>y.append("index",j));let x=y.toString();h.search=x?`?${x}`:""}}return(!l||l===".")&&f.route.index&&(h.search=h.search?h.search.replace(/^\?/,"?index&"):"?index"),c!=="/"&&(h.pathname=h.pathname==="/"?c:Yt([c,h.pathname])),Ql(h)}function Qy(l,{relative:r}={}){let c=b.useContext(cm);Be(c!=null,"`useViewTransitionState` must be used within `react-router-dom`'s `RouterProvider`. Did you accidentally import `RouterProvider` from `react-router`?");let{basename:o}=ym("useViewTransitionState"),f=Jl(l,{relative:r});if(!c.isTransitioning)return!1;let h=fa(c.currentLocation.pathname,o)||c.currentLocation.pathname,g=fa(c.nextLocation.pathname,o)||c.nextLocation.pathname;return vs(f.pathname,g)!=null||vs(f.pathname,h)!=null}const $y='<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 xm({size:l=20,idSuffix:r,className:c}){const o=`pc-brand-mark-clip-${r}`;return i.jsx("svg",{width:l,height:l,viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",className:c,dangerouslySetInnerHTML:{__html:`<g clip-path="url(#${o})">${$y}</g><defs><clipPath id="${o}"><rect width="24" height="24" fill="white"/></clipPath></defs>`}})}const jc="Parachute";let ln=null,Zl=null;const Jy=3e4;function Ky(){return"/admin/host-admin-token"}function Fy(){const l=`${window.location.pathname}${window.location.search}`;return`/login?next=${encodeURIComponent(l)}`}function Et(){return window.location.replace(Fy()),new Promise(()=>{})}function Wy(){return window.location.replace("/account/"),new Promise(()=>{})}async function Me(){const l=Date.now();return ln&&ln.expiresAt-l>Jy?ln.token:Zl||(Zl=Iy().finally(()=>{Zl=null}),Zl)}async function Iy(){const l=await fetch(Ky(),{method:"GET",headers:{accept:"application/json"},credentials:"same-origin"});if(l.status===401)return ln=null,Et();if(l.status===403)return ln=null,Wy();if(!l.ok)throw new Error(`/admin/host-admin-token failed: ${l.status} ${await l.text()}`);const r=await l.json();if(!r.token||!r.expires_at)throw new Error("/admin/host-admin-token returned malformed body");return ln={token:r.token,expiresAt:new Date(r.expires_at).getTime()},r.token}function je(){ln=null}class V extends Error{constructor(r,c){super(c),this.status=r,this.name="HttpError"}}async function Sc(){const l=await fetch("/.well-known/parachute.json",{headers:{accept:"application/json"}});if(!l.ok)throw new V(l.status,`well-known fetch failed: ${l.status}`);const r=await l.json(),c=r.vaults??[],o=r.services??[];return{moduleInstalled:o.some(h=>h.name==="parachute-vault"||h.name.startsWith("parachute-vault-")),vaults:c.map(h=>{const g={name:h.name,url:h.url,version:h.version,path:Py(h.name,h.url,o)};return h.managementUrl&&(g.managementUrl=h.managementUrl),g})}}function Py(l,r,c){const o=c.find(f=>f.name===`parachute-vault-${l}`||f.path===`/vault/${l}`);if(o)return o.path;try{return new URL(r).pathname}catch{return`/vault/${l}`}}async function jm(l){const r=await fetch(`/admin/vault-admin-token/${encodeURIComponent(l)}`,{method:"GET",headers:{accept:"application/json"},credentials:"same-origin"});if(r.status===401)return Et();if(!r.ok)throw new V(r.status,await $(r));const c=await r.json();if(!c.token||!c.expires_at)throw new V(500,"/admin/vault-admin-token returned malformed body");return{token:c.token,expiresAt:c.expires_at,scopes:c.scopes??[]}}async function e1(l){var g,y;const r=await jm(l),c=await fetch(`/vault/${encodeURIComponent(l)}/.parachute/usage`,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${r.token}`}});if(!c.ok)throw new V(c.status,await $(c));const o=await c.json(),f=(g=o.counts)==null?void 0:g.notes,h=(y=o.bytes)==null?void 0:y.total;if(typeof f!="number"||typeof h!="number")throw new V(500,"/.parachute/usage returned malformed body");return{notes:f,totalBytes:h}}function Sm(l){if(!Number.isFinite(l)||l<0)return"0 B";if(l<1024)return`${Math.round(l)} B`;const r=l/1024;if(r<1024)return`${Math.round(r)} KB`;const c=r/1024;return c<1024?`${c.toFixed(1)} MB`:`${(c/1024).toFixed(1)} GB`}async function t1(l={}){const r=await Me(),c=l.vault?`?vault=${encodeURIComponent(l.vault)}`:"",o=await fetch(`/api/grants${c}`,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${r}`}});if(o.status===401||o.status===403)throw je(),new V(o.status,await $(o));if(!o.ok)throw new V(o.status,await $(o));return(await o.json()).grants??[]}async function a1(l){const r=await Me(),c=await fetch(`/api/grants/${encodeURIComponent(l)}`,{method:"DELETE",headers:{authorization:`Bearer ${r}`}});if(c.status===401||c.status===403)throw je(),new V(c.status,await $(c));if(!c.ok)throw new V(c.status,await $(c))}async function wc(){const l=await fetch("/api/me",{method:"GET",headers:{accept:"application/json"},credentials:"same-origin"});if(!l.ok)throw new V(l.status,await $(l));return await l.json()}async function n1(l){const r=new URLSearchParams({__csrf:l}),c=await fetch("/logout",{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded",accept:"text/html, application/json"},credentials:"same-origin",body:r,redirect:"manual"});if(!(c.status===0||c.ok||c.status===302))throw new V(c.status,await $(c))}let Xh=!1;function l1(l,r){if(/^https?:\/\//i.test(r))return r;const c=l.replace(/\/+$/,"");return r==="/admin"||r==="/admin/"?(Xh||(Xh=!0,console.warn(`resolveManagementUrl: vault declares the legacy per-instance managementUrl ${JSON.stringify(r)}; joining under the vault URL for one release. New semantics: relative ("admin/") = per-instance join, leading-"/" = origin-absolute.`)),`${c}${r}`):r.startsWith("/")?new URL(r,`${c}/`).toString():`${c}/${r}`}async function Qh(l={}){const r=new URLSearchParams;l.revoked&&r.set("revoked",l.revoked),l.subject&&r.set("subject",l.subject),l.createdVia&&r.set("created_via",l.createdVia),l.cursor&&r.set("cursor",l.cursor);const c=r.toString(),o=c?`/api/auth/tokens?${c}`:"/api/auth/tokens",f=await Me(),h=await fetch(o,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${f}`}});if(h.status===401||h.status===403)throw je(),new V(h.status,await $(h));if(!h.ok)throw new V(h.status,await $(h));return await h.json()}async function wm(l){const r=await Me(),c=await fetch("/api/auth/mint-token",{method:"POST",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${r}`},body:JSON.stringify(l)});if(c.status===401||c.status===403)throw je(),new V(c.status,await $(c));if(!c.ok)throw new V(c.status,await $(c));return await c.json()}async function i1(l){const r=await Me(),c=await fetch("/api/auth/revoke-token",{method:"POST",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${r}`},body:JSON.stringify({jti:l})});if(c.status===401||c.status===403)throw je(),new V(c.status,await $(c));if(!c.ok)throw new V(c.status,await $(c));return await c.json()}async function s1(l){const r=await Me(),c=await fetch(`/api/oauth/clients/${encodeURIComponent(l)}`,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${r}`}});if(c.status===401||c.status===403)throw je(),new V(c.status,await $(c));if(!c.ok)throw new V(c.status,await $(c));return await c.json()}async function u1(l,r){const o={method:"POST",headers:{accept:"application/json",authorization:`Bearer ${await Me()}`}};r!==void 0&&(o.headers={...o.headers,"content-type":"application/json"},o.body=JSON.stringify({return_to:r}));const f=await fetch(`/api/oauth/clients/${encodeURIComponent(l)}/approve`,o);if(f.status===401||f.status===403)throw je(),new V(f.status,await $(f));if(!f.ok)throw new V(f.status,await $(f));return await f.json()}async function Cs(){const l=await Me(),r=await fetch("/api/modules",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${l}`}});if(r.status===401||r.status===403)throw je(),new V(r.status,await $(r));if(!r.ok)throw new V(r.status,await $(r));return await r.json()}async function Ns(l,r){const c=await Me();return await fetch(`/api/modules/${encodeURIComponent(l)}/${r}`,{method:"POST",headers:{accept:"application/json",authorization:`Bearer ${c}`}})}async function r1(l){const r=await Ns(l,"install");if(r.status===401||r.status===403)throw je(),new V(r.status,await $(r));if(!r.ok)throw new V(r.status,await $(r));return(await r.json()).operation_id}async function c1(l){const r=await Ns(l,"upgrade");if(r.status===401||r.status===403)throw je(),new V(r.status,await $(r));if(!r.ok)throw new V(r.status,await $(r));return(await r.json()).operation_id}async function o1(l){const r=await Ns(l,"restart");if(r.status===401||r.status===403)throw je(),new V(r.status,await $(r));if(!r.ok)throw new V(r.status,await $(r));return await r.json()}async function d1(l){const r=await Ns(l,"uninstall");if(r.status===401||r.status===403)throw je(),new V(r.status,await $(r));if(!r.ok)throw new V(r.status,await $(r));return await r.json()}async function f1(l){const r=await Me(),c=await fetch("/api/modules/channel",{method:"PUT",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${r}`},body:JSON.stringify({channel:l})});if(c.status===401||c.status===403)throw je(),new V(c.status,await $(c));if(!c.ok)throw new V(c.status,await $(c));return(await c.json()).channel}async function Cm(){const l=await Me(),r=await fetch("/api/settings/hub-origin",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${l}`}});if(r.status===401||r.status===403)throw je(),new V(r.status,await $(r));if(!r.ok)throw new V(r.status,await $(r));return await r.json()}async function $h(l){const r=await Me(),c=await fetch("/api/settings/hub-origin",{method:"PUT",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${r}`},body:JSON.stringify({hub_origin:l})});if(c.status===401||c.status===403)throw je(),new V(c.status,await $(c));if(!c.ok)throw new V(c.status,await $(c));return(await c.json()).hub_origin}async function h1(l){const r=await Me(),c=await fetch(`/api/modules/operations/${encodeURIComponent(l)}`,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${r}`}});if(c.status===401||c.status===403)throw je(),new V(c.status,await $(c));if(!c.ok)throw new V(c.status,await $(c));return await c.json()}async function m1(){const l=await Me(),r=await fetch("/api/users",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${l}`}});if(r.status===401||r.status===403)throw je(),new V(r.status,await $(r));if(!r.ok)throw new V(r.status,await $(r));return(await r.json()).users??[]}async function p1(l){const r=await Me(),c=await fetch("/api/users",{method:"POST",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${r}`},body:JSON.stringify(l)});if(c.status===401||c.status===403)throw je(),new V(c.status,await $(c));if(!c.ok)throw new V(c.status,await $(c));return(await c.json()).user}async function g1(l){const r=await Me(),c=await fetch(`/api/users/${encodeURIComponent(l)}`,{method:"DELETE",headers:{authorization:`Bearer ${r}`}});if(c.status===401||c.status===403)throw c.status===401&&je(),new V(c.status,await $(c));if(!c.ok)throw new V(c.status,await $(c));const o=await c.json().catch(()=>({}));return{revocationLagSeconds:typeof o.revocation_lag_seconds=="number"?o.revocation_lag_seconds:60}}async function v1(l,r){const c=await Me(),o=await fetch(`/api/users/${encodeURIComponent(l)}/reset-password`,{method:"POST",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${c}`},body:JSON.stringify({new_password:r})});if(o.status===401||o.status===403)throw o.status===401&&je(),new V(o.status,await $(o));if(!o.ok)throw new V(o.status,await $(o));const f=await o.json().catch(()=>({}));return{revocationLagSeconds:typeof f.revocation_lag_seconds=="number"?f.revocation_lag_seconds:60}}async function y1(l,r){const c=await Me(),o=await fetch(`/api/users/${encodeURIComponent(l)}/vaults`,{method:"PATCH",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${c}`},body:JSON.stringify({assigned_vaults:r})});if(o.status===401||o.status===403)throw o.status===401&&je(),new V(o.status,await $(o));if(!o.ok)throw new V(o.status,await $(o))}async function Nm(){const l=await Me(),r=await fetch("/api/users/vaults",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${l}`}});if(r.status===401||r.status===403)throw je(),new V(r.status,await $(r));if(!r.ok)throw new V(r.status,await $(r));return(await r.json()).vaults??[]}async function b1(){const l=await Me(),r=await fetch("/api/vault-caps",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${l}`}});if(r.status===401||r.status===403)throw je(),new V(r.status,await $(r));if(!r.ok)throw new V(r.status,await $(r));return(await r.json()).vault_caps??[]}async function x1(l,r){const c=await Me(),o=await fetch(`/api/vault-caps/${encodeURIComponent(l)}`,{method:"PUT",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${c}`},body:JSON.stringify({cap_bytes:r})});if(o.status===401||o.status===403)throw je(),new V(o.status,await $(o));if(!o.ok)throw new V(o.status,await $(o));return(await o.json()).vault_cap}async function j1(){const l=await Me(),r=await fetch("/api/invites",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${l}`}});if(r.status===401||r.status===403)throw je(),new V(r.status,await $(r));if(!r.ok)throw new V(r.status,await $(r));return(await r.json()).invites??[]}async function S1(l){const r=await Me(),c=await fetch("/api/invites",{method:"POST",headers:{"content-type":"application/json",accept:"application/json",authorization:`Bearer ${r}`},body:JSON.stringify(l)});if(c.status===401||c.status===403)throw je(),new V(c.status,await $(c));if(!c.ok)throw new V(c.status,await $(c));return await c.json()}async function w1(l){const r=await Me(),c=await fetch(`/api/invites/${encodeURIComponent(l)}`,{method:"DELETE",headers:{accept:"application/json",authorization:`Bearer ${r}`}});if(c.status===401)throw je(),new V(c.status,await $(c));if(!c.ok)throw new V(c.status,await $(c))}async function oc(){const l=await Me(),r=await fetch("/api/hub",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${l}`}});if(r.status===401||r.status===403)throw je(),new V(r.status,await $(r));if(!r.ok)throw new V(r.status,await $(r));return await r.json()}async function C1(l){const r=await Me(),c=await fetch("/api/hub/upgrade",{method:"POST",headers:{accept:"application/json",authorization:`Bearer ${r}`}});if(c.status===401||c.status===403)throw je(),new V(c.status,await $(c));if(!c.ok)throw new V(c.status,await $(c));return await c.json()}async function N1(){const l=await Me(),r=await fetch("/api/hub/upgrade/status",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${l}`}});if(r.status===404)return null;if(r.status===401||r.status===403)throw je(),new V(r.status,await $(r));if(!r.ok)throw new V(r.status,await $(r));return await r.json()}async function Cc(){const l=await fetch("/api/admin-lock",{method:"GET",headers:{accept:"application/json"},credentials:"same-origin"});if(l.status===401)return Et();if(!l.ok)throw new V(l.status,await $(l));return await l.json()}async function $n(l,r,c={}){return await fetch(`/api/admin-lock${l}`,{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},credentials:"same-origin",body:JSON.stringify({__csrf:r,...c})})}async function E1(l,r){const c=await $n("/unlock",l,{pin:r});if(!c.ok)throw new V(c.status,await $(c));return await c.json().catch(()=>({})),Cc()}async function k1(l){const r=await $n("/lock",l);if(!r.ok)throw new V(r.status,await $(r))}async function T1(l){const r=await $n("/heartbeat",l);if(!r.ok)throw new V(r.status,await $(r));return await r.json()}async function _1(l,r,c){const o={pin:r};c!==void 0&&(o.idle_seconds=c);const f=await $n("/set",l,o);if(!f.ok)throw new V(f.status,await $(f))}async function A1(l,r,c,o){const f={new_pin:r};c&&(f.current_pin=c),o!==void 0&&(f.idle_seconds=o);const h=await $n("/change",l,f);if(!h.ok)throw new V(h.status,await $(h))}async function R1(l,r){const c={};r&&(c.current_pin=r);const o=await $n("/remove",l,c);if(!o.ok)throw new V(o.status,await $(o))}async function $(l){try{const r=await l.text(),c=JSON.parse(r);if(c.error_description)return c.error_description;if(c.error)return c.error;if(r)return r}catch{}return`${l.status} ${l.statusText}`}async function M1(){const l=await fetch("/api/connections/catalog",{method:"GET",headers:{accept:"application/json"},credentials:"same-origin"});if(l.status===401)return Et();if(!l.ok)throw new V(l.status,await $(l));const r=await l.json(),c=Array.isArray(r.events)?r.events:[],o=Array.isArray(r.actions)?r.actions:[],f=Array.isArray(r.templates)?r.templates:[];return{events:c,actions:o,templates:f}}async function z1(){const l=await fetch("/admin/connections",{method:"GET",headers:{accept:"application/json"},credentials:"same-origin"});if(l.status===401)return Et();if(!l.ok)throw new V(l.status,await $(l));const r=await l.json();return Array.isArray(r.connections)?r.connections:[]}async function O1(l){const r=await fetch("/admin/connections",{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},credentials:"same-origin",body:JSON.stringify(l)});if(r.status===401)return Et();if(!r.ok)throw new V(r.status,await $(r));const c=await r.json();if(!c.connection||typeof c.connection.id!="string")throw new V(500,"/admin/connections returned a malformed connection body");return{connection:c.connection,...c.connect?{connect:c.connect}:{}}}async function U1(l){const r=await fetch(`/admin/connections/${encodeURIComponent(l)}/approve`,{method:"POST",headers:{accept:"application/json"},credentials:"same-origin"});if(r.status===401){await Et();return}if(!r.ok)throw new V(r.status,await $(r))}async function D1(l){const r=await fetch(`/admin/connections/${encodeURIComponent(l)}`,{method:"DELETE",headers:{accept:"application/json"},credentials:"same-origin"});if(r.status===401){await Et();return}if(r.status===207){let c="connection teardown partially failed";try{const o=await r.json();Array.isArray(o.errors)&&o.errors.length>0&&(c=o.errors.map(f=>`${f.step??"step"}: ${f.detail??"failed"}`).join("; "))}catch{}throw new V(207,c)}if(!r.ok)throw new V(r.status,await $(r))}async function H1(){const l=await Me(),r=await fetch("/admin/grants",{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${l}`}});if(r.status===401||r.status===403)throw je(),new V(r.status,await $(r));if(!r.ok)throw new V(r.status,await $(r));const c=await r.json();return Array.isArray(c.grants)?c.grants:[]}async function tc(l,r){const c=await fetch(`/admin/grants/${encodeURIComponent(l)}/approve`,{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},credentials:"same-origin",body:JSON.stringify(r!==void 0?{token:r}:{})});if(c.status===401)return Et();if(!c.ok)throw new V(c.status,await $(c));return await c.json()}async function B1(l){const r=await fetch(`/admin/grants/${encodeURIComponent(l)}/revoke`,{method:"POST",headers:{accept:"application/json"},credentials:"same-origin"});if(r.status===401){await Et();return}if(!r.ok)throw new V(r.status,await $(r))}async function Es(l,r,c={}){return await fetch(`/api/account${l}`,{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},credentials:"same-origin",body:JSON.stringify({__csrf:r,...c})})}async function L1(l){const r=await Es("/2fa/start",l);if(r.status===401)return Et();if(!r.ok)throw new V(r.status,await $(r));return await r.json()}async function q1(l,r,c){const o=await Es("/2fa/confirm",l,{secret:r,code:c});if(o.status===401)return Et();if(!o.ok)throw new V(o.status,await $(o));return await o.json()}async function Y1(l,r){const c=await Es("/2fa/disable",l,{password:r});if(c.status===401){await Et();return}if(!c.ok)throw new V(c.status,await $(c))}async function G1(l,r,c){const o=await Es("/password",l,{current_password:r,new_password:c});if(o.status===401)throw new V(o.status,await $(o));if(!o.ok)throw new V(o.status,await $(o))}const V1=3e4;function Jh(l){const r=Math.max(0,l),c=Math.floor(r/1e3);if(c<60)return`${c}s`;const o=Math.floor(c/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 ac(l){const r=new Date(l);if(Number.isNaN(r.getTime()))return l;const c=o=>String(o).padStart(2,"0");return`${r.getUTCFullYear()}-${c(r.getUTCMonth()+1)}-${c(r.getUTCDate())} ${c(r.getUTCHours())}:${c(r.getUTCMinutes())}:${c(r.getUTCSeconds())} UTC`}function Z1(l){var r;if(l.source==="container")return"container";if(l.source==="npm")return"npm";if(l.source==="bun-linked"){const c=(r=l.bun_linked_path)==null?void 0:r.split("/").filter(Boolean).pop();return c&&l.git_head?`bun-linked → ${c} @ ${l.git_head}`:c?`bun-linked → ${c}`:"bun-linked"}return"unknown"}function X1(){const[l,r]=b.useState(null),[c,o]=b.useState(!1),[f,h]=b.useState(!1),[g,y]=b.useState(null),m=b.useCallback(async()=>{h(!0);try{const v=await oc();r(v),y(new Date)}catch{}finally{h(!1)}},[]);return b.useEffect(()=>{m()},[m]),b.useEffect(()=>{const v=()=>{m()};window.addEventListener("focus",v);const x=window.setInterval(()=>{m()},V1);return()=>{window.removeEventListener("focus",v),window.clearInterval(x)}},[m]),l?i.jsxs("footer",{className:"hub-version-badge","data-testid":"hub-version-badge",children:[i.jsxs("button",{type:"button",className:"hub-version-badge-summary",onClick:()=>o(v=>!v),"aria-expanded":c,"data-testid":"hub-version-badge-summary",children:["Hub ",i.jsx("strong",{children:l.version})," · running"," ",i.jsx("span",{"data-testid":"hub-version-badge-uptime",children:Jh(l.uptime_ms)})," ·"," ",i.jsx("span",{className:"hub-version-badge-source","data-testid":"hub-version-badge-source",children:l.source})]}),c?i.jsxs("div",{className:"hub-version-badge-panel","data-testid":"hub-version-badge-panel",children:[i.jsxs("dl",{children:[i.jsx("dt",{children:"Hub"}),i.jsx("dd",{children:i.jsxs("code",{children:["@openparachute/hub ",l.version]})}),i.jsx("dt",{children:"Source"}),i.jsx("dd",{children:Z1(l)}),i.jsx("dt",{children:"Started"}),i.jsxs("dd",{children:[ac(l.started_at)," (",Jh(l.uptime_ms)," ago)"]}),l.container_build_time?i.jsxs(i.Fragment,{children:[i.jsx("dt",{children:"Built"}),i.jsx("dd",{children:ac(l.container_build_time)})]}):null,l.render_commit?i.jsxs(i.Fragment,{children:[i.jsx("dt",{children:"Commit"}),i.jsxs("dd",{children:[i.jsx("code",{children:l.render_commit.slice(0,8)}),l.render_branch?i.jsxs(i.Fragment,{children:[" on ",i.jsx("code",{children:l.render_branch})]}):null]})]}):null,g?i.jsxs(i.Fragment,{children:[i.jsx("dt",{children:"Last checked"}),i.jsx("dd",{children:ac(g.toISOString())})]}):null]}),i.jsx("button",{type:"button",className:"hub-version-badge-refresh secondary",onClick:()=>void m(),disabled:f,"data-testid":"hub-version-badge-refresh",children:f?"Refreshing…":"Refresh"})]}):null]}):null}function Q1({csrf:l,onUnlocked:r}){const[c,o]=b.useState(""),[f,h]=b.useState(!1),[g,y]=b.useState(null),m=b.useRef(null);b.useEffect(()=>{var x;(x=m.current)==null||x.focus()},[]);async function v(x){var j;if(x.preventDefault(),!f){h(!0),y(null);try{await E1(l,c),o(""),r()}catch(_){const k=_ instanceof V?_.status===429?"Too many attempts — wait a moment and try again.":"Incorrect PIN.":_ instanceof Error?_.message:"Unlock failed.";y(k),o(""),(j=m.current)==null||j.focus()}finally{h(!1)}}}return i.jsx("main",{className:"lock-screen","data-testid":"admin-lock-screen","aria-label":"Admin console locked",children:i.jsxs("div",{className:"lock-card",children:[i.jsx(xm,{size:32,idSuffix:"lock",className:"lock-brand-mark"}),i.jsxs("h1",{className:"lock-title",children:[jc," is locked"]}),i.jsx("p",{className:"muted lock-sub",children:"Enter your PIN to unlock the admin console."}),i.jsxs("form",{onSubmit:x=>void v(x),className:"lock-form",children:[i.jsx("label",{htmlFor:"admin-lock-pin",className:"visually-hidden",children:"PIN"}),i.jsx("input",{id:"admin-lock-pin",ref:m,type:"password",inputMode:"numeric",autoComplete:"off",pattern:"[0-9]*",maxLength:12,placeholder:"••••",value:c,disabled:f,onChange:x=>o(x.target.value.replace(/[^0-9]/g,"")),className:"lock-pin-input","data-testid":"admin-lock-pin-input","aria-invalid":g?"true":void 0}),i.jsx("button",{type:"submit",disabled:f||c.length<4,"data-testid":"admin-lock-unlock",children:f?"Unlocking…":"Unlock"})]}),g&&i.jsx("div",{className:"error lock-error",role:"alert","data-testid":"admin-lock-error",children:g})]})})}const $1=3e4;function J1(l,r){const[c,o]=b.useState(!0),[f,h]=b.useState(!1),[g,y]=b.useState(!1),m=b.useRef(null),v=b.useRef(0),x=b.useRef(900),j=b.useRef(l),_=b.useRef(!1);j.current=l,_.current=f;const k=b.useCallback(()=>{m.current&&(clearTimeout(m.current),m.current=null)},[]),C=b.useCallback(()=>{k();const z=Number.isFinite(x.current)?x.current:900;m.current=setTimeout(()=>h(!0),Math.max(1,z)*1e3)},[k]),O=b.useCallback(z=>{y(z.configured),h(z.locked),Number.isFinite(z.idle_seconds)&&(x.current=z.idle_seconds),z.configured&&!z.locked?C():k()},[C,k]),L=b.useCallback(()=>{r&&Cc().then(O).catch(()=>{}).finally(()=>o(!1))},[r,O]);return b.useEffect(()=>{if(!r){o(!1);return}L()},[r,L]),b.useEffect(()=>{if(!r||!g)return;function z(){if(_.current)return;C();const P=Date.now();if(P-v.current<$1)return;v.current=P;const K=j.current;K&&T1(K).then(J=>{J.locked?h(!0):Number.isFinite(J.idle_seconds)&&(x.current=J.idle_seconds)}).catch(()=>{})}const Z=["pointerdown","keydown","scroll","mousemove"];for(const P of Z)window.addEventListener(P,z,{passive:!0});function X(){L()}return window.addEventListener("focus",X),()=>{for(const P of Z)window.removeEventListener(P,z);window.removeEventListener("focus",X)}},[r,g,C,L]),b.useEffect(()=>()=>k(),[k]),{loading:c,locked:f,configured:g,refresh:L}}function K1(){const[l,r]=b.useState({kind:"loading"}),c=b.useCallback(async()=>{try{const o=await wc();if(!o.hasSession){r({kind:"signed-out"});return}r({kind:"ok",csrf:o.csrf,twoFactorEnabled:o.two_factor_enabled})}catch{r({kind:"signed-out"})}},[]);return b.useEffect(()=>{c()},[c]),l.kind==="loading"?i.jsx("div",{className:"empty",children:"Loading account…"}):l.kind==="signed-out"?i.jsxs("div",{className:"empty",children:["You're not signed in."," ",i.jsx("a",{href:`/login?next=${encodeURIComponent(window.location.pathname)}`,children:"Sign in"})," to manage your account."]}):i.jsxs("section",{className:"settings","data-testid":"account-page",children:[i.jsx("h1",{children:"My account"}),i.jsx("p",{className:"muted",children:"Manage your own sign-in credentials. Changes here apply to your account only."}),i.jsx(F1,{csrf:l.csrf}),i.jsx(W1,{csrf:l.csrf,enabled:l.twoFactorEnabled,onChanged:()=>void c()})]})}function F1({csrf:l}){const[r,c]=b.useState(""),[o,f]=b.useState(""),[h,g]=b.useState(""),[y,m]=b.useState(!1),[v,x]=b.useState(null),[j,_]=b.useState(null);async function k(C){if(C.preventDefault(),!y){if(x(null),_(null),!r||!o||!h){x("All three fields are required.");return}if(o.length<12){x("New password must be at least 12 characters (a passphrase is fine).");return}if(o!==h){x("New password and confirmation do not match.");return}m(!0);try{await G1(l,r,o),c(""),f(""),g(""),_("Password changed. Tokens minted under your old password were revoked.")}catch(O){x(O instanceof Error?O.message:String(O))}finally{m(!1)}}}return i.jsxs("section",{className:"settings-block","aria-labelledby":"account-password-heading","data-testid":"account-password",children:[i.jsx("h2",{id:"account-password-heading",children:"Password"}),i.jsx("p",{className:"muted",children:"Change the password you use to sign in to this hub."}),i.jsxs("form",{onSubmit:C=>void k(C),className:"settings-form",children:[i.jsxs("label",{children:["Current password",i.jsx("input",{type:"password",autoComplete:"current-password",value:r,disabled:y,onChange:C=>c(C.target.value),"data-testid":"account-current-password"})]}),i.jsxs("label",{children:["New password (12+ characters)",i.jsx("input",{type:"password",autoComplete:"new-password",value:o,disabled:y,onChange:C=>f(C.target.value),"data-testid":"account-new-password"})]}),i.jsxs("label",{children:["Confirm new password",i.jsx("input",{type:"password",autoComplete:"new-password",value:h,disabled:y,onChange:C=>g(C.target.value),"data-testid":"account-confirm-password"})]}),i.jsx("div",{className:"actions",children:i.jsx("button",{type:"submit",disabled:y,"data-testid":"account-change-password",children:y?"Saving…":"Change password"})})]}),v&&i.jsx("div",{className:"error","data-testid":"account-password-error",children:v}),j&&i.jsx("p",{className:"muted","data-testid":"account-password-notice",children:j})]})}function W1({csrf:l,enabled:r,onChanged:c}){const[o,f]=b.useState({kind:"idle"}),[h,g]=b.useState(!1),[y,m]=b.useState(null),[v,x]=b.useState(""),[j,_]=b.useState("");async function k(){if(!h){m(null),g(!0);try{const z=await L1(l);f({kind:"enrolling",start:z}),x("")}catch(z){m(z instanceof Error?z.message:String(z))}finally{g(!1)}}}async function C(z){if(z.preventDefault(),!(h||o.kind!=="enrolling")){if(m(null),!/^\d{6}$/.test(v.trim())){m("Enter the 6-digit code from your authenticator app.");return}g(!0);try{const Z=await q1(l,o.start.secret,v.trim());f({kind:"backup-codes",codes:Z.backup_codes}),x(""),c()}catch(Z){m(Z instanceof Error?Z.message:String(Z))}finally{g(!1)}}}function O(){f({kind:"idle"}),x(""),m(null)}async function L(z){if(z.preventDefault(),!h){if(m(null),!j){m("Enter your current password to turn off two-factor.");return}g(!0);try{await Y1(l,j),_(""),f({kind:"idle"}),c()}catch(Z){m(Z instanceof Error?Z.message:String(Z))}finally{g(!1)}}}return i.jsxs("section",{className:"settings-block","aria-labelledby":"account-2fa-heading","data-testid":"account-2fa",children:[i.jsx("h2",{id:"account-2fa-heading",children:"Two-factor authentication"}),i.jsx("p",{className:"muted",children:"Add a time-based one-time code (TOTP) from an authenticator app as a second step at sign-in."}),i.jsxs("p",{children:["Status:"," ",i.jsx("span",{className:`lock-status-pill ${r?"lock-status-on":"lock-status-off"}`,"data-testid":"account-2fa-status",children:r?"Enabled":"Off"})]}),o.kind==="backup-codes"?i.jsxs("div",{"data-testid":"account-2fa-backup-codes",children:[i.jsxs("p",{children:[i.jsx("strong",{children:"Save these backup codes now."})," Each can be used once if you lose your authenticator. They won't be shown again."]}),i.jsx("ul",{className:"backup-codes",children:o.codes.map(z=>i.jsx("li",{children:i.jsx("code",{children:z})},z))}),i.jsx("div",{className:"actions",children:i.jsx("button",{type:"button",onClick:()=>f({kind:"idle"}),"data-testid":"account-2fa-codes-done",children:"I've saved my codes"})})]}):r?i.jsxs("form",{onSubmit:z=>void L(z),className:"settings-form",children:[i.jsx("p",{className:"muted",children:"Turning off two-factor requires your current password."}),i.jsxs("label",{children:["Current password",i.jsx("input",{type:"password",autoComplete:"current-password",value:j,disabled:h,onChange:z=>_(z.target.value),"data-testid":"account-2fa-disable-password"})]}),i.jsx("div",{className:"actions",children:i.jsx("button",{type:"submit",className:"destructive",disabled:h,"data-testid":"account-2fa-disable",children:h?"Turning off…":"Turn off two-factor"})})]}):o.kind==="enrolling"?i.jsxs("form",{onSubmit:z=>void C(z),className:"settings-form",children:[i.jsx("p",{children:"Scan this QR code with your authenticator app, then enter the 6-digit code it shows to confirm."}),i.jsx("img",{src:o.start.qr_data_url,alt:"Two-factor QR code",width:180,height:180,"data-testid":"account-2fa-qr"}),i.jsxs("p",{className:"muted",children:["Can't scan? Enter this key manually:"," ",i.jsx("code",{"data-testid":"account-2fa-secret",children:o.start.secret})]}),i.jsxs("label",{children:["6-digit code",i.jsx("input",{type:"text",inputMode:"numeric",autoComplete:"one-time-code",maxLength:6,value:v,disabled:h,onChange:z=>x(z.target.value.replace(/[^0-9]/g,"")),"data-testid":"account-2fa-code"})]}),i.jsxs("div",{className:"actions",children:[i.jsx("button",{type:"submit",disabled:h,"data-testid":"account-2fa-confirm",children:h?"Verifying…":"Verify and enable"}),i.jsx("button",{type:"button",className:"destructive",disabled:h,onClick:O,"data-testid":"account-2fa-cancel",children:"Cancel"})]})]}):i.jsx("div",{className:"actions",children:i.jsx("button",{type:"button",disabled:h,onClick:()=>void k(),"data-testid":"account-2fa-enroll",children:h?"Starting…":"Set up two-factor"})}),y&&i.jsx("div",{className:"error","data-testid":"account-2fa-error",children:y})]})}function I1(){const{clientId:l}=Wv(),r=l??"",[c]=bm(),o=P1(c.get("return_to")),[f,h]=b.useState({kind:"loading"}),[g,y]=b.useState({kind:"idle"}),[m,v]=b.useState(0);b.useEffect(()=>{if(!r){h({kind:"error",message:"missing client id in URL"});return}let j=!1;return h({kind:"loading"}),s1(r).then(_=>{j||(h({kind:"ok",client:_}),_.status==="approved"&&(y({kind:"approved",alreadyApproved:!0}),o&&window.location.assign(o)))}).catch(_=>{if(j)return;if(_ instanceof V&&_.status===404){h({kind:"not_found"});return}const k=_ instanceof Error?_.message:String(_);h({kind:"error",message:k})}),()=>{j=!0}},[r,m,o]);async function x(){y({kind:"approving"});try{const j=await u1(r,o||void 0);if(j.redirect_to&&Em(j.redirect_to)){window.location.assign(j.redirect_to);return}y({kind:"approved",alreadyApproved:j.already_approved})}catch(j){const _=j instanceof V?`approve failed (${j.status}): ${j.message}`:j instanceof Error?j.message:String(j);y({kind:"error",message:_})}}return i.jsxs("div",{children:[i.jsx("div",{className:"list-header",children:i.jsx("h2",{children:"Approve app"})}),e0({loadState:f,action:g,onApprove:x,onRetry:()=>v(j=>j+1)})]})}function Em(l){return!(!l||!l.startsWith("/")||l.startsWith("//"))}function P1(l){return l===null?null:Em(l)?l:null}function e0({loadState:l,action:r,onApprove:c,onRetry:o}){if(l.kind==="loading")return i.jsx("p",{className:"muted","data-loading":"true",children:"Loading…"});if(l.kind==="not_found")return i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"Unknown client."}),i.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."," ",i.jsx(lt,{to:"/permissions",children:"Back to permissions"}),"."]})]});if(l.kind==="error")return i.jsxs(i.Fragment,{children:[i.jsxs("div",{className:"error-banner",children:["Couldn't load the client: ",i.jsx("code",{children:l.message})]}),i.jsx("button",{type:"button",onClick:o,className:"secondary",children:"Retry"})]});const{client:f}=l,h=f.client_name??f.client_id;if(r.kind==="approved")return i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:r.alreadyApproved?"Already approved.":"Approved."}),i.jsxs("p",{className:"muted",children:[i.jsx("code",{children:h})," ",r.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."]}),i.jsx("p",{className:"muted",style:{marginTop:"1rem"},children:i.jsx(lt,{to:"/permissions",children:"View permissions"})})]});const g=r.kind==="approving",y=r.kind==="error"?r.message:null;return i.jsxs("div",{children:[i.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."}),i.jsx("div",{className:"vault-row",style:{marginTop:"1rem"},children:i.jsxs("div",{className:"body",children:[i.jsx("div",{className:"name",children:i.jsx("code",{children:h})}),i.jsxs("div",{className:"dim",children:[i.jsx("span",{className:"muted",children:"client_id: "}),i.jsx("code",{children:f.client_id})]}),i.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[i.jsx("span",{className:"muted",children:"redirect_uris: "}),f.redirect_uris.map((m,v)=>i.jsxs("span",{children:[i.jsx("code",{children:m}),v<f.redirect_uris.length-1?" ":null]},m))]}),f.scopes.length>0?i.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[i.jsx("span",{className:"muted",children:"requested scopes: "}),f.scopes.map((m,v)=>i.jsxs("span",{children:[i.jsx("code",{children:t0(m)}),v<f.scopes.length-1?" ":null]},m)),f.scopes.some(km)?i.jsxs("p",{className:"muted",style:{marginTop:"0.4rem",fontStyle:"italic",fontSize:"0.85rem"},children:[i.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,i.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[i.jsx("span",{className:"muted",children:"registered: "}),i.jsx("code",{title:f.registered_at,children:f.registered_at})]}),y?i.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:i.jsx("code",{children:y})}):null]})}),i.jsxs("div",{style:{marginTop:"1rem",display:"flex",gap:"0.5rem"},children:[i.jsx("button",{type:"button",onClick:()=>{c()},disabled:g,children:g?"Approving…":`Approve ${h}`}),i.jsx(lt,{to:"/permissions",className:"secondary",style:{alignSelf:"center"},children:"Cancel"})]})]})}function t0(l){return km(l)?`vault:*:${l.split(":")[1]}`:l}function km(l){const r=l.split(":");if(r.length!==2||r[0]!=="vault")return!1;const c=r[1];return c==="read"||c==="write"}function a0(l){const r={};for(const c of l.parameters){const o=/^sink\.params\.(.+)$/.exec(c.target);o!=null&&o[1]&&(r[o[1]]=c.example??`my-${c.key}`)}return r}function nc(l,r){return l instanceof V?`${r} failed (${l.status}): ${l.message}`:l instanceof Error?l.message:String(l)}function Kh({value:l,label:r="Copy"}){const[c,o]=b.useState(!1);return i.jsx("button",{type:"button",className:"secondary",onClick:()=>{typeof navigator>"u"||!navigator.clipboard||navigator.clipboard.writeText(l).then(()=>{o(!0),setTimeout(()=>o(!1),2e3)})},children:c?"Copied ✓":r})}function n0(){const[l,r]=b.useState({kind:"loading"}),[c,o]=b.useState(0),[f,h]=b.useState({kind:"idle"}),[g,y]=b.useState({kind:"idle"}),[m,v]=b.useState({kind:"idle"}),[x,j]=b.useState(""),[_,k]=b.useState(""),[C,O]=b.useState(""),[L,z]=b.useState(""),[Z,X]=b.useState(""),[P,K]=b.useState(""),[J,E]=b.useState("");b.useEffect(()=>{let ie=!1;return r({kind:"loading"}),Promise.all([z1(),M1(),Sc()]).then(([W,ge,be])=>{ie||(r({kind:"ok",data:{connections:W,catalog:ge,vaults:be.vaults}}),O(H=>{var F;return H||((F=be.vaults[0])==null?void 0:F.name)||""}))}).catch(W=>{ie||r({kind:"error",message:W instanceof Error?W.message:String(W)})}),()=>{ie=!0}},[c]);function D(ie){j(ie.source.module),k(ie.source.event),X(ie.sink.module),K(ie.sink.action),z(ie.source.filter?JSON.stringify(ie.source.filter,null,2):"");const W=a0(ie);E(Object.keys(W).length>0?JSON.stringify(W,null,2):""),h({kind:"idle"})}async function Q(ie){if(ie.preventDefault(),!x||!_||!Z||!P){h({kind:"error",message:"Pick a source event and a sink action."});return}let W,ge;try{W=L.trim()?JSON.parse(L):void 0}catch{h({kind:"error",message:"Filter is not valid JSON."});return}try{ge=J.trim()?JSON.parse(J):void 0}catch{h({kind:"error",message:"Action params are not valid JSON."});return}if(x==="vault"&&!C){h({kind:"error",message:"Pick a vault for the source event."});return}h({kind:"submitting"});try{const be=await O1({source:{module:x,...x==="vault"?{vault:C}:{},event:_,...W?{filter:W}:{}},sink:{module:Z,action:P,...ge?{params:ge}:{}}});h({kind:"created",...be.connect?{connect:be.connect}:{}}),o(H=>H+1)}catch(be){h({kind:"error",message:nc(be,"Create")})}}async function te(ie){y({kind:"removing",id:ie});try{await D1(ie),y({kind:"idle"}),o(W=>W+1)}catch(W){y({kind:"error",id:ie,message:nc(W,"Remove")})}}async function le(ie){if(m.kind!=="approving"){v({kind:"approving",id:ie});try{await U1(ie),v({kind:"idle"}),o(W=>W+1)}catch(W){v({kind:"error",id:ie,message:nc(W,"Approve")})}}}return i.jsxs("div",{"data-route-content":"true",children:[i.jsx("div",{className:"list-header",children:i.jsx("h1",{children:"Connections"})}),i.jsxs("p",{className:"muted",children:["A connection wires ",i.jsx("em",{children:"when [event] in a module → do [action] in another module"}),". The hub mints the tokens and registers the trigger. Modules declare presets for the common cases — pick one below to pre-fill the builder."]}),l0(l,g,y,te,m,le,()=>o(ie=>ie+1)),l.kind==="ok"&&i.jsx(c0,{catalog:l.data.catalog,vaults:l.data.vaults,sourceModule:x,setSourceModule:j,sourceEvent:_,setSourceEvent:k,vault:C,setVault:O,filterText:L,setFilterText:z,sinkModule:Z,setSinkModule:X,sinkAction:P,setSinkAction:K,paramsText:J,setParamsText:E,createSt:f,setCreateSt:h,onSubmit:Q,onApplyTemplate:D})]})}function l0(l,r,c,o,f,h,g){if(l.kind==="loading")return i.jsx("p",{className:"muted",children:"Loading connections…"});if(l.kind==="error")return i.jsxs(i.Fragment,{children:[i.jsxs("div",{className:"error-banner",children:["Couldn't load connections: ",i.jsx("code",{children:l.message})]}),i.jsx("button",{type:"button",onClick:g,className:"secondary",children:"Retry"})]});const{connections:y}=l.data;return y.length===0?i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No connections yet."}),i.jsx("p",{className:"muted",children:"Build one below, or start from a module's preset."})]}):i.jsx(u0,{connections:y,removeSt:r,setRemoveSt:c,onConfirmRemove:o,approveSt:f,onApprove:h})}function Tm(l){return l.requested_by&&l.requested_by.length>0?l.requested_by:"custom"}function i0(l){return l==="custom"?"Custom (built here)":`Added from ${l}`}function s0(l){const r=new Map;for(const c of l){const o=Tm(c),f=r.get(o);f?f.push(c):r.set(o,[c])}return Array.from(r.entries()).sort(([c],[o])=>c===o?0:c==="custom"?1:o==="custom"?-1:c.localeCompare(o))}function u0({connections:l,removeSt:r,setRemoveSt:c,onConfirmRemove:o,approveSt:f,onApprove:h}){const g=s0(l);return i.jsx(i.Fragment,{children:g.map(([y,m])=>i.jsxs("div",{className:"channel-list",style:{marginTop:"1rem"},"data-provenance-group":y,children:[i.jsx("h3",{style:{marginBottom:"0.25rem"},children:i0(y)}),i.jsx("div",{className:"table-scroll",children:i.jsxs("table",{className:"channel-table",children:[i.jsx("thead",{children:i.jsxs("tr",{children:[i.jsx("th",{scope:"col",children:"Connection"}),i.jsx("th",{scope:"col",children:"When"}),i.jsx("th",{scope:"col",children:"Do"}),i.jsx("th",{scope:"col",children:"Source"}),i.jsx("th",{scope:"col",children:"Actions"})]})}),i.jsx("tbody",{children:m.map(v=>r0(v,r,c,o,f,h))})]})})]},y))})}function r0(l,r,c,o,f,h){const g=r.kind==="confirming"&&r.id===l.id,y=r.kind==="removing"&&r.id===l.id,m=r.kind==="error"&&r.id===l.id?r.message:null,v=l.status==="pending",x=f.kind==="approving"&&f.id===l.id,j=f.kind==="error"&&f.id===l.id?f.message:null,_=`${l.source.module}.${l.source.event}${l.source.vault?` (${l.source.vault})`:""}`,k=`${l.sink.module}.${l.sink.action}`,C=Tm(l);return i.jsxs("tr",{"data-connection-id":l.id,"data-requested-by":C,children:[i.jsxs("td",{children:[i.jsx("code",{children:l.id}),v&&i.jsxs(i.Fragment,{children:[" ",i.jsx("span",{className:"tag","data-testid":"pending-badge",title:`A module claimed a credential it already holds${l.provisioned.scope?` (${l.provisioned.scope})`:""} — approving lets it renew before expiry. Nothing new is granted.`,children:"pending approval"})]})]}),i.jsx("td",{children:i.jsx("code",{children:_})}),i.jsx("td",{children:i.jsx("code",{children:k})}),i.jsx("td",{children:C==="custom"?i.jsx("span",{className:"muted",children:"custom"}):i.jsx("span",{className:"tag","data-testid":"provenance-badge",children:C})}),i.jsxs("td",{children:[v&&i.jsx("button",{type:"button",disabled:x,onClick:()=>void h(l.id),"aria-label":`Approve ${l.id}`,"data-testid":"approve-connection",style:{marginRight:"0.5rem"},children:x?"Approving…":"Approve"}),g?i.jsxs("dialog",{open:!0,className:"error-banner",style:{marginTop:"0.25rem",background:"var(--bg-warn, #fffbe6)"},"aria-label":`Confirm remove ${l.id}`,children:[i.jsxs("p",{children:["Remove connection ",i.jsx("code",{children:l.id}),"? This tears down the provisioned trigger (and channel config, if any)."]}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"button",className:"destructive",onClick:()=>{o(l.id)},disabled:y,children:y?"Removing…":"Remove"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>c({kind:"idle"}),disabled:y,children:"Cancel"})]})]}):i.jsx("button",{type:"button",className:"secondary",disabled:y,onClick:()=>c({kind:"confirming",id:l.id}),"aria-label":`Remove ${l.id}`,children:y?"Removing…":"Remove"}),m&&i.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:i.jsx("code",{children:m})}),j&&i.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:i.jsx("code",{children:j})})]})]},l.id)}function c0(l){const{catalog:r,createSt:c}=l,o=c.kind==="submitting",f=m=>r.events.filter(v=>v.module===m),h=m=>r.actions.filter(v=>v.module===m),g=Array.from(new Set(r.events.map(m=>m.module))),y=Array.from(new Set(r.actions.map(m=>m.module)));return i.jsxs("section",{style:{marginTop:"1.5rem"},children:[i.jsxs("div",{style:{display:"flex",gap:"0.5rem",alignItems:"center",flexWrap:"wrap"},children:[i.jsx("h3",{style:{marginRight:"auto"},children:"Build a connection"}),r.templates.map(m=>i.jsxs("button",{type:"button",className:"secondary",onClick:()=>l.onApplyTemplate(m),"data-testid":`preset-${m.module}-${m.key}`,...m.description?{title:m.description}:{},children:["Preset: ",m.title]},`${m.module}/${m.key}`))]}),i.jsxs("form",{onSubmit:m=>void l.onSubmit(m),"aria-label":"Build a connection",children:[i.jsxs("fieldset",{style:{border:"1px solid var(--border, #ddd)",padding:"0.75rem"},children:[i.jsx("legend",{children:"When (source event)"}),i.jsxs("p",{children:[i.jsx("label",{htmlFor:"conn-source-module",children:"Module"}),i.jsx("br",{}),i.jsxs("select",{id:"conn-source-module",value:l.sourceModule,disabled:o,onChange:m=>{l.setSourceModule(m.target.value),l.setSourceEvent("")},children:[i.jsx("option",{value:"",children:"— pick a module —"}),g.map(m=>i.jsx("option",{value:m,children:m},m))]})]}),l.sourceModule&&i.jsxs("p",{children:[i.jsx("label",{htmlFor:"conn-source-event",children:"Event"}),i.jsx("br",{}),i.jsxs("select",{id:"conn-source-event",value:l.sourceEvent,disabled:o,onChange:m=>l.setSourceEvent(m.target.value),children:[i.jsx("option",{value:"",children:"— pick an event —"}),f(l.sourceModule).map(m=>i.jsxs("option",{value:m.key,children:[m.title," (",m.key,")"]},m.key))]})]}),l.sourceModule==="vault"&&i.jsxs("p",{children:[i.jsx("label",{htmlFor:"conn-vault",children:"Vault"}),i.jsx("br",{}),i.jsx("select",{id:"conn-vault",value:l.vault,disabled:o,onChange:m=>l.setVault(m.target.value),children:l.vaults.map(m=>i.jsx("option",{value:m.name,children:m.name},m.name))})]}),i.jsxs("p",{children:[i.jsxs("label",{htmlFor:"conn-filter",children:["Filter ",i.jsx("span",{className:"muted",children:"(JSON — tags / has_metadata / missing_metadata)"})]}),i.jsx("br",{}),i.jsx("textarea",{id:"conn-filter",value:l.filterText,disabled:o,rows:4,style:{width:"100%",fontFamily:"monospace"},onChange:m=>l.setFilterText(m.target.value),placeholder:'{ "tags": ["#agent-message/inbound"] }'})]})]}),i.jsxs("fieldset",{style:{border:"1px solid var(--border, #ddd)",padding:"0.75rem",marginTop:"0.75rem"},children:[i.jsx("legend",{children:"Do (sink action)"}),i.jsxs("p",{children:[i.jsx("label",{htmlFor:"conn-sink-module",children:"Module"}),i.jsx("br",{}),i.jsxs("select",{id:"conn-sink-module",value:l.sinkModule,disabled:o,onChange:m=>{l.setSinkModule(m.target.value),l.setSinkAction("")},children:[i.jsx("option",{value:"",children:"— pick a module —"}),y.map(m=>i.jsx("option",{value:m,children:m},m))]})]}),l.sinkModule&&i.jsxs("p",{children:[i.jsx("label",{htmlFor:"conn-sink-action",children:"Action"}),i.jsx("br",{}),i.jsxs("select",{id:"conn-sink-action",value:l.sinkAction,disabled:o,onChange:m=>l.setSinkAction(m.target.value),children:[i.jsx("option",{value:"",children:"— pick an action —"}),h(l.sinkModule).map(m=>i.jsxs("option",{value:m.key,children:[m.title," (",m.key,")"]},m.key))]})]}),i.jsxs("p",{children:[i.jsxs("label",{htmlFor:"conn-params",children:["Action params ",i.jsxs("span",{className:"muted",children:["(JSON — e.g. ",'{ "channel": "eng" }',")"]})]}),i.jsx("br",{}),i.jsx("textarea",{id:"conn-params",value:l.paramsText,disabled:o,rows:3,style:{width:"100%",fontFamily:"monospace"},onChange:m=>l.setParamsText(m.target.value),placeholder:'{ "channel": "eng" }'})]})]}),c.kind==="error"&&i.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:i.jsx("code",{children:c.message})}),i.jsx("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.75rem"},children:i.jsx("button",{type:"submit",disabled:o,children:o?"Creating…":"Create connection"})})]}),c.kind==="created"&&c.connect&&i.jsx(o0,{connect:c.connect,onDismiss:()=>l.setCreateSt({kind:"idle"})}),c.kind==="created"&&!c.connect&&i.jsx("div",{className:"success-banner",style:{marginTop:"1rem"},"data-testid":"connection-created",children:"Connection created."})]})}function o0({connect:l,onDismiss:r}){return i.jsxs("div",{className:"mcp-connect-card",style:{marginTop:"1rem"},"data-testid":"connection-connect-panel",children:[i.jsx("h3",{children:"Connect a session"}),i.jsx("p",{className:"muted",children:"Run these where your Claude Code session will live; authorize in the browser when prompted."}),i.jsxs("div",{className:"mcp-field",children:[i.jsx("span",{className:"mcp-field-label",children:"1 · Register the channel (MCP)"}),i.jsxs("div",{className:"token-box",children:[i.jsx("code",{"data-testid":"connection-mcp-add",children:l.mcpAdd}),i.jsx(Kh,{value:l.mcpAdd})]})]}),i.jsxs("div",{className:"mcp-field",children:[i.jsx("span",{className:"mcp-field-label",children:"2 · Launch a session on the channel"}),i.jsxs("div",{className:"token-box",children:[i.jsx("code",{"data-testid":"connection-launch",children:l.launch}),i.jsx(Kh,{value:l.launch})]}),i.jsxs("p",{className:"muted",style:{marginTop:"0.4rem"},children:["⚠ This launches Claude Code with unrestricted tool access (",i.jsx("code",{children:"--dangerously-skip-permissions"}),") — run it only on a machine you trust for that session."]})]}),i.jsx("div",{style:{marginTop:"0.75rem"},children:i.jsx("button",{type:"button",className:"secondary",onClick:r,children:"Dismiss"})})]})}function Xl(l,r){return l instanceof V||l instanceof Error?l.message:`${r} failed`}function d0(){const[l,r]=b.useState({kind:"loading"}),[c,o]=b.useState({kind:"idle"}),[f,h]=b.useState(0);b.useEffect(()=>{let x=!1;return r({kind:"loading"}),H1().then(j=>{x||r({kind:"ok",grants:j})}).catch(j=>{x||r({kind:"error",message:Xl(j,"Load")})}),()=>{x=!0}},[f]);async function g(x){o({kind:"working",id:x});try{await tc(x),o({kind:"idle"}),h(j=>j+1)}catch(j){o({kind:"error",id:x,message:Xl(j,"Approve")})}}async function y(x,j){if(j.trim().length===0){o({kind:"error",id:x,message:"Paste the API token before approving."});return}o({kind:"working",id:x});try{await tc(x,j),o({kind:"idle"}),h(_=>_+1)}catch(_){o({kind:"error",id:x,message:Xl(_,"Approve")})}}async function m(x){o({kind:"working",id:x});try{const j=await tc(x);if(j.authorizeUrl){window.location.assign(j.authorizeUrl);return}o({kind:"idle"}),h(_=>_+1)}catch(j){o({kind:"error",id:x,message:Xl(j,"Connect")})}}async function v(x){o({kind:"working",id:x});try{await B1(x),o({kind:"idle"}),h(j=>j+1)}catch(j){o({kind:"error",id:x,message:Xl(j,"Revoke")})}}return i.jsxs("div",{"data-route-content":"true",children:[i.jsx("div",{className:"list-header",children:i.jsx("h1",{children:"Grants"})}),i.jsxs("p",{className:"muted",children:["Agents declare connections they want beyond their own vault — other vaults (tag-scoped) and external service credentials. Each one waits here for your approval. A vault note can only"," ",i.jsx("em",{children:"request"}),"; nothing is granted until you approve it below."]}),f0(l,c,o,{onApproveVault:g,onApproveService:y,onConnectMcp:m,onRevoke:v,onRetry:()=>h(x=>x+1)})]})}function f0(l,r,c,o){if(l.kind==="loading")return i.jsx("p",{className:"muted",children:"Loading grants…"});if(l.kind==="error")return i.jsxs("div",{children:[i.jsx("div",{className:"error-banner",children:l.message}),i.jsx("button",{type:"button",onClick:o.onRetry,className:"secondary",children:"Retry"})]});if(l.grants.length===0)return i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No grant requests yet."}),i.jsx("p",{className:"muted",children:"When an agent declares a connection it wants, it shows up here for approval."})]});const f=new Map;for(const g of l.grants){const y=f.get(g.agent)??[];y.push(g),f.set(g.agent,y)}const h=[...f.keys()].sort();return i.jsx("div",{className:"grants-groups",children:h.map(g=>i.jsxs("section",{"aria-label":`Grants for ${g}`,style:{marginBottom:"1.5rem"},children:[i.jsx("h3",{style:{marginBottom:"0.25rem"},children:g}),i.jsx("div",{className:"table-scroll",children:i.jsxs("table",{className:"channel-table",children:[i.jsx("thead",{children:i.jsxs("tr",{children:[i.jsx("th",{children:"Connection"}),i.jsx("th",{children:"Status"}),i.jsx("th",{children:"Actions"})]})}),i.jsx("tbody",{children:(f.get(g)??[]).map(y=>i.jsx(m0,{grant:y,rowSt:r,setRowSt:c,actions:o},y.id))})]})})]},g))})}function h0(l){if(l.kind==="vault"){const r=l.access??"read",c=l.tags&&l.tags.length>0?` (${l.tags.join(", ")})`:"";return`vault: ${l.target} · ${r}${c}`}if(l.kind==="service"){const r=l.inject&&l.inject.length>0?` · inject: ${l.inject.join("+")}`:"";return`service: ${l.target}${r}`}return`mcp: ${l.target}`}function m0({grant:l,rowSt:r,setRowSt:c,actions:o}){const f=r.kind==="working"&&r.id===l.id,h=r.kind==="error"&&r.id===l.id?r.message:null,g=l.connection,y=g.kind==="mcp",m=g.kind==="service",v=l.status!=="approved",x=r.kind==="pasting"&&r.id===l.id?r.token:null;return i.jsxs("tr",{"data-grant-id":l.id,children:[i.jsxs("td",{children:[i.jsx("code",{children:h0(g)}),y&&i.jsx("span",{className:"muted",children:" — remote MCP (OAuth)"})]}),i.jsxs("td",{children:[i.jsx("span",{className:`status status-${l.status}`,children:l.status}),l.reason&&i.jsxs("span",{className:"muted",children:[" — ",l.reason]})]}),i.jsxs("td",{children:[v&&g.kind==="vault"&&i.jsx("button",{type:"button",className:"primary",disabled:f,onClick:()=>o.onApproveVault(l.id),children:f?"Approving…":l.status==="revoked"?"Re-approve":"Approve"}),v&&y&&x===null&&i.jsxs("span",{style:{display:"inline-flex",gap:"0.5rem",alignItems:"center"},children:[i.jsx("button",{type:"button",className:"primary",disabled:f,onClick:()=>o.onConnectMcp(l.id),children:f?"Connecting…":l.status==="needs_consent"?"Reconnect":"Connect"}),i.jsx("button",{type:"button",className:"secondary",disabled:f,onClick:()=>c({kind:"pasting",id:l.id,token:""}),children:"Paste a token instead"})]}),v&&m&&x===null&&i.jsx("button",{type:"button",className:"primary",disabled:f,onClick:()=>c({kind:"pasting",id:l.id,token:""}),children:l.status==="revoked"?"Re-approve…":"Approve…"}),v&&(m||y)&&x!==null&&i.jsxs("span",{style:{display:"inline-flex",gap:"0.5rem",alignItems:"center"},children:[i.jsx("input",{type:"password","aria-label":`API token for ${g.target}`,placeholder:`${g.target} API token`,value:x,onChange:j=>c({kind:"pasting",id:l.id,token:j.target.value})}),i.jsx("button",{type:"button",className:"primary",disabled:f,onClick:()=>o.onApproveService(l.id,x),children:f?"Approving…":"Save & approve"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>c({kind:"idle"}),children:"Cancel"})]}),l.status==="approved"&&i.jsx("button",{type:"button",className:"secondary",disabled:f,onClick:()=>o.onRevoke(l.id),children:f?"Revoking…":"Revoke"}),h&&i.jsxs("span",{className:"error-inline",children:[" ",h]})]})]})}const p0=[{to:"/connections",label:"Connections",desc:"Wire a module event to another module's action."},{to:"/modules",label:"Modules",desc:"Install, upgrade, and manage modules."},{to:"/users",label:"Users",desc:"Manage operators and invite members."},{to:"/tokens",label:"Tokens",desc:"Mint and revoke access tokens."},{to:"/permissions",label:"Permissions",desc:"OAuth consent grants per app."},{to:"/settings",label:"Settings",desc:"Canonical URL, install channel, more."}];function g0(){const[l,r]=b.useState({kind:"loading"}),[c,o]=b.useState(null);b.useEffect(()=>{let g=!1;return Cs().then(y=>{g||r({kind:"ok",modules:y.modules.filter(m=>m.installed)})}).catch(y=>{if(g)return;const m=y instanceof Error?y.message:String(y);r({kind:"error",message:m})}),Sc().then(y=>{g||o(y.vaults.length)}).catch(()=>{g||o(null)}),()=>{g=!0}},[]);const f=l.kind==="ok"?l.modules:[],h=f.flatMap(g=>g.uis.map(y=>({key:`${g.short}:${y.name}`,label:y.display_name,tagline:y.tagline,href:y.path,ownerLabel:g.display_name})));return i.jsxs("div",{"data-route-content":"true",className:"admin-home",children:[i.jsx("div",{className:"list-header",children:i.jsx("h1",{children:"Hub"})}),i.jsxs("p",{className:"muted",children:["Everything on this hub, in one place. Hub-native sections open here in the admin shell; module surfaces (marked ",i.jsx("span",{className:"ext-mark",children:"↗"}),") open the module's own UI."]}),i.jsxs("section",{className:"home-group","data-testid":"home-administer",children:[i.jsxs("div",{className:"home-group-head",children:[i.jsx("h2",{children:"Administer"}),i.jsx("span",{className:"home-group-tag home-group-tag-hub",children:"hub"})]}),i.jsx("p",{className:"muted home-group-sub",children:"Cross-cutting host admin. Opens here in the shell — the nav stays with you."}),i.jsx("div",{className:"home-grid",children:p0.map(g=>i.jsxs(lt,{to:g.to,className:"home-card home-card-hub","data-section":g.to,children:[i.jsx("span",{className:"home-card-title",children:g.label}),i.jsx("span",{className:"home-card-desc",children:g.desc})]},g.to))})]}),i.jsxs("section",{className:"home-group","data-testid":"home-modules",children:[i.jsxs("div",{className:"home-group-head",children:[i.jsx("h2",{children:"Modules"}),i.jsx("span",{className:"home-group-tag home-group-tag-module",children:"modules"})]}),i.jsxs("p",{className:"muted home-group-sub",children:["Installed modules. Each opens its own admin surface outside the hub shell. Manage install / upgrade / restart from ",i.jsx(lt,{to:"/modules",children:"Modules"}),"."]}),l.kind==="loading"?i.jsx("p",{className:"muted","data-loading":"true",children:"Loading modules…"}):l.kind==="error"?i.jsxs("p",{className:"muted","data-testid":"home-modules-error",children:["Couldn't load modules (",l.message,"). Open ",i.jsx(lt,{to:"/modules",children:"Modules"})," ","directly."]}):f.length===0?i.jsxs("p",{className:"muted","data-testid":"home-modules-empty",children:["No modules installed yet. ",i.jsx(lt,{to:"/modules",children:"Install one →"})]}):i.jsx("div",{className:"home-grid",children:f.map(g=>g.short==="vault"&&c===0?i.jsx(y0,{module:g},g.short):i.jsx(v0,{module:g},g.short))})]}),h.length>0?i.jsxs("section",{className:"home-group","data-testid":"home-surfaces",children:[i.jsxs("div",{className:"home-group-head",children:[i.jsx("h2",{children:"Your surfaces"}),i.jsx("span",{className:"home-group-tag home-group-tag-module",children:"module-owned"})]}),i.jsx("p",{className:"muted home-group-sub",children:"The user-facing apps these modules host. Opens the app — outside the hub shell."}),i.jsx("div",{className:"home-grid",children:h.map(g=>i.jsxs("a",{href:g.href,className:"home-card home-card-surface","data-testid":`home-surface-${g.key}`,children:[i.jsxs("span",{className:"home-card-title",children:[g.label," ",i.jsx("span",{className:"ext-mark",children:"↗"})]}),g.tagline?i.jsx("span",{className:"home-card-desc",children:g.tagline}):null,i.jsxs("span",{className:"home-card-owner",children:["opens ",g.ownerLabel,"'s surface"]})]},g.key))})]}):null]})}function v0({module:l}){const r=l.config_ui_url??l.management_url;return r?i.jsxs("a",{href:r,className:"home-card home-card-module","data-testid":`home-module-${l.short}`,children:[i.jsxs("span",{className:"home-card-title",children:[l.display_name," ",i.jsx("span",{className:"ext-mark",children:"↗"})]}),l.tagline?i.jsx("span",{className:"home-card-desc",children:l.tagline}):null,i.jsxs("span",{className:"home-card-owner",children:["opens ",l.display_name,"'s own admin"]})]}):i.jsxs("div",{className:"home-card home-card-module home-card-disabled","data-testid":`home-module-${l.short}`,"aria-disabled":"true",children:[i.jsx("span",{className:"home-card-title",children:l.display_name}),l.tagline?i.jsx("span",{className:"home-card-desc",children:l.tagline}):null,i.jsx("span",{className:"home-card-owner home-card-owner-empty",children:"no admin UI yet"})]})}function y0({module:l}){return i.jsxs("a",{href:"/admin/setup?step=vault",className:"home-card home-card-hub","data-testid":`home-module-${l.short}`,children:[i.jsx("span",{className:"home-card-title",children:l.display_name}),i.jsx("span",{className:"home-card-desc",children:"No vaults yet — create your first vault to start storing notes, tokens, and secrets."}),i.jsx("span",{className:"home-card-owner",children:"create your first vault — opens the setup wizard"})]})}const b0=2e3,x0=12e4;function j0(){const[l,r]=b.useState(null),[c,o]=b.useState({kind:"idle"}),f=b.useRef(null),h=b.useRef(0),g=b.useCallback(async()=>{try{r(await oc())}catch{}},[]);b.useEffect(()=>{g()},[g]),b.useEffect(()=>()=>{f.current&&clearInterval(f.current)},[]);const y=b.useCallback(()=>{f.current&&(clearInterval(f.current),f.current=null)},[]),m=b.useCallback((x,j)=>{h.current=Date.now()+x0,y(),f.current=setInterval(()=>{(async()=>{let _=null,k=null;try{_=await oc()}catch{}try{k=await N1()}catch{}if(_){const C=_.version!==x,O=j!==null&&_.version===j;if(C||O){y(),r(_),o({kind:"succeeded",newVersion:_.version});return}}if((k==null?void 0:k.phase)==="failed"){y(),o({kind:"error",message:k.error??"the hub upgrade failed — check the platform log"});return}if(Date.now()>=h.current){y();const C=(k==null?void 0:k.log.at(-1))??null;o({kind:"timeout",lastLog:C})}})()},b0)},[y]);async function v(){if(!l)return;const x=l.version;o({kind:"starting"});try{const j=await C1();if(j.mode==="redeploy-required"){o({kind:"redeploy-required",targetVersion:j.target_version});return}o({kind:"upgrading",previousVersion:x,targetVersion:j.target_version}),m(x,j.target_version)}catch(j){o({kind:"error",message:j instanceof Error?j.message:String(j)})}}return l?i.jsxs("section",{className:"hub-upgrade-card install-card","data-testid":"hub-upgrade-card",children:[i.jsxs("div",{className:"install-card-body",children:[i.jsxs("h3",{children:["Hub ",i.jsx("span",{className:"muted",children:"(@openparachute/hub)"})]}),i.jsxs("p",{className:"install-card-meta muted",children:["Installed ",i.jsxs("code",{"data-testid":"hub-current-version",children:["v",l.version]})," · source"," ",i.jsx("code",{children:l.source})]}),i.jsx(w0,{state:c})]}),i.jsx("div",{className:"install-card-actions",children:i.jsx(S0,{state:c,onUpgrade:()=>void v()})})]}):null}function S0({state:l,onUpgrade:r}){if(l.kind==="redeploy-required")return i.jsx("span",{className:"muted","data-testid":"hub-redeploy-hint",children:"Redeploy from your platform dashboard"});const c=l.kind==="starting"||l.kind==="upgrading";return i.jsx("button",{type:"button",onClick:r,disabled:c,"data-testid":"hub-upgrade-button",children:c?"Upgrading…":"Upgrade hub"})}function w0({state:l}){switch(l.kind){case"idle":return null;case"starting":return i.jsx("p",{className:"muted","data-testid":"hub-upgrade-state-starting",children:"Starting the hub upgrade…"});case"upgrading":return i.jsxs("p",{className:"muted","data-testid":"hub-upgrade-state-upgrading",children:["Upgrading",l.targetVersion?` to v${l.targetVersion}`:"","… the hub will restart; this page reconnects automatically."]});case"succeeded":return i.jsxs("p",{className:"muted","data-testid":"hub-upgrade-state-success",children:["Upgraded — the hub is now running ",i.jsxs("code",{children:["v",l.newVersion]}),"."]});case"timeout":return i.jsxs("p",{className:"warn-banner","data-testid":"hub-upgrade-state-timeout",children:["The hub may still be coming up — refresh shortly.",l.lastLog?i.jsxs(i.Fragment,{children:[" ",i.jsxs("span",{className:"dim",children:["Last status: ",l.lastLog]})]}):null]});case"redeploy-required":return i.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."," ",l.targetVersion?i.jsxs(i.Fragment,{children:["Redeploy from your platform dashboard to move to ",i.jsxs("code",{children:["v",l.targetVersion]}),"."]}):i.jsx(i.Fragment,{children:"Redeploy from your platform dashboard to pick up the latest hub."})]});case"error":return i.jsxs("p",{className:"error-banner","data-testid":"hub-upgrade-state-error",children:["Upgrade failed: ",l.message]})}}function C0(){if(typeof navigator>"u")return"other";const l=(navigator.platform||"").toLowerCase();return l.includes("mac")?"darwin":l.includes("linux")?"linux":"other"}function N0(l,r){const c=[];if(l.darwin&&c.push({label:"macOS",command:l.darwin,preferred:r==="darwin"}),l.linux&&c.push({label:"Linux",command:l.linux,preferred:r==="linux"}),l.generic){const o=c.some(f=>f.preferred);c.push({label:"Any platform",command:l.generic,preferred:!o})}return c.sort((o,f)=>Number(f.preferred)-Number(o.preferred))}function E0({command:l}){const[r,c]=b.useState(!1);async function o(){try{await navigator.clipboard.writeText(l),c(!0),setTimeout(()=>c(!1),1500)}catch{}}return i.jsxs("div",{className:"depcard-cmd",children:[i.jsx("pre",{className:"depcard-cmd-text",children:l}),i.jsx("button",{type:"button",className:"btn btn-secondary depcard-copy",onClick:()=>void o(),"aria-label":"Copy install command",children:r?"Copied":"Copy"})]})}function k0({wire:l}){const r=C0(),c=N0(l.install,r);return i.jsxs("div",{className:"depcard","data-testid":"missing-dependency-card",children:[i.jsxs("h3",{className:"depcard-heading",children:[l.binary," isn't installed"]}),l.why?i.jsxs("p",{className:"depcard-why muted",children:["It's needed to ",l.why,"."]}):null,c.length>0?i.jsxs("div",{className:"depcard-installs",children:[i.jsx("p",{className:"depcard-installs-label",children:"Install it:"}),c.map(o=>i.jsxs("div",{className:o.preferred?"depcard-install preferred":"depcard-install",children:[i.jsx("span",{className:"depcard-os",children:o.label}),i.jsx(E0,{command:o.command})]},o.label))]}):null,l.docs_url?i.jsx("p",{className:"depcard-docs",children:i.jsx("a",{href:l.docs_url,target:"_blank",rel:"noreferrer noopener",children:"Documentation"})}):null,l.sysadmin_hint?i.jsx("p",{className:"depcard-hint muted",children:l.sysadmin_hint}):null]})}function T0(l){const{error:r,errorDetail:c}=l;return(c==null?void 0:c.error_type)==="missing_dependency"?i.jsx(k0,{wire:c}):c!=null&&c.error_description?i.jsx("span",{className:"depcard-fallback",children:c.error_description}):r?i.jsx("span",{className:"depcard-fallback",children:r}):null}function _0(l){switch(l){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 A0(l){if(l==null)return"active";switch(l){case"active":case"pending":case"inactive":case"failing":return l;case"pending-oauth":return"pending";case"disabled":return"inactive";default:return"active"}}const R0=1e3;function M0(){const[l,r]=b.useState({kind:"loading"}),[c,o]=b.useState([]),[f,h]=b.useState({}),[g,y]=b.useState({}),m=b.useCallback(async()=>{try{const E=await Cs();r({kind:"ok",catalog:E})}catch(E){const D=E instanceof Error?E.message:String(E);r({kind:"error",message:D})}},[]);b.useEffect(()=>{m()},[m]);const v=b.useRef(null);b.useEffect(()=>{if(c.length===0){v.current&&(clearInterval(v.current),v.current=null);return}if(!v.current)return v.current=setInterval(()=>{Promise.all(c.map(async E=>{var D;try{const Q=await h1(E.operationId);o(te=>te.map(le=>le.operationId===Q.id?{...le,log:Q.log,status:Q.status,error:Q.error,errorDetail:Q.error_detail}:le)),(Q.status==="succeeded"||Q.status==="failed")&&(Q.status==="failed"&&((D=Q.error_detail)==null?void 0:D.error_type)==="missing_dependency"?m():setTimeout(()=>{o(le=>le.filter(ie=>ie.operationId!==Q.id)),m()},1500))}catch(Q){const te=Q instanceof Error?Q.message:String(Q);o(le=>le.map(ie=>ie.operationId===E.operationId?{...ie,status:"failed",error:te}:ie))}}))},R0),()=>{v.current&&(clearInterval(v.current),v.current=null)}},[c,m]);async function x(E){y(D=>{const Q={...D};return delete Q[E],Q});try{const D=await r1(E);o(Q=>[...Q,{kind:"install",operationId:D,short:E,log:[],status:"pending"}])}catch(D){y(Q=>({...Q,[E]:D instanceof Error?D.message:String(D)}))}}async function j(E){y(D=>{const Q={...D};return delete Q[E],Q});try{const D=await c1(E);o(Q=>[...Q,{kind:"upgrade",operationId:D,short:E,log:[],status:"pending"}])}catch(D){y(Q=>({...Q,[E]:D instanceof Error?D.message:String(D)}))}}async function _(E){if(!f[E]){h(D=>({...D,[E]:!0})),y(D=>{const Q={...D};return delete Q[E],Q});try{await o1(E),await m()}catch(D){y(Q=>({...Q,[E]:D instanceof Error?D.message:String(D)}))}finally{h(D=>({...D,[E]:!1}))}}}async function k(E){if(!f[E]&&window.confirm(`Uninstall ${E}? 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(D=>({...D,[E]:!0})),y(D=>{const Q={...D};return delete Q[E],Q});try{await d1(E),await m()}catch(D){y(Q=>({...Q,[E]:D instanceof Error?D.message:String(D)}))}finally{h(D=>({...D,[E]:!1}))}}}if(l.kind==="loading")return i.jsx("div",{className:"empty",children:"Loading modules…"});if(l.kind==="error")return l.message.includes("setup_required")?i.jsxs("div",{className:"empty",children:["Hub not yet configured. ",i.jsx("a",{href:"/admin/setup",children:"Finish first-boot setup"})," first."]}):i.jsxs("div",{className:"empty",children:["Failed to load modules: ",l.message,"."," ",i.jsx("button",{type:"button",onClick:()=>void m(),children:"Retry"})]});const C=l.catalog,{modules:O,supervisor_available:L,module_install_channel:z}=C,Z=O.filter(E=>E.installed),X=O.filter(E=>!E.installed&&E.available_to_install),P=new Set(c.filter(E=>E.kind==="install").map(E=>E.short)),K=X.filter(E=>!P.has(E.short));async function J(E){if(E!==z){r({kind:"ok",catalog:{...C,module_install_channel:E}});try{const D=await f1(E);r(Q=>Q.kind==="ok"?{kind:"ok",catalog:{...Q.catalog,module_install_channel:D}}:Q)}catch(D){const Q=D instanceof Error?D.message:String(D);r({kind:"error",message:`Failed to update channel — ${Q}`})}}}return i.jsxs("section",{className:"modules",children:[i.jsx("h1",{children:"Modules"}),i.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."}),i.jsx(j0,{}),i.jsx(H0,{channel:z,disabled:!L,onChange:E=>void J(E)}),!L&&i.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 ",i.jsx("code",{children:"parachute serve"}),". On-box installs use"," ",i.jsx("code",{children:"parachute install vault"})," etc. from a shell instead."]}),c.length>0&&i.jsxs("div",{className:"banner","data-testid":"pending-ops-banner",children:[i.jsx("strong",{children:"In progress:"}),i.jsx("ul",{children:c.map(E=>i.jsxs("li",{children:[i.jsx("code",{children:E.short})," · ",E.kind," · status: ",E.status,E.status==="failed"&&(E.errorDetail||E.error)?i.jsx("div",{className:"depcard-wrap",children:T0({error:E.error,errorDetail:E.errorDetail})}):null,E.log.length>0&&i.jsxs("details",{children:[i.jsxs("summary",{children:[E.log.length," log line(s)"]}),i.jsx("pre",{children:E.log.join(`
61
- `)})]})]},E.operationId))})]}),i.jsxs("section",{className:"modules-installed","data-testid":"installed-section",children:[i.jsx("h2",{children:"Installed modules"}),Z.length===0?i.jsxs("p",{className:"muted","data-testid":"installed-empty",children:["No modules installed yet. Pick one from ",i.jsx("strong",{children:"Install a module"})," below to get started."]}):Fh(Z,E=>i.jsx("ul",{className:"module-list",children:E.map(D=>i.jsx(z0,{module:D,supervisorAvailable:L,syncBusy:!!f[D.short],errorMessage:g[D.short],onUpgrade:()=>void j(D.short),onRestart:()=>void _(D.short),onUninstall:()=>void k(D.short)},D.short))}),"installed")]}),i.jsxs("section",{className:"modules-installable","data-testid":"installable-section",children:[i.jsx("h2",{children:"Install a module"}),K.length===0?i.jsx("p",{className:"muted","data-testid":"installable-empty",children:P.size>0?"Install in progress — see In progress above.":"All available modules are installed."}):Fh(K,E=>i.jsx("ul",{className:"install-list",children:E.map(D=>i.jsx(O0,{module:D,supervisorAvailable:L,installing:P.has(D.short),errorMessage:g[D.short],onInstall:()=>void x(D.short)},D.short))}),"installable")]})]})}function Fh(l,r,c){const o=l.filter(g=>g.focus==="core"),f=l.filter(g=>g.focus==="experimental"),h=l.filter(g=>g.focus==="deprecated");return i.jsxs(i.Fragment,{children:[o.length>0&&i.jsx("div",{"data-testid":`${c}-core-group`,children:r(o)}),f.length>0&&i.jsxs("div",{className:"modules-experimental","data-testid":`${c}-experimental-group`,children:[i.jsxs("h3",{className:"muted experimental-heading",children:["Experimental"," ",i.jsx("span",{className:"muted",children:"— exploration modules; not part of the core surface"})]}),r(f)]}),h.length>0&&i.jsxs("div",{className:"modules-deprecated","data-testid":`${c}-deprecated-group`,children:[i.jsxs("h3",{className:"muted deprecated-heading",children:["Deprecated"," ",i.jsx("span",{className:"muted",children:"— retained for existing installs; not offered for new setups"})]}),r(h)]})]})}function z0({module:l,supervisorAvailable:r,syncBusy:c,errorMessage:o,onUpgrade:f,onRestart:h,onUninstall:g}){const y=r&&!c,m=l.installed_version!==l.latest_version&&l.latest_version!==null,v=l.management_url,x=l.config_ui_url;return i.jsxs("li",{className:"module-row","data-short":l.short,children:[i.jsxs("header",{children:[i.jsxs("h2",{children:[l.display_name," ",i.jsxs("span",{className:"muted",children:["(",l.short,")"]})]}),(()=>{const j=U0(l);return i.jsx("span",{className:`status status-${j.cssState}`,"data-state":j.cssState,"data-testid":`module-status-${l.short}`,children:j.label})})()]}),l.tagline?i.jsx("p",{className:"tagline",children:l.tagline}):null,i.jsxs("dl",{className:"meta",children:[i.jsx("dt",{children:"Package"}),i.jsx("dd",{children:i.jsx("code",{children:l.package})}),i.jsx("dt",{children:"Installed"}),i.jsxs("dd",{children:["v",l.installed_version??"unknown",m&&i.jsxs("span",{className:"badge",children:["v",l.latest_version," available"]})]}),l.pid&&i.jsxs(i.Fragment,{children:[i.jsx("dt",{children:"PID"}),i.jsx("dd",{children:l.pid})]})]}),l.uis.length>0&&i.jsx(D0,{uis:l.uis}),i.jsxs("div",{className:"actions",children:[v?i.jsx("a",{className:"btn",href:v,"data-testid":`open-${l.short}`,children:"Open"}):i.jsx("button",{type:"button",className:"btn",disabled:!0,title:x?"No separate Open surface — use Configure for this module's admin UI.":"This module hasn't shipped an admin UI yet.","data-testid":`open-${l.short}`,children:"Open"}),x?i.jsx("a",{className:"btn",href:x,"data-testid":`configure-${l.short}`,children:"Configure"}):null,i.jsx("button",{type:"button",disabled:!y,onClick:h,children:"Restart"}),i.jsx("button",{type:"button",disabled:!y||!m,onClick:f,children:m?`Upgrade to v${l.latest_version}`:"Up to date"}),i.jsx("button",{type:"button",className:"destructive",disabled:!y,onClick:g,children:"Uninstall"})]}),o&&i.jsx("div",{className:"error",children:o})]})}function O0({module:l,supervisorAvailable:r,installing:c,errorMessage:o,onInstall:f}){const h=r&&!c;return i.jsxs("li",{className:"install-card","data-short":l.short,children:[i.jsxs("div",{className:"install-card-body",children:[i.jsxs("h3",{children:[l.display_name," ",i.jsxs("span",{className:"muted",children:["(",l.short,")"]})]}),l.tagline?i.jsx("p",{className:"tagline",children:l.tagline}):null,i.jsxs("p",{className:"muted install-card-meta",children:[i.jsx("code",{children:l.package}),l.latest_version?i.jsxs(i.Fragment,{children:[" · ","latest ",i.jsxs("code",{children:["v",l.latest_version]})]}):null]})]}),i.jsx("div",{className:"install-card-actions",children:i.jsx("button",{type:"button",disabled:!h,onClick:f,children:c?"Installing…":"Install"})}),o&&i.jsx("div",{className:"error",children:o})]})}function U0(l){if(!l.installed)return{cssState:"absent",label:"not installed"};if(!l.supervisor_status)return{cssState:"absent",label:"not supervised"};const r=_0(l.supervisor_status);return{cssState:r,label:r}}function D0({uis:l}){return i.jsxs("details",{className:"module-uis","data-testid":"module-uis",open:!0,children:[i.jsxs("summary",{children:["Hosted UIs ",i.jsxs("span",{className:"muted",children:["(",l.length,")"]})]}),i.jsx("ul",{className:"ui-sub-units",children:l.map(r=>{const c=A0(r.status);return i.jsxs("li",{className:"ui-sub-unit","data-name":r.name,children:[r.icon_url&&i.jsx("img",{src:r.icon_url,alt:"",className:"ui-icon",width:20,height:20,loading:"lazy"}),i.jsxs("div",{className:"ui-sub-unit-body",children:[i.jsxs("a",{href:r.path,className:"ui-sub-unit-link",children:[i.jsx("strong",{children:r.display_name}),i.jsxs("span",{className:"muted",children:[" · ",r.path]})]}),r.tagline?i.jsx("p",{className:"tagline",children:r.tagline}):null]}),i.jsx("span",{className:`status status-${c}`,"data-state":c,"data-testid":`ui-status-${r.name}`,children:c})]},r.name)})})]})}function H0({channel:l,disabled:r,onChange:c}){return i.jsxs("fieldset",{className:"channel-toggle","data-testid":"channel-toggle",children:[i.jsx("legend",{children:"Install channel"}),i.jsxs("label",{children:[i.jsx("input",{type:"radio",name:"module-install-channel",value:"latest",checked:l==="latest",disabled:r,onChange:()=>c("latest")}),"Stable (",i.jsx("code",{children:"latest"}),")"]}),i.jsxs("label",{children:[i.jsx("input",{type:"radio",name:"module-install-channel",value:"rc",checked:l==="rc",disabled:r,onChange:()=>c("rc")}),"Release candidates (",i.jsx("code",{children:"rc"}),")"]}),i.jsxs("p",{className:"muted",children:["All future module installs and upgrades use this channel. Existing installed modules are unaffected — use ",i.jsx("strong",{children:"Upgrade"})," to pull a newer version."]}),i.jsxs("p",{className:"muted",children:["More hub settings (canonical URL, etc.) at ",i.jsx(lt,{to:"/settings",children:"Settings"}),"."]})]})}function B0(){const[l,r]=b.useState({kind:"loading"}),[c,o]=b.useState(""),[f,h]=b.useState(""),[g,y]=b.useState(0),[m,v]=b.useState({kind:"idle"});b.useEffect(()=>{let k=!1;return r({kind:"loading"}),t1(f?{vault:f}:{}).then(O=>{k||r({kind:"ok",grants:O})}).catch(O=>{if(k)return;const L=O instanceof Error?O.message:String(O);r({kind:"error",message:L})}),()=>{k=!0}},[g,f]);function x(k){k.preventDefault(),h(c.trim())}function j(){o(""),h("")}async function _(k){v({kind:"revoking",clientId:k.client_id});try{await a1(k.client_id),v({kind:"idle"}),y(C=>C+1)}catch(C){const O=C instanceof V?`revoke failed (${C.status}): ${C.message}`:C instanceof Error?C.message:String(C);v({kind:"error",clientId:k.client_id,message:O})}}return i.jsxs("div",{"data-route-content":"true",children:[i.jsx("div",{className:"list-header",children:i.jsx("h1",{children:"Permissions"})}),i.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 ",i.jsx("em",{children:"not"})," invalidate tokens already issued."]}),i.jsxs("form",{onSubmit:x,style:{marginTop:"1rem",marginBottom:"1rem"},children:[i.jsx("label",{htmlFor:"vault-filter",className:"muted",style:{marginRight:"0.5rem"},children:"Filter by vault:"}),i.jsx("input",{id:"vault-filter",type:"text",value:c,onChange:k=>o(k.target.value),placeholder:"e.g. work",style:{marginRight:"0.5rem"}}),i.jsx("button",{type:"submit",children:"Apply"}),f?i.jsx("button",{type:"button",onClick:j,className:"secondary",style:{marginLeft:"0.5rem"},children:"Clear"}):null]}),L0({state:l,revoke:m,setRevoke:v,onConfirm:_,onRetry:()=>y(k=>k+1)})]})}function L0({state:l,revoke:r,setRevoke:c,onConfirm:o,onRetry:f}){return l.kind==="loading"?i.jsx("p",{className:"muted","data-loading":"true",children:"Loading…"}):l.kind==="error"?i.jsxs(i.Fragment,{children:[i.jsxs("div",{className:"error-banner",children:["Couldn't load grants: ",i.jsx("code",{children:l.message})]}),i.jsx("button",{type:"button",onClick:f,className:"secondary",children:"Retry"})]}):l.grants.length===0?i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No grants."}),i.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."})]}):i.jsx("div",{style:{marginTop:"1rem"},children:l.grants.map(h=>{const g=r.kind==="revoking"&&r.clientId===h.client_id,y=r.kind==="error"&&r.clientId===h.client_id?r:null,m=r.kind==="confirming"&&r.grant.client_id===h.client_id;return i.jsxs("div",{className:"vault-row",children:[i.jsxs("div",{className:"body",children:[i.jsx("div",{className:"name",children:i.jsx("code",{children:h.client_name??h.client_id})}),i.jsxs("div",{className:"dim",children:[i.jsx("span",{className:"muted",children:"granted "}),i.jsx("code",{title:h.granted_at,children:q0(h.granted_at)})]}),i.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[i.jsx("span",{className:"muted",children:"scopes: "}),h.scopes.map((v,x)=>i.jsxs("span",{children:[i.jsx("code",{children:v}),x<h.scopes.length-1?" ":null]},v))]}),y?i.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:i.jsx("code",{children:y.message})}):null,m?i.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:[i.jsxs("p",{children:["Revoke ",i.jsx("code",{children:h.client_name??h.client_id}),"? Next OAuth flow for this app will prompt you to consent again."]}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"button",onClick:()=>{o(h)},disabled:g,children:g?"Revoking…":"Revoke"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>c({kind:"idle"}),disabled:g,children:"Cancel"})]})]}):null]}),m?null:i.jsx("button",{type:"button",className:"secondary",onClick:()=>c({kind:"confirming",grant:h}),"aria-label":`Revoke ${h.client_name??h.client_id}`,children:"Revoke"})]},h.client_id)})})}function q0(l){const r=new Date(l);return Number.isNaN(r.getTime())?l:r.toLocaleString()}function Y0(){const[l,r]=b.useState({kind:"loading"}),[c,o]=b.useState(""),[f,h]=b.useState(!1),[g,y]=b.useState(null),m=b.useCallback(async()=>{try{const C=await Cm();r({kind:"ok",setting:C}),o(C.hub_origin??""),y(null)}catch(C){const O=C instanceof Error?C.message:String(C);r({kind:"error",message:O})}},[]);b.useEffect(()=>{m()},[m]);async function v(C){if(C.preventDefault(),f||l.kind!=="ok")return;const O=c.trim(),L=O.length===0?null:O;h(!0),y(null);try{await $h(L),await m()}catch(z){const Z=z instanceof Error?z.message:String(z);y(Z)}finally{h(!1)}}async function x(){if(!f&&l.kind==="ok"){h(!0),y(null);try{await $h(null),await m()}catch(C){const O=C instanceof Error?C.message:String(C);y(O)}finally{h(!1)}}}if(l.kind==="loading")return i.jsx("div",{className:"empty",children:"Loading settings…"});if(l.kind==="error")return i.jsxs("div",{className:"empty",children:["Failed to load settings: ",l.message,"."," ",i.jsx("button",{type:"button",onClick:()=>void m(),children:"Retry"})]});const{setting:j}=l,_=j.hub_origin!==null,k=c.trim()!==(j.hub_origin??"");return i.jsxs("section",{className:"settings",children:[i.jsx("h1",{children:"Hub settings"}),i.jsx("p",{className:"muted",children:"Hub-wide operator controls. Settings here apply to every module + every minted token."}),i.jsxs("section",{className:"settings-block","aria-labelledby":"canonical-hub-url-heading",children:[i.jsx("h2",{id:"canonical-hub-url-heading",children:"Canonical hub URL"}),i.jsxs("dl",{className:"meta","data-testid":"hub-origin-current",children:[i.jsx("dt",{children:"Current value"}),i.jsxs("dd",{children:[i.jsx("code",{children:j.resolved_issuer})," ",i.jsx(V0,{source:j.source,hasStored:_})]})]}),i.jsxs("form",{onSubmit:C=>void v(C),className:"settings-form","data-testid":"hub-origin-form",children:[i.jsxs("label",{htmlFor:"hub-origin-input",children:[i.jsx("span",{children:"Canonical URL"}),i.jsx("input",{id:"hub-origin-input",type:"url",inputMode:"url",placeholder:"https://hub.example.com",value:c,disabled:f,onChange:C=>o(C.target.value),autoComplete:"off",spellCheck:!1})]}),i.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 ",i.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)."]}),i.jsxs("div",{className:"actions",children:[i.jsx("button",{type:"submit",disabled:f||!k,children:f?"Saving…":"Save"}),i.jsx("button",{type:"button",className:"destructive",disabled:f||!_,onClick:()=>void x(),children:"Reset to default"})]}),g&&i.jsx("div",{className:"error","data-testid":"hub-origin-save-error",children:g})]})]}),i.jsx(G0,{})]})}function G0(){const[l,r]=b.useState(null),[c,o]=b.useState(null),[f,h]=b.useState(null),[g,y]=b.useState(""),[m,v]=b.useState(""),[x,j]=b.useState(""),[_,k]=b.useState("15"),[C,O]=b.useState(!1),[L,z]=b.useState(null),[Z,X]=b.useState(null),P=b.useCallback(async()=>{try{const[te,le]=await Promise.all([Cc(),wc()]);r(te),o(le.hasSession?le.csrf:null),k(String(Math.round(te.idle_seconds/60))),h(null)}catch(te){h(te instanceof Error?te.message:String(te))}},[]);b.useEffect(()=>{P()},[P]);function K(){const te=Number.parseInt(_,10);if(Number.isFinite(te))return Math.max(1,te)*60}async function J(te){if(te.preventDefault(),!(C||!c)){if(z(null),X(null),!/^[0-9]{4,12}$/.test(g)){z("PIN must be 4–12 digits.");return}if(g!==m){z("PINs don't match.");return}O(!0);try{await _1(c,g,K()),y(""),v(""),X("Screen lock enabled."),await P()}catch(le){z(le instanceof Error?le.message:String(le))}finally{O(!1)}}}async function E(te){if(te.preventDefault(),!(C||!c)){if(z(null),X(null),!/^[0-9]{4,12}$/.test(g)){z("New PIN must be 4–12 digits.");return}if(g!==m){z("PINs don't match.");return}O(!0);try{await A1(c,g,x||void 0,K()),y(""),v(""),j(""),X("PIN updated."),await P()}catch(le){z(le instanceof Error?le.message:String(le))}finally{O(!1)}}}async function D(){if(!(C||!c)){z(null),X(null),O(!0);try{await R1(c,x||void 0),j(""),X("Screen lock disabled."),await P()}catch(te){z(te instanceof Error?te.message:String(te))}finally{O(!1)}}}const Q=(l==null?void 0:l.configured)??!1;return i.jsxs("section",{className:"settings-block","aria-labelledby":"admin-lock-heading","data-testid":"admin-lock-settings",children:[i.jsx("h2",{id:"admin-lock-heading",children:"Admin screen lock"}),i.jsxs("p",{className:"muted",children:["An optional PIN that locks the whole admin console after a period of inactivity — a phone-style lock for a remotely-exposed hub. Off by default; the OAuth flow for your connected apps is never affected. ",i.jsx("strong",{children:"Note:"})," this guards the exposed web portal only — anyone with shell access to the box bypasses it (that's an OS / disk-encryption concern)."]}),f?i.jsxs("div",{className:"error","data-testid":"admin-lock-load-error",children:["Failed to load lock status: ",f,"."," ",i.jsx("button",{type:"button",onClick:()=>void P(),children:"Retry"})]}):i.jsxs(i.Fragment,{children:[i.jsxs("p",{children:["Status:"," ",i.jsx("span",{className:`lock-status-pill ${Q?"lock-status-on":"lock-status-off"}`,"data-testid":"admin-lock-status-pill",children:Q?"Enabled":"Off"})]}),Q?i.jsxs("form",{onSubmit:te=>void E(te),className:"lock-settings-form",children:[i.jsxs("label",{children:["Current PIN",i.jsx("input",{type:"password",inputMode:"numeric",autoComplete:"off",value:x,disabled:C,onChange:te=>j(te.target.value.replace(/[^0-9]/g,"")),"data-testid":"admin-lock-current-pin"})]}),i.jsxs("label",{children:["New PIN (4–12 digits)",i.jsx("input",{type:"password",inputMode:"numeric",autoComplete:"off",value:g,disabled:C,onChange:te=>y(te.target.value.replace(/[^0-9]/g,"")),"data-testid":"admin-lock-new-pin"})]}),i.jsxs("label",{children:["Confirm new PIN",i.jsx("input",{type:"password",inputMode:"numeric",autoComplete:"off",value:m,disabled:C,onChange:te=>v(te.target.value.replace(/[^0-9]/g,"")),"data-testid":"admin-lock-new-pin2"})]}),i.jsxs("label",{children:["Lock after (minutes idle)",i.jsx("input",{type:"number",min:1,max:1440,value:_,disabled:C,onChange:te=>k(te.target.value),"data-testid":"admin-lock-idle"})]}),i.jsxs("div",{className:"actions",children:[i.jsx("button",{type:"submit",disabled:C,"data-testid":"admin-lock-change",children:C?"Saving…":"Update PIN"}),i.jsx("button",{type:"button",className:"destructive",disabled:C,onClick:()=>void D(),"data-testid":"admin-lock-remove",children:"Remove lock"})]})]}):i.jsxs("form",{onSubmit:te=>void J(te),className:"lock-settings-form",children:[i.jsxs("label",{children:["PIN (4–12 digits)",i.jsx("input",{type:"password",inputMode:"numeric",autoComplete:"off",value:g,disabled:C,onChange:te=>y(te.target.value.replace(/[^0-9]/g,"")),"data-testid":"admin-lock-set-pin"})]}),i.jsxs("label",{children:["Confirm PIN",i.jsx("input",{type:"password",inputMode:"numeric",autoComplete:"off",value:m,disabled:C,onChange:te=>v(te.target.value.replace(/[^0-9]/g,"")),"data-testid":"admin-lock-set-pin2"})]}),i.jsxs("label",{children:["Lock after (minutes idle)",i.jsx("input",{type:"number",min:1,max:1440,value:_,disabled:C,onChange:te=>k(te.target.value),"data-testid":"admin-lock-idle"})]}),i.jsx("div",{className:"actions",children:i.jsx("button",{type:"submit",disabled:C,"data-testid":"admin-lock-enable",children:C?"Saving…":"Enable screen lock"})})]}),L&&i.jsx("div",{className:"error","data-testid":"admin-lock-form-error",children:L}),Z&&i.jsx("p",{className:"muted","data-testid":"admin-lock-notice",children:Z})]})]})}function V0({source:l,hasStored:r}){return l==="settings"?i.jsx("span",{className:"badge badge-info","data-testid":"hub-origin-source",children:"from settings"}):l==="env"?i.jsxs("span",{className:"badge badge-info","data-testid":"hub-origin-source",children:["from env var ",i.jsx("code",{children:"PARACHUTE_HUB_ORIGIN"})]}):l==="expose"?i.jsxs("span",{className:"badge badge-info","data-testid":"hub-origin-source",children:["from your ",i.jsx("code",{children:"parachute expose"})," config"]}):i.jsx("span",{className:"badge badge-info","data-testid":"hub-origin-source",children:"from request origin"})}const lc={scope:"",audience:"",expiresIn:"",subject:"",permissions:""};function Z0(){const[l,r]=bm(),c=$0(l.get("status")),o=J0(l.get("source")),[f,h]=b.useState({kind:"loading"}),[g,y]=b.useState(0),[m,v]=b.useState({kind:"idle"}),[x,j]=b.useState(lc),[_,k]=b.useState({kind:"idle"}),[C,O]=b.useState(!1),[L,z]=b.useState(!1);function Z(E,D){r(Q=>{const te=new URLSearchParams(Q);return D==="all"?te.delete(E):te.set(E,D),te},{replace:!0})}b.useEffect(()=>{let E=!1;return h({kind:"loading"}),Qh(Wh(c,o)).then(D=>{E||h({kind:"ok",tokens:D.tokens,nextCursor:D.next_cursor})}).catch(D=>{if(E)return;const Q=D instanceof Error?D.message:String(D);h({kind:"error",message:Q})}),()=>{E=!0}},[g,c,o]);async function X(){if(f.kind!=="ok"||!f.nextCursor||L)return;const E={...Wh(c,o),cursor:f.nextCursor};z(!0);try{const D=await Qh(E);h({kind:"ok",tokens:[...f.tokens,...D.tokens],nextCursor:D.next_cursor})}catch(D){const Q=D instanceof Error?D.message:String(D);h({kind:"error",message:Q})}finally{z(!1)}}async function P(E){E.preventDefault();const D=x.scope.trim();if(D.length===0){v({kind:"error",message:"scope is required"});return}let Q;if(x.permissions.trim().length>0)try{const le=JSON.parse(x.permissions);if(le===null||typeof le!="object"||Array.isArray(le)){v({kind:"error",message:'permissions must be a JSON object (e.g. {"vault":{"default":...}})'});return}Q=le}catch(le){const ie=le instanceof Error?le.message:String(le);v({kind:"error",message:`permissions is not valid JSON — ${ie}`});return}let te;if(x.expiresIn.trim().length>0){const le=Number(x.expiresIn);if(!Number.isInteger(le)||le<=0){v({kind:"error",message:"expires_in must be a positive integer (seconds)"});return}te=le}v({kind:"submitting"});try{const le=await wm({scope:D,...x.audience.trim()?{audience:x.audience.trim()}:{},...te!==void 0?{expires_in:te}:{},...x.subject.trim()?{subject:x.subject.trim()}:{},...Q?{permissions:Q}:{}});v({kind:"minted",token:le}),j(lc),O(!1),y(ie=>ie+1)}catch(le){const ie=le instanceof V?`mint failed (${le.status}): ${le.message}`:le instanceof Error?le.message:String(le);v({kind:"error",message:ie})}}async function K(E){k({kind:"revoking",jti:E});try{await i1(E),k({kind:"idle"}),y(D=>D+1)}catch(D){const Q=D instanceof V?`revoke failed (${D.status}): ${D.message}`:D instanceof Error?D.message:String(D);k({kind:"error",jti:E,message:Q})}}function J(E){typeof navigator<"u"&&navigator.clipboard&&navigator.clipboard.writeText(E)}return i.jsxs("div",{children:[i.jsxs("div",{className:"list-header",children:[i.jsx("h1",{children:"Tokens"}),i.jsx("button",{type:"button",onClick:()=>O(E=>!E),children:C?"Hide form":"Mint new token"})]}),i.jsxs("p",{className:"muted",children:["The hub's token registry. Every CLI / OAuth / operator-mint writes a row here. Revoking flips ",i.jsx("code",{children:"revoked_at"}),"; resource servers on"," ",i.jsx("code",{children:"@openparachute/scope-guard@^0.2.0"})," reject within ~60s of the next poll."]}),m.kind==="minted"?i.jsxs("div",{className:"mint-banner",children:[i.jsx("h3",{children:"Minted"}),i.jsxs("p",{children:["Your new access token (jti: ",i.jsx("code",{children:m.token.jti}),"):"]}),i.jsx("div",{className:"token-box",children:i.jsx("code",{children:m.token.token})}),i.jsx("p",{className:"warn",children:"This is the only time the JWT is shown. Copy it now — there is no DB-side recovery."}),i.jsxs("div",{className:"actions",children:[i.jsx("button",{type:"button",onClick:()=>J(m.token.token),children:"Copy"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>v({kind:"idle"}),children:"Dismiss"})]})]}):null,C?i.jsx("div",{className:"section",children:i.jsxs("form",{onSubmit:P,children:[i.jsxs("div",{className:"row",children:[i.jsx("label",{htmlFor:"mint-scope",children:"Scope (space-separated)"}),i.jsx("input",{id:"mint-scope",type:"text",value:x.scope,onChange:E=>j({...x,scope:E.target.value}),placeholder:"e.g. scribe:transcribe vault:default:read",required:!0}),i.jsxs("div",{className:"field-hint",children:["Space-separated ",i.jsx("code",{children:"resource:verb"})," or ",i.jsx("code",{children:"resource:name:verb"})," ","tuples. The hub rejects non-requestable scopes (admin, host:*) per the privilege-diffusion guard."]})]}),i.jsxs("div",{className:"row",children:[i.jsx("label",{htmlFor:"mint-audience",children:"Audience (optional)"}),i.jsx("input",{id:"mint-audience",type:"text",value:x.audience,onChange:E=>j({...x,audience:E.target.value}),placeholder:"inferred from scope when blank"}),i.jsxs("div",{className:"field-hint",children:["Inferred from scope if omitted. ",i.jsx("code",{children:"vault:<name>:<verb>"})," →"," ",i.jsx("code",{children:"vault.<name>"}),"; otherwise the first colon-prefixed scope's namespace; fallback ",i.jsx("code",{children:"hub"}),"."]})]}),i.jsxs("div",{className:"row",children:[i.jsx("label",{htmlFor:"mint-expires-in",children:"Expires in (seconds, optional)"}),i.jsx("input",{id:"mint-expires-in",type:"text",inputMode:"numeric",value:x.expiresIn,onChange:E=>j({...x,expiresIn:E.target.value}),placeholder:"default 90d (7776000)"})]}),i.jsxs("div",{className:"row",children:[i.jsx("label",{htmlFor:"mint-subject",children:"Subject (optional)"}),i.jsx("input",{id:"mint-subject",type:"text",value:x.subject,onChange:E=>j({...x,subject:E.target.value}),placeholder:"defaults to operator's sub"})]}),i.jsxs("div",{className:"row",children:[i.jsx("label",{htmlFor:"mint-permissions",children:"Permissions (JSON object, optional)"}),i.jsx("textarea",{id:"mint-permissions",value:x.permissions,onChange:E=>j({...x,permissions:E.target.value}),placeholder:'e.g. {"vault":{"default":{"write_tags":["health"]}}}',rows:3,style:{width:"100%",fontFamily:"monospace",fontSize:"0.9rem"}})]}),m.kind==="error"?i.jsx("div",{className:"field-error",children:i.jsx("code",{children:m.message})}):null,i.jsxs("div",{className:"actions",children:[i.jsx("button",{type:"submit",disabled:m.kind==="submitting",children:m.kind==="submitting"?"Minting…":"Mint"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>{O(!1),j(lc),v({kind:"idle"})},children:"Cancel"})]})]})}):null,i.jsxs("div",{style:{marginTop:"1rem",marginBottom:"0.5rem",display:"flex",flexWrap:"wrap",gap:"0.5rem",alignItems:"center"},children:[i.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(E=>i.jsx("button",{type:"button",onClick:()=>Z("status",E.value),className:c===E.value?void 0:"secondary","aria-pressed":c===E.value,children:E.label},E.value))]}),i.jsxs("div",{style:{marginBottom:"1rem",display:"flex",flexWrap:"wrap",gap:"0.5rem",alignItems:"center"},children:[i.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(E=>i.jsx("button",{type:"button",onClick:()=>Z("source",E.value),className:o===E.value?void 0:"secondary","aria-pressed":o===E.value,children:E.label},E.value))]}),X0({list:f,revoke:_,setRevoke:k,onConfirm:K,onLoadMore:X,onRetry:()=>y(E=>E+1),loadingMore:L,filtersActive:c!=="all"||o!=="all"})]})}function X0({list:l,revoke:r,setRevoke:c,onConfirm:o,onLoadMore:f,onRetry:h,loadingMore:g,filtersActive:y}){return l.kind==="loading"?i.jsx("p",{className:"muted","data-loading":"true",children:"Loading…"}):l.kind==="error"?i.jsxs(i.Fragment,{children:[i.jsxs("div",{className:"error-banner",children:["Couldn't load tokens: ",i.jsx("code",{children:l.message})]}),i.jsx("button",{type:"button",onClick:h,className:"secondary",children:"Retry"})]}):l.tokens.length===0?y?i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No tokens match the current filter."}),i.jsx("p",{className:"muted",children:'Try widening the Status or Source pills above. The default "Show all / All sources" view shows every registry row.'})]}):i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No tokens."}),i.jsxs("p",{className:"muted",children:["Every CLI mint, OAuth grant, and operator-token rotation lands here. Mint one with the form above, or via ",i.jsx("code",{children:"parachute auth mint-token"}),"."]})]}):i.jsxs("div",{"data-route-content":"true",children:[l.tokens.map(m=>{const v=Q0(m),x=r.kind==="revoking"&&r.jti===m.jti,j=r.kind==="confirming"&&r.jti===m.jti,_=r.kind==="error"&&r.jti===m.jti?r:null,k=m.user_id??m.subject??"(unknown)";return i.jsxs("div",{className:"vault-row",children:[i.jsxs("div",{className:"body",children:[i.jsxs("div",{className:"name",children:[i.jsx("code",{title:m.jti,children:ms(m.jti)}),i.jsx("span",{className:`tag${v==="live"?"":" muted"}`,children:v}),i.jsx("span",{className:`tag source-${F0(m.created_via)}`,title:`created_via: ${m.created_via}`,children:K0(m.created_via)})]}),i.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[i.jsx("span",{className:"muted",children:"identity: "}),i.jsx("code",{children:k}),m.client_id?i.jsxs(i.Fragment,{children:[i.jsx("span",{className:"muted",children:" · client: "}),i.jsx("code",{children:m.client_id})]}):null]}),i.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[i.jsx("span",{className:"muted",children:"scope: "}),m.scopes.map((C,O)=>i.jsxs("span",{children:[i.jsx("code",{children:C}),O<m.scopes.length-1?" ":null]},C))]}),i.jsxs("div",{className:"dim",style:{marginTop:"0.25rem",fontSize:"0.82rem"},children:[i.jsx("span",{className:"muted",children:"created "}),i.jsx("code",{title:m.created_at,children:ic(m.created_at)}),i.jsx("span",{className:"muted",children:" · expires "}),i.jsx("code",{title:m.expires_at,children:ic(m.expires_at)}),m.revoked_at?i.jsxs(i.Fragment,{children:[i.jsx("span",{className:"muted",children:" · revoked "}),i.jsx("code",{title:m.revoked_at,children:ic(m.revoked_at)})]}):null]}),m.permissions?i.jsxs("details",{style:{marginTop:"0.35rem"},children:[i.jsx("summary",{className:"muted",style:{cursor:"pointer",fontSize:"0.85rem"},children:"permissions"}),i.jsx("pre",{style:{fontSize:"0.82rem",marginTop:"0.25rem"},children:JSON.stringify(m.permissions,null,2)})]}):null,_?i.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:i.jsx("code",{children:_.message})}):null,j?i.jsxs("dialog",{open:!0,className:"error-banner",style:{marginTop:"0.5rem",background:"var(--bg-warn, #fffbe6)"},"aria-label":`Confirm revoke ${ms(m.jti)}`,children:[i.jsxs("p",{children:["Revoke ",i.jsx("code",{children:ms(m.jti)}),"? Resource servers reject within ~60s of the next revocation-list poll. This cannot be undone."]}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"button",onClick:()=>{o(m.jti)},disabled:x,children:x?"Revoking…":"Revoke"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>c({kind:"idle"}),disabled:x,children:"Cancel"})]})]}):null]}),!j&&v==="live"?i.jsx("button",{type:"button",className:"secondary",onClick:()=>c({kind:"confirming",jti:m.jti}),"aria-label":`Revoke ${ms(m.jti)}`,children:"Revoke"}):null]},m.jti)}),l.nextCursor?i.jsx("div",{style:{marginTop:"1rem"},children:i.jsx("button",{type:"button",className:"secondary",disabled:g,onClick:()=>{f()},children:g?"Loading…":"Load more"})}):null]})}function Q0(l){if(l.revoked_at)return"revoked";const r=new Date(l.expires_at).getTime();return!Number.isNaN(r)&&r<Date.now()?"expired":"live"}function $0(l){return l==="live"||l==="revoked"||l==="all"?l:"all"}function J0(l){return l==="oauth_refresh"||l==="operator_mint"||l==="cli_mint"||l==="all"?l:"all"}function Wh(l,r){const c={};return l==="live"?c.revoked="false":l==="revoked"?c.revoked="true":c.revoked="all",r!=="all"&&(c.createdVia=r),c}function K0(l){switch(l){case"oauth_refresh":return"OAuth";case"operator_mint":return"Operator";case"cli_mint":return"CLI";default:return l}}function F0(l){switch(l){case"oauth_refresh":return"oauth";case"operator_mint":return"operator";case"cli_mint":return"cli";default:return"unknown"}}function ms(l){return l.length<=14?l:`${l.slice(0,8)}…${l.slice(-4)}`}function ic(l){const r=new Date(l);return Number.isNaN(r.getTime())?l:r.toLocaleString()}const W0=1440*60;function I0(l){return l.provision_vault?"owner (new vault)":l.vault_name===null?"account only":l.role==="read"?"read-only (shared)":"read & write (shared)"}function Ih(l){const r=l.username!==null?`an account for "${l.username}"`:"an account (they pick the username)";if(l.provision_vault){const o=l.vault_name!==null?`their own new vault "${l.vault_name}"`:"their own new vault (they name it)";return`Creates ${r} + ${o}, as owner.`}if(l.vault_name===null)return`Creates ${r} with no vault access yet.`;const c=l.role==="read"?"read-only":"read & write";return`Creates ${r} with ${c} access to your existing vault "${l.vault_name}".`}function P0(l,r,c){const o=l.username!==null?` Your username will be "${l.username}".`:"",f=l.provision_vault?l.vault_name!==null?` You'll get your own new vault ("${l.vault_name}").`:" You'll get your own new vault.":l.vault_name!==null?` You'll get ${l.role==="read"?"read-only":"read & write"} access to the vault "${l.vault_name}".`:"",h=new Date(c).toLocaleDateString();return`You're invited to my Parachute. Open this link to set your password and claim your account.${o}${f} The link works once and expires ${h}: ${r}`}function eb({hubOrigin:l}){const[r,c]=b.useState({kind:"loading"}),[o,f]=b.useState(0),[h,g]=b.useState(""),[y,m]=b.useState("provision"),[v,x]=b.useState(""),[j,_]=b.useState(""),[k,C]=b.useState("read"),[O,L]=b.useState([]),[z,Z]=b.useState("7"),[X,P]=b.useState({kind:"idle"}),[K,J]=b.useState(null),[E,D]=b.useState(null),Q={username:h.trim()!==""?h.trim():null,vault_name:y==="share"?j!==""?j:null:v.trim()!==""?v.trim():null,role:y==="share"?k:"write",provision_vault:y==="provision"};b.useEffect(()=>{let W=!1;return c({kind:"loading"}),Promise.all([j1(),Nm()]).then(([ge,be])=>{W||(c({kind:"ok",invites:ge}),L(be))}).catch(ge=>{W||c({kind:"error",message:ge instanceof Error?ge.message:String(ge)})}),()=>{W=!0}},[o]);async function te(W){if(W.preventDefault(),X.kind==="submitting")return;if(y==="share"&&j===""){P({kind:"error",message:"Pick which existing vault to share before creating the invite."});return}P({kind:"submitting"}),J(null);const ge=Number.parseInt(z,10),be={...h.trim()!==""?{username:h.trim()}:{},...Number.isFinite(ge)&&ge>0?{expires_in:ge*W0}:{},...y==="share"?{vault_name:j,provision_vault:!1,role:k}:{...v.trim()!==""?{vault_name:v.trim()}:{},provision_vault:!0}};try{const H=await S1(be);P({kind:"created",result:H}),g(""),x(""),_(""),f(F=>F+1)}catch(H){P({kind:"error",message:H instanceof Error?H.message:String(H)})}}async function le(W){if(!E){D(W);try{await w1(W),f(ge=>ge+1)}catch(ge){c({kind:"error",message:ge instanceof Error?ge.message:String(ge)})}finally{D(null)}}}function ie(W,ge){var be;(be=navigator.clipboard)==null||be.writeText(W).then(()=>{J(ge),setTimeout(()=>J(null),2e3)})}return i.jsxs("section",{style:{marginTop:"2rem"},"data-testid":"invites-section",children:[i.jsx("div",{className:"list-header",children:i.jsx("h2",{children:"Invite links"})}),i.jsx("p",{className:"muted",children:"Generate a one-time, expiring link. The recipient opens it and picks their password — no admin-typed default password. Either provision them a new vault of their own, or share an existing vault read-only or read & write. Pre-naming the username makes the link a named deliverable (the recipient can't change it)."}),i.jsxs("form",{onSubmit:W=>void te(W),style:{marginBottom:"1rem"},children:[i.jsxs("div",{style:{display:"flex",gap:"0.75rem",flexWrap:"wrap",alignItems:"flex-end"},children:[i.jsxs("label",{className:"field",style:{flex:"1 1 10rem"},children:[i.jsx("span",{className:"field-label",children:"Username (optional)"}),i.jsx("input",{type:"text",value:h,onChange:W=>g(W.target.value),placeholder:"leave blank → they choose",pattern:"[a-z0-9_-]*",minLength:2,maxLength:32,autoComplete:"off",spellCheck:!1})]}),i.jsxs("label",{className:"field",style:{flex:"0 0 14rem"},children:[i.jsx("span",{className:"field-label",children:"Vault access"}),i.jsxs("select",{value:y,onChange:W=>m(W.target.value==="share"?"share":"provision"),children:[i.jsx("option",{value:"provision",children:"Create a new vault for them"}),i.jsx("option",{value:"share",children:"Share an existing vault"})]})]}),y==="provision"?i.jsxs("label",{className:"field",style:{flex:"1 1 12rem"},children:[i.jsx("span",{className:"field-label",children:"New vault name (optional)"}),i.jsx("input",{type:"text",value:v,onChange:W=>x(W.target.value),placeholder:"leave blank → they choose",pattern:"[a-z0-9_-]*",autoComplete:"off",spellCheck:!1})]}):i.jsxs(i.Fragment,{children:[i.jsxs("label",{className:"field",style:{flex:"1 1 10rem"},children:[i.jsx("span",{className:"field-label",children:"Vault"}),i.jsxs("select",{value:j,onChange:W=>_(W.target.value),required:!0,children:[i.jsx("option",{value:"",children:"Pick a vault…"}),O.map(W=>i.jsx("option",{value:W,children:W},W))]})]}),i.jsxs("label",{className:"field",style:{flex:"0 0 9rem"},children:[i.jsx("span",{className:"field-label",children:"Role"}),i.jsxs("select",{value:k,onChange:W=>C(W.target.value==="write"?"write":"read"),children:[i.jsx("option",{value:"read",children:"Read-only"}),i.jsx("option",{value:"write",children:"Read & write"})]})]})]}),i.jsxs("label",{className:"field",style:{flex:"0 0 8rem"},children:[i.jsx("span",{className:"field-label",children:"Expires (days)"}),i.jsx("input",{type:"number",min:1,max:90,value:z,onChange:W=>Z(W.target.value)})]}),i.jsx("button",{type:"submit",disabled:X.kind==="submitting",children:X.kind==="submitting"?"Creating…":"Create invite"})]}),i.jsx("p",{className:"muted",style:{marginTop:"0.5rem"},"data-testid":"invite-preview",children:y==="share"&&j===""?"Pick the existing vault to share to finish configuring this link.":Ih(Q)})]}),X.kind==="error"&&i.jsx("div",{className:"error-banner",style:{marginBottom:"1rem"},children:X.message}),X.kind==="created"&&i.jsxs("output",{className:"success-banner",style:{display:"block",marginBottom:"1rem"},children:[i.jsx("strong",{children:"Invite created."})," ",Ih(X.result.invite),i.jsx("br",{}),`Copy it now — the link is shown only once and can't be retrieved later (the hub stores only a hash). "Copy message" includes a ready-to-send note telling the recipient what the link does.`,i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem",alignItems:"center",flexWrap:"wrap"},children:[i.jsx("code",{style:{wordBreak:"break-all",flex:"1 1 16rem"},children:X.result.url}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>ie(X.result.url,"link"),children:K==="link"?"Copied!":"Copy link"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>ie(P0(X.result.invite,X.result.url,X.result.invite.expires_at),"message"),children:K==="message"?"Copied!":"Copy message"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>P({kind:"idle"}),children:"Dismiss"})]})]}),r.kind==="loading"&&i.jsx("p",{className:"muted",children:"Loading invites…"}),r.kind==="error"&&i.jsxs("div",{className:"error-banner",children:[r.message,i.jsx("div",{style:{marginTop:"0.5rem"},children:i.jsx("button",{type:"button",className:"secondary",onClick:()=>f(W=>W+1),children:"Retry"})})]}),r.kind==="ok"&&r.invites.length===0&&i.jsx("p",{className:"muted",children:"No invites yet."}),r.kind==="ok"&&r.invites.length>0&&i.jsx("div",{className:"table-scroll",children:i.jsxs("table",{className:"user-table",children:[i.jsx("thead",{children:i.jsxs("tr",{children:[i.jsx("th",{children:"For"}),i.jsx("th",{children:"Vault"}),i.jsx("th",{children:"Access"}),i.jsx("th",{children:"Status"}),i.jsx("th",{children:"Expires"}),i.jsx("th",{children:"Created"}),i.jsx("th",{})]})}),i.jsx("tbody",{children:r.invites.map(W=>i.jsxs("tr",{children:[i.jsx("td",{children:W.username??i.jsx("span",{className:"muted",children:"they choose"})}),i.jsx("td",{children:W.vault_name??i.jsx("span",{className:"muted",children:"redeemer chooses"})}),i.jsx("td",{children:I0(W)}),i.jsx("td",{children:i.jsx("span",{className:`status status-${W.status}`,children:W.status})}),i.jsx("td",{className:"muted",children:new Date(W.expires_at).toLocaleDateString()}),i.jsx("td",{className:"muted",children:new Date(W.created_at).toLocaleDateString()}),i.jsx("td",{children:W.status==="pending"?i.jsx("button",{type:"button",className:"secondary",disabled:E===W.id,onClick:()=>void le(W.id),children:E===W.id?"Revoking…":"Revoke"}):null})]},W.id))})]})}),l?i.jsxs("p",{className:"muted",style:{marginTop:"0.5rem",fontSize:"0.85em"},children:["Redemption links are served from ",i.jsxs("code",{children:[l,"/account/setup/<token>"]}),"."]}):null]})}const Ba=12,tb=/^[a-z0-9_-]+$/,bs=2,xs=32,Ph={username:"",password:"",assignedVaults:[]};function _m({username:l,hubOrigin:r}){const c=`${r||""}/login`;return i.jsxs("div",{className:"muted",style:{marginTop:"0.5rem",fontSize:"0.9em"},children:["Send them to ",i.jsx("code",{children:c})," with username ",i.jsx("code",{children:l})," and this password — they'll be prompted to set a new one on first sign-in."]})}function ab(){const[l,r]=b.useState({kind:"loading"}),[c,o]=b.useState(0),[f,h]=b.useState(!1),[g,y]=b.useState(Ph),[m,v]=b.useState({kind:"idle"}),[x,j]=b.useState({kind:"idle"}),[_,k]=b.useState({kind:"idle"}),[C,O]=b.useState({kind:"idle"});b.useEffect(()=>{let K=!1;return r({kind:"loading"}),Promise.all([m1(),Nm(),Cm()]).then(([J,E,D])=>{if(K)return;const Q=D.resolved_issuer.replace(/\/+$/,"");r({kind:"ok",data:{users:J,vaults:E,hubOrigin:Q}})}).catch(J=>{if(K)return;const E=J instanceof Error?J.message:String(J);r({kind:"error",message:E})}),()=>{K=!0}},[c]);function L(K){return K.username.length<bs||K.username.length>xs?`Username must be ${bs}-${xs} characters.`:tb.test(K.username)?K.password.length<Ba?`Password must be at least ${Ba} characters.`:null:"Username may only contain lowercase letters, digits, hyphens, and underscores."}async function z(K){K.preventDefault();const J=L(g);if(J){v({kind:"error",message:J});return}v({kind:"submitting"});const E={username:g.username,password:g.password,assignedVaults:g.assignedVaults};try{const D=await p1(E);y(Ph),v({kind:"created",username:D.username}),o(Q=>Q+1)}catch(D){const Q=D instanceof V?`Create failed (${D.status}): ${D.message}`:D instanceof Error?D.message:String(D);v({kind:"error",message:Q})}}async function Z(K){j({kind:"deleting",userId:K.id});try{const{revocationLagSeconds:J}=await g1(K.id);j({kind:"done",username:K.username,revocationLagSeconds:J}),o(E=>E+1)}catch(J){const E=J instanceof V?`Delete failed (${J.status}): ${J.message}`:J instanceof Error?J.message:String(J);j({kind:"error",userId:K.id,message:E})}}async function X(K,J){O({kind:"submitting",userId:K.id,selected:J});try{await y1(K.id,J),O({kind:"done",userId:K.id,username:K.username}),o(E=>E+1)}catch(E){const D=E instanceof V?`Edit vaults failed (${E.status}): ${E.message}`:E instanceof Error?E.message:String(E);O({kind:"error",userId:K.id,selected:J,message:D})}}async function P(K,J){if(J.length<Ba){k({kind:"error",userId:K.id,password:J,message:`Password must be at least ${Ba} characters.`});return}k({kind:"submitting",userId:K.id,password:J});try{const{revocationLagSeconds:E}=await v1(K.id,J);k({kind:"done",userId:K.id,username:K.username,revocationLagSeconds:E}),o(D=>D+1)}catch(E){const D=E instanceof V?`Reset failed (${E.status}): ${E.message}`:E instanceof Error?E.message:String(E);k({kind:"error",userId:K.id,password:J,message:D})}}return i.jsxs("div",{"data-route-content":"true",children:[i.jsx("div",{className:"list-header",children:i.jsx("h1",{children:"Users"})}),i.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 ",i.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",l.kind==="ok"?i.jsxs(i.Fragment,{children:[" ","at ",i.jsxs("code",{children:[l.data.hubOrigin,"/login"]})]}):null,"."]}),x.kind==="done"&&i.jsxs("output",{className:"success-banner",style:{display:"block",marginBottom:"0.75rem"},"data-testid":"delete-done-banner",children:["Deleted ",i.jsx("code",{children:x.username}),". Their tokens are revoked. Resource servers (vault, scribe, etc.) cache the revocation list for up to ",x.revocationLagSeconds," ","seconds — if you're deleting because of a suspected compromise, also restart the affected services (e.g. ",i.jsx("code",{children:"parachute restart vault"}),") to flush their cache immediately.",i.jsx("div",{style:{marginTop:"0.5rem"},children:i.jsx("button",{type:"button",className:"secondary",onClick:()=>j({kind:"idle"}),children:"Dismiss"})})]}),nb(l,x,j,Z,_,k,P,C,O,X,l.kind==="ok"?l.data.vaults:[],l.kind==="ok"?l.data.hubOrigin:"",()=>o(K=>K+1)),l.kind==="ok"&&i.jsx(ub,{show:f,setShow:h,form:g,setForm:y,vaults:l.data.vaults,hubOrigin:l.data.hubOrigin,createState:m,setCreateState:v,onSubmit:z}),l.kind==="ok"&&i.jsx(eb,{hubOrigin:l.data.hubOrigin}),l.kind==="ok"&&i.jsx(cb,{})]})}function nb(l,r,c,o,f,h,g,y,m,v,x,j,_){var O;if(l.kind==="loading")return i.jsx("p",{className:"muted",children:"Loading users…"});if(l.kind==="error")return i.jsxs(i.Fragment,{children:[i.jsxs("div",{className:"error-banner",children:["Couldn't load users: ",i.jsx("code",{children:l.message})]}),i.jsx("button",{type:"button",onClick:_,className:"secondary",children:"Retry"})]});const{users:k}=l.data;if(k.length===0)return i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No users yet."}),i.jsx("p",{className:"muted",children:"Click Create User below to invite someone."})]});const C=(O=k[0])==null?void 0:O.id;return i.jsx(lb,{users:k,firstAdminId:C,deleteSt:r,setDeleteSt:c,onConfirmDelete:o,resetSt:f,setResetSt:h,onSubmitReset:g,editVaultsSt:y,setEditVaultsSt:m,onSubmitEditVaults:v,availableVaults:x,hubOrigin:j})}function lb({users:l,firstAdminId:r,deleteSt:c,setDeleteSt:o,onConfirmDelete:f,resetSt:h,setResetSt:g,onSubmitReset:y,editVaultsSt:m,setEditVaultsSt:v,onSubmitEditVaults:x,availableVaults:j,hubOrigin:_}){return i.jsx("div",{className:"user-list",style:{marginTop:"1rem"},children:i.jsx("div",{className:"table-scroll",children:i.jsxs("table",{className:"user-table",children:[i.jsx("thead",{children:i.jsxs("tr",{children:[i.jsx("th",{scope:"col",children:"Username"}),i.jsx("th",{scope:"col",children:"Email"}),i.jsx("th",{scope:"col",children:"Role"}),i.jsx("th",{scope:"col",children:"Assigned vaults"}),i.jsx("th",{scope:"col",children:"Password set"}),i.jsx("th",{scope:"col",children:"Created"}),i.jsx("th",{scope:"col",children:"Actions"})]})}),i.jsx("tbody",{children:l.map(k=>{const C=k.id===r,O=c.kind==="deleting"&&c.userId===k.id,L=c.kind==="confirming"&&c.user.id===k.id,z=c.kind==="error"&&c.userId===k.id?c:null,Z=(h.kind==="open"||h.kind==="submitting"||h.kind==="error")&&h.userId===k.id?h:null,X=h.kind==="done"&&h.userId===k.id?h:null,P=(m.kind==="open"||m.kind==="submitting"||m.kind==="error")&&m.userId===k.id?m:null,K=m.kind==="done"&&m.userId===k.id?m:null;return i.jsxs("tr",{"data-user-id":k.id,children:[i.jsxs("td",{children:[i.jsx("code",{children:k.username}),C&&i.jsx("span",{className:"badge",style:{marginLeft:"0.5rem"},children:"first admin"})]}),i.jsx("td",{children:k.email?k.email.includes("@")?i.jsx("a",{href:`mailto:${k.email}`,children:k.email}):i.jsx("span",{children:k.email}):i.jsx("span",{className:"muted",title:"No email on file (account predates signup-email capture)",children:"—"})}),i.jsx("td",{children:C?i.jsx("span",{className:"badge",children:"admin"}):i.jsx("span",{className:"muted",children:"member"})}),i.jsx("td",{children:k.assigned_vaults.length>0?i.jsx("span",{style:{display:"inline-flex",flexWrap:"wrap",gap:"0.25rem"},children:k.assigned_vaults.map(J=>i.jsx("code",{children:J},J))}):i.jsx("span",{className:"muted",title:C?"First admin is unrestricted (admin posture)":"No vaults assigned — user can't authorize any vault yet",children:"—"})}),i.jsx("td",{children:k.password_changed?i.jsx("span",{"aria-label":"changed",children:"✓"}):i.jsx("span",{className:"status status-pending",title:"User hasn't completed first-sign-in change-password yet",children:"pending first login"})}),i.jsx("td",{children:i.jsx("span",{title:k.created_at,children:Am(k.created_at)})}),i.jsxs("td",{children:[L?null:i.jsxs("div",{style:{display:"flex",flexWrap:"wrap",gap:"0.5rem",alignItems:"center"},children:[C&&i.jsxs(i.Fragment,{children:[i.jsx("span",{id:`first-admin-tooltip-${k.id}`,className:"sr-only",children:"First admin can't be deleted (would self-lock the hub)"}),i.jsx("span",{id:`first-admin-reset-tooltip-${k.id}`,className:"sr-only",children:"First admin uses /account/change-password directly"}),i.jsx("span",{id:`first-admin-vaults-tooltip-${k.id}`,className:"sr-only",children:"First admin's vault membership is unrestricted by design"})]}),i.jsx("button",{type:"button",className:"secondary",disabled:C||P!==null||m.kind==="submitting"&&m.userId===k.id,title:C?"First admin's vault membership is unrestricted by design":void 0,"aria-describedby":C?`first-admin-vaults-tooltip-${k.id}`:void 0,onClick:()=>v({kind:"open",userId:k.id,selected:[...k.assigned_vaults]}),"aria-label":`Edit vaults for ${k.username}`,children:"Edit vaults"}),i.jsx("button",{type:"button",className:"secondary",disabled:C||Z!==null||h.kind==="submitting"&&h.userId===k.id,title:C?"First admin uses /account/change-password directly":void 0,"aria-describedby":C?`first-admin-reset-tooltip-${k.id}`:void 0,onClick:()=>g({kind:"open",userId:k.id,password:""}),"aria-label":`Reset password for ${k.username}`,children:"Reset password"}),i.jsx("button",{type:"button",className:"secondary",disabled:C||O,title:C?"First admin can't be deleted (would self-lock the hub)":void 0,"aria-describedby":C?`first-admin-tooltip-${k.id}`:void 0,onClick:()=>o({kind:"confirming",user:k}),"aria-label":`Delete ${k.username}`,children:O?"Deleting…":"Delete"})]}),L&&i.jsxs("dialog",{open:!0,className:"error-banner",style:{marginTop:"0.25rem",background:"var(--bg-warn, #fffbe6)"},"aria-label":`Confirm delete ${k.username}`,children:[i.jsxs("p",{children:["Delete ",i.jsx("code",{children:k.username}),"? This revokes their tokens, drops their sessions and grants, and removes the account. The audit trail is preserved — tokens stay with ",i.jsx("code",{children:"revoked_at"})," set, anonymised."]}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"button",className:"destructive",onClick:()=>{f(k)},disabled:O,children:O?"Deleting…":"Delete"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>o({kind:"idle"}),disabled:O,children:"Cancel"})]})]}),z&&i.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:i.jsx("code",{children:z.message})}),Z&&i.jsx(ib,{user:k,state:Z,onCancel:()=>g({kind:"idle"}),onPasswordChange:J=>g({kind:"open",userId:k.id,password:J}),onSubmit:J=>{y(k,J)}}),X&&i.jsxs("output",{className:"success-banner",style:{marginTop:"0.25rem",display:"block"},children:["Password reset for ",i.jsx("code",{children:X.username}),". Hand them the new password and tell them they'll be prompted to change it on first sign-in.",i.jsx(_m,{username:X.username,hubOrigin:_}),i.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 ",X.revocationLagSeconds," ","seconds — if you're resetting because of a suspected compromise, also restart the affected services (e.g. ",i.jsx("code",{children:"parachute restart vault"}),") to flush their cache immediately."]}),i.jsx("div",{style:{marginTop:"0.5rem"},children:i.jsx("button",{type:"button",className:"secondary",onClick:()=>g({kind:"idle"}),children:"Dismiss"})})]}),P&&i.jsx(sb,{user:k,state:P,availableVaults:j,onCancel:()=>v({kind:"idle"}),onSelectedChange:J=>v({kind:"open",userId:k.id,selected:J}),onSubmit:J=>{x(k,J)}}),K&&i.jsxs("output",{className:"success-banner",style:{marginTop:"0.25rem",display:"block"},children:["Vault assignments updated for ",i.jsx("code",{children:K.username}),".",i.jsx("div",{style:{marginTop:"0.5rem"},children:i.jsx("button",{type:"button",className:"secondary",onClick:()=>v({kind:"idle"}),children:"Dismiss"})})]})]})]},k.id)})})]})})})}function ib({user:l,state:r,onCancel:c,onPasswordChange:o,onSubmit:f}){const h=r.kind==="submitting",g=r.kind==="error"?r.message:null,y=`reset-password-input-${l.id}`;return i.jsxs("form",{onSubmit:m=>{m.preventDefault(),f(r.password)},"aria-label":`Reset password for ${l.username}`,style:{marginTop:"0.5rem",padding:"0.5rem",background:"var(--bg-soft, #f5f5f5)",borderRadius:"4px"},children:[i.jsxs("p",{style:{margin:0},children:[i.jsxs("label",{htmlFor:y,children:["New temporary password for ",i.jsx("code",{children:l.username})," ",i.jsxs("span",{className:"muted",children:["(min ",Ba," chars)"]})]}),i.jsx("br",{}),i.jsx("input",{id:y,type:"password",required:!0,autoComplete:"new-password",minLength:Ba,value:r.password,disabled:h,onChange:m=>o(m.target.value)})]}),g&&i.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:i.jsx("code",{children:g})}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"submit",disabled:h,children:h?"Setting…":"Set new password"}),i.jsx("button",{type:"button",className:"secondary",onClick:c,disabled:h,children:"Cancel"})]})]})}function sb({user:l,state:r,availableVaults:c,onCancel:o,onSelectedChange:f,onSubmit:h}){const g=r.kind==="submitting",y=r.kind==="error"?r.message:null,m=`edit-vaults-select-${l.id}`;function v(x){const j=Array.from(x.target.selectedOptions).map(_=>_.value);f(j)}return i.jsxs("form",{onSubmit:x=>{x.preventDefault(),h(r.selected)},"aria-label":`Edit vaults for ${l.username}`,style:{marginTop:"0.5rem",padding:"0.5rem",background:"var(--bg-soft, #f5f5f5)",borderRadius:"4px"},children:[i.jsx("p",{style:{margin:0},children:i.jsxs("label",{htmlFor:m,children:["Vault assignments for ",i.jsx("code",{children:l.username})," ",i.jsx("span",{className:"muted",children:"(empty = no narrowing; shift-click to multi-select)"})]})}),r.selected.length>0&&i.jsx("div",{"data-testid":`edit-vaults-chips-${l.id}`,style:{marginTop:"0.4rem",display:"flex",flexWrap:"wrap",gap:"0.25rem"},children:r.selected.map(x=>i.jsx("code",{style:{padding:"0.1rem 0.4rem",borderRadius:"4px"},children:x},x))}),i.jsx("select",{id:m,multiple:!0,value:r.selected,onChange:v,disabled:g,size:Math.min(Math.max(c.length,3),8),style:{marginTop:"0.4rem",minWidth:"12rem"},children:c.map(x=>i.jsx("option",{value:x,children:x},x))}),c.length===0&&i.jsx("p",{className:"muted",style:{marginTop:"0.25rem"},children:"No vaults registered on this hub yet."}),y&&i.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:i.jsx("code",{children:y})}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"submit",disabled:g,children:g?"Saving…":"Save vault assignments"}),i.jsx("button",{type:"button",className:"secondary",onClick:o,disabled:g,children:"Cancel"})]})]})}function ub({show:l,setShow:r,form:c,setForm:o,vaults:f,hubOrigin:h,createState:g,setCreateState:y,onSubmit:m}){const v=g.kind==="submitting";return i.jsx("section",{style:{marginTop:"1.5rem"},children:l?i.jsxs("form",{onSubmit:x=>void m(x),"aria-label":"Create user",children:[i.jsx("h3",{children:"Create user"}),i.jsxs("p",{children:[i.jsxs("label",{htmlFor:"new-user-username",children:["Username"," ",i.jsxs("span",{className:"muted",children:["(",bs,"-",xs," chars, lowercase letters/digits/hyphens/ underscores)"]})]}),i.jsx("br",{}),i.jsx("input",{id:"new-user-username",type:"text",required:!0,autoComplete:"off",value:c.username,minLength:bs,maxLength:xs,pattern:"[a-z0-9_\\-]+",onChange:x=>o({...c,username:x.target.value})})]}),i.jsxs("p",{children:[i.jsxs("label",{htmlFor:"new-user-password",children:["Password ",i.jsxs("span",{className:"muted",children:["(min ",Ba," chars)"]})]}),i.jsx("br",{}),i.jsx("input",{id:"new-user-password",type:"password",required:!0,autoComplete:"new-password",value:c.password,minLength:Ba,onChange:x=>o({...c,password:x.target.value})})]}),i.jsxs("p",{children:[i.jsxs("label",{htmlFor:"new-user-vaults",children:["Assigned vaults"," ",i.jsx("span",{className:"muted",children:"(empty = no restriction; shift-click to select multiple)"})]}),i.jsx("br",{}),c.assignedVaults.length>0&&i.jsx("span",{"data-testid":"new-user-vault-chips",style:{display:"inline-flex",flexWrap:"wrap",gap:"0.25rem",marginBottom:"0.25rem"},children:c.assignedVaults.map(x=>i.jsx("code",{children:x},x))}),i.jsx("br",{}),i.jsx("select",{id:"new-user-vaults",multiple:!0,value:c.assignedVaults,onChange:x=>o({...c,assignedVaults:Array.from(x.target.selectedOptions).map(j=>j.value)}),size:Math.min(Math.max(f.length,3),8),style:{minWidth:"12rem"},children:f.map(x=>i.jsx("option",{value:x,children:x},x))}),f.length===0&&i.jsx("span",{className:"muted",style:{marginLeft:"0.5rem"},children:"No vaults registered on this hub yet."})]}),g.kind==="error"&&i.jsx("div",{className:"error-banner",children:i.jsx("code",{children:g.message})}),g.kind==="created"&&i.jsxs("output",{className:"success-banner",children:["User ",i.jsx("code",{children:g.username})," created. They'll be prompted to change their password on first sign-in.",i.jsx(_m,{username:g.username,hubOrigin:h})]}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"submit",disabled:v,children:v?"Creating…":"Create user"}),i.jsx("button",{type:"button",className:"secondary",disabled:v,onClick:()=>{r(!1),y({kind:"idle"})},children:"Close"})]})]}):i.jsx("button",{type:"button",onClick:()=>r(!0),children:"Create User"})})}function Am(l){const r=new Date(l);return Number.isNaN(r.getTime())?l:r.toLocaleString()}const Rm=1024*1024*1024;function rb(l){if(l===null)return"";const r=l/Rm;return Number.isInteger(r)?String(r):r.toFixed(2)}function cb(){const[l,r]=b.useState({kind:"loading"}),[c,o]=b.useState(0),[f,h]=b.useState({kind:"idle"});b.useEffect(()=>{let y=!1;return r({kind:"loading"}),b1().then(m=>{y||r({kind:"ok",caps:m})}).catch(m=>{if(y)return;const v=m instanceof Error?m.message:String(m);r({kind:"error",message:v})}),()=>{y=!0}},[c]);async function g(y,m){const v=Number(m);if(!Number.isFinite(v)||v<=0){h({kind:"error",vaultName:y,gibInput:m,message:"Cap must be a positive number of GB."});return}h({kind:"submitting",vaultName:y,gibInput:m});try{await x1(y,Math.round(v*Rm)),h({kind:"idle"}),o(x=>x+1)}catch(x){const j=x instanceof V?`Set cap failed (${x.status}): ${x.message}`:x instanceof Error?x.message:String(x);h({kind:"error",vaultName:y,gibInput:m,message:j})}}return i.jsxs("section",{style:{marginTop:"1.5rem"},"data-testid":"vault-caps-section",children:[i.jsx("h2",{children:"Vault caps"}),i.jsx("p",{className:"muted",children:"Per-vault storage ceilings for this hub. Public-signup vaults are stamped with a default cap (~1 GB); admin-provisioned vaults are uncapped until you set one. Edit a cap to raise or lower a vault's ceiling."}),l.kind==="loading"&&i.jsx("p",{className:"muted",children:"Loading vault caps…"}),l.kind==="error"&&i.jsxs(i.Fragment,{children:[i.jsxs("div",{className:"error-banner",children:["Couldn't load vault caps: ",i.jsx("code",{children:l.message})]}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>o(y=>y+1),children:"Retry"})]}),l.kind==="ok"&&l.caps.length===0&&i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No vaults on this hub yet."}),i.jsx("p",{className:"muted",children:"Caps appear here once a vault is registered."})]}),l.kind==="ok"&&l.caps.length>0&&i.jsx("div",{className:"table-scroll",style:{marginTop:"1rem"},children:i.jsxs("table",{className:"user-table",children:[i.jsx("thead",{children:i.jsxs("tr",{children:[i.jsx("th",{scope:"col",children:"Vault"}),i.jsx("th",{scope:"col",children:"Cap"}),i.jsx("th",{scope:"col",children:"Updated"}),i.jsx("th",{scope:"col",children:"Actions"})]})}),i.jsx("tbody",{children:l.caps.map(y=>{const m=(f.kind==="open"||f.kind==="submitting"||f.kind==="error")&&f.vaultName===y.vault_name?f:null;return i.jsxs("tr",{"data-vault-name":y.vault_name,children:[i.jsx("td",{children:i.jsx("code",{children:y.vault_name})}),i.jsx("td",{"data-testid":`vault-cap-${y.vault_name}`,children:y.cap_bytes!==null?Sm(y.cap_bytes):i.jsx("span",{className:"muted",title:"No cap set — this vault is uncapped",children:"uncapped"})}),i.jsx("td",{children:y.updated_at?i.jsx("span",{title:y.updated_at,children:Am(y.updated_at)}):i.jsx("span",{className:"muted",children:"—"})}),i.jsx("td",{children:m?i.jsx(ob,{vaultName:y.vault_name,state:m,onCancel:()=>h({kind:"idle"}),onChange:v=>h({kind:"open",vaultName:y.vault_name,gibInput:v}),onSubmit:v=>{g(y.vault_name,v)}}):i.jsx("button",{type:"button",className:"secondary",onClick:()=>h({kind:"open",vaultName:y.vault_name,gibInput:rb(y.cap_bytes)}),"aria-label":`Edit cap for ${y.vault_name}`,children:"Edit cap"})})]},y.vault_name)})})]})})]})}function ob({vaultName:l,state:r,onCancel:c,onChange:o,onSubmit:f}){const h=r.kind==="submitting",g=r.kind==="error"?r.message:null,y=`cap-input-${l}`;return i.jsxs("form",{onSubmit:m=>{m.preventDefault(),f(r.gibInput)},"aria-label":`Edit cap for ${l}`,style:{padding:"0.5rem",background:"var(--bg-soft, #f5f5f5)",borderRadius:"4px"},children:[i.jsxs("label",{htmlFor:y,children:["Cap for ",i.jsx("code",{children:l})," ",i.jsx("span",{className:"muted",children:"(GB)"})]}),i.jsx("br",{}),i.jsx("input",{id:y,type:"number",step:"0.01",value:r.gibInput,disabled:h,onChange:m=>o(m.target.value),style:{width:"8rem",marginTop:"0.25rem"}}),g&&i.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:i.jsx("code",{children:g})}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"submit",disabled:h,children:h?"Saving…":"Save cap"}),i.jsx("button",{type:"button",className:"secondary",onClick:c,disabled:h,children:"Cancel"})]})]})}const db="https://parachute.computer/install#connect-mcp-clients";function Mm(l){return`${l.replace(/\/+$/,"")}/mcp`}function zm(l,r){return`claude mcp add --transport http parachute-${l} ${Mm(r)}`}function fb(l,r,c){return`${zm(l,r)} --header "Authorization: Bearer ${c}"`}function sc({value:l,label:r="Copy"}){const[c,o]=b.useState(!1);return i.jsx("button",{type:"button",className:"secondary",onClick:()=>{typeof navigator>"u"||!navigator.clipboard||navigator.clipboard.writeText(l).then(()=>{o(!0),setTimeout(()=>o(!1),2e3)})},children:c?"Copied ✓":r})}function hb({vaultName:l,vaultUrl:r,embedded:c=!1}){const o=Mm(r),f=zm(l,r),[h,g]=b.useState(!1),[y,m]=b.useState({kind:"idle"});async function v(){if(y.kind!=="submitting"){m({kind:"submitting"});try{const _=await wm({scope:`vault:${l}:read vault:${l}:write`});m({kind:"minted",token:_})}catch(_){const k=_ instanceof V?`mint failed (${_.status}): ${_.message}`:_ instanceof Error?_.message:String(_);m({kind:"error",message:k})}}}const x=y.kind==="minted"?fb(l,r,y.token.token):null,j=i.jsxs(i.Fragment,{children:[i.jsx("h3",{children:"Connect an MCP client (or connector)"}),i.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 ",i.jsx("code",{children:l})," vault. The client signs in to this hub and reads/writes vault data over MCP."]}),i.jsxs("div",{className:"mcp-field",children:[i.jsx("span",{className:"mcp-field-label",children:"Endpoint"}),i.jsxs("div",{className:"token-box",children:[i.jsx("code",{"data-testid":"mcp-endpoint",children:o}),i.jsx(sc,{value:o})]})]}),i.jsxs("div",{className:"mcp-field",children:[i.jsx("span",{className:"mcp-field-label",children:"Claude Code"}),i.jsxs("div",{className:"token-box",children:[i.jsx("code",{"data-testid":"mcp-add-command",children:f}),i.jsx(sc,{value:f})]}),i.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."})]}),i.jsxs("details",{className:"mcp-token-path",open:h,onToggle:_=>g(_.currentTarget.open),children:[i.jsx("summary",{children:"Use a token instead (headless / CI clients)"}),i.jsxs("p",{className:"dim",children:["For clients that can't do browser OAuth, mint a scope-narrow hub token (",i.jsxs("code",{children:["vault:",l,":read vault:",l,":write"]})," ","— not admin) and pass it as a header. Revealed once; copy it now."]}),y.kind==="minted"&&x?i.jsxs("div",{className:"mint-banner","data-testid":"mcp-header-banner",children:[i.jsx("h3",{children:"Token minted"}),i.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 ",i.jsx("code",{children:"Authorization"})," header."]}),i.jsxs("div",{className:"token-box",children:[i.jsx("code",{"data-testid":"mcp-header-command",children:x}),i.jsx(sc,{value:x})]}),i.jsxs("p",{className:"warn",children:["⚠ Manage and revoke this token at ",i.jsx("code",{children:"/admin/tokens"}),"."]})]}):i.jsx("div",{className:"actions",children:i.jsx("button",{type:"button",className:"secondary",onClick:()=>{v()},disabled:y.kind==="submitting",children:y.kind==="submitting"?"Minting…":"Mint a token"})}),y.kind==="error"?i.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:i.jsx("code",{children:y.message})}):null]}),i.jsx("p",{className:"dim mcp-docs-link",children:i.jsx("a",{href:db,target:"_blank",rel:"noreferrer",children:"Full connect docs →"})})]});return c?i.jsx("div",{className:"mcp-connect-card mcp-connect-card-embedded",children:j}):i.jsx("div",{className:"mcp-connect-card",children:j})}const em="/vault/admin/";function mb(){const[l,r]=b.useState("detecting"),[c,o]=b.useState({kind:"loading"}),[f,h]=b.useState(0),[g,y]=b.useState({kind:"idle"}),[m,v]=b.useState(null),[x,j]=b.useState({});b.useEffect(()=>{let C=!1;return Cs().then(O=>{if(C)return;const L=O.modules.find(z=>z.short==="vault");if(L!=null&&L.installed&&L.config_ui_url===em){r("redirecting"),window.location.replace(em);return}r("legacy")}).catch(()=>{C||r("legacy")}),()=>{C=!0}},[]);async function _(C){if(C.managementUrl){y({kind:"minting",name:C.name});try{const O=await jm(C.name),L=l1(C.url,C.managementUrl),z=L.includes("#")?"&":"#";window.location.assign(`${L}${z}token=${O.token}`)}catch(O){const L=O instanceof V?`mint failed (${O.status}): ${O.message}`:O instanceof Error?O.message:String(O);y({kind:"error",name:C.name,message:L})}}}if(b.useEffect(()=>{if(l!=="legacy")return;let C=!1;return Sc().then(O=>{C||o({kind:"ok",vaults:O.vaults,moduleInstalled:O.moduleInstalled})}).catch(O=>{if(C)return;const L=O instanceof Error?O.message:String(O);o({kind:"error",message:L})}),()=>{C=!0}},[f,l]),b.useEffect(()=>{if(c.kind!=="ok")return;const C=c.vaults.map(L=>L.name);if(C.length===0)return;let O=!1;j(L=>{const z={...L};for(const Z of C)z[Z]||(z[Z]={kind:"loading"});return z});for(const L of C)e1(L).then(z=>{O||j(Z=>({...Z,[L]:{kind:"ok",usage:z}}))}).catch(()=>{O||j(z=>({...z,[L]:{kind:"error"}}))});return()=>{O=!0}},[c]),l!=="legacy")return i.jsxs("div",{children:[i.jsx("div",{className:"list-header",children:i.jsx("h1",{children:"Vaults"})}),i.jsx("p",{className:"muted","data-testid":"vaults-detecting",children:l==="redirecting"?"Opening the vault admin…":"Loading…"})]});if(c.kind==="loading")return i.jsxs("div",{children:[i.jsx("div",{className:"list-header",children:i.jsx("h1",{children:"Vaults"})}),i.jsx("p",{className:"muted",children:"Loading…"})]});if(c.kind==="error")return i.jsxs("div",{children:[i.jsx("div",{className:"list-header",children:i.jsx("h1",{children:"Vaults"})}),i.jsxs("div",{className:"error-banner",children:["Couldn't load vaults: ",i.jsx("code",{children:c.message})]}),i.jsx("button",{type:"button",onClick:()=>h(C=>C+1),className:"secondary",children:"Retry"})]});const k=c.vaults.length===0&&!c.moduleInstalled;return i.jsxs("div",{"data-route-content":"true",children:[i.jsxs("div",{className:"list-header",children:[i.jsxs("h1",{children:["Vaults (",c.vaults.length,")"]}),k?i.jsx(lt,{to:"/modules",children:i.jsx("button",{type:"button",children:"Install vault module"})}):null]}),i.jsxs("p",{className:"muted",children:["Vaults registered with this hub at ",i.jsx("code",{children:"/.well-known/parachute.json"}),"."," ",i.jsx("strong",{children:"Manage"})," opens the vault's own admin app (outside the hub shell); use your browser's back button to return here."]}),c.vaults.length>0?i.jsxs("p",{className:"muted","data-testid":"vaults-legacy-create-hint",children:["To create another vault, upgrade the vault module from ",i.jsx(lt,{to:"/modules",children:"Modules"})," ","(its own admin app includes create), or run"," ",i.jsx("code",{children:"parachute vault create <name>"})," on the host."]}):null,c.vaults.length===0?k?i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No vault module installed."}),i.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."}),i.jsx("p",{style:{marginTop:"0.75rem"},children:i.jsx(lt,{to:"/modules",children:"Install vault module →"})})]}):i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No vaults yet."}),i.jsx("p",{className:"muted",children:"Create your first vault to start storing tokens, secrets, and notes scoped to this hub."}),i.jsx("p",{style:{marginTop:"0.75rem"},children:i.jsx("a",{href:"/admin/setup?step=vault",children:"Create a vault →"})})]}):i.jsx("div",{style:{marginTop:"1rem"},children:c.vaults.map(C=>{const O=g.kind==="minting"&&g.name===C.name,L=g.kind==="error"&&g.name===C.name?g:null,z=m===C.name,Z=x[C.name],X=(Z==null?void 0:Z.kind)==="ok"?`${Z.usage.notes} ${Z.usage.notes===1?"note":"notes"} · ${Sm(Z.usage.totalBytes)}`:(Z==null?void 0:Z.kind)==="loading"?"…":"—";return i.jsxs("div",{className:"vault-row-group",children:[i.jsxs("div",{className:"vault-row",children:[i.jsxs("div",{className:"body",children:[i.jsxs("div",{className:"name",children:[i.jsx("code",{children:C.name}),i.jsxs("span",{className:"tag muted",title:"Vault version",children:["v",C.version]})]}),i.jsx("div",{className:"dim url",children:i.jsx("code",{children:C.url})}),i.jsx("div",{className:"dim vault-usage","data-testid":`vault-usage-${C.name}`,children:X}),L?i.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:i.jsx("code",{children:L.message})}):null]}),i.jsxs("div",{className:"vault-row-actions",children:[i.jsx("button",{type:"button",className:z?void 0:"secondary","aria-expanded":z,"aria-label":`Connect an MCP client to vault ${C.name}`,onClick:()=>v(P=>P===C.name?null:C.name),children:z?"Hide connect":"Connect"}),C.managementUrl?i.jsx("button",{type:"button",onClick:()=>{_(C)},disabled:O,"aria-label":`Manage vault ${C.name}`,children:O?"Opening…":"Manage"}):i.jsx("span",{className:"muted",title:"This vault has no admin SPA — manage with parachute-vault on the host.",children:"CLI only"})]})]}),z?i.jsx(hb,{vaultName:C.name,vaultUrl:C.url}):null]},C.name)})})]})}function Om(l){return l==="/permissions"||l.startsWith("/permissions/")?"permissions":l==="/tokens"||l.startsWith("/tokens/")?"tokens":l==="/modules"||l.startsWith("/modules/")?"modules":l==="/users"||l.startsWith("/users/")?"users":l==="/connections"||l.startsWith("/connections/")?"connections":l==="/vaults"||l.startsWith("/vaults/")?"vaults":l==="/settings"||l.startsWith("/settings/")?"settings":l==="/account"||l.startsWith("/account/")?"my account":l.startsWith("/approve-client/")?"approve app":"home"}function pb(l){return`${Om(l).split(" ").map(c=>c.length>0?c[0].toUpperCase()+c.slice(1):c).join(" ")} · ${jc}`}function gb(){const{pathname:l}=Nt(),r=Om(l),[c,o]=b.useState(null);b.useEffect(()=>{document.title=pb(l)},[l]);const[f,h]=b.useState(!1),[g,y]=b.useState([]),m=c!=null&&c.hasSession?c.csrf:null,v=J1(m,!!(c!=null&&c.hasSession));b.useEffect(()=>{let _=!1;return wc().then(k=>{_||o(k)}).catch(()=>{_||o({hasSession:!1})}),()=>{_=!0}},[]),b.useEffect(()=>{if(!c||!c.hasSession)return;let _=!1;return Cs().then(k=>{_||y(k.modules.filter(C=>C.installed))}).catch(()=>{_||y([])}),()=>{_=!0}},[c]),b.useEffect(()=>{v.locked&&je()},[v.locked]);async function x(){if(m){try{await k1(m)}catch{}je(),v.refresh()}}async function j(_){h(!0);try{await n1(_),window.location.href="/"}catch{h(!1)}}return v.locked&&m?i.jsx(Q1,{csrf:m,onUnlocked:v.refresh}):i.jsxs("div",{className:"page",children:[i.jsxs("nav",{className:"nav",children:[i.jsxs(lt,{to:"/",className:"brand",children:[i.jsx(xm,{size:18,idSuffix:"spa-nav",className:"brand-mark-icon"}),i.jsx("span",{className:"brand-wordmark",children:jc}),i.jsx("span",{className:"sub",children:r})]}),i.jsx(yb,{me:c,signingOut:f,onSignOut:j}),c!=null&&c.hasSession&&v.configured?i.jsx("button",{type:"button",className:"auth-spa-signout","data-testid":"admin-lock-now",title:"Lock the admin console now",onClick:()=>void x(),children:"Lock now"}):null,i.jsx(da,{to:"/",label:"Home",exact:!0}),i.jsx(da,{to:"/connections",label:"Connections"}),i.jsx(da,{to:"/grants",label:"Grants"}),i.jsx(da,{to:"/modules",label:"Modules"}),i.jsx(da,{to:"/users",label:"Users"}),i.jsx(da,{to:"/tokens",label:"Tokens"}),i.jsx(da,{to:"/permissions",label:"Permissions"}),i.jsx(da,{to:"/settings",label:"Settings"}),i.jsx(da,{to:"/account",label:"My account"}),i.jsx("span",{className:"nav-divider","aria-hidden":"true"}),i.jsx(vb,{services:g}),i.jsx("a",{href:"/hub.html",title:"Hub discovery page (top-level)",children:"Discovery"})]}),i.jsxs(fy,{children:[i.jsx(nt,{path:"/",element:i.jsx(g0,{})}),i.jsx(nt,{path:"/vaults",element:i.jsx(mb,{})}),i.jsx(nt,{path:"/vaults/new",element:i.jsx(Pr,{to:"/vaults",replace:!0})}),i.jsx(nt,{path:"/modules",element:i.jsx(M0,{})}),i.jsx(nt,{path:"/users",element:i.jsx(ab,{})}),i.jsx(nt,{path:"/connections",element:i.jsx(n0,{})}),i.jsx(nt,{path:"/grants",element:i.jsx(d0,{})}),i.jsx(nt,{path:"/channels",element:i.jsx(Pr,{to:"/connections",replace:!0})}),i.jsx(nt,{path:"/channels/*",element:i.jsx(Pr,{to:"/connections",replace:!0})}),i.jsx(nt,{path:"/permissions",element:i.jsx(B0,{})}),i.jsx(nt,{path:"/tokens",element:i.jsx(Z0,{})}),i.jsx(nt,{path:"/settings",element:i.jsx(Y0,{})}),i.jsx(nt,{path:"/account",element:i.jsx(K1,{})}),i.jsx(nt,{path:"/approve-client/:clientId",element:i.jsx(I1,{})}),i.jsx(nt,{path:"*",element:i.jsxs("div",{className:"empty",children:["404 — back to ",i.jsx(lt,{to:"/",children:"Home"}),"."]})})]}),c!=null&&c.hasSession?i.jsx(X1,{}):null]})}function da({to:l,label:r,alsoActiveAt:c,exact:o}){const{pathname:f}=Nt(),y=(o?f===l:f===l||f.startsWith(`${l}/`))||c!==void 0&&f===c;return i.jsx(lt,{to:l,className:y?"nav-link nav-link-active":"nav-link","aria-current":y?"page":void 0,children:r})}function vb({services:l}){return l.length===0?null:i.jsxs("details",{className:"nav-dropdown","data-testid":"installed-services-dropdown",children:[i.jsx("summary",{className:"nav-dropdown-summary",children:"Services"}),i.jsx("div",{className:"nav-dropdown-panel",role:"menu",children:l.map(r=>{const c=r.management_url;return c?i.jsx("a",{className:"nav-dropdown-item",href:c,role:"menuitem","data-testid":`nav-service-${r.short}`,children:r.display_name},r.short):i.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-${r.short}`,children:r.display_name},r.short)})})]})}function yb({me:l,signingOut:r,onSignOut:c}){return l===null?null:l.hasSession?i.jsxs("span",{className:"auth-spa",children:[i.jsxs("span",{className:"muted",children:["Signed in as ",i.jsx("strong",{children:l.user.displayName})]})," ","·"," ",i.jsx("button",{type:"button",className:"auth-spa-signout",disabled:r,onClick:()=>{c(l.csrf)},children:r?"Signing out…":"Sign out"})]}):i.jsx("a",{href:`/login?next=${encodeURIComponent(window.location.pathname)}`,className:"auth-spa",children:"Sign in"})}const Um=document.getElementById("root");if(!Um)throw new Error("#root not found");function bb(){const l=window.location.pathname;return l==="/admin"||l.startsWith("/admin/")?"/admin":""}mv.createRoot(Um).render(i.jsx(b.StrictMode,{children:i.jsx(Hy,{basename:bb(),children:i.jsx(gb,{})})}));
61
+ `)})]})]},E.operationId))})]}),i.jsxs("section",{className:"modules-installed","data-testid":"installed-section",children:[i.jsx("h2",{children:"Installed modules"}),Z.length===0?i.jsxs("p",{className:"muted","data-testid":"installed-empty",children:["No modules installed yet. Pick one from ",i.jsx("strong",{children:"Install a module"})," below to get started."]}):Fh(Z,E=>i.jsx("ul",{className:"module-list",children:E.map(D=>i.jsx(z0,{module:D,supervisorAvailable:L,syncBusy:!!f[D.short],errorMessage:g[D.short],onUpgrade:()=>void j(D.short),onRestart:()=>void _(D.short),onUninstall:()=>void k(D.short)},D.short))}),"installed")]}),i.jsxs("section",{className:"modules-installable","data-testid":"installable-section",children:[i.jsx("h2",{children:"Install a module"}),K.length===0?i.jsx("p",{className:"muted","data-testid":"installable-empty",children:P.size>0?"Install in progress — see In progress above.":"All available modules are installed."}):Fh(K,E=>i.jsx("ul",{className:"install-list",children:E.map(D=>i.jsx(O0,{module:D,supervisorAvailable:L,installing:P.has(D.short),errorMessage:g[D.short],onInstall:()=>void x(D.short)},D.short))}),"installable")]})]})}function Fh(l,r,c){const o=l.filter(g=>g.focus==="core"),f=l.filter(g=>g.focus==="experimental"),h=l.filter(g=>g.focus==="deprecated");return i.jsxs(i.Fragment,{children:[o.length>0&&i.jsx("div",{"data-testid":`${c}-core-group`,children:r(o)}),f.length>0&&i.jsxs("div",{className:"modules-experimental","data-testid":`${c}-experimental-group`,children:[i.jsxs("h3",{className:"muted experimental-heading",children:["Experimental"," ",i.jsx("span",{className:"muted",children:"— exploration modules; not part of the core surface"})]}),r(f)]}),h.length>0&&i.jsxs("div",{className:"modules-deprecated","data-testid":`${c}-deprecated-group`,children:[i.jsxs("h3",{className:"muted deprecated-heading",children:["Deprecated"," ",i.jsx("span",{className:"muted",children:"— retained for existing installs; not offered for new setups"})]}),r(h)]})]})}function z0({module:l,supervisorAvailable:r,syncBusy:c,errorMessage:o,onUpgrade:f,onRestart:h,onUninstall:g}){const y=r&&!c,m=l.upgrade_available,v=l.management_url,x=l.config_ui_url;return i.jsxs("li",{className:"module-row","data-short":l.short,children:[i.jsxs("header",{children:[i.jsxs("h2",{children:[l.display_name," ",i.jsxs("span",{className:"muted",children:["(",l.short,")"]})]}),(()=>{const j=U0(l);return i.jsx("span",{className:`status status-${j.cssState}`,"data-state":j.cssState,"data-testid":`module-status-${l.short}`,children:j.label})})()]}),l.tagline?i.jsx("p",{className:"tagline",children:l.tagline}):null,i.jsxs("dl",{className:"meta",children:[i.jsx("dt",{children:"Package"}),i.jsx("dd",{children:i.jsx("code",{children:l.package})}),i.jsx("dt",{children:"Installed"}),i.jsxs("dd",{children:["v",l.installed_version??"unknown",m&&i.jsxs("span",{className:"badge",children:["v",l.latest_version," available"]})]}),l.pid&&i.jsxs(i.Fragment,{children:[i.jsx("dt",{children:"PID"}),i.jsx("dd",{children:l.pid})]})]}),l.uis.length>0&&i.jsx(D0,{uis:l.uis}),i.jsxs("div",{className:"actions",children:[v?i.jsx("a",{className:"btn",href:v,"data-testid":`open-${l.short}`,children:"Open"}):i.jsx("button",{type:"button",className:"btn",disabled:!0,title:x?"No separate Open surface — use Configure for this module's admin UI.":"This module hasn't shipped an admin UI yet.","data-testid":`open-${l.short}`,children:"Open"}),x?i.jsx("a",{className:"btn",href:x,"data-testid":`configure-${l.short}`,children:"Configure"}):null,i.jsx("button",{type:"button",disabled:!y,onClick:h,children:"Restart"}),i.jsx("button",{type:"button",disabled:!y||!m,onClick:f,children:m?`Upgrade to v${l.latest_version}`:"Up to date"}),i.jsx("button",{type:"button",className:"destructive",disabled:!y,onClick:g,children:"Uninstall"})]}),o&&i.jsx("div",{className:"error",children:o})]})}function O0({module:l,supervisorAvailable:r,installing:c,errorMessage:o,onInstall:f}){const h=r&&!c;return i.jsxs("li",{className:"install-card","data-short":l.short,children:[i.jsxs("div",{className:"install-card-body",children:[i.jsxs("h3",{children:[l.display_name," ",i.jsxs("span",{className:"muted",children:["(",l.short,")"]})]}),l.tagline?i.jsx("p",{className:"tagline",children:l.tagline}):null,i.jsxs("p",{className:"muted install-card-meta",children:[i.jsx("code",{children:l.package}),l.latest_version?i.jsxs(i.Fragment,{children:[" · ","latest ",i.jsxs("code",{children:["v",l.latest_version]})]}):null]})]}),i.jsx("div",{className:"install-card-actions",children:i.jsx("button",{type:"button",disabled:!h,onClick:f,children:c?"Installing…":"Install"})}),o&&i.jsx("div",{className:"error",children:o})]})}function U0(l){if(!l.installed)return{cssState:"absent",label:"not installed"};if(!l.supervisor_status)return{cssState:"absent",label:"not supervised"};const r=_0(l.supervisor_status);return{cssState:r,label:r}}function D0({uis:l}){return i.jsxs("details",{className:"module-uis","data-testid":"module-uis",open:!0,children:[i.jsxs("summary",{children:["Hosted UIs ",i.jsxs("span",{className:"muted",children:["(",l.length,")"]})]}),i.jsx("ul",{className:"ui-sub-units",children:l.map(r=>{const c=A0(r.status);return i.jsxs("li",{className:"ui-sub-unit","data-name":r.name,children:[r.icon_url&&i.jsx("img",{src:r.icon_url,alt:"",className:"ui-icon",width:20,height:20,loading:"lazy"}),i.jsxs("div",{className:"ui-sub-unit-body",children:[i.jsxs("a",{href:r.path,className:"ui-sub-unit-link",children:[i.jsx("strong",{children:r.display_name}),i.jsxs("span",{className:"muted",children:[" · ",r.path]})]}),r.tagline?i.jsx("p",{className:"tagline",children:r.tagline}):null]}),i.jsx("span",{className:`status status-${c}`,"data-state":c,"data-testid":`ui-status-${r.name}`,children:c})]},r.name)})})]})}function H0({channel:l,disabled:r,onChange:c}){return i.jsxs("fieldset",{className:"channel-toggle","data-testid":"channel-toggle",children:[i.jsx("legend",{children:"Install channel"}),i.jsxs("label",{children:[i.jsx("input",{type:"radio",name:"module-install-channel",value:"latest",checked:l==="latest",disabled:r,onChange:()=>c("latest")}),"Stable (",i.jsx("code",{children:"latest"}),")"]}),i.jsxs("label",{children:[i.jsx("input",{type:"radio",name:"module-install-channel",value:"rc",checked:l==="rc",disabled:r,onChange:()=>c("rc")}),"Release candidates (",i.jsx("code",{children:"rc"}),")"]}),i.jsxs("p",{className:"muted",children:["All future module installs and upgrades use this channel. Existing installed modules are unaffected — use ",i.jsx("strong",{children:"Upgrade"})," to pull a newer version."]}),i.jsxs("p",{className:"muted",children:["More hub settings (canonical URL, etc.) at ",i.jsx(lt,{to:"/settings",children:"Settings"}),"."]})]})}function B0(){const[l,r]=b.useState({kind:"loading"}),[c,o]=b.useState(""),[f,h]=b.useState(""),[g,y]=b.useState(0),[m,v]=b.useState({kind:"idle"});b.useEffect(()=>{let k=!1;return r({kind:"loading"}),t1(f?{vault:f}:{}).then(O=>{k||r({kind:"ok",grants:O})}).catch(O=>{if(k)return;const L=O instanceof Error?O.message:String(O);r({kind:"error",message:L})}),()=>{k=!0}},[g,f]);function x(k){k.preventDefault(),h(c.trim())}function j(){o(""),h("")}async function _(k){v({kind:"revoking",clientId:k.client_id});try{await a1(k.client_id),v({kind:"idle"}),y(C=>C+1)}catch(C){const O=C instanceof V?`revoke failed (${C.status}): ${C.message}`:C instanceof Error?C.message:String(C);v({kind:"error",clientId:k.client_id,message:O})}}return i.jsxs("div",{"data-route-content":"true",children:[i.jsx("div",{className:"list-header",children:i.jsx("h1",{children:"Permissions"})}),i.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 ",i.jsx("em",{children:"not"})," invalidate tokens already issued."]}),i.jsxs("form",{onSubmit:x,style:{marginTop:"1rem",marginBottom:"1rem"},children:[i.jsx("label",{htmlFor:"vault-filter",className:"muted",style:{marginRight:"0.5rem"},children:"Filter by vault:"}),i.jsx("input",{id:"vault-filter",type:"text",value:c,onChange:k=>o(k.target.value),placeholder:"e.g. work",style:{marginRight:"0.5rem"}}),i.jsx("button",{type:"submit",children:"Apply"}),f?i.jsx("button",{type:"button",onClick:j,className:"secondary",style:{marginLeft:"0.5rem"},children:"Clear"}):null]}),L0({state:l,revoke:m,setRevoke:v,onConfirm:_,onRetry:()=>y(k=>k+1)})]})}function L0({state:l,revoke:r,setRevoke:c,onConfirm:o,onRetry:f}){return l.kind==="loading"?i.jsx("p",{className:"muted","data-loading":"true",children:"Loading…"}):l.kind==="error"?i.jsxs(i.Fragment,{children:[i.jsxs("div",{className:"error-banner",children:["Couldn't load grants: ",i.jsx("code",{children:l.message})]}),i.jsx("button",{type:"button",onClick:f,className:"secondary",children:"Retry"})]}):l.grants.length===0?i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No grants."}),i.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."})]}):i.jsx("div",{style:{marginTop:"1rem"},children:l.grants.map(h=>{const g=r.kind==="revoking"&&r.clientId===h.client_id,y=r.kind==="error"&&r.clientId===h.client_id?r:null,m=r.kind==="confirming"&&r.grant.client_id===h.client_id;return i.jsxs("div",{className:"vault-row",children:[i.jsxs("div",{className:"body",children:[i.jsx("div",{className:"name",children:i.jsx("code",{children:h.client_name??h.client_id})}),i.jsxs("div",{className:"dim",children:[i.jsx("span",{className:"muted",children:"granted "}),i.jsx("code",{title:h.granted_at,children:q0(h.granted_at)})]}),i.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[i.jsx("span",{className:"muted",children:"scopes: "}),h.scopes.map((v,x)=>i.jsxs("span",{children:[i.jsx("code",{children:v}),x<h.scopes.length-1?" ":null]},v))]}),y?i.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:i.jsx("code",{children:y.message})}):null,m?i.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:[i.jsxs("p",{children:["Revoke ",i.jsx("code",{children:h.client_name??h.client_id}),"? Next OAuth flow for this app will prompt you to consent again."]}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"button",onClick:()=>{o(h)},disabled:g,children:g?"Revoking…":"Revoke"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>c({kind:"idle"}),disabled:g,children:"Cancel"})]})]}):null]}),m?null:i.jsx("button",{type:"button",className:"secondary",onClick:()=>c({kind:"confirming",grant:h}),"aria-label":`Revoke ${h.client_name??h.client_id}`,children:"Revoke"})]},h.client_id)})})}function q0(l){const r=new Date(l);return Number.isNaN(r.getTime())?l:r.toLocaleString()}function Y0(){const[l,r]=b.useState({kind:"loading"}),[c,o]=b.useState(""),[f,h]=b.useState(!1),[g,y]=b.useState(null),m=b.useCallback(async()=>{try{const C=await Cm();r({kind:"ok",setting:C}),o(C.hub_origin??""),y(null)}catch(C){const O=C instanceof Error?C.message:String(C);r({kind:"error",message:O})}},[]);b.useEffect(()=>{m()},[m]);async function v(C){if(C.preventDefault(),f||l.kind!=="ok")return;const O=c.trim(),L=O.length===0?null:O;h(!0),y(null);try{await $h(L),await m()}catch(z){const Z=z instanceof Error?z.message:String(z);y(Z)}finally{h(!1)}}async function x(){if(!f&&l.kind==="ok"){h(!0),y(null);try{await $h(null),await m()}catch(C){const O=C instanceof Error?C.message:String(C);y(O)}finally{h(!1)}}}if(l.kind==="loading")return i.jsx("div",{className:"empty",children:"Loading settings…"});if(l.kind==="error")return i.jsxs("div",{className:"empty",children:["Failed to load settings: ",l.message,"."," ",i.jsx("button",{type:"button",onClick:()=>void m(),children:"Retry"})]});const{setting:j}=l,_=j.hub_origin!==null,k=c.trim()!==(j.hub_origin??"");return i.jsxs("section",{className:"settings",children:[i.jsx("h1",{children:"Hub settings"}),i.jsx("p",{className:"muted",children:"Hub-wide operator controls. Settings here apply to every module + every minted token."}),i.jsxs("section",{className:"settings-block","aria-labelledby":"canonical-hub-url-heading",children:[i.jsx("h2",{id:"canonical-hub-url-heading",children:"Canonical hub URL"}),i.jsxs("dl",{className:"meta","data-testid":"hub-origin-current",children:[i.jsx("dt",{children:"Current value"}),i.jsxs("dd",{children:[i.jsx("code",{children:j.resolved_issuer})," ",i.jsx(V0,{source:j.source,hasStored:_})]})]}),i.jsxs("form",{onSubmit:C=>void v(C),className:"settings-form","data-testid":"hub-origin-form",children:[i.jsxs("label",{htmlFor:"hub-origin-input",children:[i.jsx("span",{children:"Canonical URL"}),i.jsx("input",{id:"hub-origin-input",type:"url",inputMode:"url",placeholder:"https://hub.example.com",value:c,disabled:f,onChange:C=>o(C.target.value),autoComplete:"off",spellCheck:!1})]}),i.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 ",i.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)."]}),i.jsxs("div",{className:"actions",children:[i.jsx("button",{type:"submit",disabled:f||!k,children:f?"Saving…":"Save"}),i.jsx("button",{type:"button",className:"destructive",disabled:f||!_,onClick:()=>void x(),children:"Reset to default"})]}),g&&i.jsx("div",{className:"error","data-testid":"hub-origin-save-error",children:g})]})]}),i.jsx(G0,{})]})}function G0(){const[l,r]=b.useState(null),[c,o]=b.useState(null),[f,h]=b.useState(null),[g,y]=b.useState(""),[m,v]=b.useState(""),[x,j]=b.useState(""),[_,k]=b.useState("15"),[C,O]=b.useState(!1),[L,z]=b.useState(null),[Z,X]=b.useState(null),P=b.useCallback(async()=>{try{const[te,le]=await Promise.all([Cc(),wc()]);r(te),o(le.hasSession?le.csrf:null),k(String(Math.round(te.idle_seconds/60))),h(null)}catch(te){h(te instanceof Error?te.message:String(te))}},[]);b.useEffect(()=>{P()},[P]);function K(){const te=Number.parseInt(_,10);if(Number.isFinite(te))return Math.max(1,te)*60}async function J(te){if(te.preventDefault(),!(C||!c)){if(z(null),X(null),!/^[0-9]{4,12}$/.test(g)){z("PIN must be 4–12 digits.");return}if(g!==m){z("PINs don't match.");return}O(!0);try{await _1(c,g,K()),y(""),v(""),X("Screen lock enabled."),await P()}catch(le){z(le instanceof Error?le.message:String(le))}finally{O(!1)}}}async function E(te){if(te.preventDefault(),!(C||!c)){if(z(null),X(null),!/^[0-9]{4,12}$/.test(g)){z("New PIN must be 4–12 digits.");return}if(g!==m){z("PINs don't match.");return}O(!0);try{await A1(c,g,x||void 0,K()),y(""),v(""),j(""),X("PIN updated."),await P()}catch(le){z(le instanceof Error?le.message:String(le))}finally{O(!1)}}}async function D(){if(!(C||!c)){z(null),X(null),O(!0);try{await R1(c,x||void 0),j(""),X("Screen lock disabled."),await P()}catch(te){z(te instanceof Error?te.message:String(te))}finally{O(!1)}}}const Q=(l==null?void 0:l.configured)??!1;return i.jsxs("section",{className:"settings-block","aria-labelledby":"admin-lock-heading","data-testid":"admin-lock-settings",children:[i.jsx("h2",{id:"admin-lock-heading",children:"Admin screen lock"}),i.jsxs("p",{className:"muted",children:["An optional PIN that locks the whole admin console after a period of inactivity — a phone-style lock for a remotely-exposed hub. Off by default; the OAuth flow for your connected apps is never affected. ",i.jsx("strong",{children:"Note:"})," this guards the exposed web portal only — anyone with shell access to the box bypasses it (that's an OS / disk-encryption concern)."]}),f?i.jsxs("div",{className:"error","data-testid":"admin-lock-load-error",children:["Failed to load lock status: ",f,"."," ",i.jsx("button",{type:"button",onClick:()=>void P(),children:"Retry"})]}):i.jsxs(i.Fragment,{children:[i.jsxs("p",{children:["Status:"," ",i.jsx("span",{className:`lock-status-pill ${Q?"lock-status-on":"lock-status-off"}`,"data-testid":"admin-lock-status-pill",children:Q?"Enabled":"Off"})]}),Q?i.jsxs("form",{onSubmit:te=>void E(te),className:"lock-settings-form",children:[i.jsxs("label",{children:["Current PIN",i.jsx("input",{type:"password",inputMode:"numeric",autoComplete:"off",value:x,disabled:C,onChange:te=>j(te.target.value.replace(/[^0-9]/g,"")),"data-testid":"admin-lock-current-pin"})]}),i.jsxs("label",{children:["New PIN (4–12 digits)",i.jsx("input",{type:"password",inputMode:"numeric",autoComplete:"off",value:g,disabled:C,onChange:te=>y(te.target.value.replace(/[^0-9]/g,"")),"data-testid":"admin-lock-new-pin"})]}),i.jsxs("label",{children:["Confirm new PIN",i.jsx("input",{type:"password",inputMode:"numeric",autoComplete:"off",value:m,disabled:C,onChange:te=>v(te.target.value.replace(/[^0-9]/g,"")),"data-testid":"admin-lock-new-pin2"})]}),i.jsxs("label",{children:["Lock after (minutes idle)",i.jsx("input",{type:"number",min:1,max:1440,value:_,disabled:C,onChange:te=>k(te.target.value),"data-testid":"admin-lock-idle"})]}),i.jsxs("div",{className:"actions",children:[i.jsx("button",{type:"submit",disabled:C,"data-testid":"admin-lock-change",children:C?"Saving…":"Update PIN"}),i.jsx("button",{type:"button",className:"destructive",disabled:C,onClick:()=>void D(),"data-testid":"admin-lock-remove",children:"Remove lock"})]})]}):i.jsxs("form",{onSubmit:te=>void J(te),className:"lock-settings-form",children:[i.jsxs("label",{children:["PIN (4–12 digits)",i.jsx("input",{type:"password",inputMode:"numeric",autoComplete:"off",value:g,disabled:C,onChange:te=>y(te.target.value.replace(/[^0-9]/g,"")),"data-testid":"admin-lock-set-pin"})]}),i.jsxs("label",{children:["Confirm PIN",i.jsx("input",{type:"password",inputMode:"numeric",autoComplete:"off",value:m,disabled:C,onChange:te=>v(te.target.value.replace(/[^0-9]/g,"")),"data-testid":"admin-lock-set-pin2"})]}),i.jsxs("label",{children:["Lock after (minutes idle)",i.jsx("input",{type:"number",min:1,max:1440,value:_,disabled:C,onChange:te=>k(te.target.value),"data-testid":"admin-lock-idle"})]}),i.jsx("div",{className:"actions",children:i.jsx("button",{type:"submit",disabled:C,"data-testid":"admin-lock-enable",children:C?"Saving…":"Enable screen lock"})})]}),L&&i.jsx("div",{className:"error","data-testid":"admin-lock-form-error",children:L}),Z&&i.jsx("p",{className:"muted","data-testid":"admin-lock-notice",children:Z})]})]})}function V0({source:l,hasStored:r}){return l==="settings"?i.jsx("span",{className:"badge badge-info","data-testid":"hub-origin-source",children:"from settings"}):l==="env"?i.jsxs("span",{className:"badge badge-info","data-testid":"hub-origin-source",children:["from env var ",i.jsx("code",{children:"PARACHUTE_HUB_ORIGIN"})]}):l==="expose"?i.jsxs("span",{className:"badge badge-info","data-testid":"hub-origin-source",children:["from your ",i.jsx("code",{children:"parachute expose"})," config"]}):i.jsx("span",{className:"badge badge-info","data-testid":"hub-origin-source",children:"from request origin"})}const lc={scope:"",audience:"",expiresIn:"",subject:"",permissions:""};function Z0(){const[l,r]=bm(),c=$0(l.get("status")),o=J0(l.get("source")),[f,h]=b.useState({kind:"loading"}),[g,y]=b.useState(0),[m,v]=b.useState({kind:"idle"}),[x,j]=b.useState(lc),[_,k]=b.useState({kind:"idle"}),[C,O]=b.useState(!1),[L,z]=b.useState(!1);function Z(E,D){r(Q=>{const te=new URLSearchParams(Q);return D==="all"?te.delete(E):te.set(E,D),te},{replace:!0})}b.useEffect(()=>{let E=!1;return h({kind:"loading"}),Qh(Wh(c,o)).then(D=>{E||h({kind:"ok",tokens:D.tokens,nextCursor:D.next_cursor})}).catch(D=>{if(E)return;const Q=D instanceof Error?D.message:String(D);h({kind:"error",message:Q})}),()=>{E=!0}},[g,c,o]);async function X(){if(f.kind!=="ok"||!f.nextCursor||L)return;const E={...Wh(c,o),cursor:f.nextCursor};z(!0);try{const D=await Qh(E);h({kind:"ok",tokens:[...f.tokens,...D.tokens],nextCursor:D.next_cursor})}catch(D){const Q=D instanceof Error?D.message:String(D);h({kind:"error",message:Q})}finally{z(!1)}}async function P(E){E.preventDefault();const D=x.scope.trim();if(D.length===0){v({kind:"error",message:"scope is required"});return}let Q;if(x.permissions.trim().length>0)try{const le=JSON.parse(x.permissions);if(le===null||typeof le!="object"||Array.isArray(le)){v({kind:"error",message:'permissions must be a JSON object (e.g. {"vault":{"default":...}})'});return}Q=le}catch(le){const ie=le instanceof Error?le.message:String(le);v({kind:"error",message:`permissions is not valid JSON — ${ie}`});return}let te;if(x.expiresIn.trim().length>0){const le=Number(x.expiresIn);if(!Number.isInteger(le)||le<=0){v({kind:"error",message:"expires_in must be a positive integer (seconds)"});return}te=le}v({kind:"submitting"});try{const le=await wm({scope:D,...x.audience.trim()?{audience:x.audience.trim()}:{},...te!==void 0?{expires_in:te}:{},...x.subject.trim()?{subject:x.subject.trim()}:{},...Q?{permissions:Q}:{}});v({kind:"minted",token:le}),j(lc),O(!1),y(ie=>ie+1)}catch(le){const ie=le instanceof V?`mint failed (${le.status}): ${le.message}`:le instanceof Error?le.message:String(le);v({kind:"error",message:ie})}}async function K(E){k({kind:"revoking",jti:E});try{await i1(E),k({kind:"idle"}),y(D=>D+1)}catch(D){const Q=D instanceof V?`revoke failed (${D.status}): ${D.message}`:D instanceof Error?D.message:String(D);k({kind:"error",jti:E,message:Q})}}function J(E){typeof navigator<"u"&&navigator.clipboard&&navigator.clipboard.writeText(E)}return i.jsxs("div",{children:[i.jsxs("div",{className:"list-header",children:[i.jsx("h1",{children:"Tokens"}),i.jsx("button",{type:"button",onClick:()=>O(E=>!E),children:C?"Hide form":"Mint new token"})]}),i.jsxs("p",{className:"muted",children:["The hub's token registry. Every CLI / OAuth / operator-mint writes a row here. Revoking flips ",i.jsx("code",{children:"revoked_at"}),"; resource servers on"," ",i.jsx("code",{children:"@openparachute/scope-guard@^0.2.0"})," reject within ~60s of the next poll."]}),m.kind==="minted"?i.jsxs("div",{className:"mint-banner",children:[i.jsx("h3",{children:"Minted"}),i.jsxs("p",{children:["Your new access token (jti: ",i.jsx("code",{children:m.token.jti}),"):"]}),i.jsx("div",{className:"token-box",children:i.jsx("code",{children:m.token.token})}),i.jsx("p",{className:"warn",children:"This is the only time the JWT is shown. Copy it now — there is no DB-side recovery."}),i.jsxs("div",{className:"actions",children:[i.jsx("button",{type:"button",onClick:()=>J(m.token.token),children:"Copy"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>v({kind:"idle"}),children:"Dismiss"})]})]}):null,C?i.jsx("div",{className:"section",children:i.jsxs("form",{onSubmit:P,children:[i.jsxs("div",{className:"row",children:[i.jsx("label",{htmlFor:"mint-scope",children:"Scope (space-separated)"}),i.jsx("input",{id:"mint-scope",type:"text",value:x.scope,onChange:E=>j({...x,scope:E.target.value}),placeholder:"e.g. scribe:transcribe vault:default:read",required:!0}),i.jsxs("div",{className:"field-hint",children:["Space-separated ",i.jsx("code",{children:"resource:verb"})," or ",i.jsx("code",{children:"resource:name:verb"})," ","tuples. The hub rejects non-requestable scopes (admin, host:*) per the privilege-diffusion guard."]})]}),i.jsxs("div",{className:"row",children:[i.jsx("label",{htmlFor:"mint-audience",children:"Audience (optional)"}),i.jsx("input",{id:"mint-audience",type:"text",value:x.audience,onChange:E=>j({...x,audience:E.target.value}),placeholder:"inferred from scope when blank"}),i.jsxs("div",{className:"field-hint",children:["Inferred from scope if omitted. ",i.jsx("code",{children:"vault:<name>:<verb>"})," →"," ",i.jsx("code",{children:"vault.<name>"}),"; otherwise the first colon-prefixed scope's namespace; fallback ",i.jsx("code",{children:"hub"}),"."]})]}),i.jsxs("div",{className:"row",children:[i.jsx("label",{htmlFor:"mint-expires-in",children:"Expires in (seconds, optional)"}),i.jsx("input",{id:"mint-expires-in",type:"text",inputMode:"numeric",value:x.expiresIn,onChange:E=>j({...x,expiresIn:E.target.value}),placeholder:"default 90d (7776000)"})]}),i.jsxs("div",{className:"row",children:[i.jsx("label",{htmlFor:"mint-subject",children:"Subject (optional)"}),i.jsx("input",{id:"mint-subject",type:"text",value:x.subject,onChange:E=>j({...x,subject:E.target.value}),placeholder:"defaults to operator's sub"})]}),i.jsxs("div",{className:"row",children:[i.jsx("label",{htmlFor:"mint-permissions",children:"Permissions (JSON object, optional)"}),i.jsx("textarea",{id:"mint-permissions",value:x.permissions,onChange:E=>j({...x,permissions:E.target.value}),placeholder:'e.g. {"vault":{"default":{"write_tags":["health"]}}}',rows:3,style:{width:"100%",fontFamily:"monospace",fontSize:"0.9rem"}})]}),m.kind==="error"?i.jsx("div",{className:"field-error",children:i.jsx("code",{children:m.message})}):null,i.jsxs("div",{className:"actions",children:[i.jsx("button",{type:"submit",disabled:m.kind==="submitting",children:m.kind==="submitting"?"Minting…":"Mint"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>{O(!1),j(lc),v({kind:"idle"})},children:"Cancel"})]})]})}):null,i.jsxs("div",{style:{marginTop:"1rem",marginBottom:"0.5rem",display:"flex",flexWrap:"wrap",gap:"0.5rem",alignItems:"center"},children:[i.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(E=>i.jsx("button",{type:"button",onClick:()=>Z("status",E.value),className:c===E.value?void 0:"secondary","aria-pressed":c===E.value,children:E.label},E.value))]}),i.jsxs("div",{style:{marginBottom:"1rem",display:"flex",flexWrap:"wrap",gap:"0.5rem",alignItems:"center"},children:[i.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(E=>i.jsx("button",{type:"button",onClick:()=>Z("source",E.value),className:o===E.value?void 0:"secondary","aria-pressed":o===E.value,children:E.label},E.value))]}),X0({list:f,revoke:_,setRevoke:k,onConfirm:K,onLoadMore:X,onRetry:()=>y(E=>E+1),loadingMore:L,filtersActive:c!=="all"||o!=="all"})]})}function X0({list:l,revoke:r,setRevoke:c,onConfirm:o,onLoadMore:f,onRetry:h,loadingMore:g,filtersActive:y}){return l.kind==="loading"?i.jsx("p",{className:"muted","data-loading":"true",children:"Loading…"}):l.kind==="error"?i.jsxs(i.Fragment,{children:[i.jsxs("div",{className:"error-banner",children:["Couldn't load tokens: ",i.jsx("code",{children:l.message})]}),i.jsx("button",{type:"button",onClick:h,className:"secondary",children:"Retry"})]}):l.tokens.length===0?y?i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No tokens match the current filter."}),i.jsx("p",{className:"muted",children:'Try widening the Status or Source pills above. The default "Show all / All sources" view shows every registry row.'})]}):i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No tokens."}),i.jsxs("p",{className:"muted",children:["Every CLI mint, OAuth grant, and operator-token rotation lands here. Mint one with the form above, or via ",i.jsx("code",{children:"parachute auth mint-token"}),"."]})]}):i.jsxs("div",{"data-route-content":"true",children:[l.tokens.map(m=>{const v=Q0(m),x=r.kind==="revoking"&&r.jti===m.jti,j=r.kind==="confirming"&&r.jti===m.jti,_=r.kind==="error"&&r.jti===m.jti?r:null,k=m.user_id??m.subject??"(unknown)";return i.jsxs("div",{className:"vault-row",children:[i.jsxs("div",{className:"body",children:[i.jsxs("div",{className:"name",children:[i.jsx("code",{title:m.jti,children:ms(m.jti)}),i.jsx("span",{className:`tag${v==="live"?"":" muted"}`,children:v}),i.jsx("span",{className:`tag source-${F0(m.created_via)}`,title:`created_via: ${m.created_via}`,children:K0(m.created_via)})]}),i.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[i.jsx("span",{className:"muted",children:"identity: "}),i.jsx("code",{children:k}),m.client_id?i.jsxs(i.Fragment,{children:[i.jsx("span",{className:"muted",children:" · client: "}),i.jsx("code",{children:m.client_id})]}):null]}),i.jsxs("div",{className:"dim",style:{marginTop:"0.25rem"},children:[i.jsx("span",{className:"muted",children:"scope: "}),m.scopes.map((C,O)=>i.jsxs("span",{children:[i.jsx("code",{children:C}),O<m.scopes.length-1?" ":null]},C))]}),i.jsxs("div",{className:"dim",style:{marginTop:"0.25rem",fontSize:"0.82rem"},children:[i.jsx("span",{className:"muted",children:"created "}),i.jsx("code",{title:m.created_at,children:ic(m.created_at)}),i.jsx("span",{className:"muted",children:" · expires "}),i.jsx("code",{title:m.expires_at,children:ic(m.expires_at)}),m.revoked_at?i.jsxs(i.Fragment,{children:[i.jsx("span",{className:"muted",children:" · revoked "}),i.jsx("code",{title:m.revoked_at,children:ic(m.revoked_at)})]}):null]}),m.permissions?i.jsxs("details",{style:{marginTop:"0.35rem"},children:[i.jsx("summary",{className:"muted",style:{cursor:"pointer",fontSize:"0.85rem"},children:"permissions"}),i.jsx("pre",{style:{fontSize:"0.82rem",marginTop:"0.25rem"},children:JSON.stringify(m.permissions,null,2)})]}):null,_?i.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:i.jsx("code",{children:_.message})}):null,j?i.jsxs("dialog",{open:!0,className:"error-banner",style:{marginTop:"0.5rem",background:"var(--bg-warn, #fffbe6)"},"aria-label":`Confirm revoke ${ms(m.jti)}`,children:[i.jsxs("p",{children:["Revoke ",i.jsx("code",{children:ms(m.jti)}),"? Resource servers reject within ~60s of the next revocation-list poll. This cannot be undone."]}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"button",onClick:()=>{o(m.jti)},disabled:x,children:x?"Revoking…":"Revoke"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>c({kind:"idle"}),disabled:x,children:"Cancel"})]})]}):null]}),!j&&v==="live"?i.jsx("button",{type:"button",className:"secondary",onClick:()=>c({kind:"confirming",jti:m.jti}),"aria-label":`Revoke ${ms(m.jti)}`,children:"Revoke"}):null]},m.jti)}),l.nextCursor?i.jsx("div",{style:{marginTop:"1rem"},children:i.jsx("button",{type:"button",className:"secondary",disabled:g,onClick:()=>{f()},children:g?"Loading…":"Load more"})}):null]})}function Q0(l){if(l.revoked_at)return"revoked";const r=new Date(l.expires_at).getTime();return!Number.isNaN(r)&&r<Date.now()?"expired":"live"}function $0(l){return l==="live"||l==="revoked"||l==="all"?l:"all"}function J0(l){return l==="oauth_refresh"||l==="operator_mint"||l==="cli_mint"||l==="all"?l:"all"}function Wh(l,r){const c={};return l==="live"?c.revoked="false":l==="revoked"?c.revoked="true":c.revoked="all",r!=="all"&&(c.createdVia=r),c}function K0(l){switch(l){case"oauth_refresh":return"OAuth";case"operator_mint":return"Operator";case"cli_mint":return"CLI";default:return l}}function F0(l){switch(l){case"oauth_refresh":return"oauth";case"operator_mint":return"operator";case"cli_mint":return"cli";default:return"unknown"}}function ms(l){return l.length<=14?l:`${l.slice(0,8)}…${l.slice(-4)}`}function ic(l){const r=new Date(l);return Number.isNaN(r.getTime())?l:r.toLocaleString()}const W0=1440*60;function I0(l){return l.provision_vault?"owner (new vault)":l.vault_name===null?"account only":l.role==="read"?"read-only (shared)":"read & write (shared)"}function Ih(l){const r=l.username!==null?`an account for "${l.username}"`:"an account (they pick the username)";if(l.provision_vault){const o=l.vault_name!==null?`their own new vault "${l.vault_name}"`:"their own new vault (they name it)";return`Creates ${r} + ${o}, as owner.`}if(l.vault_name===null)return`Creates ${r} with no vault access yet.`;const c=l.role==="read"?"read-only":"read & write";return`Creates ${r} with ${c} access to your existing vault "${l.vault_name}".`}function P0(l,r,c){const o=l.username!==null?` Your username will be "${l.username}".`:"",f=l.provision_vault?l.vault_name!==null?` You'll get your own new vault ("${l.vault_name}").`:" You'll get your own new vault.":l.vault_name!==null?` You'll get ${l.role==="read"?"read-only":"read & write"} access to the vault "${l.vault_name}".`:"",h=new Date(c).toLocaleDateString();return`You're invited to my Parachute. Open this link to set your password and claim your account.${o}${f} The link works once and expires ${h}: ${r}`}function eb({hubOrigin:l}){const[r,c]=b.useState({kind:"loading"}),[o,f]=b.useState(0),[h,g]=b.useState(""),[y,m]=b.useState("provision"),[v,x]=b.useState(""),[j,_]=b.useState(""),[k,C]=b.useState("read"),[O,L]=b.useState([]),[z,Z]=b.useState("7"),[X,P]=b.useState({kind:"idle"}),[K,J]=b.useState(null),[E,D]=b.useState(null),Q={username:h.trim()!==""?h.trim():null,vault_name:y==="share"?j!==""?j:null:v.trim()!==""?v.trim():null,role:y==="share"?k:"write",provision_vault:y==="provision"};b.useEffect(()=>{let W=!1;return c({kind:"loading"}),Promise.all([j1(),Nm()]).then(([ge,be])=>{W||(c({kind:"ok",invites:ge}),L(be))}).catch(ge=>{W||c({kind:"error",message:ge instanceof Error?ge.message:String(ge)})}),()=>{W=!0}},[o]);async function te(W){if(W.preventDefault(),X.kind==="submitting")return;if(y==="share"&&j===""){P({kind:"error",message:"Pick which existing vault to share before creating the invite."});return}P({kind:"submitting"}),J(null);const ge=Number.parseInt(z,10),be={...h.trim()!==""?{username:h.trim()}:{},...Number.isFinite(ge)&&ge>0?{expires_in:ge*W0}:{},...y==="share"?{vault_name:j,provision_vault:!1,role:k}:{...v.trim()!==""?{vault_name:v.trim()}:{},provision_vault:!0}};try{const H=await S1(be);P({kind:"created",result:H}),g(""),x(""),_(""),f(F=>F+1)}catch(H){P({kind:"error",message:H instanceof Error?H.message:String(H)})}}async function le(W){if(!E){D(W);try{await w1(W),f(ge=>ge+1)}catch(ge){c({kind:"error",message:ge instanceof Error?ge.message:String(ge)})}finally{D(null)}}}function ie(W,ge){var be;(be=navigator.clipboard)==null||be.writeText(W).then(()=>{J(ge),setTimeout(()=>J(null),2e3)})}return i.jsxs("section",{style:{marginTop:"2rem"},"data-testid":"invites-section",children:[i.jsx("div",{className:"list-header",children:i.jsx("h2",{children:"Invite links"})}),i.jsx("p",{className:"muted",children:"Generate a one-time, expiring link. The recipient opens it and picks their password — no admin-typed default password. Either provision them a new vault of their own, or share an existing vault read-only or read & write. Pre-naming the username makes the link a named deliverable (the recipient can't change it)."}),i.jsxs("form",{onSubmit:W=>void te(W),style:{marginBottom:"1rem"},children:[i.jsxs("div",{style:{display:"flex",gap:"0.75rem",flexWrap:"wrap",alignItems:"flex-end"},children:[i.jsxs("label",{className:"field",style:{flex:"1 1 10rem"},children:[i.jsx("span",{className:"field-label",children:"Username (optional)"}),i.jsx("input",{type:"text",value:h,onChange:W=>g(W.target.value),placeholder:"leave blank → they choose",pattern:"[a-z0-9_-]*",minLength:2,maxLength:32,autoComplete:"off",spellCheck:!1})]}),i.jsxs("label",{className:"field",style:{flex:"0 0 14rem"},children:[i.jsx("span",{className:"field-label",children:"Vault access"}),i.jsxs("select",{value:y,onChange:W=>m(W.target.value==="share"?"share":"provision"),children:[i.jsx("option",{value:"provision",children:"Create a new vault for them"}),i.jsx("option",{value:"share",children:"Share an existing vault"})]})]}),y==="provision"?i.jsxs("label",{className:"field",style:{flex:"1 1 12rem"},children:[i.jsx("span",{className:"field-label",children:"New vault name (optional)"}),i.jsx("input",{type:"text",value:v,onChange:W=>x(W.target.value),placeholder:"leave blank → they choose",pattern:"[a-z0-9_-]*",autoComplete:"off",spellCheck:!1})]}):i.jsxs(i.Fragment,{children:[i.jsxs("label",{className:"field",style:{flex:"1 1 10rem"},children:[i.jsx("span",{className:"field-label",children:"Vault"}),i.jsxs("select",{value:j,onChange:W=>_(W.target.value),required:!0,children:[i.jsx("option",{value:"",children:"Pick a vault…"}),O.map(W=>i.jsx("option",{value:W,children:W},W))]})]}),i.jsxs("label",{className:"field",style:{flex:"0 0 9rem"},children:[i.jsx("span",{className:"field-label",children:"Role"}),i.jsxs("select",{value:k,onChange:W=>C(W.target.value==="write"?"write":"read"),children:[i.jsx("option",{value:"read",children:"Read-only"}),i.jsx("option",{value:"write",children:"Read & write"})]})]})]}),i.jsxs("label",{className:"field",style:{flex:"0 0 8rem"},children:[i.jsx("span",{className:"field-label",children:"Expires (days)"}),i.jsx("input",{type:"number",min:1,max:90,value:z,onChange:W=>Z(W.target.value)})]}),i.jsx("button",{type:"submit",disabled:X.kind==="submitting",children:X.kind==="submitting"?"Creating…":"Create invite"})]}),i.jsx("p",{className:"muted",style:{marginTop:"0.5rem"},"data-testid":"invite-preview",children:y==="share"&&j===""?"Pick the existing vault to share to finish configuring this link.":Ih(Q)})]}),X.kind==="error"&&i.jsx("div",{className:"error-banner",style:{marginBottom:"1rem"},children:X.message}),X.kind==="created"&&i.jsxs("output",{className:"success-banner",style:{display:"block",marginBottom:"1rem"},children:[i.jsx("strong",{children:"Invite created."})," ",Ih(X.result.invite),i.jsx("br",{}),`Copy it now — the link is shown only once and can't be retrieved later (the hub stores only a hash). "Copy message" includes a ready-to-send note telling the recipient what the link does.`,i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem",alignItems:"center",flexWrap:"wrap"},children:[i.jsx("code",{style:{wordBreak:"break-all",flex:"1 1 16rem"},children:X.result.url}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>ie(X.result.url,"link"),children:K==="link"?"Copied!":"Copy link"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>ie(P0(X.result.invite,X.result.url,X.result.invite.expires_at),"message"),children:K==="message"?"Copied!":"Copy message"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>P({kind:"idle"}),children:"Dismiss"})]})]}),r.kind==="loading"&&i.jsx("p",{className:"muted",children:"Loading invites…"}),r.kind==="error"&&i.jsxs("div",{className:"error-banner",children:[r.message,i.jsx("div",{style:{marginTop:"0.5rem"},children:i.jsx("button",{type:"button",className:"secondary",onClick:()=>f(W=>W+1),children:"Retry"})})]}),r.kind==="ok"&&r.invites.length===0&&i.jsx("p",{className:"muted",children:"No invites yet."}),r.kind==="ok"&&r.invites.length>0&&i.jsx("div",{className:"table-scroll",children:i.jsxs("table",{className:"user-table",children:[i.jsx("thead",{children:i.jsxs("tr",{children:[i.jsx("th",{children:"For"}),i.jsx("th",{children:"Vault"}),i.jsx("th",{children:"Access"}),i.jsx("th",{children:"Status"}),i.jsx("th",{children:"Expires"}),i.jsx("th",{children:"Created"}),i.jsx("th",{})]})}),i.jsx("tbody",{children:r.invites.map(W=>i.jsxs("tr",{children:[i.jsx("td",{children:W.username??i.jsx("span",{className:"muted",children:"they choose"})}),i.jsx("td",{children:W.vault_name??i.jsx("span",{className:"muted",children:"redeemer chooses"})}),i.jsx("td",{children:I0(W)}),i.jsx("td",{children:i.jsx("span",{className:`status status-${W.status}`,children:W.status})}),i.jsx("td",{className:"muted",children:new Date(W.expires_at).toLocaleDateString()}),i.jsx("td",{className:"muted",children:new Date(W.created_at).toLocaleDateString()}),i.jsx("td",{children:W.status==="pending"?i.jsx("button",{type:"button",className:"secondary",disabled:E===W.id,onClick:()=>void le(W.id),children:E===W.id?"Revoking…":"Revoke"}):null})]},W.id))})]})}),l?i.jsxs("p",{className:"muted",style:{marginTop:"0.5rem",fontSize:"0.85em"},children:["Redemption links are served from ",i.jsxs("code",{children:[l,"/account/setup/<token>"]}),"."]}):null]})}const Ba=12,tb=/^[a-z0-9_-]+$/,bs=2,xs=32,Ph={username:"",password:"",assignedVaults:[]};function _m({username:l,hubOrigin:r}){const c=`${r||""}/login`;return i.jsxs("div",{className:"muted",style:{marginTop:"0.5rem",fontSize:"0.9em"},children:["Send them to ",i.jsx("code",{children:c})," with username ",i.jsx("code",{children:l})," and this password — they'll be prompted to set a new one on first sign-in."]})}function ab(){const[l,r]=b.useState({kind:"loading"}),[c,o]=b.useState(0),[f,h]=b.useState(!1),[g,y]=b.useState(Ph),[m,v]=b.useState({kind:"idle"}),[x,j]=b.useState({kind:"idle"}),[_,k]=b.useState({kind:"idle"}),[C,O]=b.useState({kind:"idle"});b.useEffect(()=>{let K=!1;return r({kind:"loading"}),Promise.all([m1(),Nm(),Cm()]).then(([J,E,D])=>{if(K)return;const Q=D.resolved_issuer.replace(/\/+$/,"");r({kind:"ok",data:{users:J,vaults:E,hubOrigin:Q}})}).catch(J=>{if(K)return;const E=J instanceof Error?J.message:String(J);r({kind:"error",message:E})}),()=>{K=!0}},[c]);function L(K){return K.username.length<bs||K.username.length>xs?`Username must be ${bs}-${xs} characters.`:tb.test(K.username)?K.password.length<Ba?`Password must be at least ${Ba} characters.`:null:"Username may only contain lowercase letters, digits, hyphens, and underscores."}async function z(K){K.preventDefault();const J=L(g);if(J){v({kind:"error",message:J});return}v({kind:"submitting"});const E={username:g.username,password:g.password,assignedVaults:g.assignedVaults};try{const D=await p1(E);y(Ph),v({kind:"created",username:D.username}),o(Q=>Q+1)}catch(D){const Q=D instanceof V?`Create failed (${D.status}): ${D.message}`:D instanceof Error?D.message:String(D);v({kind:"error",message:Q})}}async function Z(K){j({kind:"deleting",userId:K.id});try{const{revocationLagSeconds:J}=await g1(K.id);j({kind:"done",username:K.username,revocationLagSeconds:J}),o(E=>E+1)}catch(J){const E=J instanceof V?`Delete failed (${J.status}): ${J.message}`:J instanceof Error?J.message:String(J);j({kind:"error",userId:K.id,message:E})}}async function X(K,J){O({kind:"submitting",userId:K.id,selected:J});try{await y1(K.id,J),O({kind:"done",userId:K.id,username:K.username}),o(E=>E+1)}catch(E){const D=E instanceof V?`Edit vaults failed (${E.status}): ${E.message}`:E instanceof Error?E.message:String(E);O({kind:"error",userId:K.id,selected:J,message:D})}}async function P(K,J){if(J.length<Ba){k({kind:"error",userId:K.id,password:J,message:`Password must be at least ${Ba} characters.`});return}k({kind:"submitting",userId:K.id,password:J});try{const{revocationLagSeconds:E}=await v1(K.id,J);k({kind:"done",userId:K.id,username:K.username,revocationLagSeconds:E}),o(D=>D+1)}catch(E){const D=E instanceof V?`Reset failed (${E.status}): ${E.message}`:E instanceof Error?E.message:String(E);k({kind:"error",userId:K.id,password:J,message:D})}}return i.jsxs("div",{"data-route-content":"true",children:[i.jsx("div",{className:"list-header",children:i.jsx("h1",{children:"Users"})}),i.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 ",i.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",l.kind==="ok"?i.jsxs(i.Fragment,{children:[" ","at ",i.jsxs("code",{children:[l.data.hubOrigin,"/login"]})]}):null,"."]}),x.kind==="done"&&i.jsxs("output",{className:"success-banner",style:{display:"block",marginBottom:"0.75rem"},"data-testid":"delete-done-banner",children:["Deleted ",i.jsx("code",{children:x.username}),". Their tokens are revoked. Resource servers (vault, scribe, etc.) cache the revocation list for up to ",x.revocationLagSeconds," ","seconds — if you're deleting because of a suspected compromise, also restart the affected services (e.g. ",i.jsx("code",{children:"parachute restart vault"}),") to flush their cache immediately.",i.jsx("div",{style:{marginTop:"0.5rem"},children:i.jsx("button",{type:"button",className:"secondary",onClick:()=>j({kind:"idle"}),children:"Dismiss"})})]}),nb(l,x,j,Z,_,k,P,C,O,X,l.kind==="ok"?l.data.vaults:[],l.kind==="ok"?l.data.hubOrigin:"",()=>o(K=>K+1)),l.kind==="ok"&&i.jsx(ub,{show:f,setShow:h,form:g,setForm:y,vaults:l.data.vaults,hubOrigin:l.data.hubOrigin,createState:m,setCreateState:v,onSubmit:z}),l.kind==="ok"&&i.jsx(eb,{hubOrigin:l.data.hubOrigin}),l.kind==="ok"&&i.jsx(cb,{})]})}function nb(l,r,c,o,f,h,g,y,m,v,x,j,_){var O;if(l.kind==="loading")return i.jsx("p",{className:"muted",children:"Loading users…"});if(l.kind==="error")return i.jsxs(i.Fragment,{children:[i.jsxs("div",{className:"error-banner",children:["Couldn't load users: ",i.jsx("code",{children:l.message})]}),i.jsx("button",{type:"button",onClick:_,className:"secondary",children:"Retry"})]});const{users:k}=l.data;if(k.length===0)return i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No users yet."}),i.jsx("p",{className:"muted",children:"Click Create User below to invite someone."})]});const C=(O=k[0])==null?void 0:O.id;return i.jsx(lb,{users:k,firstAdminId:C,deleteSt:r,setDeleteSt:c,onConfirmDelete:o,resetSt:f,setResetSt:h,onSubmitReset:g,editVaultsSt:y,setEditVaultsSt:m,onSubmitEditVaults:v,availableVaults:x,hubOrigin:j})}function lb({users:l,firstAdminId:r,deleteSt:c,setDeleteSt:o,onConfirmDelete:f,resetSt:h,setResetSt:g,onSubmitReset:y,editVaultsSt:m,setEditVaultsSt:v,onSubmitEditVaults:x,availableVaults:j,hubOrigin:_}){return i.jsx("div",{className:"user-list",style:{marginTop:"1rem"},children:i.jsx("div",{className:"table-scroll",children:i.jsxs("table",{className:"user-table",children:[i.jsx("thead",{children:i.jsxs("tr",{children:[i.jsx("th",{scope:"col",children:"Username"}),i.jsx("th",{scope:"col",children:"Email"}),i.jsx("th",{scope:"col",children:"Role"}),i.jsx("th",{scope:"col",children:"Assigned vaults"}),i.jsx("th",{scope:"col",children:"Password set"}),i.jsx("th",{scope:"col",children:"Created"}),i.jsx("th",{scope:"col",children:"Actions"})]})}),i.jsx("tbody",{children:l.map(k=>{const C=k.id===r,O=c.kind==="deleting"&&c.userId===k.id,L=c.kind==="confirming"&&c.user.id===k.id,z=c.kind==="error"&&c.userId===k.id?c:null,Z=(h.kind==="open"||h.kind==="submitting"||h.kind==="error")&&h.userId===k.id?h:null,X=h.kind==="done"&&h.userId===k.id?h:null,P=(m.kind==="open"||m.kind==="submitting"||m.kind==="error")&&m.userId===k.id?m:null,K=m.kind==="done"&&m.userId===k.id?m:null;return i.jsxs("tr",{"data-user-id":k.id,children:[i.jsxs("td",{children:[i.jsx("code",{children:k.username}),C&&i.jsx("span",{className:"badge",style:{marginLeft:"0.5rem"},children:"first admin"})]}),i.jsx("td",{children:k.email?k.email.includes("@")?i.jsx("a",{href:`mailto:${k.email}`,children:k.email}):i.jsx("span",{children:k.email}):i.jsx("span",{className:"muted",title:"No email on file (account predates signup-email capture)",children:"—"})}),i.jsx("td",{children:C?i.jsx("span",{className:"badge",children:"admin"}):i.jsx("span",{className:"muted",children:"member"})}),i.jsx("td",{children:k.assigned_vaults.length>0?i.jsx("span",{style:{display:"inline-flex",flexWrap:"wrap",gap:"0.25rem"},children:k.assigned_vaults.map(J=>i.jsx("code",{children:J},J))}):i.jsx("span",{className:"muted",title:C?"First admin is unrestricted (admin posture)":"No vaults assigned — user can't authorize any vault yet",children:"—"})}),i.jsx("td",{children:k.password_changed?i.jsx("span",{"aria-label":"changed",children:"✓"}):i.jsx("span",{className:"status status-pending",title:"User hasn't completed first-sign-in change-password yet",children:"pending first login"})}),i.jsx("td",{children:i.jsx("span",{title:k.created_at,children:Am(k.created_at)})}),i.jsxs("td",{children:[L?null:i.jsxs("div",{style:{display:"flex",flexWrap:"wrap",gap:"0.5rem",alignItems:"center"},children:[C&&i.jsxs(i.Fragment,{children:[i.jsx("span",{id:`first-admin-tooltip-${k.id}`,className:"sr-only",children:"First admin can't be deleted (would self-lock the hub)"}),i.jsx("span",{id:`first-admin-reset-tooltip-${k.id}`,className:"sr-only",children:"First admin uses /account/change-password directly"}),i.jsx("span",{id:`first-admin-vaults-tooltip-${k.id}`,className:"sr-only",children:"First admin's vault membership is unrestricted by design"})]}),i.jsx("button",{type:"button",className:"secondary",disabled:C||P!==null||m.kind==="submitting"&&m.userId===k.id,title:C?"First admin's vault membership is unrestricted by design":void 0,"aria-describedby":C?`first-admin-vaults-tooltip-${k.id}`:void 0,onClick:()=>v({kind:"open",userId:k.id,selected:[...k.assigned_vaults]}),"aria-label":`Edit vaults for ${k.username}`,children:"Edit vaults"}),i.jsx("button",{type:"button",className:"secondary",disabled:C||Z!==null||h.kind==="submitting"&&h.userId===k.id,title:C?"First admin uses /account/change-password directly":void 0,"aria-describedby":C?`first-admin-reset-tooltip-${k.id}`:void 0,onClick:()=>g({kind:"open",userId:k.id,password:""}),"aria-label":`Reset password for ${k.username}`,children:"Reset password"}),i.jsx("button",{type:"button",className:"secondary",disabled:C||O,title:C?"First admin can't be deleted (would self-lock the hub)":void 0,"aria-describedby":C?`first-admin-tooltip-${k.id}`:void 0,onClick:()=>o({kind:"confirming",user:k}),"aria-label":`Delete ${k.username}`,children:O?"Deleting…":"Delete"})]}),L&&i.jsxs("dialog",{open:!0,className:"error-banner",style:{marginTop:"0.25rem",background:"var(--bg-warn, #fffbe6)"},"aria-label":`Confirm delete ${k.username}`,children:[i.jsxs("p",{children:["Delete ",i.jsx("code",{children:k.username}),"? This revokes their tokens, drops their sessions and grants, and removes the account. The audit trail is preserved — tokens stay with ",i.jsx("code",{children:"revoked_at"})," set, anonymised."]}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"button",className:"destructive",onClick:()=>{f(k)},disabled:O,children:O?"Deleting…":"Delete"}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>o({kind:"idle"}),disabled:O,children:"Cancel"})]})]}),z&&i.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:i.jsx("code",{children:z.message})}),Z&&i.jsx(ib,{user:k,state:Z,onCancel:()=>g({kind:"idle"}),onPasswordChange:J=>g({kind:"open",userId:k.id,password:J}),onSubmit:J=>{y(k,J)}}),X&&i.jsxs("output",{className:"success-banner",style:{marginTop:"0.25rem",display:"block"},children:["Password reset for ",i.jsx("code",{children:X.username}),". Hand them the new password and tell them they'll be prompted to change it on first sign-in.",i.jsx(_m,{username:X.username,hubOrigin:_}),i.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 ",X.revocationLagSeconds," ","seconds — if you're resetting because of a suspected compromise, also restart the affected services (e.g. ",i.jsx("code",{children:"parachute restart vault"}),") to flush their cache immediately."]}),i.jsx("div",{style:{marginTop:"0.5rem"},children:i.jsx("button",{type:"button",className:"secondary",onClick:()=>g({kind:"idle"}),children:"Dismiss"})})]}),P&&i.jsx(sb,{user:k,state:P,availableVaults:j,onCancel:()=>v({kind:"idle"}),onSelectedChange:J=>v({kind:"open",userId:k.id,selected:J}),onSubmit:J=>{x(k,J)}}),K&&i.jsxs("output",{className:"success-banner",style:{marginTop:"0.25rem",display:"block"},children:["Vault assignments updated for ",i.jsx("code",{children:K.username}),".",i.jsx("div",{style:{marginTop:"0.5rem"},children:i.jsx("button",{type:"button",className:"secondary",onClick:()=>v({kind:"idle"}),children:"Dismiss"})})]})]})]},k.id)})})]})})})}function ib({user:l,state:r,onCancel:c,onPasswordChange:o,onSubmit:f}){const h=r.kind==="submitting",g=r.kind==="error"?r.message:null,y=`reset-password-input-${l.id}`;return i.jsxs("form",{onSubmit:m=>{m.preventDefault(),f(r.password)},"aria-label":`Reset password for ${l.username}`,style:{marginTop:"0.5rem",padding:"0.5rem",background:"var(--bg-soft, #f5f5f5)",borderRadius:"4px"},children:[i.jsxs("p",{style:{margin:0},children:[i.jsxs("label",{htmlFor:y,children:["New temporary password for ",i.jsx("code",{children:l.username})," ",i.jsxs("span",{className:"muted",children:["(min ",Ba," chars)"]})]}),i.jsx("br",{}),i.jsx("input",{id:y,type:"password",required:!0,autoComplete:"new-password",minLength:Ba,value:r.password,disabled:h,onChange:m=>o(m.target.value)})]}),g&&i.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:i.jsx("code",{children:g})}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"submit",disabled:h,children:h?"Setting…":"Set new password"}),i.jsx("button",{type:"button",className:"secondary",onClick:c,disabled:h,children:"Cancel"})]})]})}function sb({user:l,state:r,availableVaults:c,onCancel:o,onSelectedChange:f,onSubmit:h}){const g=r.kind==="submitting",y=r.kind==="error"?r.message:null,m=`edit-vaults-select-${l.id}`;function v(x){const j=Array.from(x.target.selectedOptions).map(_=>_.value);f(j)}return i.jsxs("form",{onSubmit:x=>{x.preventDefault(),h(r.selected)},"aria-label":`Edit vaults for ${l.username}`,style:{marginTop:"0.5rem",padding:"0.5rem",background:"var(--bg-soft, #f5f5f5)",borderRadius:"4px"},children:[i.jsx("p",{style:{margin:0},children:i.jsxs("label",{htmlFor:m,children:["Vault assignments for ",i.jsx("code",{children:l.username})," ",i.jsx("span",{className:"muted",children:"(empty = no narrowing; shift-click to multi-select)"})]})}),r.selected.length>0&&i.jsx("div",{"data-testid":`edit-vaults-chips-${l.id}`,style:{marginTop:"0.4rem",display:"flex",flexWrap:"wrap",gap:"0.25rem"},children:r.selected.map(x=>i.jsx("code",{style:{padding:"0.1rem 0.4rem",borderRadius:"4px"},children:x},x))}),i.jsx("select",{id:m,multiple:!0,value:r.selected,onChange:v,disabled:g,size:Math.min(Math.max(c.length,3),8),style:{marginTop:"0.4rem",minWidth:"12rem"},children:c.map(x=>i.jsx("option",{value:x,children:x},x))}),c.length===0&&i.jsx("p",{className:"muted",style:{marginTop:"0.25rem"},children:"No vaults registered on this hub yet."}),y&&i.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:i.jsx("code",{children:y})}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"submit",disabled:g,children:g?"Saving…":"Save vault assignments"}),i.jsx("button",{type:"button",className:"secondary",onClick:o,disabled:g,children:"Cancel"})]})]})}function ub({show:l,setShow:r,form:c,setForm:o,vaults:f,hubOrigin:h,createState:g,setCreateState:y,onSubmit:m}){const v=g.kind==="submitting";return i.jsx("section",{style:{marginTop:"1.5rem"},children:l?i.jsxs("form",{onSubmit:x=>void m(x),"aria-label":"Create user",children:[i.jsx("h3",{children:"Create user"}),i.jsxs("p",{children:[i.jsxs("label",{htmlFor:"new-user-username",children:["Username"," ",i.jsxs("span",{className:"muted",children:["(",bs,"-",xs," chars, lowercase letters/digits/hyphens/ underscores)"]})]}),i.jsx("br",{}),i.jsx("input",{id:"new-user-username",type:"text",required:!0,autoComplete:"off",value:c.username,minLength:bs,maxLength:xs,pattern:"[a-z0-9_\\-]+",onChange:x=>o({...c,username:x.target.value})})]}),i.jsxs("p",{children:[i.jsxs("label",{htmlFor:"new-user-password",children:["Password ",i.jsxs("span",{className:"muted",children:["(min ",Ba," chars)"]})]}),i.jsx("br",{}),i.jsx("input",{id:"new-user-password",type:"password",required:!0,autoComplete:"new-password",value:c.password,minLength:Ba,onChange:x=>o({...c,password:x.target.value})})]}),i.jsxs("p",{children:[i.jsxs("label",{htmlFor:"new-user-vaults",children:["Assigned vaults"," ",i.jsx("span",{className:"muted",children:"(empty = no restriction; shift-click to select multiple)"})]}),i.jsx("br",{}),c.assignedVaults.length>0&&i.jsx("span",{"data-testid":"new-user-vault-chips",style:{display:"inline-flex",flexWrap:"wrap",gap:"0.25rem",marginBottom:"0.25rem"},children:c.assignedVaults.map(x=>i.jsx("code",{children:x},x))}),i.jsx("br",{}),i.jsx("select",{id:"new-user-vaults",multiple:!0,value:c.assignedVaults,onChange:x=>o({...c,assignedVaults:Array.from(x.target.selectedOptions).map(j=>j.value)}),size:Math.min(Math.max(f.length,3),8),style:{minWidth:"12rem"},children:f.map(x=>i.jsx("option",{value:x,children:x},x))}),f.length===0&&i.jsx("span",{className:"muted",style:{marginLeft:"0.5rem"},children:"No vaults registered on this hub yet."})]}),g.kind==="error"&&i.jsx("div",{className:"error-banner",children:i.jsx("code",{children:g.message})}),g.kind==="created"&&i.jsxs("output",{className:"success-banner",children:["User ",i.jsx("code",{children:g.username})," created. They'll be prompted to change their password on first sign-in.",i.jsx(_m,{username:g.username,hubOrigin:h})]}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"submit",disabled:v,children:v?"Creating…":"Create user"}),i.jsx("button",{type:"button",className:"secondary",disabled:v,onClick:()=>{r(!1),y({kind:"idle"})},children:"Close"})]})]}):i.jsx("button",{type:"button",onClick:()=>r(!0),children:"Create User"})})}function Am(l){const r=new Date(l);return Number.isNaN(r.getTime())?l:r.toLocaleString()}const Rm=1024*1024*1024;function rb(l){if(l===null)return"";const r=l/Rm;return Number.isInteger(r)?String(r):r.toFixed(2)}function cb(){const[l,r]=b.useState({kind:"loading"}),[c,o]=b.useState(0),[f,h]=b.useState({kind:"idle"});b.useEffect(()=>{let y=!1;return r({kind:"loading"}),b1().then(m=>{y||r({kind:"ok",caps:m})}).catch(m=>{if(y)return;const v=m instanceof Error?m.message:String(m);r({kind:"error",message:v})}),()=>{y=!0}},[c]);async function g(y,m){const v=Number(m);if(!Number.isFinite(v)||v<=0){h({kind:"error",vaultName:y,gibInput:m,message:"Cap must be a positive number of GB."});return}h({kind:"submitting",vaultName:y,gibInput:m});try{await x1(y,Math.round(v*Rm)),h({kind:"idle"}),o(x=>x+1)}catch(x){const j=x instanceof V?`Set cap failed (${x.status}): ${x.message}`:x instanceof Error?x.message:String(x);h({kind:"error",vaultName:y,gibInput:m,message:j})}}return i.jsxs("section",{style:{marginTop:"1.5rem"},"data-testid":"vault-caps-section",children:[i.jsx("h2",{children:"Vault caps"}),i.jsx("p",{className:"muted",children:"Per-vault storage ceilings for this hub. Public-signup vaults are stamped with a default cap (~1 GB); admin-provisioned vaults are uncapped until you set one. Edit a cap to raise or lower a vault's ceiling."}),l.kind==="loading"&&i.jsx("p",{className:"muted",children:"Loading vault caps…"}),l.kind==="error"&&i.jsxs(i.Fragment,{children:[i.jsxs("div",{className:"error-banner",children:["Couldn't load vault caps: ",i.jsx("code",{children:l.message})]}),i.jsx("button",{type:"button",className:"secondary",onClick:()=>o(y=>y+1),children:"Retry"})]}),l.kind==="ok"&&l.caps.length===0&&i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No vaults on this hub yet."}),i.jsx("p",{className:"muted",children:"Caps appear here once a vault is registered."})]}),l.kind==="ok"&&l.caps.length>0&&i.jsx("div",{className:"table-scroll",style:{marginTop:"1rem"},children:i.jsxs("table",{className:"user-table",children:[i.jsx("thead",{children:i.jsxs("tr",{children:[i.jsx("th",{scope:"col",children:"Vault"}),i.jsx("th",{scope:"col",children:"Cap"}),i.jsx("th",{scope:"col",children:"Updated"}),i.jsx("th",{scope:"col",children:"Actions"})]})}),i.jsx("tbody",{children:l.caps.map(y=>{const m=(f.kind==="open"||f.kind==="submitting"||f.kind==="error")&&f.vaultName===y.vault_name?f:null;return i.jsxs("tr",{"data-vault-name":y.vault_name,children:[i.jsx("td",{children:i.jsx("code",{children:y.vault_name})}),i.jsx("td",{"data-testid":`vault-cap-${y.vault_name}`,children:y.cap_bytes!==null?Sm(y.cap_bytes):i.jsx("span",{className:"muted",title:"No cap set — this vault is uncapped",children:"uncapped"})}),i.jsx("td",{children:y.updated_at?i.jsx("span",{title:y.updated_at,children:Am(y.updated_at)}):i.jsx("span",{className:"muted",children:"—"})}),i.jsx("td",{children:m?i.jsx(ob,{vaultName:y.vault_name,state:m,onCancel:()=>h({kind:"idle"}),onChange:v=>h({kind:"open",vaultName:y.vault_name,gibInput:v}),onSubmit:v=>{g(y.vault_name,v)}}):i.jsx("button",{type:"button",className:"secondary",onClick:()=>h({kind:"open",vaultName:y.vault_name,gibInput:rb(y.cap_bytes)}),"aria-label":`Edit cap for ${y.vault_name}`,children:"Edit cap"})})]},y.vault_name)})})]})})]})}function ob({vaultName:l,state:r,onCancel:c,onChange:o,onSubmit:f}){const h=r.kind==="submitting",g=r.kind==="error"?r.message:null,y=`cap-input-${l}`;return i.jsxs("form",{onSubmit:m=>{m.preventDefault(),f(r.gibInput)},"aria-label":`Edit cap for ${l}`,style:{padding:"0.5rem",background:"var(--bg-soft, #f5f5f5)",borderRadius:"4px"},children:[i.jsxs("label",{htmlFor:y,children:["Cap for ",i.jsx("code",{children:l})," ",i.jsx("span",{className:"muted",children:"(GB)"})]}),i.jsx("br",{}),i.jsx("input",{id:y,type:"number",step:"0.01",value:r.gibInput,disabled:h,onChange:m=>o(m.target.value),style:{width:"8rem",marginTop:"0.25rem"}}),g&&i.jsx("div",{className:"error-banner",style:{marginTop:"0.25rem"},children:i.jsx("code",{children:g})}),i.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"0.5rem"},children:[i.jsx("button",{type:"submit",disabled:h,children:h?"Saving…":"Save cap"}),i.jsx("button",{type:"button",className:"secondary",onClick:c,disabled:h,children:"Cancel"})]})]})}const db="https://parachute.computer/install#connect-mcp-clients";function Mm(l){return`${l.replace(/\/+$/,"")}/mcp`}function zm(l,r){return`claude mcp add --transport http parachute-${l} ${Mm(r)}`}function fb(l,r,c){return`${zm(l,r)} --header "Authorization: Bearer ${c}"`}function sc({value:l,label:r="Copy"}){const[c,o]=b.useState(!1);return i.jsx("button",{type:"button",className:"secondary",onClick:()=>{typeof navigator>"u"||!navigator.clipboard||navigator.clipboard.writeText(l).then(()=>{o(!0),setTimeout(()=>o(!1),2e3)})},children:c?"Copied ✓":r})}function hb({vaultName:l,vaultUrl:r,embedded:c=!1}){const o=Mm(r),f=zm(l,r),[h,g]=b.useState(!1),[y,m]=b.useState({kind:"idle"});async function v(){if(y.kind!=="submitting"){m({kind:"submitting"});try{const _=await wm({scope:`vault:${l}:read vault:${l}:write`});m({kind:"minted",token:_})}catch(_){const k=_ instanceof V?`mint failed (${_.status}): ${_.message}`:_ instanceof Error?_.message:String(_);m({kind:"error",message:k})}}}const x=y.kind==="minted"?fb(l,r,y.token.token):null,j=i.jsxs(i.Fragment,{children:[i.jsx("h3",{children:"Connect an MCP client (or connector)"}),i.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 ",i.jsx("code",{children:l})," vault. The client signs in to this hub and reads/writes vault data over MCP."]}),i.jsxs("div",{className:"mcp-field",children:[i.jsx("span",{className:"mcp-field-label",children:"Endpoint"}),i.jsxs("div",{className:"token-box",children:[i.jsx("code",{"data-testid":"mcp-endpoint",children:o}),i.jsx(sc,{value:o})]})]}),i.jsxs("div",{className:"mcp-field",children:[i.jsx("span",{className:"mcp-field-label",children:"Claude Code"}),i.jsxs("div",{className:"token-box",children:[i.jsx("code",{"data-testid":"mcp-add-command",children:f}),i.jsx(sc,{value:f})]}),i.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."})]}),i.jsxs("details",{className:"mcp-token-path",open:h,onToggle:_=>g(_.currentTarget.open),children:[i.jsx("summary",{children:"Use a token instead (headless / CI clients)"}),i.jsxs("p",{className:"dim",children:["For clients that can't do browser OAuth, mint a scope-narrow hub token (",i.jsxs("code",{children:["vault:",l,":read vault:",l,":write"]})," ","— not admin) and pass it as a header. Revealed once; copy it now."]}),y.kind==="minted"&&x?i.jsxs("div",{className:"mint-banner","data-testid":"mcp-header-banner",children:[i.jsx("h3",{children:"Token minted"}),i.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 ",i.jsx("code",{children:"Authorization"})," header."]}),i.jsxs("div",{className:"token-box",children:[i.jsx("code",{"data-testid":"mcp-header-command",children:x}),i.jsx(sc,{value:x})]}),i.jsxs("p",{className:"warn",children:["⚠ Manage and revoke this token at ",i.jsx("code",{children:"/admin/tokens"}),"."]})]}):i.jsx("div",{className:"actions",children:i.jsx("button",{type:"button",className:"secondary",onClick:()=>{v()},disabled:y.kind==="submitting",children:y.kind==="submitting"?"Minting…":"Mint a token"})}),y.kind==="error"?i.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:i.jsx("code",{children:y.message})}):null]}),i.jsx("p",{className:"dim mcp-docs-link",children:i.jsx("a",{href:db,target:"_blank",rel:"noreferrer",children:"Full connect docs →"})})]});return c?i.jsx("div",{className:"mcp-connect-card mcp-connect-card-embedded",children:j}):i.jsx("div",{className:"mcp-connect-card",children:j})}const em="/vault/admin/";function mb(){const[l,r]=b.useState("detecting"),[c,o]=b.useState({kind:"loading"}),[f,h]=b.useState(0),[g,y]=b.useState({kind:"idle"}),[m,v]=b.useState(null),[x,j]=b.useState({});b.useEffect(()=>{let C=!1;return Cs().then(O=>{if(C)return;const L=O.modules.find(z=>z.short==="vault");if(L!=null&&L.installed&&L.config_ui_url===em){r("redirecting"),window.location.replace(em);return}r("legacy")}).catch(()=>{C||r("legacy")}),()=>{C=!0}},[]);async function _(C){if(C.managementUrl){y({kind:"minting",name:C.name});try{const O=await jm(C.name),L=l1(C.url,C.managementUrl),z=L.includes("#")?"&":"#";window.location.assign(`${L}${z}token=${O.token}`)}catch(O){const L=O instanceof V?`mint failed (${O.status}): ${O.message}`:O instanceof Error?O.message:String(O);y({kind:"error",name:C.name,message:L})}}}if(b.useEffect(()=>{if(l!=="legacy")return;let C=!1;return Sc().then(O=>{C||o({kind:"ok",vaults:O.vaults,moduleInstalled:O.moduleInstalled})}).catch(O=>{if(C)return;const L=O instanceof Error?O.message:String(O);o({kind:"error",message:L})}),()=>{C=!0}},[f,l]),b.useEffect(()=>{if(c.kind!=="ok")return;const C=c.vaults.map(L=>L.name);if(C.length===0)return;let O=!1;j(L=>{const z={...L};for(const Z of C)z[Z]||(z[Z]={kind:"loading"});return z});for(const L of C)e1(L).then(z=>{O||j(Z=>({...Z,[L]:{kind:"ok",usage:z}}))}).catch(()=>{O||j(z=>({...z,[L]:{kind:"error"}}))});return()=>{O=!0}},[c]),l!=="legacy")return i.jsxs("div",{children:[i.jsx("div",{className:"list-header",children:i.jsx("h1",{children:"Vaults"})}),i.jsx("p",{className:"muted","data-testid":"vaults-detecting",children:l==="redirecting"?"Opening the vault admin…":"Loading…"})]});if(c.kind==="loading")return i.jsxs("div",{children:[i.jsx("div",{className:"list-header",children:i.jsx("h1",{children:"Vaults"})}),i.jsx("p",{className:"muted",children:"Loading…"})]});if(c.kind==="error")return i.jsxs("div",{children:[i.jsx("div",{className:"list-header",children:i.jsx("h1",{children:"Vaults"})}),i.jsxs("div",{className:"error-banner",children:["Couldn't load vaults: ",i.jsx("code",{children:c.message})]}),i.jsx("button",{type:"button",onClick:()=>h(C=>C+1),className:"secondary",children:"Retry"})]});const k=c.vaults.length===0&&!c.moduleInstalled;return i.jsxs("div",{"data-route-content":"true",children:[i.jsxs("div",{className:"list-header",children:[i.jsxs("h1",{children:["Vaults (",c.vaults.length,")"]}),k?i.jsx(lt,{to:"/modules",children:i.jsx("button",{type:"button",children:"Install vault module"})}):null]}),i.jsxs("p",{className:"muted",children:["Vaults registered with this hub at ",i.jsx("code",{children:"/.well-known/parachute.json"}),"."," ",i.jsx("strong",{children:"Manage"})," opens the vault's own admin app (outside the hub shell); use your browser's back button to return here."]}),c.vaults.length>0?i.jsxs("p",{className:"muted","data-testid":"vaults-legacy-create-hint",children:["To create another vault, upgrade the vault module from ",i.jsx(lt,{to:"/modules",children:"Modules"})," ","(its own admin app includes create), or run"," ",i.jsx("code",{children:"parachute vault create <name>"})," on the host."]}):null,c.vaults.length===0?k?i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No vault module installed."}),i.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."}),i.jsx("p",{style:{marginTop:"0.75rem"},children:i.jsx(lt,{to:"/modules",children:"Install vault module →"})})]}):i.jsxs("div",{className:"empty empty-rich",children:[i.jsx("p",{className:"empty-headline",children:"No vaults yet."}),i.jsx("p",{className:"muted",children:"Create your first vault to start storing tokens, secrets, and notes scoped to this hub."}),i.jsx("p",{style:{marginTop:"0.75rem"},children:i.jsx("a",{href:"/admin/setup?step=vault",children:"Create a vault →"})})]}):i.jsx("div",{style:{marginTop:"1rem"},children:c.vaults.map(C=>{const O=g.kind==="minting"&&g.name===C.name,L=g.kind==="error"&&g.name===C.name?g:null,z=m===C.name,Z=x[C.name],X=(Z==null?void 0:Z.kind)==="ok"?`${Z.usage.notes} ${Z.usage.notes===1?"note":"notes"} · ${Sm(Z.usage.totalBytes)}`:(Z==null?void 0:Z.kind)==="loading"?"…":"—";return i.jsxs("div",{className:"vault-row-group",children:[i.jsxs("div",{className:"vault-row",children:[i.jsxs("div",{className:"body",children:[i.jsxs("div",{className:"name",children:[i.jsx("code",{children:C.name}),i.jsxs("span",{className:"tag muted",title:"Vault version",children:["v",C.version]})]}),i.jsx("div",{className:"dim url",children:i.jsx("code",{children:C.url})}),i.jsx("div",{className:"dim vault-usage","data-testid":`vault-usage-${C.name}`,children:X}),L?i.jsx("div",{className:"error-banner",style:{marginTop:"0.5rem"},children:i.jsx("code",{children:L.message})}):null]}),i.jsxs("div",{className:"vault-row-actions",children:[i.jsx("button",{type:"button",className:z?void 0:"secondary","aria-expanded":z,"aria-label":`Connect an MCP client to vault ${C.name}`,onClick:()=>v(P=>P===C.name?null:C.name),children:z?"Hide connect":"Connect"}),C.managementUrl?i.jsx("button",{type:"button",onClick:()=>{_(C)},disabled:O,"aria-label":`Manage vault ${C.name}`,children:O?"Opening…":"Manage"}):i.jsx("span",{className:"muted",title:"This vault has no admin SPA — manage with parachute-vault on the host.",children:"CLI only"})]})]}),z?i.jsx(hb,{vaultName:C.name,vaultUrl:C.url}):null]},C.name)})})]})}function Om(l){return l==="/permissions"||l.startsWith("/permissions/")?"permissions":l==="/tokens"||l.startsWith("/tokens/")?"tokens":l==="/modules"||l.startsWith("/modules/")?"modules":l==="/users"||l.startsWith("/users/")?"users":l==="/connections"||l.startsWith("/connections/")?"connections":l==="/vaults"||l.startsWith("/vaults/")?"vaults":l==="/settings"||l.startsWith("/settings/")?"settings":l==="/account"||l.startsWith("/account/")?"my account":l.startsWith("/approve-client/")?"approve app":"home"}function pb(l){return`${Om(l).split(" ").map(c=>c.length>0?c[0].toUpperCase()+c.slice(1):c).join(" ")} · ${jc}`}function gb(){const{pathname:l}=Nt(),r=Om(l),[c,o]=b.useState(null);b.useEffect(()=>{document.title=pb(l)},[l]);const[f,h]=b.useState(!1),[g,y]=b.useState([]),m=c!=null&&c.hasSession?c.csrf:null,v=J1(m,!!(c!=null&&c.hasSession));b.useEffect(()=>{let _=!1;return wc().then(k=>{_||o(k)}).catch(()=>{_||o({hasSession:!1})}),()=>{_=!0}},[]),b.useEffect(()=>{if(!c||!c.hasSession)return;let _=!1;return Cs().then(k=>{_||y(k.modules.filter(C=>C.installed))}).catch(()=>{_||y([])}),()=>{_=!0}},[c]),b.useEffect(()=>{v.locked&&je()},[v.locked]);async function x(){if(m){try{await k1(m)}catch{}je(),v.refresh()}}async function j(_){h(!0);try{await n1(_),window.location.href="/"}catch{h(!1)}}return v.locked&&m?i.jsx(Q1,{csrf:m,onUnlocked:v.refresh}):i.jsxs("div",{className:"page",children:[i.jsxs("nav",{className:"nav",children:[i.jsxs(lt,{to:"/",className:"brand",children:[i.jsx(xm,{size:18,idSuffix:"spa-nav",className:"brand-mark-icon"}),i.jsx("span",{className:"brand-wordmark",children:jc}),i.jsx("span",{className:"sub",children:r})]}),i.jsx(yb,{me:c,signingOut:f,onSignOut:j}),c!=null&&c.hasSession&&v.configured?i.jsx("button",{type:"button",className:"auth-spa-signout","data-testid":"admin-lock-now",title:"Lock the admin console now",onClick:()=>void x(),children:"Lock now"}):null,i.jsx(da,{to:"/",label:"Home",exact:!0}),i.jsx(da,{to:"/connections",label:"Connections"}),i.jsx(da,{to:"/grants",label:"Grants"}),i.jsx(da,{to:"/modules",label:"Modules"}),i.jsx(da,{to:"/users",label:"Users"}),i.jsx(da,{to:"/tokens",label:"Tokens"}),i.jsx(da,{to:"/permissions",label:"Permissions"}),i.jsx(da,{to:"/settings",label:"Settings"}),i.jsx(da,{to:"/account",label:"My account"}),i.jsx("span",{className:"nav-divider","aria-hidden":"true"}),i.jsx(vb,{services:g}),i.jsx("a",{href:"/hub.html",title:"Hub discovery page (top-level)",children:"Discovery"})]}),i.jsxs(fy,{children:[i.jsx(nt,{path:"/",element:i.jsx(g0,{})}),i.jsx(nt,{path:"/vaults",element:i.jsx(mb,{})}),i.jsx(nt,{path:"/vaults/new",element:i.jsx(Pr,{to:"/vaults",replace:!0})}),i.jsx(nt,{path:"/modules",element:i.jsx(M0,{})}),i.jsx(nt,{path:"/users",element:i.jsx(ab,{})}),i.jsx(nt,{path:"/connections",element:i.jsx(n0,{})}),i.jsx(nt,{path:"/grants",element:i.jsx(d0,{})}),i.jsx(nt,{path:"/channels",element:i.jsx(Pr,{to:"/connections",replace:!0})}),i.jsx(nt,{path:"/channels/*",element:i.jsx(Pr,{to:"/connections",replace:!0})}),i.jsx(nt,{path:"/permissions",element:i.jsx(B0,{})}),i.jsx(nt,{path:"/tokens",element:i.jsx(Z0,{})}),i.jsx(nt,{path:"/settings",element:i.jsx(Y0,{})}),i.jsx(nt,{path:"/account",element:i.jsx(K1,{})}),i.jsx(nt,{path:"/approve-client/:clientId",element:i.jsx(I1,{})}),i.jsx(nt,{path:"*",element:i.jsxs("div",{className:"empty",children:["404 — back to ",i.jsx(lt,{to:"/",children:"Home"}),"."]})})]}),c!=null&&c.hasSession?i.jsx(X1,{}):null]})}function da({to:l,label:r,alsoActiveAt:c,exact:o}){const{pathname:f}=Nt(),y=(o?f===l:f===l||f.startsWith(`${l}/`))||c!==void 0&&f===c;return i.jsx(lt,{to:l,className:y?"nav-link nav-link-active":"nav-link","aria-current":y?"page":void 0,children:r})}function vb({services:l}){return l.length===0?null:i.jsxs("details",{className:"nav-dropdown","data-testid":"installed-services-dropdown",children:[i.jsx("summary",{className:"nav-dropdown-summary",children:"Services"}),i.jsx("div",{className:"nav-dropdown-panel",role:"menu",children:l.map(r=>{const c=r.management_url;return c?i.jsx("a",{className:"nav-dropdown-item",href:c,role:"menuitem","data-testid":`nav-service-${r.short}`,children:r.display_name},r.short):i.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-${r.short}`,children:r.display_name},r.short)})})]})}function yb({me:l,signingOut:r,onSignOut:c}){return l===null?null:l.hasSession?i.jsxs("span",{className:"auth-spa",children:[i.jsxs("span",{className:"muted",children:["Signed in as ",i.jsx("strong",{children:l.user.displayName})]})," ","·"," ",i.jsx("button",{type:"button",className:"auth-spa-signout",disabled:r,onClick:()=>{c(l.csrf)},children:r?"Signing out…":"Sign out"})]}):i.jsx("a",{href:`/login?next=${encodeURIComponent(window.location.pathname)}`,className:"auth-spa",children:"Sign in"})}const Um=document.getElementById("root");if(!Um)throw new Error("#root not found");function bb(){const l=window.location.pathname;return l==="/admin"||l.startsWith("/admin/")?"/admin":""}mv.createRoot(Um).render(i.jsx(b.StrictMode,{children:i.jsx(Hy,{basename:bb(),children:i.jsx(gb,{})})}));
@@ -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-CkKBaPaO.js"></script>
8
+ <script type="module" crossorigin src="/admin/assets/index-CVqK1cV5.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/admin/assets/index-BcC4U5gM.css">
10
10
  </head>
11
11
  <body>