diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..399ff9f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.git +ui/node_modules diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..94a89dd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# syntax=docker/dockerfile:1.7 +FROM golang:1.24-alpine AS build +WORKDIR /src +RUN apk add --no-cache git +COPY go.mod go.sum ./ +RUN go mod download +COPY cmd ./cmd +COPY internal ./internal +RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /treatvault ./cmd/treatvault + +FROM alpine:3.21 +RUN apk add --no-cache ca-certificates docker-cli age +COPY --from=build /treatvault /usr/local/bin/treatvault +EXPOSE 9090 +ENTRYPOINT ["/usr/local/bin/treatvault"] +CMD ["serve", "--file", "/var/lib/treatvault/secrets.age", "--identity", "/run/secrets/barkstack_treatvault_identity", "--listen", ":9090"] diff --git a/README.md b/README.md index 656a428..3d942b2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,56 @@ -# treatvault +# TreatVault + +TreatVault manages Barkstack's namespaced secrets. It keeps an age-encrypted source-of-truth file on disk, watches it for changes, and reconciles its contents with Docker Swarm secrets. + +## Model + +- The Barkfile `treatvault` block names the encrypted file (a bind-mounted path) and the bootstrap Docker secret holding the age X25519 identity (`barkstack_`). +- The encrypted file is safe to commit and back up: contents are age-encrypted with the X25519 identity; only the identity secret itself stays outside source control. +- Each logical secret is stored as a versioned record. Sync creates one immutable Docker secret per record revision, named `barkstack_tv__`, labeled with the logical name and vault ID. +- Consumers mount secrets at the stable target path `barkstack_`; rotations swap the mounted source and update the service, leaving a rollback path. +- Obsolete Barkstack-managed secret objects are deleted; objects still mounted by a managed service are reported in the sync status and refused for deletion via the API until their consumers are updated. + +## CLI + +```sh +treatvault keygen --identity ./treatvault-identity.txt +treatvault init --file ./secrets.age --identity ./treatvault-identity.txt +treatvault set --name database_password --file ./secrets.age --identity ./treatvault-identity.txt # value on stdin +treatvault list --file ./secrets.age --identity ./treatvault-identity.txt +treatvault delete --name database_password --file ./secrets.age --identity ./treatvault-identity.txt +treatvault serve --file ./secrets.age --identity ./treatvault-identity.txt --listen :9090 +``` + +## HTTP API + +- `GET /barkstack/ui/manifest.json` - Barkstack plugin manifest (id `treatvault`, mount `/treatvault`). +- `GET /barkstack/api/secrets` - secret names, sync state, consumers; never returns values. +- `PUT /barkstack/api/secrets/{name}` - create or rotate (`{"value":"..."}`); triggers an immediate sync. +- `DELETE /barkstack/api/secrets/{name}` - delete; `409 Conflict` while a managed service still mounts the secret. +- `GET /healthz` - ready/degraded sync state. + +## Barkstack deployment + +Add a `treatvault` block to the Barkfile and run `barkstack init` on a Swarm manager: + +```text +treatvault { + file ./secrets.age + identity_secret treatvault_identity +} +``` + +`barkstack init` provisions the `barkstack-treatvault` service (manager-constrained) with the encrypted file bind-mounted read-write at its host path, the Docker socket, the `barkstack_treatvault_identity` secret mounted at `/run/secrets/barkstack_treatvault_identity`, and no published ports; the console reaches it over the overlay network at `:9090`. + +Services that consume vault secrets carry the `io.barkstack.treatvault.secrets=true` label plus an `io.barkstack.treatvault.names` label listing the logical names they need; `barkstack init` labels the PawSQL service automatically. Each sync mounts the current revision at the stable `/run/secrets/barkstack_` target, so consumers read the same path across rotations. + +The Barkstack console automatically gains a TreatVault page (plugin id `treatvault`) for creating, rotating, and deleting secrets. Deletion is refused with `409 Conflict` while a labeled service still mounts the secret. + +## Build + +```sh +cd ui && bun install && bun run build && cd .. +go build -o treatvault ./cmd/treatvault +docker build -t treatvault . +``` diff --git a/cmd/treatvault/main.go b/cmd/treatvault/main.go new file mode 100644 index 0000000..575f166 --- /dev/null +++ b/cmd/treatvault/main.go @@ -0,0 +1,221 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "io" + "log/slog" + "net" + "net/http" + "os" + "os/signal" + "sort" + "syscall" + "time" + + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/adminui" + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/manager" + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/provider" + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/vault" +) + +func main() { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + if err := run(ctx, os.Args[1:], os.Stdin, os.Stdout, os.Stderr); err != nil { + fmt.Fprintln(os.Stderr, "treatvault:", err) + os.Exit(1) + } +} + +func run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error { + if len(args) == 0 { + return usage(stderr) + } + switch args[0] { + case "keygen": + return runKeygen(args[1:], stdout, stderr) + case "init": + return runInit(args[1:], stderr) + case "set": + return runSet(args[1:], stdin, stderr) + case "delete": + return runDelete(args[1:], stderr) + case "list": + return runList(args[1:], stdout, stderr) + case "serve": + return runServe(ctx, args[1:], stderr) + default: + return fmt.Errorf("unknown command %q", args[0]) + } +} + +func runKeygen(args []string, stdout, stderr io.Writer) error { + flags := flag.NewFlagSet("keygen", flag.ContinueOnError) + flags.SetOutput(stderr) + identityPath := flags.String("identity", "treatvault-identity.txt", "age identity output path") + if err := parseNoArguments(flags, args); err != nil { + return err + } + recipient, err := vault.GenerateIdentity(*identityPath) + if err != nil { + return err + } + _, _ = fmt.Fprintf(stdout, "Identity written to %s\nRecipient: %s\n", *identityPath, recipient) + return nil +} + +func runInit(args []string, stderr io.Writer) error { + flags, filePath, identityPath := fileFlags("init", stderr) + if err := parseNoArguments(flags, args); err != nil { + return err + } + identity, err := vault.ReadIdentity(*identityPath) + if err != nil { + return err + } + return vault.Initialize(*filePath, identity) +} + +func runSet(args []string, stdin io.Reader, stderr io.Writer) error { + flags, filePath, identityPath := fileFlags("set", stderr) + name := flags.String("name", "", "short secret name") + valuePath := flags.String("value-file", "-", "secret value file, or - for stdin") + if err := parseNoArguments(flags, args); err != nil { + return err + } + if *name == "" { + return errors.New("--name is required") + } + value, err := readValue(*valuePath, stdin) + if err != nil { + return err + } + store, err := vault.Open(*filePath, *identityPath) + if err != nil { + return err + } + _, err = store.Set(*name, value) + return err +} + +func runDelete(args []string, stderr io.Writer) error { + flags, filePath, identityPath := fileFlags("delete", stderr) + name := flags.String("name", "", "short secret name") + if err := parseNoArguments(flags, args); err != nil { + return err + } + if *name == "" { + return errors.New("--name is required") + } + store, err := vault.Open(*filePath, *identityPath) + if err != nil { + return err + } + _, err = store.Delete(*name) + return err +} + +func runList(args []string, stdout, stderr io.Writer) error { + flags, filePath, identityPath := fileFlags("list", stderr) + if err := parseNoArguments(flags, args); err != nil { + return err + } + store, err := vault.Open(*filePath, *identityPath) + if err != nil { + return err + } + snapshot, err := store.Load() + if err != nil { + return err + } + names := make([]string, 0, len(snapshot.Secrets)) + for name := range snapshot.Secrets { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + _, _ = fmt.Fprintln(stdout, name) + } + return nil +} + +func runServe(ctx context.Context, args []string, stderr io.Writer) error { + flags, filePath, identityPath := fileFlags("serve", stderr) + listen := flags.String("listen", ":9090", "HTTP listen address") + dockerPath := flags.String("docker", "docker", "Docker CLI path") + if err := parseNoArguments(flags, args); err != nil { + return err + } + store, err := vault.Open(*filePath, *identityPath) + if err != nil { + return err + } + secretManager := manager.New(store, provider.Docker{Runner: provider.ExecRunner{Path: *dockerPath}}, 500*time.Millisecond) + go secretManager.Run(ctx) + listener, err := net.Listen("tcp", *listen) + if err != nil { + return fmt.Errorf("listen on %s: %w", *listen, err) + } + logger := slog.New(slog.NewTextHandler(stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) + logger.Info("TreatVault listening", "address", listener.Addr().String()) + server := &http.Server{Handler: adminui.Handler(secretManager), ReadHeaderTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second} + result := make(chan error, 1) + go func() { result <- server.Serve(listener) }() + select { + case <-ctx.Done(): + shutdownContext, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := server.Shutdown(shutdownContext); err != nil { + return fmt.Errorf("shut down HTTP server: %w", err) + } + err := <-result + if err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + return nil + case err := <-result: + if errors.Is(err, http.ErrServerClosed) { + return nil + } + return err + } +} + +func fileFlags(name string, stderr io.Writer) (*flag.FlagSet, *string, *string) { + flags := flag.NewFlagSet(name, flag.ContinueOnError) + flags.SetOutput(stderr) + filePath := flags.String("file", "secrets.age", "encrypted secret file") + identityPath := flags.String("identity", "treatvault-identity.txt", "age identity file") + return flags, filePath, identityPath +} + +func parseNoArguments(flags *flag.FlagSet, args []string) error { + if err := flags.Parse(args); err != nil { + return err + } + if flags.NArg() != 0 { + return fmt.Errorf("unexpected arguments: %v", flags.Args()) + } + return nil +} + +func readValue(path string, stdin io.Reader) ([]byte, error) { + if path == "-" { + return io.ReadAll(io.LimitReader(stdin, 500*1024+1)) + } + return os.ReadFile(path) +} + +func usage(stderr io.Writer) error { + _, _ = fmt.Fprintln(stderr, "usage:") + _, _ = fmt.Fprintln(stderr, " treatvault keygen [--identity FILE]") + _, _ = fmt.Fprintln(stderr, " treatvault init [--file FILE] [--identity FILE]") + _, _ = fmt.Fprintln(stderr, " treatvault set --name NAME [--value-file FILE|-] [--file FILE] [--identity FILE]") + _, _ = fmt.Fprintln(stderr, " treatvault delete --name NAME [--file FILE] [--identity FILE]") + _, _ = fmt.Fprintln(stderr, " treatvault list [--file FILE] [--identity FILE]") + _, _ = fmt.Fprintln(stderr, " treatvault serve [--file FILE] [--identity FILE] [--listen :9090] [--docker docker]") + return errors.New("a command is required") +} diff --git a/cmd/treatvault/main_test.go b/cmd/treatvault/main_test.go new file mode 100644 index 0000000..75ca727 --- /dev/null +++ b/cmd/treatvault/main_test.go @@ -0,0 +1,86 @@ +package main + +import ( + "bytes" + "context" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/vault" + "filippo.io/age" +) + +func TestCLIEndToEndWithoutDocker(t *testing.T) { + directory := t.TempDir() + filePath := filepath.Join(directory, "secrets.age") + identityPath := filepath.Join(directory, "identity.txt") + + var stdout, stderr strings.Builder + mustRun(t, []string{"keygen", "--identity", identityPath}, nil, &stdout, &stderr) + if !strings.Contains(stdout.String(), "Recipient: age1") { + t.Fatalf("keygen output = %q", stdout.String()) + } + mustRun(t, []string{"init", "--file", filePath, "--identity", identityPath}, nil, &strings.Builder{}, &strings.Builder{}) + mustRun(t, []string{"set", "--name", "database_password", "--value-file", "-", "--file", filePath, "--identity", identityPath}, strings.NewReader("correct horse battery staple"), &strings.Builder{}, &strings.Builder{}) + var list strings.Builder + mustRun(t, []string{"list", "--file", filePath, "--identity", identityPath}, nil, &list, &strings.Builder{}) + if got := strings.TrimSpace(list.String()); got != "database_password" { + t.Fatalf("list = %q", got) + } + + snapshot, err := vault.Open(filePath, identityPath) + if err != nil { + t.Fatal(err) + } + loaded, err := snapshot.Load() + if err != nil { + t.Fatal(err) + } + if string(loaded.Secrets["database_password"].Value) != "correct horse battery staple" { + t.Fatalf("stored value = %q", loaded.Secrets["database_password"].Value) + } + + mustRun(t, []string{"delete", "--name", "database_password", "--file", filePath, "--identity", identityPath}, nil, &strings.Builder{}, &strings.Builder{}) + loaded, err = snapshot.Load() + if err != nil { + t.Fatal(err) + } + if len(loaded.Secrets) != 0 { + t.Fatalf("secrets after delete = %#v", loaded.Secrets) + } +} + +func TestSetRejectsEmptyValue(t *testing.T) { + directory := t.TempDir() + filePath := filepath.Join(directory, "secrets.age") + identityPath := filepath.Join(directory, "identity.txt") + identity, err := age.GenerateX25519Identity() + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(identityPath, []byte(identity.String()), 0o600); err != nil { + t.Fatal(err) + } + if err := vault.Initialize(filePath, identity); err != nil { + t.Fatal(err) + } + var stderr strings.Builder + err = run(context.Background(), []string{"set", "--name", "database_password", "--value-file", "-", "--file", filePath, "--identity", identityPath}, strings.NewReader(""), &strings.Builder{}, &stderr) + if err == nil || !strings.Contains(err.Error(), "must not be empty") { + t.Fatalf("set empty value error = %v", err) + } +} + +func mustRun(t *testing.T, args []string, stdin *strings.Reader, stdout, stderr *strings.Builder) { + t.Helper() + var reader io.Reader = bytes.NewReader(nil) + if stdin != nil { + reader = stdin + } + if err := run(context.Background(), args, reader, stdout, stderr); err != nil { + t.Fatalf("run(%v) error = %v", args, err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..893ee34 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module cloud.campbellwireless.net/git/barkstack/treatvault + +go 1.24 + +require ( + cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2 v2.1.0 + filippo.io/age v1.2.1 +) + +require ( + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/sys v0.21.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..83508e9 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= +c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= +cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2 v2.1.0 h1:kqEalBOysM2w2ET7ISh1LblIsvo6dT2rOk9dqaQjDMQ= +cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2 v2.1.0/go.mod h1:um5p/9XtFqZqhh087AlPB2nr4NxhrQxw5sxe32uMbzw= +filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= +filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/adminui/dist/assets/entry.js b/internal/adminui/dist/assets/entry.js new file mode 100644 index 0000000..097b09d --- /dev/null +++ b/internal/adminui/dist/assets/entry.js @@ -0,0 +1,3414 @@ +typeof window < "u" && ((window.__svelte ??= {}).v ??= /* @__PURE__ */ new Set()).add("5"); +const ws = 1, ys = 2, ks = 16, $s = 1, Es = 2, nn = "[", _r = "[!", Hr = "[?", br = "]", Je = {}, R = /* @__PURE__ */ Symbol("uninitialized"), qs = "http://www.w3.org/1999/xhtml", sn = !1; +var an = Array.isArray, xs = Array.prototype.indexOf, St = Array.prototype.includes, Pt = Array.from, At = Object.keys, vt = Object.defineProperty, Ke = Object.getOwnPropertyDescriptor, Ts = Object.prototype, Ss = Array.prototype, As = Object.getPrototypeOf, Ur = Object.isExtensible; +const Ns = () => { +}; +function Cs(e) { + for (var t = 0; t < e.length; t++) + e[t](); +} +function ln() { + var e, t, r = new Promise((n, s) => { + e = n, t = s; + }); + return { promise: r, resolve: e, reject: t }; +} +const I = 2, Xe = 4, Lt = 8, on = 1 << 24, re = 16, Q = 32, de = 64, lr = 128, gr = 256, Z = 512, O = 1024, N = 2048, ie = 4096, j = 8192, U = 16384, Ve = 32768, Nt = 1 << 25, Ze = 65536, Ct = 1 << 17, Rs = 1 << 18, je = 1 << 19, Os = 1 << 20, me = 1 << 25, Pe = 65536, Rt = 1 << 21, We = 1 << 22, Ae = 1 << 23, $t = /* @__PURE__ */ Symbol("$state"), un = /* @__PURE__ */ Symbol("component"), Ms = /* @__PURE__ */ Symbol("legacy props"), Ds = /* @__PURE__ */ Symbol(""), fn = /* @__PURE__ */ Symbol("attributes"), or = /* @__PURE__ */ Symbol("class"), Is = /* @__PURE__ */ Symbol("style"), ur = /* @__PURE__ */ Symbol("text"), ft = /* @__PURE__ */ Symbol("form reset"), pt = new class extends Error { + name = "StaleReactionError"; + message = "The reaction that called `getAbortSignal()` was re-run or destroyed"; +}(), Ps = ( + // We gotta write it like this because after downleveling the pure comment may end up in the wrong location + !!globalThis.document?.contentType && /* @__PURE__ */ globalThis.document.contentType.includes("xml") +), Ft = 3, _t = 8; +function Ls() { + console.warn("https://svelte.dev/e/derived_inert"); +} +function Vt(e) { + console.warn("https://svelte.dev/e/hydration_mismatch"); +} +function Fs() { + console.warn("https://svelte.dev/e/svelte_boundary_reset_noop"); +} +let y = !1; +function we(e) { + y = e; +} +let k; +function K(e) { + if (e === null) + throw Vt(), Je; + return k = e; +} +function jt() { + return K(/* @__PURE__ */ pe(k)); +} +function M(e) { + if (y) { + if (/* @__PURE__ */ pe(k) !== null) + throw Vt(), Je; + k = e; + } +} +function cn(e = 1) { + if (y) { + for (var t = e, r = k; t--; ) + r = /** @type {TemplateNode} */ + /* @__PURE__ */ pe(r); + k = r; + } +} +function Ot(e = !0) { + for (var t = 0, r = k; ; ) { + if (r.nodeType === _t) { + var n = ( + /** @type {Comment} */ + r.data + ); + if (n === br) { + if (t === 0) return r; + t -= 1; + } else (n === nn || n === _r || // "[1", "[2", etc. for if blocks + n[0] === "[" && !isNaN(Number(n.slice(1)))) && (t += 1); + } + var s = ( + /** @type {TemplateNode} */ + /* @__PURE__ */ pe(r) + ); + e && r.remove(), r = s; + } +} +function dn(e) { + if (!e || e.nodeType !== _t) + throw Vt(), Je; + return ( + /** @type {Comment} */ + e.data + ); +} +function vn(e) { + return e === this.v; +} +function Vs(e, t) { + return e != e ? t == t : e !== t || e !== null && typeof e == "object" || typeof e == "function"; +} +function hn(e) { + return !Vs(e, this.v); +} +function js(e) { + throw new Error("https://svelte.dev/e/lifecycle_outside_component"); +} +function zs() { + throw new Error("https://svelte.dev/e/async_derived_orphan"); +} +function Hs(e, t, r) { + throw new Error("https://svelte.dev/e/each_key_duplicate"); +} +function Us(e) { + throw new Error("https://svelte.dev/e/effect_in_teardown"); +} +function Bs() { + throw new Error("https://svelte.dev/e/effect_in_unowned_derived"); +} +function Ys(e) { + throw new Error("https://svelte.dev/e/effect_orphan"); +} +function Gs() { + throw new Error("https://svelte.dev/e/effect_update_depth_exceeded"); +} +function Ks() { + throw new Error("https://svelte.dev/e/hydration_failed"); +} +function Ws() { + throw new Error("https://svelte.dev/e/state_descriptors_fixed"); +} +function Js() { + throw new Error("https://svelte.dev/e/state_prototype_fixed"); +} +function Xs() { + throw new Error("https://svelte.dev/e/state_unsafe_mutation"); +} +function Zs() { + throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror"); +} +let Qs = !1, L = null; +function Qe(e) { + L = e; +} +function pn(e, t = !1, r) { + L = { + p: L, + i: !1, + c: null, + e: null, + s: e, + x: null, + r: ( + /** @type {Effect} */ + g + ), + l: null + }; +} +function _n(e) { + var t = ( + /** @type {ComponentContext} */ + L + ), r = t.e; + if (r !== null) { + t.e = null; + for (var n of r) + Pn(n); + } + return e !== void 0 && (t.x = e), t.i = !0, L = t.p, mr(e); +} +function mr(e = {}) { + return vt(e, un, { value: !0 }), e; +} +function bn() { + return !0; +} +let Oe = []; +function gn() { + var e = Oe; + Oe = [], Cs(e); +} +function Se(e) { + if (Oe.length === 0 && !ct) { + var t = Oe; + queueMicrotask(() => { + t === Oe && gn(); + }); + } + Oe.push(e); +} +function ei() { + for (; Oe.length > 0; ) + gn(); +} +const ti = -7169; +function T(e, t) { + e.f = e.f & ti | t; +} +function wr(e) { + (e.f & Z) !== 0 || e.deps === null ? T(e, O) : T(e, ie); +} +function mn(e) { + if (e !== null) + for (const t of e) + (t.f & I) === 0 || (t.f & Pe) === 0 || (t.f ^= Pe, mn( + /** @type {Derived} */ + t.deps + )); +} +function wn(e, t, r) { + (e.f & N) !== 0 ? t.add(e) : (e.f & ie) !== 0 && r.add(e), mn(e.deps), T(e, O); +} +let Br = !1; +function yn() { + Br || (Br = !0, document.addEventListener( + "reset", + (e) => { + Promise.resolve().then(() => { + if (!e.defaultPrevented) + for ( + const t of + /**@type {HTMLFormElement} */ + e.target.elements + ) + t[ft]?.(); + }); + }, + // In the capture phase to guarantee we get noticed of it (no possibility of stopPropagation) + { capture: !0 } + )); +} +function bt(e) { + var t = w, r = g; + ee(null), he(null); + try { + return e(); + } finally { + ee(t), he(r); + } +} +function ri(e, t, r, n = r) { + e.addEventListener(t, () => bt(r)); + const s = ( + /** @type {any} */ + e[ft] + ); + s ? e[ft] = () => { + s(), n(!0); + } : e[ft] = () => n(!0), yn(); +} +function ni(e, t, r, n) { + const s = yr; + var i = e.filter((h) => !h.settled), l = t.map(s); + if (r.length === 0 && i.length === 0) { + n(l); + return; + } + var a = ( + /** @type {Effect} */ + g + ), o = si(), u = i.length === 1 ? i[0].promise : i.length > 1 ? Promise.all(i.map((h) => h.promise)) : null; + function c(h) { + if ((a.f & U) === 0) { + o(); + try { + n([...l, ...h]); + } catch (f) { + ue(f, a); + } + Mt(); + } + } + var d = kn(); + if (r.length === 0) { + u.then(() => c([])).finally(d); + return; + } + function v() { + Promise.all(r.map((h) => /* @__PURE__ */ ii(h))).then(c).catch((h) => ue(h, a)).finally(d); + } + u ? u.then(() => { + o(), v(), Mt(); + }) : v(); +} +function si() { + var e = ( + /** @type {Effect} */ + g + ), t = w, r = L, n = ( + /** @type {Batch} */ + b + ); + return function(i = !0) { + he(e), ee(t), Qe(r), i && (e.f & U) === 0 && (n?.activate(), n?.apply()); + }; +} +function Mt(e = !0) { + he(null), ee(null), Qe(null), e && b?.deactivate(); +} +function kn() { + var e = ( + /** @type {Effect} */ + g + ), t = e.b, r = ( + /** @type {Batch} */ + b + ), n = !!t?.is_rendered(); + return t?.update_pending_count(1, r), r.increment(n, e), () => { + t?.update_pending_count(-1, r), r.decrement(n, e); + }; +} +// @__NO_SIDE_EFFECTS__ +function yr(e) { + var t = I | N; + return g !== null && (g.f |= je), { + ctx: L, + deps: null, + effects: null, + equals: vn, + f: t, + fn: e, + reactions: null, + rv: 0, + v: ( + /** @type {V} */ + R + ), + wv: 0, + parent: g, + ac: null + }; +} +const at = /* @__PURE__ */ Symbol("obsolete"); +// @__NO_SIDE_EFFECTS__ +function ii(e, t, r) { + let n = ( + /** @type {Effect | null} */ + g + ); + n === null && zs(); + var s = ( + /** @type {Promise} */ + /** @type {unknown} */ + void 0 + ), i = Le( + /** @type {V} */ + R + ), l = !w, a = /* @__PURE__ */ new Set(); + return gi(() => { + var o = ( + /** @type {Effect} */ + g + ), u = ln(); + s = u.promise; + try { + Promise.resolve(e()).then(u.resolve, (h) => { + h !== pt && u.reject(h); + }).finally(Mt); + } catch (h) { + u.reject(h), Mt(); + } + var c = ( + /** @type {Batch} */ + b + ); + if (l) { + if ((o.f & Ve) !== 0) + var d = kn(); + if ( + // boundary can be null if the async derived is inside an $effect.root not connected to the component render tree + n.b?.is_rendered() + ) + c.async_deriveds.get(o)?.reject(at); + else + for (const h of a.values()) + h.reject(at); + a.add(u), c.async_deriveds.set(o, u); + } + const v = (h, f = void 0) => { + d?.(), a.delete(u), f !== at && (c.activate(), f ? (i.f |= Ae, et(i, f)) : ((i.f & Ae) !== 0 && (i.f ^= Ae), et(i, h)), c.deactivate()); + }; + u.promise.then(v, (h) => v(null, h || "unknown")); + }), pi(() => { + for (const o of a) + o.reject(at); + }), new Promise((o) => { + function u(c) { + function d() { + c === s ? o(i) : u(s); + } + c.then(d, d); + } + u(s); + }); +} +// @__NO_SIDE_EFFECTS__ +function ai(e) { + const t = /* @__PURE__ */ yr(e); + return t.equals = hn, t; +} +function li(e) { + var t = e.effects; + if (t !== null) { + e.effects = null; + for (var r = 0; r < t.length; r += 1) + z( + /** @type {Effect} */ + t[r] + ); + } +} +function kr(e) { + var t, r = g, n = e.parent; + if (!ke && n !== null && e.v !== R && // if it was never evaluated before, it's guaranteed to fail downstream, so we try to execute instead + (n.f & (U | j)) !== 0) + return Ls(), e.v; + he(n); + try { + e.f &= ~Pe, li(e), t = Yn(e); + } finally { + he(r); + } + return t; +} +function $n(e) { + var t = kr(e); + if (!e.equals(t) && (e.wv = Un(), (!b?.is_fork || e.deps === null) && (b !== null ? (b.capture(e, t, !0), fr?.capture(e, t, !0)) : e.v = t, e.deps === null))) { + T(e, O); + return; + } + ke || (ne !== null ? (Sr() || b?.is_fork) && ne.set(e, t) : wr(e)); +} +function oi(e) { + if (e.effects !== null) + for (const t of e.effects) + (t.teardown || t.ac) && (t.teardown?.(), t.ac !== null && bt(() => { + t.ac.abort(pt), t.ac = null; + }), t.fn !== null && (t.teardown = Ns), ht(t, 0), Nr(t)); +} +function En(e) { + if (e.effects !== null) + for (const t of e.effects) + t.teardown && t.fn !== null && tt(t); +} +let Xt = null, Ue = null, b = null, fr = null, ne = null, cr = null, ct = !1, Zt = !1, Ge = null, Et = null; +var Yr = 0; +let ui = 1; +class ye { + id = ui++; + /** True as soon as `#process` was called */ + #e = !1; + linked = !0; + /** @type {Batch | null} */ + #t = null; + /** @type {Batch | null} */ + #r = null; + /** @type {Map>>} */ + async_deriveds = /* @__PURE__ */ new Map(); + /** + * The current values of any signals that are updated in this batch. + * Tuple format: [value, is_derived] (note: is_derived is false for deriveds, too, if they were overridden via assignment) + * They keys of this map are identical to `this.#previous` + * @type {Map} + */ + current = /* @__PURE__ */ new Map(); + /** + * The values of any signals (sources and deriveds) that are updated in this batch _before_ those updates took place. + * They keys of this map are identical to `this.#current` + * @type {Map} + */ + previous = /* @__PURE__ */ new Map(); + /** + * When the batch is committed (and the DOM is updated), we need to remove old branches + * and append new ones by calling the functions added inside (if/each/key/etc) blocks + * @type {Set<(batch: Batch) => void>} + */ + #o = /* @__PURE__ */ new Set(); + /** + * If a fork is discarded, we need to destroy any effects that are no longer needed + * @type {Set<(batch: Batch) => void>} + */ + #s = /* @__PURE__ */ new Set(); + /** + * The number of async effects that are currently in flight + */ + #a = 0; + /** + * Async effects that are currently in flight, _not_ inside a pending boundary + * @type {Map} + */ + #n = /* @__PURE__ */ new Map(); + /** + * A deferred that resolves when the batch is committed, used with `settled()` + * TODO replace with Promise.withResolvers once supported widely enough + * @type {{ promise: Promise, resolve: (value?: any) => void, reject: (reason: unknown) => void } | null} + */ + #l = null; + /** + * The root effects that need to be flushed + * @type {Effect[]} + */ + #i = []; + /** + * Effects created while this batch was active. + * @type {Effect[]} + */ + #p = []; + /** + * Deferred effects (which run after async work has completed) that are DIRTY + * @type {Set} + */ + #u = /* @__PURE__ */ new Set(); + /** + * Deferred effects that are MAYBE_DIRTY + * @type {Set} + */ + #f = /* @__PURE__ */ new Set(); + /** + * A map of branches that still exist, but will be destroyed when this batch + * is committed — we skip over these during `process`. + * The value contains child effects that were dirty/maybe_dirty before being reset, + * so they can be rescheduled if the branch survives. + * @type {Map} + */ + #d = /* @__PURE__ */ new Map(); + /** + * Inverse of #skipped_branches which we need to tell prior batches to unskip them when committing + * @type {Set} + */ + #_ = /* @__PURE__ */ new Set(); + is_fork = !1; + #c = !1; + constructor() { + Ue === null ? Xt = Ue = this : (Ue.#r = this, this.#t = Ue), Ue = this; + } + #m() { + if (this.is_fork) return !0; + for (const n of this.#n.keys()) { + for (var t = n, r = !1; t.parent !== null; ) { + if (this.#d.has(t)) { + r = !0; + break; + } + t = t.parent; + } + if (!r) + return !0; + } + return !1; + } + /** + * Add an effect to the #skipped_branches map and reset its children + * @param {Effect} effect + */ + skip_effect(t) { + this.#d.has(t) || this.#d.set(t, { d: [], m: [] }), this.#_.delete(t); + } + /** + * Remove an effect from the #skipped_branches map and reschedule + * any tracked dirty/maybe_dirty child effects + * @param {Effect} effect + * @param {(e: Effect) => void} callback + */ + unskip_effect(t, r = (n) => this.schedule(n)) { + var n = this.#d.get(t); + if (n) { + this.#d.delete(t); + for (var s of n.d) + T(s, N), r(s); + for (s of n.m) + T(s, ie), r(s); + } + this.#_.add(t); + } + #b() { + this.#e = !0, Yr++ > 1e3 && (this.#h(), fi()); + for (const o of this.#u) + this.#f.delete(o), T(o, N), this.schedule(o); + for (const o of this.#f) + T(o, ie), this.schedule(o); + const t = this.#i; + this.#i = [], this.apply(); + var r = Ge = [], n = [], s = Et = []; + for (const o of t) + try { + this.#w(o, r, n); + } catch (u) { + throw Tn(o), this.#m() || this.discard(), u; + } + if (b = null, s.length > 0) { + var i = ye.ensure(); + for (const o of s) + i.schedule(o); + } + if (Ge = null, Et = null, this.#m()) { + this.#v(n), this.#v(r); + for (const [o, u] of this.#d) + xn(o, u); + s.length > 0 && /** @type {unknown} */ + b.#b(); + return; + } + const l = this.#y(); + if (l) { + this.#v(n), this.#v(r), l.#k(this); + return; + } + this.#u.clear(), this.#f.clear(); + for (const o of this.#o) o(this); + this.#o.clear(), fr = this, Gr(n), Gr(r), fr = null, this.#l?.resolve(); + var a = ( + /** @type {Batch | null} */ + /** @type {unknown} */ + b + ); + if (this.#a === 0 && (this.#i.length === 0 || a !== null) && this.#h(), this.#i.length > 0) + if (a !== null) { + const o = a; + o.#i.push(...this.#i.filter((u) => !o.#i.includes(u))); + } else + a = this; + a !== null && (fe.clear(), a.#b()); + } + /** + * Traverse the effect tree, executing effects or stashing + * them for later execution as appropriate + * @param {Effect} root + * @param {Effect[]} effects + * @param {Effect[]} render_effects + */ + #w(t, r, n) { + t.f ^= O; + for (var s = t.first; s !== null; ) { + var i = s.f, l = (i & (Q | de)) !== 0, a = l && (i & O) !== 0, o = a || (i & j) !== 0 || this.#d.has(s); + if (!o && s.fn !== null) { + l ? s.f ^= O : (i & Xe) !== 0 ? r.push(s) : gt(s) && ((i & re) !== 0 && this.#f.add(s), tt(s)); + var u = s.first; + if (u !== null) { + s = u; + continue; + } + } + for (; s !== null; ) { + var c = s.next; + if (c !== null) { + s = c; + break; + } + s = s.parent; + } + } + } + #y() { + for (var t = this.#t; t !== null; ) { + if (!t.is_fork) { + for (const [r, [, n]] of this.current) + if (t.current.has(r) && !n) + return t; + } + t = t.#t; + } + return null; + } + /** + * @param {Batch} batch + */ + #k(t) { + for (const [n, s] of t.current) + !this.previous.has(n) && t.previous.has(n) && this.previous.set(n, t.previous.get(n)), this.current.set(n, s); + for (const [n, s] of t.async_deriveds) { + const i = this.async_deriveds.get(n); + i && s.promise.then(i.resolve).catch(i.reject); + } + t.async_deriveds.clear(), this.transfer_effects(t.#u, t.#f); + const r = (n) => { + var s = n.reactions; + if (s !== null && !((n.f & I) !== 0 && (n.f & (N | ie)) === 0)) + for (const a of s) { + var i = a.f; + if ((i & I) !== 0) + r( + /** @type {Derived} */ + a + ); + else { + var l = ( + /** @type {Effect} */ + a + ); + i & (We | re) && !this.async_deriveds.has(l) && (this.#f.delete(l), T(l, N), this.schedule(l)); + } + } + }; + for (const n of this.current.keys()) + r(n); + this.oncommit(() => t.discard()), t.#h(), b = this, this.#b(); + } + /** + * @param {Effect[]} effects + */ + #v(t) { + for (var r = 0; r < t.length; r += 1) + wn(t[r], this.#u, this.#f); + } + /** + * Associate a change to a given source with the current + * batch, noting its previous and current values + * @param {Value} source + * @param {any} value + * @param {boolean} [is_derived] + */ + capture(t, r, n = !1) { + t.v !== R && !this.previous.has(t) && this.previous.set(t, t.v), (t.f & Ae) === 0 && (this.current.set(t, [r, n]), ne?.set(t, r)), this.is_fork || (t.v = r); + } + activate() { + b = this; + } + deactivate() { + b = null, ne = null; + } + flush() { + try { + Zt = !0, b = this, this.#b(); + } finally { + Yr = 0, cr = null, Ge = null, Et = null, Zt = !1, b = null, ne = null, fe.clear(); + } + } + discard() { + for (const t of this.#s) t(this); + this.#s.clear(); + for (const t of this.async_deriveds.values()) + t.reject(at); + this.#h(), this.#l?.resolve(); + } + /** + * @param {Effect} effect + */ + register_created_effect(t) { + this.#p.push(t); + } + #g() { + for (let d = Xt; d !== null; d = d.#r) { + var t = d.id < this.id, r = []; + for (const [v, [h, f]] of this.current) { + if (d.current.has(v)) { + var n = ( + /** @type {[any, boolean]} */ + d.current.get(v)[0] + ); + if (t && h !== n) + d.current.set(v, [h, f]); + else + continue; + } + r.push(v); + } + if (t) + for (const [v, h] of this.async_deriveds) { + const f = d.async_deriveds.get(v); + f && h.promise.then(f.resolve).catch(f.reject); + } + var s = [...d.current.keys()].filter( + (v) => !/** @type {[any, boolean]} */ + d.current.get(v)[1] + ); + if (!(!d.#e || s.length === 0)) { + var i = s.filter((v) => !this.current.has(v)); + if (i.length === 0) + t && d.discard(); + else if (r.length > 0) { + if (t) + for (const v of this.#_) + d.unskip_effect(v, (h) => { + (h.f & (re | We)) !== 0 ? d.schedule(h) : d.#v([h]); + }); + d.activate(); + var l = /* @__PURE__ */ new Set(), a = /* @__PURE__ */ new Map(); + for (var o of r) + qn(o, i, l, a); + a = /* @__PURE__ */ new Map(); + var u = [...d.current].filter(([v, h]) => { + const f = this.current.get(v); + return f ? f[0] !== h[0] || f[1] !== h[1] : !0; + }).map(([v]) => v); + if (u.length > 0) + for (const v of this.#p) + (v.f & (U | j | Ct)) === 0 && Er(v, u, a) && ((v.f & (We | re)) !== 0 ? (T(v, N), d.schedule(v)) : d.#u.add(v)); + if (d.#i.length > 0 && !d.#c) { + d.apply(); + for (var c of d.#i) + d.#w(c, [], []); + d.#i = []; + } + d.deactivate(); + } + } + } + } + /** + * @param {boolean} blocking + * @param {Effect} effect + */ + increment(t, r) { + if (this.#a += 1, t) { + let n = this.#n.get(r) ?? 0; + this.#n.set(r, n + 1); + } + } + /** + * @param {boolean} blocking + * @param {Effect} effect + */ + decrement(t, r) { + if (this.#a -= 1, t) { + let n = this.#n.get(r) ?? 0; + n === 1 ? this.#n.delete(r) : this.#n.set(r, n - 1); + } + this.#c || (this.#c = !0, Se(() => { + this.#c = !1, this.linked && this.flush(); + })); + } + /** + * @param {Set} dirty_effects + * @param {Set} maybe_dirty_effects + */ + transfer_effects(t, r) { + for (const n of t) + this.#u.add(n); + for (const n of r) + this.#f.add(n); + t.clear(), r.clear(); + } + /** @param {(batch: Batch) => void} fn */ + oncommit(t) { + this.#o.add(t); + } + /** @param {(batch: Batch) => void} fn */ + ondiscard(t) { + this.#s.add(t); + } + settled() { + return (this.#l ??= ln()).promise; + } + static ensure() { + if (b === null) { + const t = b = new ye(); + !Zt && !ct && Se(() => { + t.#e || t.flush(); + }); + } + return b; + } + apply() { + { + ne = null; + return; + } + } + /** + * + * @param {Effect} effect + */ + schedule(t) { + if (cr = t, t.b?.is_pending && (t.f & (Xe | Lt | on)) !== 0 && (t.f & Ve) === 0) { + t.b.defer_effect(t); + return; + } + for (var r = t; r.parent !== null; ) { + r = r.parent; + var n = r.f; + if (Ge !== null && r === g && (w === null || (w.f & I) === 0)) + return; + if ((n & (de | Q)) !== 0) { + if ((n & O) === 0) + return; + r.f ^= O; + } + } + this.#i.push(r); + } + #h() { + if (this.linked) { + var t = this.#t, r = this.#r; + t === null ? Xt = r : t.#r = r, r === null ? Ue = t : r.#t = t, this.linked = !1; + } + } +} +function $r(e) { + var t = ct; + ct = !0; + try { + for (var r; ; ) { + if (ei(), b === null) + return ( + /** @type {T} */ + r + ); + b.flush(); + } + } finally { + ct = t; + } +} +function fi() { + try { + Gs(); + } catch (e) { + ue(e, cr); + } +} +let ge = null; +function Gr(e) { + var t = e.length; + if (t !== 0) { + for (var r = 0; r < t; ) { + var n = e[r++]; + if ((n.f & (U | j)) === 0 && gt(n) && (ge = /* @__PURE__ */ new Set(), tt(n), n.deps === null && n.first === null && n.nodes === null && n.teardown === null && n.ac === null && Vn(n), ge?.size > 0)) { + fe.clear(); + for (const s of ge) { + if ((s.f & (U | j)) !== 0) continue; + const i = [s]; + let l = s.parent; + for (; l !== null; ) + ge.has(l) && (ge.delete(l), i.push(l)), l = l.parent; + for (let a = i.length - 1; a >= 0; a--) { + const o = i[a]; + (o.f & (U | j)) === 0 && tt(o); + } + } + ge.clear(); + } + } + ge = null; + } +} +function qn(e, t, r, n) { + if (!r.has(e) && (r.add(e), e.reactions !== null)) + for (const s of e.reactions) { + const i = s.f; + (i & I) !== 0 ? qn( + /** @type {Derived} */ + s, + t, + r, + n + ) : (i & (We | re)) !== 0 && (i & N) === 0 && Er(s, t, n) && (T(s, N), qr( + /** @type {Effect} */ + s + )); + } +} +function Er(e, t, r) { + const n = r.get(e); + if (n !== void 0) return n; + if (e.deps !== null) + for (const s of e.deps) { + if (St.call(t, s)) + return !0; + if ((s.f & I) !== 0 && Er( + /** @type {Derived} */ + s, + t, + r + )) + return r.set( + /** @type {Derived} */ + s, + !0 + ), !0; + } + return r.set(e, !1), !1; +} +function qr(e) { + b.schedule(e); +} +function xn(e, t) { + if (!((e.f & Q) !== 0 && (e.f & O) !== 0)) { + (e.f & N) !== 0 ? t.d.push(e) : (e.f & ie) !== 0 && t.m.push(e), T(e, O); + for (var r = e.first; r !== null; ) + xn(r, t), r = r.next; + } +} +function Tn(e) { + T(e, O); + for (var t = e.first; t !== null; ) + Tn(t), t = t.next; +} +let Dt = /* @__PURE__ */ new Set(); +const fe = /* @__PURE__ */ new Map(); +let Sn = !1; +function Le(e, t) { + var r = { + f: 0, + // TODO ideally we could skip this altogether, but it causes type errors + v: e, + reactions: null, + equals: vn, + rv: 0, + wv: 0 + }; + return r; +} +// @__NO_SIDE_EFFECTS__ +function P(e, t) { + const r = Le(e); + return yi(r), r; +} +// @__NO_SIDE_EFFECTS__ +function An(e, t = !1, r = !0) { + const n = Le(e); + return t || (n.equals = hn), n; +} +function q(e, t, r = !1) { + w !== null && // since we are untracking the function inside `$inspect.with` we need to add this check + // to ensure we error if state is set inside an inspect effect + (!se || (w.f & Ct) !== 0) && bn() && (w.f & (I | re | We | Ct)) !== 0 && (ce === null || !ce.has(e)) && Xs(); + let n = r ? lt(t) : t; + return et(e, n, Et); +} +function et(e, t, r = null) { + if (!e.equals(t)) { + ke ? fe.set(e, t) : fe.has(e) || fe.set(e, e.v); + var n = ye.ensure(); + if (n.capture(e, t), (e.f & I) !== 0) { + const s = ( + /** @type {Derived} */ + e + ); + (e.f & N) !== 0 && kr(s), ne === null && wr(s); + } + e.wv = Un(), Nn(e, N, r), g !== null && (g.f & O) !== 0 && (g.f & (Q | de)) === 0 && (J === null ? ki([e]) : J.push(e)), !n.is_fork && Dt.size > 0 && !Sn && ci(); + } + return t; +} +function ci() { + Sn = !1; + for (const e of Dt) { + (e.f & O) !== 0 && T(e, ie); + let t; + try { + t = gt(e); + } catch { + t = !0; + } + t && tt(e); + } + Dt.clear(); +} +function dt(e) { + q(e, e.v + 1); +} +function Nn(e, t, r) { + var n = e.reactions; + if (n !== null) + for (var s = n.length, i = 0; i < s; i++) { + var l = n[i], a = l.f, o = (a & N) === 0; + if (o && T(l, t), (a & Ct) !== 0) + Dt.add( + /** @type {Effect} */ + l + ); + else if ((a & I) !== 0) { + var u = ( + /** @type {Derived} */ + l + ); + ne?.delete(u), (a & Pe) === 0 && (a & Z && (g === null || (g.f & Rt) === 0) && (l.f |= Pe), Nn(u, ie, r)); + } else if (o) { + var c = ( + /** @type {Effect} */ + l + ); + (a & re) !== 0 && ge !== null && ge.add(c), r !== null ? r.push(c) : qr(c); + } + } +} +function lt(e) { + if (typeof e != "object" || e === null || $t in e || un in e) + return e; + const t = As(e); + if (t !== Ts && t !== Ss) + return e; + var r = /* @__PURE__ */ new Map(), n = an(e), s = /* @__PURE__ */ P(0), i = Ie, l = (a) => { + if (Ie === i) + return a(); + var o = w, u = Ie; + ee(null), Jr(i); + var c = a(); + return ee(o), Jr(u), c; + }; + return n && r.set("length", /* @__PURE__ */ P( + /** @type {any[]} */ + e.length + )), new Proxy( + /** @type {any} */ + e, + { + defineProperty(a, o, u) { + (!("value" in u) || u.configurable === !1 || u.enumerable === !1 || u.writable === !1) && Ws(); + var c = r.get(o); + return c === void 0 ? l(() => { + var d = /* @__PURE__ */ P(u.value); + return r.set(o, d), d; + }) : q(c, u.value, !0), !0; + }, + deleteProperty(a, o) { + var u = r.get(o); + if (u === void 0) { + if (o in a) { + const c = l(() => /* @__PURE__ */ P(R)); + r.set(o, c), dt(s); + } + } else + q(u, R), dt(s); + return !0; + }, + get(a, o, u) { + if (o === $t) + return e; + var c = r.get(o), d = o in a; + if (c === void 0 && (!d || Ke(a, o)?.writable) && (c = l(() => { + var h = lt(d ? a[o] : R), f = /* @__PURE__ */ P(h); + return f; + }), r.set(o, c)), c !== void 0) { + var v = p(c); + return v === R ? void 0 : v; + } + return Reflect.get(a, o, u); + }, + getOwnPropertyDescriptor(a, o) { + var u = Reflect.getOwnPropertyDescriptor(a, o); + if (u && "value" in u) { + var c = r.get(o); + c && (u.value = p(c)); + } else if (u === void 0) { + var d = r.get(o), v = d?.v; + if (d !== void 0 && v !== R) + return { + enumerable: !0, + configurable: !0, + value: v, + writable: !0 + }; + } + return u; + }, + has(a, o) { + if (o === $t) + return !0; + var u = r.get(o), c = u !== void 0 && u.v !== R || Reflect.has(a, o); + if (u !== void 0 || g !== null && (!c || Ke(a, o)?.writable)) { + u === void 0 && (u = l(() => { + var v = c ? lt(a[o]) : R, h = /* @__PURE__ */ P(v); + return h; + }), r.set(o, u)); + var d = p(u); + if (d === R) + return !1; + } + return c; + }, + set(a, o, u, c) { + var d = r.get(o), v = o in a; + if (n && o === "length") + for (var h = u; h < /** @type {Source} */ + d.v; h += 1) { + var f = r.get(h + ""); + f !== void 0 ? q(f, R) : h in a && (f = l(() => /* @__PURE__ */ P(R)), r.set(h + "", f)); + } + if (d === void 0) + (!v || Ke(a, o)?.writable) && (d = l(() => /* @__PURE__ */ P(void 0)), q(d, lt(u)), r.set(o, d)); + else { + v = d.v !== R; + var $ = l(() => lt(u)); + q(d, $); + } + var C = Reflect.getOwnPropertyDescriptor(a, o); + if (C?.set && C.set.call(c, u), !v) { + if (n && typeof o == "string") { + var m = ( + /** @type {Source} */ + r.get("length") + ), S = Number(o); + Number.isInteger(S) && S >= m.v && q(m, S + 1); + } + dt(s); + } + return !0; + }, + ownKeys(a) { + p(s); + var o = Reflect.ownKeys(a).filter((d) => { + var v = r.get(d); + return v === void 0 || v.v !== R; + }); + for (var [u, c] of r) + c.v !== R && !(u in a) && o.push(u); + return o; + }, + setPrototypeOf() { + Js(); + } + } + ); +} +var Kr, Cn, Rn, On; +function dr() { + if (Kr === void 0) { + Kr = window, Cn = /Firefox/.test(navigator.userAgent); + var e = Element.prototype, t = Node.prototype, r = Text.prototype; + Rn = Ke(t, "firstChild").get, On = Ke(t, "nextSibling").get, Ur(e) && (e[or] = void 0, e[fn] = null, e[Is] = void 0, e.__e = void 0), Ur(r) && (r[ur] = void 0); + } +} +function ve(e = "") { + return document.createTextNode(e); +} +// @__NO_SIDE_EFFECTS__ +function Fe(e) { + return ( + /** @type {TemplateNode | null} */ + Rn.call(e) + ); +} +// @__NO_SIDE_EFFECTS__ +function pe(e) { + return ( + /** @type {TemplateNode | null} */ + On.call(e) + ); +} +function D(e, t) { + if (!y) + return /* @__PURE__ */ Fe(e); + var r = /* @__PURE__ */ Fe(k); + if (r === null) + r = k.appendChild(ve()); + else if (t && r.nodeType !== Ft) { + var n = ve(); + return r?.before(n), K(n), n; + } + return t && Tr( + /** @type {Text} */ + r + ), K(r), r; +} +function Qt(e, t = !1) { + if (!y) { + var r = /* @__PURE__ */ Fe(e); + return r instanceof Comment && r.data === "" ? /* @__PURE__ */ pe(r) : r; + } + if (t) { + if (k?.nodeType !== Ft) { + var n = ve(); + return k?.before(n), K(n), n; + } + Tr( + /** @type {Text} */ + k + ); + } + return k; +} +function Be(e, t = !1) { + if (!y) + return /* @__PURE__ */ Fe(e); + var r = D(e, t); + return M(e), r; +} +function E(e, t = 1, r = !1) { + let n = y ? k : e; + for (var s; t--; ) + s = n, n = /** @type {TemplateNode} */ + /* @__PURE__ */ pe(n); + if (!y) + return n; + if (r) { + if (n?.nodeType !== Ft) { + var i = ve(); + return n === null ? s?.after(i) : n.before(i), K(i), i; + } + Tr( + /** @type {Text} */ + n + ); + } + return K(n), n; +} +function Mn(e) { + e.textContent = ""; +} +function Dn() { + return !1; +} +function xr(e, t, r) { + return ( + /** @type {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element} */ + document.createElement(e) + ); +} +function Tr(e) { + if ( + /** @type {string} */ + e.nodeValue.length < 65536 + ) + return; + let t = e.nextSibling; + for (; t !== null && t.nodeType === Ft; ) + t.remove(), e.nodeValue += /** @type {string} */ + t.nodeValue, t = e.nextSibling; +} +function di(e) { + var t = g; + if (t === null) + return w.f |= Ae, e; + if ((t.f & Ve) === 0 && (t.f & Xe) === 0) + throw e; + ue(e, t); +} +function ue(e, t) { + if (!(t !== null && (t.f & U) !== 0)) { + for (; t !== null; ) { + if ((t.f & lr) !== 0 && (t.f & (U | Nt)) === 0) { + if ((t.f & Ve) === 0) + throw e; + try { + t.b.error(e); + return; + } catch (r) { + e = r; + } + } + t = t.parent; + } + throw e; + } +} +function vi(e) { + g === null && (w === null && Ys(), Bs()), ke && Us(); +} +function hi(e, t) { + var r = t.last; + r === null ? t.last = t.first = e : (r.next = e, e.prev = r, t.last = e); +} +function _e(e, t) { + var r = g; + r !== null && (r.f & j) !== 0 && (e |= j); + var n = { + ctx: L, + deps: null, + nodes: null, + f: e | N | Z, + first: null, + fn: t, + last: null, + next: null, + parent: r, + b: r && r.b, + prev: null, + teardown: null, + wv: 0, + ac: null + }; + b?.register_created_effect(n); + var s = n; + if ((e & Xe) !== 0) + Ge !== null ? Ge.push(n) : ye.ensure().schedule(n); + else if (t !== null) { + try { + tt(n); + } catch (l) { + throw z(n), l; + } + s.deps === null && s.teardown === null && s.nodes === null && s.first === s.last && // either `null`, or a singular child + (s.f & je) === 0 && (s = s.first, (e & re) !== 0 && (e & Ze) !== 0 && s !== null && (s.f |= Ze)); + } + if (s !== null && (s.parent = r, r !== null && hi(s, r), w !== null && (w.f & I) !== 0 && (e & de) === 0)) { + var i = ( + /** @type {Derived} */ + w + ); + (i.effects ??= []).push(s); + } + return n; +} +function Sr() { + return w !== null && !se; +} +function pi(e) { + const t = _e(Lt, null); + return T(t, O), t.teardown = e, t; +} +function In(e) { + vi(); + var t = ( + /** @type {Effect} */ + g.f + ), r = !w && (t & Q) !== 0 && L !== null && !L.i; + if (r) { + var n = ( + /** @type {ComponentContext} */ + L + ); + (n.e ??= []).push(e); + } else + return Pn(e); +} +function Pn(e) { + return _e(Xe | Os, e); +} +function _i(e) { + ye.ensure(); + const t = _e(de | je, e); + return () => { + z(t); + }; +} +function bi(e) { + ye.ensure(); + const t = _e(de | je, e); + return (r = {}) => new Promise((n) => { + r.outro ? De(t, () => { + z(t), n(void 0); + }) : (z(t), n(void 0)); + }); +} +function Ln(e) { + return _e(Xe, e); +} +function gi(e) { + return _e(We | je, e); +} +function zt(e, t = 0) { + return _e(Lt | t, e); +} +function Ce(e, t = [], r = [], n = []) { + ni(n, t, r, (s) => { + _e(Lt, () => { + e(...s.map(p)); + }); + }); +} +function Ar(e, t = 0) { + var r = _e(re | t, e); + return r; +} +function X(e) { + return _e(Q | je, e); +} +function Fn(e) { + var t = e.teardown; + if (t !== null) { + const r = ke, n = w; + Wr(!0), ee(null); + try { + t.call(null); + } catch (s) { + ue(s, e.parent); + } finally { + Wr(r), ee(n); + } + } +} +function Nr(e, t = !1) { + var r = e.first; + for (e.first = e.last = null; r !== null; ) { + const s = r.ac; + s !== null && bt(() => { + s.abort(pt); + }); + var n = r.next; + (r.f & de) !== 0 ? r.parent = null : z(r, t), r = n; + } +} +function mi(e) { + for (var t = e.first; t !== null; ) { + var r = t.next; + (t.f & Q) === 0 && z(t), t = r; + } +} +function z(e, t = !0) { + var r = !1; + (t || (e.f & Rs) !== 0) && e.nodes !== null && e.nodes.end !== null && (wi( + e.nodes.start, + /** @type {TemplateNode} */ + e.nodes.end + ), r = !0), e.f |= Nt, Nr(e, t && !r), ht(e, 0); + var n = e.nodes && e.nodes.t; + if (n !== null) + for (const i of n) + i.stop(); + Fn(e), e.f ^= Nt, e.f |= U; + var s = e.parent; + s !== null && s.first !== null && Vn(e), e.next = e.prev = e.teardown = e.ctx = e.deps = e.fn = e.nodes = e.ac = e.b = null; +} +function wi(e, t) { + for (; e !== null; ) { + var r = e === t ? null : /* @__PURE__ */ pe(e); + e.remove(), e = r; + } +} +function Vn(e) { + var t = e.parent, r = e.prev, n = e.next; + r !== null && (r.next = n), n !== null && (n.prev = r), t !== null && (t.first === e && (t.first = n), t.last === e && (t.last = r)); +} +function De(e, t, r = !0) { + var n = []; + e.f |= gr, jn(e, n, !0); + var s = () => { + r && z(e), t && t(); + }, i = n.length; + if (i > 0) { + var l = () => --i || s(); + for (var a of n) + a.out(l); + } else + s(); +} +function jn(e, t, r) { + if ((e.f & j) === 0) { + e.f ^= j; + var n = e.nodes && e.nodes.t; + if (n !== null) + for (const a of n) + (a.is_global || r) && t.push(a); + for (var s = e.first; s !== null; ) { + var i = s.next; + if ((s.f & de) === 0) { + var l = (s.f & Ze) !== 0 || // If this is a branch effect without a block effect parent, + // it means the parent block effect was pruned. In that case, + // transparency information was transferred to the branch effect. + (s.f & Q) !== 0 && (e.f & re) !== 0; + jn(s, t, l ? r : !1); + } + s = i; + } + } +} +function It(e) { + e.f &= ~gr, zn(e, !0); +} +function zn(e, t) { + if ((e.f & gr) === 0 && (e.f & j) !== 0) { + e.f ^= j, (e.f & O) === 0 && (T(e, N), ye.ensure().schedule(e)); + for (var r = e.first; r !== null; ) { + var n = r.next, s = (r.f & Ze) !== 0 || (r.f & Q) !== 0; + zn(r, s ? t : !1), r = n; + } + var i = e.nodes && e.nodes.t; + if (i !== null) + for (const l of i) + (l.is_global || t) && l.in(); + } +} +function Cr(e, t) { + if (e.nodes) + for (var r = e.nodes.start, n = e.nodes.end; r !== null; ) { + var s = r === n ? null : /* @__PURE__ */ pe(r); + t.append(r), r = s; + } +} +let qt = !1, ke = !1; +function Wr(e) { + ke = e; +} +let w = null, se = !1; +function ee(e) { + w = e; +} +let g = null; +function he(e) { + g = e; +} +let ce = null; +function yi(e) { + w !== null && (ce ??= /* @__PURE__ */ new Set()).add(e); +} +let H = null, G = 0, J = null; +function ki(e) { + J = e; +} +let Hn = 1, Me = 0, Ie = Me; +function Jr(e) { + Ie = e; +} +function Un() { + return ++Hn; +} +function gt(e) { + var t = e.f; + if ((t & N) !== 0) + return !0; + if (t & I && (e.f &= ~Pe), (t & ie) !== 0) { + for (var r = ( + /** @type {Value[]} */ + e.deps + ), n = r.length, s = 0; s < n; s++) { + var i = r[s]; + if (gt( + /** @type {Derived} */ + i + ) && $n( + /** @type {Derived} */ + i + ), i.wv > e.wv) + return !0; + } + (t & Z) !== 0 && // During time traveling we don't want to reset the status so that + // traversal of the graph in the other batches still happens + ne === null && T(e, O); + } + return !1; +} +function Bn(e, t, r = !0) { + var n = e.reactions; + if (n !== null && !(ce !== null && ce.has(e))) + for (var s = 0; s < n.length; s++) { + var i = n[s]; + (i.f & I) !== 0 ? Bn( + /** @type {Derived} */ + i, + t, + !1 + ) : t === i && (r ? T(i, N) : (i.f & O) !== 0 && T(i, ie), qr( + /** @type {Effect} */ + i + )); + } +} +function Yn(e) { + var t = H, r = G, n = J, s = w, i = ce, l = L, a = se, o = Ie, u = e.f; + H = /** @type {null | Value[]} */ + null, G = 0, J = null, w = (u & (Q | de)) === 0 ? e : null, ce = null, Qe(e.ctx), se = !1, Ie = ++Me, e.ac !== null && (bt(() => { + e.ac.abort(pt); + }), e.ac = null); + try { + e.f |= Rt; + var c = ( + /** @type {Function} */ + e.fn + ), d = c(); + e.f |= Ve; + var v = Xr(e); + if (bn() && J !== null && !se && v !== null && (e.f & (I | ie | N)) === 0) + for (var h = 0; h < /** @type {Source[]} */ + J.length; h++) + Bn( + J[h], + /** @type {Effect} */ + e + ); + if (s !== null && s !== e) { + if (Me++, s.deps !== null) + for (let f = 0; f < r; f += 1) + s.deps[f].rv = Me; + if (t !== null) + for (const f of t) + f.rv = Me; + J !== null && (n === null ? n = J : n.push(.../** @type {Source[]} */ + J)); + } + return (e.f & Ae) !== 0 && (e.f ^= Ae), d; + } catch (f) { + return Xr(e), di(f); + } finally { + e.f ^= Rt, H = t, G = r, J = n, w = s, ce = i, Qe(l), se = a, Ie = o; + } +} +function Xr(e) { + var t = e.deps, r = b?.is_fork; + if (H !== null) { + var n; + if (r || ht(e, G), t !== null && G > 0) + for (t.length = G + H.length, n = 0; n < H.length; n++) + t[G + n] = H[n]; + else + e.deps = t = H; + if (Sr() && (e.f & Z) !== 0) + for (n = G; n < t.length; n++) + (t[n].reactions ??= []).push(e); + } else !r && t !== null && G < t.length && (ht(e, G), t.length = G); + return t; +} +function $i(e, t) { + let r = t.reactions; + if (r !== null) { + var n = xs.call(r, e); + if (n !== -1) { + var s = r.length - 1; + s === 0 ? r = t.reactions = null : (r[n] = r[s], r.pop()); + } + } + if (r === null && (t.f & I) !== 0 && // Destroying a child effect while updating a parent effect can cause a dependency to appear + // to be unused, when in fact it is used by the currently-updating parent. Checking `new_deps` + // allows us to skip the expensive work of disconnecting and immediately reconnecting it + (H === null || !St.call(H, t))) { + var i = ( + /** @type {Derived} */ + t + ); + (i.f & Z) !== 0 && (i.f ^= Z, i.f &= ~Pe), i.v !== R && wr(i), i.ac !== null && bt(() => { + i.ac.abort(pt), i.ac = null, T(i, N); + }), oi(i), ht(i, 0); + } +} +function ht(e, t) { + var r = e.deps; + if (r !== null) + for (var n = t; n < r.length; n++) + $i(e, r[n]); +} +function tt(e) { + var t = e.f; + if ((t & U) === 0) { + T(e, O); + var r = g, n = qt; + g = e, qt = (t & (Q | de)) === 0; + try { + (t & (re | on)) !== 0 ? mi(e) : Nr(e), Fn(e); + var s = Yn(e); + e.teardown = typeof s == "function" ? s : null, e.wv = Hn; + var i; + sn && Qs && (e.f & N) !== 0 && e.deps; + } finally { + qt = n, g = r; + } + } +} +async function Ei() { + await Promise.resolve(), $r(); +} +function p(e) { + var t = e.f, r = (t & I) !== 0; + if (w !== null && !se) { + var n = g !== null && (g.f & U) !== 0; + if (!n && (ce === null || !ce.has(e))) { + var s = w.deps; + if ((w.f & Rt) !== 0) + e.rv < Me && (e.rv = Me, H === null && s !== null && s[G] === e ? G++ : H === null ? H = [e] : H.push(e)); + else { + w.deps ??= [], St.call(w.deps, e) || w.deps.push(e); + var i = e.reactions; + i === null ? e.reactions = [w] : St.call(i, w) || i.push(w); + } + } + } + if (ke && fe.has(e)) + return fe.get(e); + if (r) { + var l = ( + /** @type {Derived} */ + e + ); + if (ke) { + var a = l.v; + return ((l.f & O) === 0 && l.reactions !== null || Kn(l)) && (a = kr(l)), fe.set(l, a), a; + } + var o = (l.f & Z) === 0 && !se && w !== null && (qt || (w.f & Z) !== 0), u = (l.f & Ve) === 0; + gt(l) && (o && (l.f |= Z), $n(l)), o && !u && (En(l), Gn(l)); + } + if (ne?.has(e)) + return ne.get(e); + if ((e.f & Ae) !== 0) + throw e.v; + return e.v; +} +function Gn(e) { + if (e.f |= Z, e.deps !== null) + for (const t of e.deps) + (t.reactions ??= []).push(e), (t.f & I) !== 0 && (t.f & Z) === 0 && (En( + /** @type {Derived} */ + t + ), Gn( + /** @type {Derived} */ + t + )); +} +function Kn(e) { + if (e.v === R) return !0; + if (e.deps === null) return !1; + for (const t of e.deps) + if (fe.has(t) || (t.f & I) !== 0 && Kn( + /** @type {Derived} */ + t + )) + return !0; + return !1; +} +function Ht(e) { + var t = se; + try { + return se = !0, e(); + } finally { + se = t; + } +} +const ot = /* @__PURE__ */ Symbol("events"), Wn = /* @__PURE__ */ new Set(), vr = /* @__PURE__ */ new Set(); +function xe(e, t, r) { + (t[ot] ??= {})[e] = r; +} +function qi(e) { + for (var t = 0; t < e.length; t++) + Wn.add(e[t]); + for (var r of vr) + r(e); +} +let er = null, tr = !1; +function Zr(e) { + var t = this, r = ( + /** @type {Node} */ + t.ownerDocument + ), n = e.type, s = e.composedPath?.() || [], i = ( + /** @type {null | Element} */ + s[0] || e.target + ); + er = e, tr || (tr = !0, setTimeout(() => { + tr = !1, er = null; + })); + var l = 0, a = er === e && e[ot]; + if (a) { + var o = s.indexOf(a); + if (o !== -1 && (t === document || t === /** @type {any} */ + window)) { + e[ot] = t; + return; + } + var u = s.indexOf(t); + if (u === -1) + return; + o <= u && (l = o); + } + if (i = /** @type {Element} */ + s[l] || e.target, i !== t) { + vt(e, "currentTarget", { + configurable: !0, + get() { + return i || r; + } + }); + var c = w, d = g; + ee(null), he(null); + try { + for (var v, h = []; i !== null && i !== t; ) { + try { + var f = i[ot]?.[n]; + f != null && (!/** @type {any} */ + i.disabled || // DOM could've been updated already by the time this is reached, so we check this as well + // -> the target could not have been disabled because it emits the event in the first place + e.target === i) && f.call(i, e); + } catch ($) { + v ? h.push($) : v = $; + } + if (e.cancelBubble) break; + l++, i = l < s.length ? ( + /** @type {Element} */ + s[l] + ) : null; + } + if (v) { + for (let $ of h) + queueMicrotask(() => { + throw $; + }); + throw v; + } + } finally { + e[ot] = t, delete e.currentTarget, ee(c), he(d); + } + } +} +const xi = ( + // We gotta write it like this because after downleveling the pure comment may end up in the wrong location + globalThis?.window?.trustedTypes && /* @__PURE__ */ globalThis.window.trustedTypes.createPolicy("svelte-trusted-html", { + /** @param {string} html */ + createHTML: (e) => e + }) +); +function Ti(e) { + return ( + /** @type {string} */ + xi?.createHTML(e) ?? e + ); +} +function Si(e) { + var t = xr("template"); + return t.innerHTML = Ti(e.replaceAll("", "")), t.content; +} +function xt(e, t) { + var r = ( + /** @type {Effect} */ + g + ); + r.nodes === null && (r.nodes = { start: e, end: t, a: null, t: null }); +} +// @__NO_SIDE_EFFECTS__ +function te(e, t) { + var r = (t & $s) !== 0, n = (t & Es) !== 0, s, i = !e.startsWith(""); + return () => { + if (y) + return xt(k, null), k; + s === void 0 && (s = Si(i ? e : "" + e), r || (s = /** @type {TemplateNode} */ + /* @__PURE__ */ Fe(s))); + var l = ( + /** @type {TemplateNode} */ + n || Cn ? document.importNode(s, !0) : s.cloneNode(!0) + ); + if (r) { + var a = ( + /** @type {TemplateNode} */ + /* @__PURE__ */ Fe(l) + ), o = ( + /** @type {TemplateNode} */ + l.lastChild + ); + xt(a, o); + } else + xt(l, l); + return l; + }; +} +function Y(e, t) { + if (y) { + var r = ( + /** @type {Effect & { nodes: EffectNodes }} */ + g + ); + ((r.f & Ve) === 0 || r.nodes.end === null) && (r.nodes.end = k), jt(); + return; + } + e !== null && e.before( + /** @type {Node} */ + t + ); +} +const Ai = ["touchstart", "touchmove"]; +function Ni(e) { + return Ai.includes(e); +} +function Ci(e) { + let t = 0, r = Le(0), n; + return () => { + Sr() && (p(r), zt(() => (t === 0 && (n = Ht(() => e(() => dt(r)))), t += 1, () => { + Se(() => { + t -= 1, t === 0 && (n?.(), n = void 0, dt(r)); + }); + }))); + }; +} +var Ri = Ze | je; +function Oi(e, t, r, n) { + new Mi(e, t, r, n); +} +class Mi { + /** @type {Boundary | null} */ + parent; + is_pending = !1; + /** + * API-level transformError transform function. Transforms errors before they reach the `failed` snippet. + * Inherited from parent boundary, or defaults to identity. + * @type {(error: unknown) => unknown} + */ + transform_error; + /** @type {TemplateNode} */ + #e; + /** @type {TemplateNode | null} */ + #t = y ? k : null; + /** @type {BoundaryProps} */ + #r; + /** @type {((anchor: Node) => void)} */ + #o; + /** @type {Effect} */ + #s; + /** @type {Effect | null} */ + #a = null; + /** @type {Effect | null} */ + #n = null; + /** @type {Effect | null} */ + #l = null; + /** @type {DocumentFragment | null} */ + #i = null; + #p = 0; + #u = 0; + #f = !1; + /** @type {Set} */ + #d = /* @__PURE__ */ new Set(); + /** @type {Set} */ + #_ = /* @__PURE__ */ new Set(); + /** + * A source containing the number of pending async deriveds/expressions. + * Only created if `$effect.pending()` is used inside the boundary, + * otherwise updating the source results in needless `Batch.ensure()` + * calls followed by no-op flushes + * @type {Source | null} + */ + #c = null; + #m = Ci(() => (this.#c = Le(this.#p), () => { + this.#c = null; + })); + /** + * @param {TemplateNode} node + * @param {BoundaryProps} props + * @param {((anchor: Node) => void)} children + * @param {((error: unknown) => unknown) | undefined} [transform_error] + */ + constructor(t, r, n, s) { + this.#e = t, this.#r = r, this.#o = (i) => { + var l = ( + /** @type {Effect} */ + g + ); + l.b = this, l.f |= lr, n(i); + }, this.parent = /** @type {Effect} */ + g.b, this.transform_error = s ?? this.parent?.transform_error ?? ((i) => i), this.#s = Ar(() => { + if (y) { + const i = ( + /** @type {Comment} */ + this.#t + ); + jt(); + const l = i.data === _r; + if (i.data.startsWith(Hr)) { + const o = JSON.parse(i.data.slice(Hr.length)); + this.#w(o); + } else l ? this.#k() : this.#b(); + } else + this.#v(); + }, Ri), y && (this.#e = k); + } + #b() { + try { + this.#a = X(() => this.#o(this.#e)); + } catch (t) { + this.error(t); + } + } + /** + * @param {unknown} error The deserialized error from the server's hydration comment + */ + #w(t) { + const r = this.#r.failed, { reset: n, invoke_onerror: s } = this.#y(t); + Se(s), r && (this.#l = X(() => { + r( + this.#e, + () => t, + () => n + ); + })); + } + /** + * Creates the `reset` function for a failed boundary, along with a function + * that invokes `onerror` with it (if provided) + * @param {unknown} error + * @returns {{ reset: () => void, invoke_onerror: () => void }} + */ + #y(t) { + var r = !1, n = !1; + const s = () => { + if (r) { + Fs(); + return; + } + r = !0, n && Zs(), this.#l !== null && De(this.#l, () => { + this.#l = null; + }), this.#h(() => { + this.#v(); + }); + }; + return { reset: s, invoke_onerror: () => { + try { + n = !0, this.#r.onerror?.(t, s), n = !1; + } catch (l) { + ue(l, this.#s && this.#s.parent); + } + } }; + } + #k() { + const t = this.#r.pending; + t && (this.is_pending = !0, this.#n = X(() => t(this.#e)), Se(() => { + var r = this.#i = document.createDocumentFragment(), n = ve(), s = !1; + if (r.append(n), this.#a = this.#h(() => { + try { + return X(() => this.#o(n)); + } catch (i) { + try { + this.error(i), s = !0; + } catch (l) { + ue(l, this.#s.parent); + } + return null; + } + }), this.#a === null) { + this.#i = null, s && this.#g( + /** @type {Batch} */ + b + ); + return; + } + this.#u === 0 && (this.#e.before(r), this.#i = null, De( + /** @type {Effect} */ + this.#n, + () => { + this.#n = null; + } + ), this.#g( + /** @type {Batch} */ + b + )); + })); + } + #v() { + try { + if (this.is_pending = this.has_pending_snippet(), this.#u = 0, this.#p = 0, this.#a = X(() => { + this.#o(this.#e); + }), this.#u > 0) { + var t = this.#i = document.createDocumentFragment(); + Cr(this.#a, t); + const r = ( + /** @type {(anchor: Node) => void} */ + this.#r.pending + ); + this.#n = X(() => r(this.#e)); + } else + this.#g( + /** @type {Batch} */ + b + ); + } catch (r) { + this.error(r); + } + } + /** + * @param {Batch} batch + */ + #g(t) { + this.is_pending = !1, t.transfer_effects(this.#d, this.#_); + } + /** + * Defer an effect inside a pending boundary until the boundary resolves + * @param {Effect} effect + */ + defer_effect(t) { + wn(t, this.#d, this.#_); + } + /** + * Returns `false` if the effect exists inside a boundary whose pending snippet is shown + * @returns {boolean} + */ + is_rendered() { + return !this.is_pending && (!this.parent || this.parent.is_rendered()); + } + has_pending_snippet() { + return !!this.#r.pending; + } + /** + * @template T + * @param {() => T} fn + */ + #h(t) { + var r = g, n = w, s = L; + he(this.#s), ee(this.#s), Qe(this.#s.ctx); + try { + return ye.ensure(), t(); + } finally { + he(r), ee(n), Qe(s); + } + } + /** + * Updates the pending count associated with the currently visible pending snippet, + * if any, such that we can replace the snippet with content once work is done + * @param {1 | -1} d + * @param {Batch} batch + */ + #$(t, r) { + if (!this.has_pending_snippet()) { + this.parent && this.parent.#$(t, r); + return; + } + this.#u += t, this.#u === 0 && (this.#g(r), this.#n && De(this.#n, () => { + this.#n = null; + }), this.#i && (this.#e.before(this.#i), this.#i = null)); + } + /** + * Update the source that powers `$effect.pending()` inside this boundary, + * and controls when the current `pending` snippet (if any) is removed. + * Do not call from inside the class + * @param {1 | -1} d + * @param {Batch} batch + */ + update_pending_count(t, r) { + this.#$(t, r), this.#p += t, !(!this.#c || this.#f) && (this.#f = !0, Se(() => { + this.#f = !1, this.#c && et(this.#c, this.#p); + })); + } + get_effect_pending() { + return this.#m(), p( + /** @type {Source} */ + this.#c + ); + } + /** @param {unknown} error */ + error(t) { + if (!this.#r.onerror && !this.#r.failed) + throw t; + b?.is_fork ? (this.#a && b.skip_effect(this.#a), this.#n && b.skip_effect(this.#n), this.#l && b.skip_effect(this.#l), b.oncommit(() => { + this.#E(t); + })) : this.#E(t); + } + /** + * @param {unknown} error + */ + #E(t) { + this.#a && (z(this.#a), this.#a = null), this.#n && (z(this.#n), this.#n = null), this.#l && (z(this.#l), this.#l = null), y && (K( + /** @type {TemplateNode} */ + this.#t + ), cn(), K(Ot())); + let r = this.#r.failed; + const n = (s) => { + const { reset: i, invoke_onerror: l } = this.#y(s); + l(), r && (this.#l = this.#h(() => { + try { + return X(() => { + var a = ( + /** @type {Effect} */ + g + ); + a.b = this, a.f |= lr, r( + this.#e, + () => s, + () => i + ); + }); + } catch (a) { + return ue( + a, + /** @type {Effect} */ + this.#s.parent + ), null; + } + })); + }; + Se(() => { + var s; + try { + s = this.transform_error(t); + } catch (i) { + ue(i, this.#s && this.#s.parent); + return; + } + s !== null && typeof s == "object" && typeof /** @type {any} */ + s.then == "function" ? s.then( + n, + /** @param {unknown} e */ + (i) => ue(i, this.#s && this.#s.parent) + ) : n(s); + }); + } +} +function Re(e, t) { + var r = t == null ? "" : typeof t == "object" ? `${t}` : t; + r !== /** @type {any} */ + (e[ur] ??= e.nodeValue) && (e[ur] = r, e.nodeValue = `${r}`); +} +function Jn(e, t) { + return Xn(e, t); +} +function Di(e, t) { + dr(), t.intro = t.intro ?? !1; + const r = t.target, n = y, s = k; + try { + for (var i = /* @__PURE__ */ Fe(r); i && (i.nodeType !== _t || /** @type {Comment} */ + i.data !== nn); ) + i = /* @__PURE__ */ pe(i); + if (!i) + throw Je; + we(!0), K( + /** @type {Comment} */ + i + ); + const l = Xn(e, { ...t, anchor: i }); + return we(!1), /** @type {Exports} */ + l; + } catch (l) { + if (l instanceof Error && l.message.split(` +`).some((a) => a.startsWith("https://svelte.dev/e/"))) + throw l; + return l !== Je && console.warn("Failed to hydrate: ", l), t.recover === !1 && Ks(), dr(), Mn(r), we(!1), Jn(e, t); + } finally { + we(n), K(s); + } +} +const kt = /* @__PURE__ */ new Map(); +function Xn(e, { target: t, anchor: r, props: n = {}, events: s, context: i, intro: l = !0, transformError: a }) { + dr(); + var o = void 0, u = bi(() => { + var c = r ?? t.appendChild(ve()); + Oi( + /** @type {TemplateNode} */ + c, + { + pending: () => { + } + }, + (h) => { + pn({}); + var f = ( + /** @type {ComponentContext} */ + L + ); + if (i && (f.c = i), s && (n.$$events = s), y && xt( + /** @type {TemplateNode} */ + h, + null + ), o = e(h, n) || mr(), y && (g.nodes.end = k, k === null || k.nodeType !== _t || /** @type {Comment} */ + k.data !== br)) + throw Vt(), Je; + _n(); + }, + a + ); + var d = /* @__PURE__ */ new Set(), v = (h) => { + for (var f = 0; f < h.length; f++) { + var $ = h[f]; + if (!d.has($)) { + d.add($); + var C = Ni($); + for (const ae of [t, document]) { + var m = kt.get(ae); + m === void 0 && (m = /* @__PURE__ */ new Map(), kt.set(ae, m)); + var S = m.get($); + S === void 0 ? (ae.addEventListener($, Zr, { passive: C }), m.set($, 1)) : m.set($, S + 1); + } + } + } + }; + return v(Pt(Wn)), vr.add(v), () => { + for (var h of d) + for (const C of [t, document]) { + var f = ( + /** @type {Map} */ + kt.get(C) + ), $ = ( + /** @type {number} */ + f.get(h) + ); + --$ == 0 ? (C.removeEventListener(h, Zr), f.delete(h), f.size === 0 && kt.delete(C)) : f.set(h, $); + } + vr.delete(v), c !== r && c.parentNode?.removeChild(c); + }; + }); + return hr.set(o, u), o; +} +let hr = /* @__PURE__ */ new WeakMap(); +function Ii(e, t) { + const r = hr.get(e); + return r ? (hr.delete(e), r(t)) : Promise.resolve(); +} +class Pi { + /** @type {TemplateNode} */ + anchor; + /** @type {Map} */ + #e = /* @__PURE__ */ new Map(); + /** + * Map of keys to effects that are currently rendered in the DOM. + * These effects are visible and actively part of the document tree. + * Example: + * ``` + * {#if condition} + * foo + * {:else} + * bar + * {/if} + * ``` + * Can result in the entries `true->Effect` and `false->Effect` + * @type {Map} + */ + #t = /* @__PURE__ */ new Map(); + /** + * Similar to #onscreen with respect to the keys, but contains branches that are not yet + * in the DOM, because their insertion is deferred. + * @type {Map} + */ + #r = /* @__PURE__ */ new Map(); + /** + * Keys of effects that are currently outroing + * @type {Set} + */ + #o = /* @__PURE__ */ new Set(); + /** + * Whether to pause (i.e. outro) on change, or destroy immediately. + * This is necessary for `` + */ + #s = !0; + /** + * @param {TemplateNode} anchor + * @param {boolean} transition + */ + constructor(t, r = !0) { + this.anchor = t, this.#s = r; + } + /** + * @param {Batch} batch + */ + #a = (t) => { + if (this.#e.has(t)) { + var r = ( + /** @type {Key} */ + this.#e.get(t) + ), n = this.#t.get(r); + if (n) + It(n), this.#o.delete(r); + else { + var s = this.#r.get(r); + s && (It(s.effect), this.#t.set(r, s.effect), this.#r.delete(r), s.fragment.lastChild.remove(), this.anchor.before(s.fragment), n = s.effect); + } + for (const [i, l] of this.#e) { + if (this.#e.delete(i), i === t) + break; + const a = this.#r.get(l); + a && (z(a.effect), this.#r.delete(l)); + } + for (const [i, l] of this.#t) { + if (i === r || this.#o.has(i)) continue; + const a = () => { + if (Array.from(this.#e.values()).includes(i)) { + var u = document.createDocumentFragment(); + Cr(l, u), u.append(ve()), this.#r.set(i, { effect: l, fragment: u }); + } else + z(l); + this.#o.delete(i), this.#t.delete(i); + }; + this.#s || !n ? (this.#o.add(i), De(l, a, !1)) : a(); + } + } + }; + /** + * @param {Batch} batch + */ + #n = (t) => { + this.#e.delete(t); + const r = Array.from(this.#e.values()); + for (const [n, s] of this.#r) + r.includes(n) || (z(s.effect), this.#r.delete(n)); + }; + /** + * + * @param {any} key + * @param {null | ((target: TemplateNode) => void)} fn + */ + ensure(t, r) { + var n = ( + /** @type {Batch} */ + b + ), s = Dn(); + if (r && !this.#t.has(t) && !this.#r.has(t)) + if (s) { + var i = document.createDocumentFragment(), l = ve(); + i.append(l), this.#r.set(t, { + effect: X(() => r(l)), + fragment: i + }); + } else + this.#t.set( + t, + X(() => r(this.anchor)) + ); + if (this.#e.set(n, t), s) { + for (const [a, o] of this.#t) + a === t ? n.unskip_effect(o) : n.skip_effect(o); + for (const [a, o] of this.#r) + a === t ? n.unskip_effect(o.effect) : n.skip_effect(o.effect); + n.oncommit(this.#a), n.ondiscard(this.#n); + } else + y && (this.anchor = k), this.#a(n); + } +} +function Li(e) { + L === null && js(), In(() => { + const t = Ht(e); + if (typeof t == "function") return ( + /** @type {() => void} */ + t + ); + }); +} +function Ye(e, t, r = !1) { + var n; + y && (n = k, jt()); + var s = new Pi(e), i = r ? Ze : 0; + function l(a, o) { + if (y) { + var u = dn( + /** @type {TemplateNode} */ + n + ); + if (a !== parseInt(u.substring(1))) { + var c = Ot(); + K(c), s.anchor = c, we(!1), s.ensure(a, o), we(!0); + return; + } + } + s.ensure(a, o); + } + Ar(() => { + var a = !1; + t((o, u = 0) => { + a = !0, l(u, o); + }), a || l(-1, null); + }, i); +} +function Fi(e, t, r) { + for (var n = [], s = t.length, i, l = t.length, a = 0; a < s; a++) { + let d = t[a]; + De( + d, + () => { + if (i) { + if (i.pending.delete(d), i.done.add(d), i.pending.size === 0) { + var v = ( + /** @type {Set} */ + e.outrogroups + ); + pr(e, Pt(i.done)), v.delete(i), v.size === 0 && (e.outrogroups = null); + } + } else + l -= 1; + }, + !1 + ); + } + if (l === 0) { + var o = n.length === 0 && r !== null && e.pending.size === 0; + if (o) { + var u = ( + /** @type {Element} */ + r + ), c = ( + /** @type {Element} */ + u.parentNode + ); + Mn(c), c.append(u), e.items.clear(); + } + pr(e, t, !o); + } else + i = { + pending: new Set(t), + done: /* @__PURE__ */ new Set() + }, (e.outrogroups ??= /* @__PURE__ */ new Set()).add(i); +} +function pr(e, t, r = !0) { + var n; + if (e.pending.size > 0) { + n = /* @__PURE__ */ new Set(); + for (const l of e.pending.values()) + for (const a of l) + n.add( + /** @type {EachItem} */ + e.items.get(a).e + ); + } + for (var s = 0; s < t.length; s++) { + var i = t[s]; + if (n?.has(i)) { + i.f |= me; + const l = document.createDocumentFragment(); + Cr(i, l); + } else + z(t[s], r); + } +} +var Qr; +function Vi(e, t, r, n, s, i = null) { + var l = e, a = /* @__PURE__ */ new Map(); + y && jt(); + var o = null, u = /* @__PURE__ */ ai(() => { + var m = r(); + return ( + /** @type {V[]} */ + an(m) ? m : m == null ? [] : Pt(m) + ); + }), c, d = /* @__PURE__ */ new Map(), v = !0; + function h(m) { + (C.effect.f & U) === 0 && (C.pending.delete(m), C.fallback = o, ji(C, c, l, t, n), o !== null && (c.length === 0 ? (o.f & me) === 0 ? It(o) : (o.f ^= me, ut(o, null, l)) : De(o, () => { + o = null; + }))); + } + function f(m) { + C.pending.delete(m); + } + var $ = Ar(() => { + c = /** @type {V[]} */ + p(u); + var m = c.length; + let S = !1; + if (y) { + var ae = dn(l) === _r; + ae !== (m === 0) && (l = Ot(), K(l), we(!1), S = !0); + } + for (var le = /* @__PURE__ */ new Set(), F = ( + /** @type {Batch} */ + b + ), $e = Dn(), oe = 0; oe < m; oe += 1) { + y && k.nodeType === _t && /** @type {Comment} */ + k.data === br && (l = /** @type {Comment} */ + k, S = !0, we(!1)); + var V = c[oe], Ee = n(V, oe), B = v ? null : a.get(Ee); + B ? (B.v && et(B.v, V), B.i && et(B.i, oe), $e && F.unskip_effect(B.e)) : (B = zi( + a, + v ? l : Qr ??= ve(), + V, + Ee, + oe, + s, + t, + r + ), v || (B.e.f |= me), a.set(Ee, B)), le.add(Ee); + } + if (m === 0 && i && !o && (v ? o = X(() => i(l)) : (o = X(() => i(Qr ??= ve())), o.f |= me)), m > le.size && Hs(), y && m > 0 && K(Ot()), !v) + if (d.set(F, le), $e) { + for (const [mt, wt] of a) + le.has(mt) || F.skip_effect(wt.e); + F.oncommit(h), F.ondiscard(f); + } else + h(F); + S && we(!0), p(u); + }), C = { effect: $, items: a, pending: d, outrogroups: null, fallback: o }; + v = !1, y && (l = k); +} +function it(e) { + for (; e !== null && (e.f & Q) === 0; ) + e = e.next; + return e; +} +function ji(e, t, r, n, s) { + var i = t.length, l = e.items, a = it(e.effect.first), o, u = null, c = [], d = [], v, h, f, $; + for ($ = 0; $ < i; $ += 1) { + if (v = t[$], h = s(v, $), f = /** @type {EachItem} */ + l.get(h).e, e.outrogroups !== null) + for (const V of e.outrogroups) + V.pending.delete(f), V.done.delete(f); + if ((f.f & j) !== 0 && It(f), (f.f & me) !== 0) + if (f.f ^= me, f === a) + ut(f, null, r); + else { + var C = u ? u.next : a; + f === e.effect.last && (e.effect.last = f.prev), f.prev && (f.prev.next = f.next), f.next && (f.next.prev = f.prev), Te(e, u, f), Te(e, f, C), ut(f, C, r), u = f, c = [], d = [], a = it(u.next); + continue; + } + if (f !== a) { + if (o !== void 0 && o.has(f)) { + if (c.length < d.length) { + var m = d[0], S; + u = m.prev; + var ae = c[0], le = c[c.length - 1]; + for (S = 0; S < c.length; S += 1) + ut(c[S], m, r); + for (S = 0; S < d.length; S += 1) + o.delete(d[S]); + Te(e, ae.prev, le.next), Te(e, u, ae), Te(e, le, m), a = m, u = le, $ -= 1, c = [], d = []; + } else + o.delete(f), ut(f, a, r), Te(e, f.prev, f.next), Te(e, f, u === null ? e.effect.first : u.next), Te(e, u, f), u = f; + continue; + } + for (c = [], d = []; a !== null && a !== f; ) + (o ??= /* @__PURE__ */ new Set()).add(a), d.push(a), a = it(a.next); + if (a === null) + continue; + } + (f.f & me) === 0 && c.push(f), u = f, a = it(f.next); + } + if (e.outrogroups !== null) { + for (const V of e.outrogroups) + V.pending.size === 0 && (pr(e, Pt(V.done)), e.outrogroups?.delete(V)); + e.outrogroups.size === 0 && (e.outrogroups = null); + } + if (a !== null || o !== void 0) { + var F = []; + if (o !== void 0) + for (f of o) + (f.f & j) === 0 && F.push(f); + for (; a !== null; ) + (a.f & j) === 0 && a !== e.fallback && F.push(a), a = it(a.next); + var $e = F.length; + if ($e > 0) { + var oe = null; + Fi(e, F, oe); + } + } +} +function zi(e, t, r, n, s, i, l, a) { + var o = (l & ws) !== 0 ? (l & ks) === 0 ? /* @__PURE__ */ An(r, !1, !1) : Le(r) : null, u = (l & ys) !== 0 ? Le(s) : null; + return { + v: o, + i: u, + e: X(() => (i(t, o ?? r, u ?? s, a), () => { + e.delete(n); + })) + }; +} +function ut(e, t, r) { + if (e.nodes) + for (var n = e.nodes.start, s = e.nodes.end, i = t && (t.f & me) === 0 ? ( + /** @type {EffectNodes} */ + t.nodes.start + ) : r; n !== null; ) { + var l = ( + /** @type {TemplateNode} */ + /* @__PURE__ */ pe(n) + ); + if (i.before(n), n === s) + return; + n = l; + } +} +function Te(e, t, r) { + t === null ? e.effect.first = r : t.next = r, r === null ? e.effect.last = t : r.prev = t; +} +function Hi(e, t) { + Ln(() => { + e = g?.parent?.nodes?.start ?? e; + var r = e.getRootNode(), n = ( + /** @type {ShadowRoot} */ + r.host ? ( + /** @type {ShadowRoot} */ + r + ) : ( + /** @type {Document} */ + r.head ?? /** @type {Document} */ + r.ownerDocument.head + ) + ); + if (!n.querySelector("#" + t.hash)) { + const s = xr("style"); + s.id = t.hash, s.textContent = t.code, n.appendChild(s); + } + }); +} +const en = [...` +\r\f \v\uFEFF`]; +function Ui(e, t, r) { + var n = e == null ? "" : "" + e; + if (r) { + for (var s of Object.keys(r)) + if (r[s]) + n = n ? n + " " + s : s; + else if (n.length) + for (var i = s.length, l = 0; (l = n.indexOf(s, l)) >= 0; ) { + var a = l + i; + (l === 0 || en.includes(n[l - 1])) && (a === n.length || en.includes(n[a])) ? n = (l === 0 ? "" : n.substring(0, l)) + n.substring(a + 1) : l = a; + } + } + return n === "" ? null : n; +} +function tn(e, t, r, n, s, i) { + var l = ( + /** @type {any} */ + e[or] + ); + if (y || l !== r || l === void 0) { + var a = Ui(r, n, i); + (!y || a !== e.getAttribute("class")) && (a == null ? e.removeAttribute("class") : e.className = a), e[or] = r; + } else if (i && s !== i) + for (var o in i) { + var u = !!i[o]; + (s == null || u !== !!s[o]) && e.classList.toggle(o, u); + } + return i; +} +const Bi = /* @__PURE__ */ Symbol("is custom element"), Yi = /* @__PURE__ */ Symbol("is html"), Gi = Ps ? "link" : "LINK"; +function rr(e) { + if (y) { + var t = !1, r = () => { + if (!t) { + if (t = !0, e.hasAttribute("value")) { + var n = e.value; + rn(e, "value", null), e.value = n; + } + if (e.hasAttribute("checked")) { + var s = e.checked; + rn(e, "checked", null), e.checked = s; + } + } + }; + e[ft] = r, Se(r), yn(); + } +} +function rn(e, t, r, n) { + var s = Ki(e); + y && (s[t] = e.getAttribute(t), t === "src" || t === "srcset" || t === "href" && e.nodeName === Gi) || s[t] !== (s[t] = r) && (t === "loading" && (e[Ds] = r), e.removeAttribute(t)); +} +function Ki(e) { + return ( + /** @type {Record} **/ + /** @type {any} */ + e[fn] ??= { + [Bi]: e.nodeName.includes("-"), + [Yi]: e.namespaceURI === qs + } + ); +} +function nr(e, t, r = t) { + var n = /* @__PURE__ */ new WeakSet(); + ri(e, "input", async (s) => { + var i = s ? e.defaultValue : e.value; + if (i = sr(e) ? ir(i) : i, r(i), b !== null && n.add(b), await Ei(), i !== (i = t())) { + var l = e.selectionStart, a = e.selectionEnd, o = e.value.length; + if (e.value = i ?? "", a !== null) { + var u = e.value.length; + l === a && a === o && u > o ? (e.selectionStart = u, e.selectionEnd = u) : (e.selectionStart = l, e.selectionEnd = Math.min(a, u)); + } + } + }), // If we are hydrating and the value has since changed, + // then use the updated value from the input instead. + (y && e.defaultValue !== e.value || // If defaultValue is set, then value == defaultValue + // TODO Svelte 6: remove input.value check and set to empty string? + Ht(t) == null && e.value) && (r(sr(e) ? ir(e.value) : e.value), b !== null && n.add(b)), zt(() => { + var s = t(); + if (e === document.activeElement) { + var i = ( + /** @type {Batch} */ + b + ); + if (n.has(i)) + return; + } + sr(e) && s === ir(e.value) || e.type === "date" && !s && !e.value || s !== e.value && (e.value = s ?? ""); + }); +} +function sr(e) { + var t = e.type; + return t === "number" || t === "range"; +} +function ir(e) { + return e === "" ? null : +e; +} +function ar(e, t) { + return e === t || e?.[$t] === t; +} +function Wi(e = mr(), t, r, n) { + var s = ( + /** @type {ComponentContext} */ + L.r + ), i = ( + /** @type {Effect} */ + g + ); + return Ln(() => { + var l, a; + return zt(() => { + l = a, a = [], Ht(() => { + ar(r(...a), e) || (t(e, ...a), l && ar(r(...l), e) && t(null, ...l)); + }); + }), () => { + let o = i; + for (; o !== s && o.parent !== null && o.parent.f & Nt; ) + o = o.parent; + const u = () => { + a && ar(r(...a), e) && t(null, ...a); + }, c = o.teardown; + o.teardown = () => { + u(), c?.(); + }; + }; + }), e; +} +function Ji(e, t, r, n) { + var s = ( + /** @type {V} */ + n + ), i = !0, l = () => (i && (i = !1, s = /** @type {V} */ + n), s), a; + a = /** @type {V} */ + e[t], a === void 0 && n !== void 0 && (a = l()); + var o; + o = () => { + var v = ( + /** @type {V} */ + e[t] + ); + return v === void 0 ? l() : (i = !0, v); + }; + var u = !1, c = /* @__PURE__ */ yr(() => (u = !1, o())), d = ( + /** @type {Effect} */ + g + ); + return ( + /** @type {() => V} */ + (function(v, h) { + if (arguments.length > 0) { + const f = h ? p(c) : v; + return q(c, f), u = !0, s !== void 0 && (s = f), v; + } + return ke && u || (d.f & U) !== 0 ? c.v : p(c); + }) + ); +} +function Xi(e) { + return new Zi(e); +} +class Zi { + /** @type {any} */ + #e; + /** @type {Record} */ + #t; + /** + * @param {ComponentConstructorOptions & { + * component: any; + * }} options + */ + constructor(t) { + var r = /* @__PURE__ */ new Map(), n = (i, l) => { + var a = /* @__PURE__ */ An(l, !1, !1); + return r.set(i, a), a; + }; + const s = new Proxy( + { ...t.props || {}, $$events: {} }, + { + get(i, l) { + return p(r.get(l) ?? n(l, Reflect.get(i, l))); + }, + has(i, l) { + return l === Ms ? !0 : (p(r.get(l) ?? n(l, Reflect.get(i, l))), Reflect.has(i, l)); + }, + set(i, l, a) { + return q(r.get(l) ?? n(l, a), a), Reflect.set(i, l, a); + } + } + ); + this.#t = (t.hydrate ? Di : Jn)(t.component, { + target: t.target, + anchor: t.anchor, + props: s, + context: t.context, + intro: t.intro ?? !1, + recover: t.recover, + transformError: t.transformError + }), (!t?.props?.$$host || t.sync === !1) && $r(), this.#e = s.$$events; + for (const i of Object.keys(this.#t)) + i === "$set" || i === "$destroy" || i === "$on" || vt(this, i, { + get() { + return this.#t[i]; + }, + /** @param {any} value */ + set(l) { + this.#t[i] = l; + }, + enumerable: !0 + }); + this.#t.$set = /** @param {Record} next */ + (i) => { + Object.assign(s, i); + }, this.#t.$destroy = () => { + Ii(this.#t); + }; + } + /** @param {Record} props */ + $set(t) { + this.#t.$set(t); + } + /** + * @param {string} event + * @param {(...args: any[]) => any} callback + * @returns {any} + */ + $on(t, r) { + this.#e[t] = this.#e[t] || []; + const n = (...s) => r.call(this, ...s); + return this.#e[t].push(n), () => { + this.#e[t] = this.#e[t].filter( + /** @param {any} fn */ + (s) => s !== n + ); + }; + } + $destroy() { + this.#t.$destroy(); + } +} +let Zn; +typeof HTMLElement == "function" && (Zn = class extends HTMLElement { + /** The Svelte component constructor */ + $$ctor; + /** Slots */ + $$s; + /** @type {any} The Svelte component instance */ + $$c; + /** Whether or not the custom element is connected */ + $$cn = !1; + /** @type {Record} Component props data */ + $$d = {}; + /** `true` if currently in the process of reflecting component props back to attributes */ + $$r = !1; + /** @type {Record} Props definition (name, reflected, type etc) */ + $$p_d = {}; + /** @type {Record} Event listeners */ + $$l = {}; + /** @type {Map} Event listener unsubscribe functions */ + $$l_u = /* @__PURE__ */ new Map(); + /** @type {any} The managed render effect for reflecting attributes */ + $$me; + /** @type {ShadowRoot | null} The ShadowRoot of the custom element */ + $$shadowRoot = null; + /** + * @param {*} $$componentCtor + * @param {*} $$slots + * @param {ShadowRootInit | undefined} shadow_root_init + */ + constructor(e, t, r) { + super(), this.$$ctor = e, this.$$s = t, r && (this.$$shadowRoot = this.attachShadow(r)); + } + /** + * @param {string} type + * @param {EventListenerOrEventListenerObject} listener + * @param {boolean | AddEventListenerOptions} [options] + */ + addEventListener(e, t, r) { + if (this.$$l[e] = this.$$l[e] || [], this.$$l[e].push(t), this.$$c) { + const n = this.$$c.$on(e, t); + this.$$l_u.set(t, n); + } + super.addEventListener(e, t, r); + } + /** + * @param {string} type + * @param {EventListenerOrEventListenerObject} listener + * @param {boolean | AddEventListenerOptions} [options] + */ + removeEventListener(e, t, r) { + if (super.removeEventListener(e, t, r), this.$$c) { + const n = this.$$l_u.get(t); + n && (n(), this.$$l_u.delete(t)); + } + } + async connectedCallback() { + if (this.$$cn = !0, !this.$$c) { + let t = function(s) { + return (i) => { + const l = xr("slot"); + s !== "default" && (l.name = s), Y(i, l); + }; + }; + var e = t; + if (await Promise.resolve(), !this.$$cn || this.$$c) + return; + const r = {}, n = Qi(this); + for (const s of this.$$s) + s in n && (s === "default" && !this.$$d.children ? (this.$$d.children = t(s), r.default = !0) : r[s] = t(s)); + for (const s of this.attributes) { + const i = this.$$g_p(s.name); + i in this.$$d || (this.$$d[i] = Tt(i, s.value, this.$$p_d, "toProp")); + } + for (const s in this.$$p_d) + !(s in this.$$d) && this[s] !== void 0 && (this.$$d[s] = this[s], delete this[s]); + this.$$c = Xi({ + component: this.$$ctor, + target: this.$$shadowRoot || this, + props: { + ...this.$$d, + $$slots: r, + $$host: this + } + }), this.$$me = _i(() => { + zt(() => { + this.$$r = !0; + for (const s of At(this.$$c)) { + if (!this.$$p_d[s]?.reflect) continue; + this.$$d[s] = this.$$c[s]; + const i = Tt( + s, + this.$$d[s], + this.$$p_d, + "toAttribute" + ); + i == null ? this.removeAttribute(this.$$p_d[s].attribute || s) : this.setAttribute(this.$$p_d[s].attribute || s, i); + } + this.$$r = !1; + }); + }); + for (const s in this.$$l) + for (const i of this.$$l[s]) { + const l = this.$$c.$on(s, i); + this.$$l_u.set(i, l); + } + this.$$l = {}; + } + } + // We don't need this when working within Svelte code, but for compatibility of people using this outside of Svelte + // and setting attributes through setAttribute etc, this is helpful + /** + * @param {string} attr + * @param {string} _oldValue + * @param {string} newValue + */ + attributeChangedCallback(e, t, r) { + this.$$r || (e = this.$$g_p(e), this.$$d[e] = Tt(e, r, this.$$p_d, "toProp"), this.$$c?.$set({ [e]: this.$$d[e] })); + } + disconnectedCallback() { + this.$$cn = !1, Promise.resolve().then(() => { + !this.$$cn && this.$$c && (this.$$c.$destroy(), this.$$me(), this.$$c = void 0); + }); + } + /** + * @param {string} attribute_name + */ + $$g_p(e) { + return At(this.$$p_d).find( + (t) => this.$$p_d[t].attribute === e || !this.$$p_d[t].attribute && t.toLowerCase() === e + ) || e; + } +}); +function Tt(e, t, r, n) { + const s = r[e]?.type; + if (t = s === "Boolean" && typeof t != "boolean" ? t != null : t, !n || !r[e]) + return t; + if (n === "toAttribute") + switch (s) { + case "Object": + case "Array": + return t == null ? null : JSON.stringify(t); + case "Boolean": + return t ? "" : null; + case "Number": + return t ?? null; + default: + return t; + } + else + switch (s) { + case "Object": + case "Array": + return t && JSON.parse(t); + case "Boolean": + return t; + // conversion already handled above + case "Number": + return t != null ? +t : t; + default: + return t; + } +} +function Qi(e) { + const t = {}; + return e.childNodes.forEach((r) => { + t[ + /** @type {Element} node */ + r.slot || "default" + ] = !0; + }), t; +} +function ea(e, t, r, n, s, i) { + let l = class extends Zn { + constructor() { + super(e, r, s), this.$$p_d = t; + } + static get observedAttributes() { + return At(t).map( + (a) => (t[a].attribute || a).toLowerCase() + ); + } + }; + return At(t).forEach((a) => { + vt(l.prototype, a, { + get() { + return this.$$c && a in this.$$c ? this.$$c[a] : this.$$d[a]; + }, + set(o) { + o = Tt(a, o, t), this.$$d[a] = o; + var u = this.$$c; + if (u) { + var c = Ke(u, a)?.get; + c ? u[a] = o : u.$set({ [a]: o }); + } + } + }); + }), n.forEach((a) => { + vt(l.prototype, a, { + get() { + return this.$$c?.[a]; + } + }); + }), e.element = /** @type {any} */ + l, l; +} +function ta(e, t) { + e.dispatchEvent(new CustomEvent("barkstack:navigate", { + bubbles: !0, + composed: !0, + detail: { path: t } + })); +} +function ra(e, t, r) { + e.dispatchEvent(new CustomEvent("barkstack:notify", { + bubbles: !0, + composed: !0, + detail: { level: t, message: r } + })); +} +function na(e, t, r) { + e.dispatchEvent(new CustomEvent("barkstack:confirm", { + bubbles: !0, + composed: !0, + detail: { message: t, resolve: r } + })); +} +var sa = /* @__PURE__ */ te(''), ia = /* @__PURE__ */ te('
'), aa = /* @__PURE__ */ te(''), la = /* @__PURE__ */ te(''), oa = /* @__PURE__ */ te(''), ua = /* @__PURE__ */ te('
No secrets are stored yet.
'), fa = /* @__PURE__ */ te(' ', 1), ca = /* @__PURE__ */ te(' ', 1), da = /* @__PURE__ */ te('
'), va = /* @__PURE__ */ te('
NameSyncConsumersActions
'), ha = /* @__PURE__ */ te('
Add secret

New namespaced secret

Values are sent to TreatVault and never displayed again. The encrypted file is the source of truth.

Vault contents

', 1), pa = /* @__PURE__ */ te('
Secret management

TreatVault

Namespaced secrets, encrypted at rest, synced to Docker Swarm.

Sync state
'); +const _a = { + hash: "svelte-1q1dueb", + code: `:host {display:block;color:var(--bark-text, #edf3fb);font-family:var(--bark-font-sans, system-ui, sans-serif);}.svelte-1q1dueb {box-sizing:border-box;}section.svelte-1q1dueb {display:grid;gap:var(--bark-space-5, 1.5rem);}.intro.svelte-1q1dueb {display:flex;justify-content:space-between;align-items:flex-start;gap:var(--bark-space-5, 1.5rem);}.eyebrow.svelte-1q1dueb {color:var(--bark-primary, #6ee7b7);font-size:.69rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;}h2.svelte-1q1dueb {margin:.55rem 0 .35rem;font-size:2.25rem;letter-spacing:-.05em;}h3.svelte-1q1dueb {margin:.4rem 0 0;font-size:1.15rem;}p.svelte-1q1dueb {margin:0;color:var(--bark-text-muted, #91a1b7);line-height:1.6;}.service-status.svelte-1q1dueb {display:flex;align-items:center;gap:.65rem;padding:.65rem .9rem;border:1px solid var(--bark-border, #27354a);border-radius:var(--bark-radius-md, .75rem);background:var(--bark-surface, #101827);}.service-status.svelte-1q1dueb > span:where(.svelte-1q1dueb) {width:.55rem;height:.55rem;border-radius:50%;background:var(--bark-success, #4ade80);box-shadow:0 0 0 5px color-mix(in srgb, var(--bark-success, #4ade80) 12%, transparent);}.service-status.offline.svelte-1q1dueb > span:where(.svelte-1q1dueb) {background:var(--bark-danger, #fb7185);}.service-status.svelte-1q1dueb small:where(.svelte-1q1dueb), .service-status.svelte-1q1dueb strong:where(.svelte-1q1dueb) {display:block;}.service-status.svelte-1q1dueb small:where(.svelte-1q1dueb) {color:var(--bark-text-muted, #91a1b7);font-size:.66rem;text-transform:uppercase;letter-spacing:.08em;}.back.svelte-1q1dueb {justify-self:start;border:0;background:none;color:var(--bark-text-muted, #91a1b7);cursor:pointer;padding:0;}.back.svelte-1q1dueb:hover {color:var(--bark-text, #edf3fb);}.panel.svelte-1q1dueb {border:1px solid var(--bark-border, #27354a);border-radius:var(--bark-radius-lg, 1rem);background:var(--bark-surface, #101827);overflow:hidden;}.panel-heading.svelte-1q1dueb {display:flex;align-items:center;justify-content:space-between;padding:var(--bark-space-5, 1.5rem);border-bottom:1px solid var(--bark-border, #27354a);}.panel-heading.svelte-1q1dueb button:where(.svelte-1q1dueb) {border:0;background:transparent;color:var(--bark-primary, #6ee7b7);cursor:pointer;}.form.svelte-1q1dueb {display:flex;gap:1rem;align-items:end;padding:var(--bark-space-5, 1.5rem);flex-wrap:wrap;}.form.svelte-1q1dueb label:where(.svelte-1q1dueb) {display:grid;gap:.4rem;font-size:.72rem;color:var(--bark-text-muted, #91a1b7);text-transform:uppercase;letter-spacing:.07em;}.form.svelte-1q1dueb input:where(.svelte-1q1dueb) {min-width:14rem;padding:.55rem .7rem;border:1px solid var(--bark-border, #27354a);border-radius:var(--bark-radius-sm, .45rem);background:var(--bark-surface-raised, #172235);color:var(--bark-text, #edf3fb);font-family:inherit;}.form.svelte-1q1dueb button:where(.svelte-1q1dueb) {padding:.55rem .9rem;border:1px solid var(--bark-primary, #6ee7b7);border-radius:var(--bark-radius-sm, .45rem);background:color-mix(in srgb, var(--bark-primary, #6ee7b7) 15%, transparent);color:var(--bark-text, #edf3fb);cursor:pointer;}.form.svelte-1q1dueb button:where(.svelte-1q1dueb):disabled {opacity:.5;cursor:default;}.hint.svelte-1q1dueb {padding:0 var(--bark-space-5, 1.5rem) var(--bark-space-4, 1rem);font-size:.78rem;}.row.svelte-1q1dueb {display:grid;grid-template-columns:1.4fr .8fr .9fr 1.8fr;gap:1rem;align-items:center;min-height:3.4rem;padding:.75rem var(--bark-space-5, 1.5rem);border-bottom:1px solid var(--bark-border, #27354a);font-size:.82rem;}.row.svelte-1q1dueb:last-child {border-bottom:0;}.row.header.svelte-1q1dueb {min-height:2.8rem;color:var(--bark-text-muted, #91a1b7);background:var(--bark-surface-raised, #172235);font-size:.68rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase;}.ready.svelte-1q1dueb {color:var(--bark-success, #4ade80);}.pending.svelte-1q1dueb {color:var(--bark-warning, #fbbf24);}.ready.svelte-1q1dueb i:where(.svelte-1q1dueb), .pending.svelte-1q1dueb i:where(.svelte-1q1dueb) {display:inline-block;width:.4rem;height:.4rem;margin-right:.4rem;border-radius:50%;background:currentColor;}.actions.svelte-1q1dueb {display:flex;gap:.5rem;align-items:center;}.actions.svelte-1q1dueb input:where(.svelte-1q1dueb) {flex:1;min-width:8rem;padding:.4rem .6rem;border:1px solid var(--bark-border, #27354a);border-radius:var(--bark-radius-sm, .45rem);background:var(--bark-surface-raised, #172235);color:var(--bark-text, #edf3fb);}.actions.svelte-1q1dueb button:where(.svelte-1q1dueb) {padding:.4rem .65rem;border:1px solid var(--bark-border, #27354a);border-radius:var(--bark-radius-sm, .45rem);background:var(--bark-surface-raised, #172235);color:var(--bark-text, #edf3fb);cursor:pointer;}.actions.svelte-1q1dueb button.quiet:where(.svelte-1q1dueb) {background:transparent;}.actions.svelte-1q1dueb button.danger:where(.svelte-1q1dueb) {border-color:color-mix(in srgb, var(--bark-danger, #fb7185) 50%, var(--bark-border, #27354a));color:var(--bark-danger, #fb7185);}.actions.svelte-1q1dueb button:where(.svelte-1q1dueb):disabled {opacity:.5;cursor:default;}.empty.svelte-1q1dueb, .error.svelte-1q1dueb, .warning.svelte-1q1dueb {padding:2.5rem var(--bark-space-5, 1.5rem);text-align:center;color:var(--bark-text-muted, #91a1b7);}.warning.svelte-1q1dueb {border:1px solid color-mix(in srgb, var(--bark-warning, #fbbf24) 35%, var(--bark-border, #27354a));border-radius:var(--bark-radius-lg, 1rem);background:var(--bark-surface, #101827);text-align:left;}.warning.svelte-1q1dueb strong:where(.svelte-1q1dueb) {color:var(--bark-warning, #fbbf24);}.error.svelte-1q1dueb {border:1px solid color-mix(in srgb, var(--bark-danger, #fb7185) 30%, var(--bark-border, #27354a));border-radius:var(--bark-radius-lg, 1rem);background:var(--bark-surface, #101827);}.error.svelte-1q1dueb strong:where(.svelte-1q1dueb) {color:var(--bark-text, #edf3fb);}.error.svelte-1q1dueb button:where(.svelte-1q1dueb), .error.svelte-1q1dueb p:where(.svelte-1q1dueb) {margin:.5rem auto 0;}.error.svelte-1q1dueb button:where(.svelte-1q1dueb) {padding:.45rem .7rem;border:1px solid var(--bark-border, #27354a);border-radius:var(--bark-radius-sm, .45rem);background:var(--bark-surface-raised, #172235);color:var(--bark-text, #edf3fb);cursor:pointer;}.loading.svelte-1q1dueb {display:grid;gap:.8rem;padding:2rem;border:1px solid var(--bark-border, #27354a);border-radius:var(--bark-radius-lg, 1rem);background:var(--bark-surface, #101827);}.loading.svelte-1q1dueb span:where(.svelte-1q1dueb) {height:1rem;border-radius:.3rem;background:var(--bark-surface-raised, #172235); animation: svelte-1q1dueb-pulse 1.4s infinite;}.loading.svelte-1q1dueb span:where(.svelte-1q1dueb):nth-child(2) {width:65%;}.loading.svelte-1q1dueb span:where(.svelte-1q1dueb):nth-child(3) {width:40%;} + @keyframes svelte-1q1dueb-pulse { 50% { opacity: .4; } } + @media (max-width: 850px) {.intro.svelte-1q1dueb {flex-direction:column;}.row.svelte-1q1dueb {grid-template-columns:1fr;gap:.35rem;padding:1rem;}.row.header.svelte-1q1dueb {display:none;}.form.svelte-1q1dueb {flex-direction:column;align-items:stretch;} + }` +}; +function ba(e, t) { + pn(t, !0), Hi(e, _a); + let r = Ji(t, "route", 7, "/"), n, s = /* @__PURE__ */ P(void 0), i = /* @__PURE__ */ P(!0), l = /* @__PURE__ */ P(""), a = /* @__PURE__ */ P(""), o = /* @__PURE__ */ P(""), u = /* @__PURE__ */ P(""), c = /* @__PURE__ */ P(""), d = /* @__PURE__ */ P(!1); + const v = /^[a-z][a-z0-9_]*$/, h = "/plugins/treatvault/api/secrets"; + async function f() { + try { + const _ = await fetch(h, { headers: { Accept: "application/json" }, cache: "no-store" }); + if (!_.ok) throw new Error(`API returned ${_.status}`); + q(s, await _.json(), !0), q(l, ""); + } catch (_) { + q(l, _ instanceof Error ? _.message : "TreatVault is unavailable", !0); + } finally { + q(i, !1); + } + } + function $(_) { + ta(n, _); + } + function C(_) { + return v.test(_) && _.length <= 54; + } + function m(_, x) { + ra(n, _, x); + } + async function S() { + if (!C(p(a))) { + m("error", "Name must be lowercase letters, digits, or underscores"); + return; + } + if (!p(o)) { + m("error", "Enter a secret value"); + return; + } + await F(async () => { + const _ = await fetch(`${h}/${p(a)}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ value: p(o) }) + }); + if (!_.ok) throw new Error(await $e(_)); + m("success", `Secret ${p(a)} stored and synced`), q(a, ""), q(o, ""); + }); + } + async function ae(_) { + if (!p(c)) { + m("error", "Enter a new value"); + return; + } + await F(async () => { + const x = await fetch(`${h}/${_}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ value: p(c) }) + }); + if (!x.ok) throw new Error(await $e(x)); + m("success", `Secret ${_} rotated`), q(u, ""), q(c, ""); + }); + } + function le(_) { + na(n, `Delete secret ${_}? Services still mounting it will keep their last mounted copy until restarted.`, async (x) => { + x && await F(async () => { + const qe = await fetch(`${h}/${_}`, { method: "DELETE" }); + if (!qe.ok) throw new Error(await $e(qe)); + m("success", `Secret ${_} deleted`); + }); + }); + } + async function F(_) { + q(d, !0); + try { + await _(), await f(); + } catch (x) { + m("error", x instanceof Error ? x.message : "Operation failed"); + } finally { + q(d, !1); + } + } + async function $e(_) { + try { + const x = await _.json(); + if (x.error) return x.error; + } catch { + } + return `Request returned ${_.status}`; + } + In(() => { + const _ = setInterval(f, 5e3); + return () => clearInterval(_); + }), Li(f); + var oe = { + get route() { + return r(); + }, + set route(_ = "/") { + r(_), $r(); + } + }, V = pa(), Ee = D(V), B = E(D(Ee), 2); + let mt; + var wt = E(D(B)), Qn = E(D(wt)), es = Be(Qn, !0); + M(wt), M(B), M(Ee); + var Rr = E(Ee, 2); + { + var ts = (_) => { + var x = sa(); + xe("click", x, () => $("/treatvault")), Y(_, x); + }; + Ye(Rr, (_) => { + r() !== "/" && _(ts); + }); + } + var rs = E(Rr, 2); + { + var ns = (_) => { + var x = ia(); + Y(_, x); + }, ss = (_) => { + var x = aa(), qe = E(D(x)), Ut = Be(qe, !0), rt = E(qe); + M(x), Ce(() => Re(Ut, p(l))), xe("click", rt, f), Y(_, x); + }, is = (_) => { + var x = ha(), qe = Qt(x); + { + var Ut = (A) => { + var W = la(), Wt = E(D(W)), nt = Be(Wt, !0); + M(W), Ce(() => Re(nt, p(s).error)), Y(A, W); + }; + Ye(qe, (A) => { + p(s)?.error && A(Ut); + }); + } + var rt = E(qe, 2), Or = E(D(rt), 2), Bt = D(Or), Mr = E(D(Bt)); + rr(Mr), M(Bt); + var Yt = E(Bt, 2), Dr = E(D(Yt)); + rr(Dr), M(Yt); + var Ir = E(Yt, 2); + M(Or), cn(2), M(rt); + var Pr = E(rt, 2), Gt = D(Pr), Kt = D(Gt), as = E(D(Kt)), ls = Be(as, !0); + M(Kt); + var os = E(Kt); + { + var us = (A) => { + var W = oa(); + xe("click", W, () => $("/treatvault/audit")), Y(A, W); + }; + Ye(os, (A) => { + r() === "/" && A(us); + }); + } + M(Gt); + var fs = E(Gt, 2); + { + var cs = (A) => { + var W = ua(); + Y(A, W); + }, ds = (A) => { + var W = va(), Wt = E(D(W), 2); + Vi(Wt, 17, () => p(s).secrets, (nt) => nt.name, (nt, be) => { + var Jt = da(), Lr = D(Jt), vs = Be(Lr, !0), yt = E(Lr, 2); + let Fr; + var hs = E(D(yt), 1, !0); + M(yt); + var Vr = E(yt, 2), ps = Be(Vr, !0), jr = E(Vr, 2), _s = D(jr); + { + var bs = (ze) => { + var st = fa(), Ne = Qt(st); + rr(Ne); + var He = E(Ne, 2), zr = E(He, 2); + Ce(() => { + He.disabled = p(d), zr.disabled = p(d); + }), nr(Ne, () => p(c), (ms) => q(c, ms)), xe("click", He, () => ae(p(be).name)), xe("click", zr, () => { + q(u, ""), q(c, ""); + }), Y(ze, st); + }, gs = (ze) => { + var st = ca(), Ne = Qt(st), He = E(Ne, 2); + Ce(() => { + Ne.disabled = p(d), He.disabled = p(d); + }), xe("click", Ne, () => { + q(u, p(be).name, !0), q(c, ""); + }), xe("click", He, () => le(p(be).name)), Y(ze, st); + }; + Ye(_s, (ze) => { + p(u) === p(be).name ? ze(bs) : ze(gs, -1); + }); + } + M(jr), M(Jt), Ce(() => { + Re(vs, p(be).name), Fr = tn(yt, 1, "svelte-1q1dueb", null, Fr, { ready: p(be).synced, pending: !p(be).synced }), Re(hs, p(be).synced ? "Synced" : "Pending"), Re(ps, p(be).inUse ? "In use" : "Unreferenced"); + }), Y(nt, Jt); + }), M(W), Y(A, W); + }; + Ye(fs, (A) => { + p(s)?.secrets.length ? A(ds, -1) : A(cs); + }); + } + M(Pr), Ce( + (A) => { + Ir.disabled = p(d), Re(ls, A); + }, + [ + () => r().startsWith("/audit") ? "All managed secrets" : "Managed secrets" + ] + ), nr(Mr, () => p(a), (A) => q(a, A)), nr(Dr, () => p(o), (A) => q(o, A)), xe("click", Ir, S), Y(_, x); + }; + Ye(rs, (_) => { + p(i) ? _(ns) : p(l) ? _(ss, 1) : _(is, -1); + }); + } + return M(V), Wi(V, (_) => n = _, () => n), Ce(() => { + mt = tn(B, 1, "service-status svelte-1q1dueb", null, mt, { offline: !!p(l) || p(s)?.state !== "ready" }), Re(es, p(l) ? "Degraded" : p(s)?.state ?? "Connecting"); + }), Y(e, V), _n(oe); +} +qi(["click"]); +customElements.define("barkstack-treatvault", ea(ba, { route: { reflect: !0, type: "String" } }, [], [], { mode: "open" })); diff --git a/internal/adminui/handler.go b/internal/adminui/handler.go new file mode 100644 index 0000000..1fc7aa4 --- /dev/null +++ b/internal/adminui/handler.go @@ -0,0 +1,208 @@ +// Package adminui serves TreatVault's write-only Barkstack plugin and API. +package adminui + +import ( + "context" + "embed" + "encoding/json" + "errors" + "io" + "io/fs" + "mime" + "net/http" + "path" + "strings" + + barkfile "cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2" + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/manager" + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/vault" +) + +//go:embed dist +var embeddedUI embed.FS + +const entryPath = "/barkstack/ui/assets/entry.js" + +type SecretManager interface { + Status() manager.Status + Set(context.Context, string, []byte) error + Delete(context.Context, string) error +} + +type manifest struct { + APIVersion string `json:"apiVersion"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Icon string `json:"icon"` + Mount string `json:"mount"` + Entry string `json:"entry"` + Element string `json:"element"` + Navigation []navigationItem `json:"navigation"` +} + +type navigationItem struct { + Label string `json:"label"` + Path string `json:"path"` + Icon string `json:"icon"` +} + +type handler struct { + assets fs.FS + manager SecretManager +} + +func Handler(secretManager SecretManager) http.Handler { + assets, err := fs.Sub(embeddedUI, "dist") + if err != nil { + panic(err) + } + return &handler{assets: assets, manager: secretManager} +} + +func (h *handler) ServeHTTP(response http.ResponseWriter, request *http.Request) { + switch { + case request.URL.Path == "/barkstack/ui/manifest.json": + h.serveManifest(response, request) + case request.URL.Path == "/barkstack/api/secrets": + h.serveSecretList(response, request) + case strings.HasPrefix(request.URL.Path, "/barkstack/api/secrets/"): + h.serveSecret(response, request) + case request.URL.Path == "/healthz": + h.serveHealth(response, request) + default: + h.serveAsset(response, request) + } +} + +func (h *handler) serveManifest(response http.ResponseWriter, request *http.Request) { + if !allow(response, request, http.MethodGet, http.MethodHead) { + return + } + h.serveJSON(response, request, http.StatusOK, manifest{ + APIVersion: "barkstack.dev/ui/v1", ID: "treatvault", Name: "TreatVault", + Description: "Encrypted secret management for Barkstack", Icon: "key", Mount: "/treatvault", + Entry: entryPath, Element: "barkstack-treatvault", + Navigation: []navigationItem{{Label: "TreatVault", Path: "/treatvault", Icon: "key"}}, + }) +} + +func (h *handler) serveSecretList(response http.ResponseWriter, request *http.Request) { + if !allow(response, request, http.MethodGet, http.MethodHead) { + return + } + h.serveJSON(response, request, http.StatusOK, h.manager.Status()) +} + +func (h *handler) serveSecret(response http.ResponseWriter, request *http.Request) { + name := strings.TrimPrefix(request.URL.Path, "/barkstack/api/secrets/") + if !barkfile.ValidSecretReference(name) || strings.Contains(name, "/") { + h.serveError(response, http.StatusBadRequest, "invalid secret name") + return + } + switch request.Method { + case http.MethodPut: + var body struct { + Value string `json:"value"` + } + decoder := json.NewDecoder(io.LimitReader(request.Body, 512*1024+1)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&body); err != nil { + h.serveError(response, http.StatusBadRequest, "invalid JSON request") + return + } + if body.Value == "" { + h.serveError(response, http.StatusBadRequest, "secret value must not be empty") + return + } + if err := h.manager.Set(request.Context(), name, []byte(body.Value)); err != nil { + h.serveError(response, http.StatusBadGateway, err.Error()) + return + } + response.WriteHeader(http.StatusNoContent) + case http.MethodDelete: + if err := h.manager.Delete(request.Context(), name); err != nil { + switch { + case errors.Is(err, vault.ErrSecretNotFound): + h.serveError(response, http.StatusNotFound, "secret not found") + case manager.IsSecretInUse(err): + h.serveError(response, http.StatusConflict, "secret is still referenced by a managed service") + default: + h.serveError(response, http.StatusBadGateway, err.Error()) + } + return + } + response.WriteHeader(http.StatusNoContent) + default: + response.Header().Set("Allow", "PUT, DELETE") + h.serveError(response, http.StatusMethodNotAllowed, "method not allowed") + } +} + +func (h *handler) serveHealth(response http.ResponseWriter, request *http.Request) { + if !allow(response, request, http.MethodGet, http.MethodHead) { + return + } + status := h.manager.Status() + code := http.StatusOK + if status.State != "ready" { + code = http.StatusServiceUnavailable + } + h.serveJSON(response, request, code, map[string]string{"status": status.State}) +} + +func (h *handler) serveAsset(response http.ResponseWriter, request *http.Request) { + if !allow(response, request, http.MethodGet, http.MethodHead) { + return + } + const prefix = "/barkstack/ui/assets/" + if !strings.HasPrefix(request.URL.Path, prefix) { + http.NotFound(response, request) + return + } + assetName := strings.TrimPrefix(request.URL.Path, "/barkstack/ui/") + if assetName == "" || !fs.ValidPath(assetName) || path.Clean(assetName) != assetName || strings.Contains(assetName, "\\") { + http.NotFound(response, request) + return + } + contents, err := fs.ReadFile(h.assets, assetName) + if err != nil { + http.NotFound(response, request) + return + } + if contentType := mime.TypeByExtension(path.Ext(assetName)); contentType != "" { + response.Header().Set("Content-Type", contentType) + } + response.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + response.Header().Set("X-Content-Type-Options", "nosniff") + if request.Method != http.MethodHead { + _, _ = response.Write(contents) + } +} + +func (h *handler) serveJSON(response http.ResponseWriter, request *http.Request, code int, value any) { + response.Header().Set("Content-Type", "application/json") + response.Header().Set("Cache-Control", "no-store") + response.WriteHeader(code) + if request.Method != http.MethodHead { + _ = json.NewEncoder(response).Encode(value) + } +} + +func (h *handler) serveError(response http.ResponseWriter, code int, message string) { + response.Header().Set("Content-Type", "application/json") + response.Header().Set("Cache-Control", "no-store") + response.WriteHeader(code) + _ = json.NewEncoder(response).Encode(map[string]string{"error": message}) +} + +func allow(response http.ResponseWriter, request *http.Request, methods ...string) bool { + for _, method := range methods { + if request.Method == method { + return true + } + } + response.Header().Set("Allow", strings.Join(methods, ", ")) + http.Error(response, "method not allowed", http.StatusMethodNotAllowed) + return false +} diff --git a/internal/adminui/handler_test.go b/internal/adminui/handler_test.go new file mode 100644 index 0000000..e437443 --- /dev/null +++ b/internal/adminui/handler_test.go @@ -0,0 +1,96 @@ +package adminui + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/manager" + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/provider" +) + +func TestManifestDescribesTreatVaultPlugin(t *testing.T) { + response := httptest.NewRecorder() + Handler(&fakeManager{}).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/barkstack/ui/manifest.json", nil)) + if response.Code != http.StatusOK { + t.Fatalf("status = %d", response.Code) + } + var got manifest + if err := json.Unmarshal(response.Body.Bytes(), &got); err != nil { + t.Fatal(err) + } + if got.ID != "treatvault" || got.Element != "barkstack-treatvault" || got.Entry != entryPath { + t.Fatalf("manifest = %#v", got) + } +} + +func TestListNeverReturnsSecretValues(t *testing.T) { + manager := &fakeManager{status: manager.Status{State: "ready", Secrets: []manager.Secret{{Name: "database_password", InUse: true, Synced: true}}}} + response := httptest.NewRecorder() + Handler(manager).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/barkstack/api/secrets", nil)) + if response.Code != http.StatusOK { + t.Fatalf("status = %d", response.Code) + } + if strings.Contains(response.Body.String(), "secret-value") || !strings.Contains(response.Body.String(), "database_password") { + t.Fatalf("body = %s", response.Body.String()) + } +} + +func TestPutSetsSecretAndReturnsNoValue(t *testing.T) { + manager := &fakeManager{} + response := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodPut, "/barkstack/api/secrets/database_password", strings.NewReader(`{"value":"secret-value"}`)) + Handler(manager).ServeHTTP(response, request) + if response.Code != http.StatusNoContent { + t.Fatalf("status = %d, body = %s", response.Code, response.Body.String()) + } + if manager.setName != "database_password" || string(manager.setValue) != "secret-value" { + t.Fatalf("set = %q %q", manager.setName, manager.setValue) + } + if strings.Contains(response.Body.String(), "secret-value") { + t.Fatal("response disclosed the secret value") + } +} + +func TestDeleteReturnsConflictForInUseSecret(t *testing.T) { + manager := &fakeManager{deleteErr: fmtInUse("database_password")} + response := httptest.NewRecorder() + Handler(manager).ServeHTTP(response, httptest.NewRequest(http.MethodDelete, "/barkstack/api/secrets/database_password", nil)) + if response.Code != http.StatusConflict { + t.Fatalf("status = %d, body = %s", response.Code, response.Body.String()) + } +} + +func TestInvalidSecretNameIsRejected(t *testing.T) { + response := httptest.NewRecorder() + Handler(&fakeManager{}).ServeHTTP(response, httptest.NewRequest(http.MethodPut, "/barkstack/api/secrets/UPPERCASE", strings.NewReader(`{"value":"value"}`))) + if response.Code != http.StatusBadRequest { + t.Fatalf("status = %d", response.Code) + } +} + +type fakeManager struct { + status manager.Status + setName string + setValue []byte + setErr error + deleteErr error +} + +func (m *fakeManager) Status() manager.Status { return m.status } + +func (m *fakeManager) Set(_ context.Context, name string, value []byte) error { + m.setName = name + m.setValue = append([]byte(nil), value...) + return m.setErr +} + +func (m *fakeManager) Delete(context.Context, string) error { return m.deleteErr } + +func fmtInUse(name string) error { + return errors.Join(provider.ErrSecretInUse, errors.New(name)) +} diff --git a/internal/manager/manager.go b/internal/manager/manager.go new file mode 100644 index 0000000..d9cb2f6 --- /dev/null +++ b/internal/manager/manager.go @@ -0,0 +1,170 @@ +// Package manager coordinates encrypted file mutations and provider reconciliation. +package manager + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "os" + "sort" + "sync" + "time" + + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/provider" + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/vault" +) + +type Provider interface { + Sync(context.Context, vault.Snapshot) ([]provider.SecretState, error) + InUse(context.Context, string) (bool, error) +} + +type Secret struct { + Name string `json:"name"` + InUse bool `json:"inUse"` + Synced bool `json:"synced"` +} + +type Status struct { + State string `json:"state"` + Secrets []Secret `json:"secrets"` + LastSync time.Time `json:"lastSync,omitempty"` + Error string `json:"error,omitempty"` +} + +type Manager struct { + store *vault.Store + provider Provider + pollInterval time.Duration + + operationMu sync.Mutex + statusMu sync.RWMutex + status Status +} + +func New(store *vault.Store, secretProvider Provider, pollInterval time.Duration) *Manager { + if pollInterval <= 0 { + pollInterval = 500 * time.Millisecond + } + return &Manager{ + store: store, provider: secretProvider, pollInterval: pollInterval, + status: Status{State: "starting", Secrets: []Secret{}}, + } +} + +func (m *Manager) Status() Status { + m.statusMu.RLock() + defer m.statusMu.RUnlock() + status := m.status + status.Secrets = append([]Secret(nil), m.status.Secrets...) + return status +} + +func (m *Manager) Sync(ctx context.Context) error { + m.operationMu.Lock() + defer m.operationMu.Unlock() + snapshot, err := m.store.Load() + if err != nil { + m.setFailure(err) + return err + } + return m.syncSnapshot(ctx, snapshot) +} + +func (m *Manager) Set(ctx context.Context, name string, value []byte) error { + m.operationMu.Lock() + defer m.operationMu.Unlock() + snapshot, err := m.store.Set(name, value) + if err != nil { + return err + } + return m.syncSnapshot(ctx, snapshot) +} + +func (m *Manager) Delete(ctx context.Context, name string) error { + m.operationMu.Lock() + defer m.operationMu.Unlock() + inUse, err := m.provider.InUse(ctx, name) + if err != nil { + return fmt.Errorf("check secret consumers: %w", err) + } + if inUse { + return fmt.Errorf("%w: %s", provider.ErrSecretInUse, name) + } + snapshot, err := m.store.Delete(name) + if err != nil { + return err + } + return m.syncSnapshot(ctx, snapshot) +} + +func (m *Manager) Run(ctx context.Context) { + _ = m.Sync(ctx) + fingerprint, _ := fileFingerprint(m.store.Path()) + ticker := time.NewTicker(m.pollInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + next, err := fileFingerprint(m.store.Path()) + status := m.Status() + if err != nil { + m.setFailure(err) + continue + } + if next == fingerprint && status.Error == "" { + continue + } + fingerprint = next + _ = m.Sync(ctx) + } + } +} + +func (m *Manager) syncSnapshot(ctx context.Context, snapshot vault.Snapshot) error { + states, err := m.provider.Sync(ctx, snapshot) + secrets := make([]Secret, 0, len(snapshot.Secrets)) + stateByName := make(map[string]provider.SecretState, len(states)) + for _, state := range states { + stateByName[state.Name] = state + } + for name := range snapshot.Secrets { + state, synced := stateByName[name] + secrets = append(secrets, Secret{Name: name, InUse: state.InUse, Synced: synced && err == nil}) + } + sort.Slice(secrets, func(i, j int) bool { return secrets[i].Name < secrets[j].Name }) + status := Status{State: "ready", Secrets: secrets, LastSync: time.Now().UTC()} + if err != nil { + status.State = "degraded" + status.Error = err.Error() + } + m.statusMu.Lock() + m.status = status + m.statusMu.Unlock() + return err +} + +func (m *Manager) setFailure(err error) { + if err == nil { + return + } + m.statusMu.Lock() + m.status.State = "degraded" + m.status.Error = err.Error() + m.statusMu.Unlock() +} + +func fileFingerprint(path string) ([sha256.Size]byte, error) { + contents, err := os.ReadFile(path) + if err != nil { + return [sha256.Size]byte{}, fmt.Errorf("read encrypted file: %w", err) + } + return sha256.Sum256(contents), nil +} + +func IsSecretInUse(err error) bool { + return errors.Is(err, provider.ErrSecretInUse) +} diff --git a/internal/manager/manager_test.go b/internal/manager/manager_test.go new file mode 100644 index 0000000..ebe4a06 --- /dev/null +++ b/internal/manager/manager_test.go @@ -0,0 +1,99 @@ +package manager + +import ( + "context" + "sync" + "testing" + "time" + + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/provider" + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/vault" + "filippo.io/age" +) + +func TestRunSynchronizesExternalEncryptedFileChanges(t *testing.T) { + identity, err := age.GenerateX25519Identity() + if err != nil { + t.Fatal(err) + } + path := t.TempDir() + "/secrets.age" + if err := vault.Initialize(path, identity); err != nil { + t.Fatal(err) + } + store := vault.New(path, identity) + provider := &recordingProvider{synced: make(chan vault.Snapshot, 4)} + manager := New(store, provider, 10*time.Millisecond) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go manager.Run(ctx) + + initial := waitSnapshot(t, provider.synced) + if len(initial.Secrets) != 0 { + t.Fatalf("initial secrets = %#v", initial.Secrets) + } + if _, err := store.Set("database_password", []byte("changed outside manager")); err != nil { + t.Fatal(err) + } + changed := waitSnapshot(t, provider.synced) + if string(changed.Secrets["database_password"].Value) != "changed outside manager" { + t.Fatalf("changed snapshot = %#v", changed) + } +} + +func TestDeleteRefusesSecretInUse(t *testing.T) { + identity, err := age.GenerateX25519Identity() + if err != nil { + t.Fatal(err) + } + path := t.TempDir() + "/secrets.age" + if err := vault.Initialize(path, identity); err != nil { + t.Fatal(err) + } + store := vault.New(path, identity) + if _, err := store.Set("database_password", []byte("value")); err != nil { + t.Fatal(err) + } + manager := New(store, &recordingProvider{inUse: true}, time.Second) + if err := manager.Delete(context.Background(), "database_password"); !IsSecretInUse(err) { + t.Fatalf("Delete() error = %v", err) + } + loaded, err := store.Load() + if err != nil { + t.Fatal(err) + } + if _, exists := loaded.Secrets["database_password"]; !exists { + t.Fatal("in-use secret was removed from encrypted file") + } +} + +type recordingProvider struct { + mu sync.Mutex + synced chan vault.Snapshot + inUse bool +} + +func (p *recordingProvider) Sync(_ context.Context, snapshot vault.Snapshot) ([]provider.SecretState, error) { + if p.synced != nil { + p.synced <- snapshot + } + states := make([]provider.SecretState, 0, len(snapshot.Secrets)) + for name, record := range snapshot.Secrets { + states = append(states, provider.SecretState{Name: name, Revision: record.Revision, InUse: p.inUse}) + } + return states, nil +} + +func (p *recordingProvider) InUse(context.Context, string) (bool, error) { + return p.inUse, nil +} + +func waitSnapshot(t *testing.T, snapshots <-chan vault.Snapshot) vault.Snapshot { + t.Helper() + select { + case snapshot := <-snapshots: + return snapshot + case <-time.After(time.Second): + t.Fatal("timed out waiting for synchronization") + return vault.Snapshot{} + } +} diff --git a/internal/provider/docker.go b/internal/provider/docker.go new file mode 100644 index 0000000..251b191 --- /dev/null +++ b/internal/provider/docker.go @@ -0,0 +1,312 @@ +// Package provider reconciles TreatVault snapshots with platform secret providers. +package provider + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os/exec" + "sort" + "strings" + + barkfile "cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2" + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/vault" +) + +var ErrSecretInUse = errors.New("secret is in use") + +type Runner interface { + Run(context.Context, []byte, ...string) (string, error) +} + +type ExecRunner struct { + Path string +} + +func (r ExecRunner) Run(ctx context.Context, input []byte, args ...string) (string, error) { + path := r.Path + if path == "" { + path = "docker" + } + command := exec.CommandContext(ctx, path, args...) + if input != nil { + command.Stdin = strings.NewReader(string(input)) + } + output, err := command.CombinedOutput() + if err != nil { + return string(output), fmt.Errorf("docker %s: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(string(output))) + } + return string(output), nil +} + +type SecretState struct { + Name string + Revision string + PhysicalName string + InUse bool +} + +type Docker struct { + Runner Runner +} + +// Sync reconciles snapshot with Docker Swarm: it creates one immutable secret +// object per record revision, rotates mounts on labeled consumer services, and +// removes obsolete objects. Names listed in a service's +// io.barkstack.treatvault.names label are mounted at barkstack_ targets. +func (d Docker) Sync(ctx context.Context, snapshot vault.Snapshot) ([]SecretState, error) { + if d.Runner == nil { + return nil, errors.New("Docker runner is required") + } + managed, err := d.managedSecrets(ctx, snapshot.VaultID) + if err != nil { + return nil, err + } + for name, record := range snapshot.Secrets { + physical := physicalName(snapshot.VaultID, record.Revision) + if containsSecret(managed, physical) { + continue + } + args := []string{ + "secret", "create", + "--label", barkfile.TreatVaultManagedLabel + "=true", + "--label", barkfile.TreatVaultSecretNameLabel + "=" + name, + "--label", barkfile.TreatVaultVaultIDLabel + "=" + snapshot.VaultID, + "--label", barkfile.TreatVaultRevisionLabel + "=" + record.Revision, + physical, "-", + } + if _, err := d.Runner.Run(ctx, record.Value, args...); err != nil { + return nil, fmt.Errorf("create Docker secret for %q: %w", name, err) + } + } + + services, err := d.managedServices(ctx) + if err != nil { + return nil, err + } + inUse := make(map[string]bool) + missing := make(map[string]struct{}) + desiredTargets := make(map[string]struct{}, len(snapshot.Secrets)) + for name := range snapshot.Secrets { + desiredTargets[barkfile.DockerSecretName(name)] = struct{}{} + } + for _, service := range services { + args := []string{"service", "update"} + changed := false + mountedByTarget := make(map[string]serviceSecret, len(service.Secrets)) + for _, mounted := range service.Secrets { + mountedByTarget[mounted.Target] = mounted + if _, managedTarget := desiredTargets[mounted.Target]; managedTarget { + inUse[strings.TrimPrefix(mounted.Target, barkfile.DockerSecretPrefix)] = true + } + } + for _, name := range service.DesiredNames { + record, stored := snapshot.Secrets[name] + if !stored { + missing[name] = struct{}{} + continue + } + inUse[name] = true + desiredPhysical := physicalName(snapshot.VaultID, record.Revision) + target := barkfile.DockerSecretName(name) + mounted, isMounted := mountedByTarget[target] + if isMounted && mounted.Source == desiredPhysical { + continue + } + if isMounted { + args = append(args, "--secret-rm", mounted.Source) + } + args = append(args, "--secret-add", secretMount(desiredPhysical, target)) + changed = true + } + for _, mounted := range service.Secrets { + logical, managedTarget := strings.CutPrefix(mounted.Target, barkfile.DockerSecretPrefix) + if !managedTarget { + continue + } + if _, desired := desiredTargets[mounted.Target]; desired { + continue + } + // Mounted under a Barkstack target but absent from the vault: + // keep the mount so consumers keep working, surface the gap. + missing[logical] = struct{}{} + } + if changed { + args = append(args, service.Name) + if _, err := d.Runner.Run(ctx, nil, args...); err != nil { + return nil, fmt.Errorf("rotate secrets on service %q: %w", service.Name, err) + } + } + } + + desiredPhysical := make(map[string]struct{}, len(snapshot.Secrets)) + for _, record := range snapshot.Secrets { + desiredPhysical[physicalName(snapshot.VaultID, record.Revision)] = struct{}{} + } + for _, secret := range managed { + if _, desired := desiredPhysical[secret.Name]; desired { + continue + } + if _, err := d.Runner.Run(ctx, nil, "secret", "rm", secret.Name); err != nil && !isInUseError(err) { + return nil, fmt.Errorf("remove obsolete Docker secret %q: %w", secret.Name, err) + } + } + + states := make([]SecretState, 0, len(snapshot.Secrets)) + for name, record := range snapshot.Secrets { + states = append(states, SecretState{ + Name: name, Revision: record.Revision, + PhysicalName: physicalName(snapshot.VaultID, record.Revision), InUse: inUse[name], + }) + } + sort.Slice(states, func(i, j int) bool { return states[i].Name < states[j].Name }) + if len(missing) != 0 { + names := make([]string, 0, len(missing)) + for name := range missing { + names = append(names, name) + } + sort.Strings(names) + return states, fmt.Errorf("%w: %s", ErrSecretInUse, strings.Join(names, ", ")) + } + return states, nil +} + +// InUse reports whether any managed service currently mounts the secret at its +// barkstack_ target. +func (d Docker) InUse(ctx context.Context, name string) (bool, error) { + services, err := d.managedServices(ctx) + if err != nil { + return false, err + } + target := barkfile.DockerSecretName(name) + for _, service := range services { + for _, secret := range service.Secrets { + if secret.Target == target { + return true, nil + } + } + } + return false, nil +} + +type dockerSecret struct { + Name string + LogicalName string + Revision string + VaultID string +} + +func containsSecret(secrets []dockerSecret, name string) bool { + for _, secret := range secrets { + if secret.Name == name { + return true + } + } + return false +} + +func (d Docker) managedSecrets(ctx context.Context, vaultID string) ([]dockerSecret, error) { + output, err := d.Runner.Run(ctx, nil, + "secret", "ls", "--quiet", + "--filter", "label="+barkfile.TreatVaultManagedLabel+"=true", + "--filter", "label="+barkfile.TreatVaultVaultIDLabel+"="+vaultID, + ) + if err != nil { + return nil, fmt.Errorf("list managed Docker secrets: %w", err) + } + ids := strings.Fields(output) + secrets := make([]dockerSecret, 0, len(ids)) + for _, id := range ids { + output, err := d.Runner.Run(ctx, nil, "secret", "inspect", "--format", "{{json .Spec}}", id) + if err != nil { + return nil, fmt.Errorf("inspect Docker secret %q: %w", id, err) + } + var spec struct { + Name string `json:"Name"` + Labels map[string]string `json:"Labels"` + } + if err := json.Unmarshal([]byte(output), &spec); err != nil { + return nil, fmt.Errorf("decode Docker secret %q: %w", id, err) + } + if spec.Labels[barkfile.TreatVaultManagedLabel] != "true" || spec.Labels[barkfile.TreatVaultVaultIDLabel] != vaultID { + continue + } + secrets = append(secrets, dockerSecret{ + Name: spec.Name, LogicalName: spec.Labels[barkfile.TreatVaultSecretNameLabel], + Revision: spec.Labels[barkfile.TreatVaultRevisionLabel], VaultID: vaultID, + }) + } + return secrets, nil +} + +type serviceSecret struct { + Source string + Target string +} + +type dockerService struct { + Name string + DesiredNames []string + Secrets []serviceSecret +} + +func (d Docker) managedServices(ctx context.Context) ([]dockerService, error) { + output, err := d.Runner.Run(ctx, nil, + "service", "ls", "--quiet", "--filter", "label="+barkfile.TreatVaultConsumerLabel+"=true", + ) + if err != nil { + return nil, fmt.Errorf("list TreatVault consumer services: %w", err) + } + ids := strings.Fields(output) + services := make([]dockerService, 0, len(ids)) + for _, id := range ids { + output, err := d.Runner.Run(ctx, nil, "service", "inspect", "--format", "{{json .Spec}}", id) + if err != nil { + return nil, fmt.Errorf("inspect Docker service %q: %w", id, err) + } + var spec struct { + Name string `json:"Name"` + Labels map[string]string `json:"Labels"` + TaskTemplate struct { + ContainerSpec struct { + Secrets []struct { + SecretName string `json:"SecretName"` + File *struct { + Name string `json:"Name"` + } `json:"File"` + } `json:"Secrets"` + } `json:"ContainerSpec"` + } `json:"TaskTemplate"` + } + if err := json.Unmarshal([]byte(output), &spec); err != nil { + return nil, fmt.Errorf("decode Docker service %q: %w", id, err) + } + names := spec.Labels[barkfile.TreatVaultNamesLabel] + var desiredNames []string + if names != "" { + desiredNames = strings.Split(names, ",") + } + service := dockerService{Name: spec.Name, DesiredNames: desiredNames} + for _, secret := range spec.TaskTemplate.ContainerSpec.Secrets { + if secret.File != nil { + service.Secrets = append(service.Secrets, serviceSecret{Source: secret.SecretName, Target: secret.File.Name}) + } + } + services = append(services, service) + } + return services, nil +} + +func physicalName(vaultID, revision string) string { + return barkfile.DockerSecretPrefix + "tv_" + vaultID + "_" + revision +} + +func secretMount(source, target string) string { + return "source=" + source + ",target=" + target +} + +func isInUseError(err error) bool { + message := strings.ToLower(err.Error()) + return strings.Contains(message, "in use") || strings.Contains(message, "used by") +} diff --git a/internal/provider/docker_test.go b/internal/provider/docker_test.go new file mode 100644 index 0000000..fe1371a --- /dev/null +++ b/internal/provider/docker_test.go @@ -0,0 +1,184 @@ +package provider + +import ( + "context" + "errors" + "strings" + "testing" + + "cloud.campbellwireless.net/git/barkstack/treatvault/internal/vault" +) + +const ( + testVaultID = "0011223344556677" + testRevision = "00112233445566778899aabbccddeeff" +) + +func serviceSpecJSON(names string, secrets string) string { + return `{"Name":"barkstack-pawsql","Labels":{"io.barkstack.treatvault.secrets":"true","io.barkstack.treatvault.names":"` + names + `"},"TaskTemplate":{"ContainerSpec":{"Secrets":[` + secrets + `]}}}` +} + +func TestSyncCreatesVersionedDockerSecret(t *testing.T) { + runner := &fakeRunner{responses: []fakeResponse{{}, {}, {}}} + provider := Docker{Runner: runner} + snapshot := vault.Snapshot{ + Version: 1, VaultID: testVaultID, + Secrets: map[string]vault.Record{"database_password": {Revision: testRevision, Value: []byte("super-secret")}}, + } + states, err := provider.Sync(context.Background(), snapshot) + if err != nil { + t.Fatal(err) + } + const physical = "barkstack_tv_" + testVaultID + "_" + testRevision + if len(states) != 1 || states[0].PhysicalName != physical { + t.Fatalf("states = %#v", states) + } + create := runner.callContaining(t, "secret create") + for _, want := range []string{ + "--label io.barkstack.treatvault=true", + "--label io.barkstack.treatvault.name=database_password", + physical + " -", + } { + if !strings.Contains(create.args, want) { + t.Errorf("create args = %q, missing %q", create.args, want) + } + } + if string(create.input) != "super-secret" || strings.Contains(create.args, "super-secret") { + t.Fatalf("secret was not passed exclusively on stdin: %#v", create) + } +} + +func TestSyncRotatesMountedSecretWithoutChangingTarget(t *testing.T) { + const ( + oldRevision = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + oldPhysical = "barkstack_tv_" + testVaultID + "_" + oldRevision + newPhysical = "barkstack_tv_" + testVaultID + "_" + testRevision + ) + runner := &fakeRunner{responses: []fakeResponse{ + {output: "old-id\n"}, // secret ls + {output: secretSpecJSON("database_password", oldRevision, oldPhysical, testVaultID)}, // secret inspect + {}, // secret create (new revision) + {output: "service-id\n"}, // service ls + {output: serviceSpecJSON("database_password", mountJSON(oldPhysical, "barkstack_database_password"))}, // service inspect + {}, // service update + {err: errors.New("secret is in use by old task")}, // secret rm + }} + provider := Docker{Runner: runner} + states, err := provider.Sync(context.Background(), vault.Snapshot{ + Version: 1, VaultID: testVaultID, + Secrets: map[string]vault.Record{"database_password": {Revision: testRevision, Value: []byte("rotated")}}, + }) + if err != nil { + t.Fatal(err) + } + if len(states) != 1 || !states[0].InUse { + t.Fatalf("states = %#v", states) + } + update := runner.callContaining(t, "service update") + for _, want := range []string{ + "--secret-rm " + oldPhysical, + "--secret-add source=" + newPhysical + ",target=barkstack_database_password", + "barkstack-pawsql", + } { + if !strings.Contains(update.args, want) { + t.Errorf("update args = %q, missing %q", update.args, want) + } + } +} + +func TestSyncMountsMissingDesiredSecretOnConsumer(t *testing.T) { + const physical = "barkstack_tv_" + testVaultID + "_" + testRevision + runner := &fakeRunner{responses: []fakeResponse{ + {}, // secret ls (empty vault listing) + {}, // secret create + {output: "service-id\n"}, + {output: serviceSpecJSON("database_password", "")}, + {}, // service update + }} + provider := Docker{Runner: runner} + states, err := provider.Sync(context.Background(), vault.Snapshot{ + Version: 1, VaultID: testVaultID, + Secrets: map[string]vault.Record{"database_password": {Revision: testRevision, Value: []byte("value")}}, + }) + if err != nil { + t.Fatal(err) + } + update := runner.callContaining(t, "service update") + if want := "--secret-add source=" + physical + ",target=barkstack_database_password"; !strings.Contains(update.args, want) { + t.Errorf("update args = %q, missing %q", update.args, want) + } + if !states[0].InUse { + t.Fatalf("states = %#v, want InUse", states) + } +} + +func TestSyncReportsConfiguredSecretMissingFromVault(t *testing.T) { + runner := &fakeRunner{responses: []fakeResponse{ + {}, // secret ls (empty) + {output: "service-id\n"}, + {output: serviceSpecJSON("database_password", "")}, + }} + _, err := (Docker{Runner: runner}).Sync(context.Background(), vault.Snapshot{Version: 1, VaultID: testVaultID, Secrets: map[string]vault.Record{}}) + if !errors.Is(err, ErrSecretInUse) { + t.Fatalf("Sync() error = %v", err) + } +} + +func TestSyncKeepsOrphanedMountAndReportsGap(t *testing.T) { + const oldPhysical = "barkstack_tv_" + testVaultID + "_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + runner := &fakeRunner{responses: []fakeResponse{ + {output: "old-id\n"}, + {output: secretSpecJSON("removed_secret", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", oldPhysical, testVaultID)}, + {output: "service-id\n"}, + {output: serviceSpecJSON("database_password", mountJSON(oldPhysical, "barkstack_database_password"))}, + {err: errors.New("secret is in use")}, + }} + _, err := (Docker{Runner: runner}).Sync(context.Background(), vault.Snapshot{Version: 1, VaultID: testVaultID, Secrets: map[string]vault.Record{}}) + if !errors.Is(err, ErrSecretInUse) { + t.Fatalf("Sync() error = %v", err) + } +} + +func secretSpecJSON(name, revision, physical, vaultID string) string { + return `{"Name":"` + physical + `","Labels":{"io.barkstack.treatvault":"true","io.barkstack.treatvault.name":"` + name + `","io.barkstack.treatvault.vault":"` + vaultID + `","io.barkstack.treatvault.revision":"` + revision + `"}}` +} + +func mountJSON(source, target string) string { + return `{"SecretName":"` + source + `","File":{"Name":"` + target + `"}}` +} + +type fakeResponse struct { + output string + err error +} + +type fakeCall struct { + args string + input []byte +} + +type fakeRunner struct { + responses []fakeResponse + calls []fakeCall +} + +func (r *fakeRunner) Run(_ context.Context, input []byte, args ...string) (string, error) { + r.calls = append(r.calls, fakeCall{args: strings.Join(args, " "), input: append([]byte(nil), input...)}) + if len(r.responses) == 0 { + return "", errors.New("unexpected Docker command") + } + response := r.responses[0] + r.responses = r.responses[1:] + return response.output, response.err +} + +func (r *fakeRunner) callContaining(t *testing.T, prefix string) fakeCall { + t.Helper() + for _, call := range r.calls { + if strings.HasPrefix(call.args, prefix) { + return call + } + } + t.Fatalf("calls = %#v, missing %q", r.calls, prefix) + return fakeCall{} +} diff --git a/internal/vault/store.go b/internal/vault/store.go new file mode 100644 index 0000000..ef323eb --- /dev/null +++ b/internal/vault/store.go @@ -0,0 +1,292 @@ +// Package vault owns TreatVault's encrypted source-of-truth file. +package vault + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "sync" + + barkfile "cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2" + "filippo.io/age" +) + +const ( + documentVersion = 1 + maxSecretBytes = 500 * 1024 + maxDocumentBytes = 16 * 1024 * 1024 +) + +var ErrSecretNotFound = errors.New("secret not found") + +type Record struct { + Revision string `json:"revision"` + Value []byte `json:"value"` +} + +type Snapshot struct { + Version int `json:"version"` + VaultID string `json:"vaultId"` + Secrets map[string]Record `json:"secrets"` +} + +type Metadata struct { + Name string + Revision string +} + +type Store struct { + path string + identity *age.X25519Identity + recipient age.Recipient + mu sync.Mutex +} + +func Open(path, identityPath string) (*Store, error) { + identity, err := ReadIdentity(identityPath) + if err != nil { + return nil, err + } + store := &Store{path: path, identity: identity, recipient: identity.Recipient()} + if _, err := store.Load(); err != nil { + return nil, err + } + return store, nil +} + +func New(path string, identity *age.X25519Identity) *Store { + return &Store{path: path, identity: identity, recipient: identity.Recipient()} +} + +func ReadIdentity(path string) (*age.X25519Identity, error) { + contents, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read age identity: %w", err) + } + identity, err := age.ParseX25519Identity(strings.TrimSpace(string(contents))) + if err != nil { + return nil, fmt.Errorf("parse age identity: %w", err) + } + return identity, nil +} + +func GenerateIdentity(path string) (string, error) { + identity, err := age.GenerateX25519Identity() + if err != nil { + return "", fmt.Errorf("generate age identity: %w", err) + } + if _, err := os.Stat(path); err == nil { + return "", fmt.Errorf("identity file %q already exists", path) + } else if !errors.Is(err, os.ErrNotExist) { + return "", err + } + if err := writeFileAtomic(path, []byte(identity.String()+"\n"), 0o600); err != nil { + return "", fmt.Errorf("write age identity: %w", err) + } + return identity.Recipient().String(), nil +} + +func Initialize(path string, identity *age.X25519Identity) error { + if _, err := os.Stat(path); err == nil { + return fmt.Errorf("encrypted file %q already exists", path) + } else if !errors.Is(err, os.ErrNotExist) { + return err + } + store := New(path, identity) + vaultID, err := randomHex(8) + if err != nil { + return err + } + return store.save(Snapshot{Version: documentVersion, VaultID: vaultID, Secrets: map[string]Record{}}) +} + +func (s *Store) Path() string { return s.path } + +func (s *Store) Load() (Snapshot, error) { + s.mu.Lock() + defer s.mu.Unlock() + return s.load() +} + +func (s *Store) Set(name string, value []byte) (Snapshot, error) { + if !barkfile.ValidSecretReference(name) { + return Snapshot{}, fmt.Errorf("invalid secret name %q", name) + } + if len(value) == 0 { + return Snapshot{}, errors.New("secret value must not be empty") + } + if len(value) > maxSecretBytes { + return Snapshot{}, fmt.Errorf("secret value exceeds Docker's %d-byte limit", maxSecretBytes) + } + s.mu.Lock() + defer s.mu.Unlock() + snapshot, err := s.load() + if err != nil { + return Snapshot{}, err + } + if existing, ok := snapshot.Secrets[name]; ok && bytes.Equal(existing.Value, value) { + return snapshot, nil + } + revision, err := randomHex(16) + if err != nil { + return Snapshot{}, err + } + snapshot.Secrets[name] = Record{Revision: revision, Value: append([]byte(nil), value...)} + if err := s.save(snapshot); err != nil { + return Snapshot{}, err + } + return snapshot, nil +} + +func (s *Store) Delete(name string) (Snapshot, error) { + s.mu.Lock() + defer s.mu.Unlock() + snapshot, err := s.load() + if err != nil { + return Snapshot{}, err + } + if _, ok := snapshot.Secrets[name]; !ok { + return Snapshot{}, ErrSecretNotFound + } + delete(snapshot.Secrets, name) + if err := s.save(snapshot); err != nil { + return Snapshot{}, err + } + return snapshot, nil +} + +func (s *Store) load() (Snapshot, error) { + encrypted, err := os.Open(s.path) + if err != nil { + return Snapshot{}, fmt.Errorf("open encrypted file: %w", err) + } + defer encrypted.Close() + plaintext, err := age.Decrypt(encrypted, s.identity) + if err != nil { + return Snapshot{}, fmt.Errorf("decrypt encrypted file: %w", err) + } + decoder := json.NewDecoder(io.LimitReader(plaintext, maxDocumentBytes+1)) + decoder.DisallowUnknownFields() + var snapshot Snapshot + if err := decoder.Decode(&snapshot); err != nil { + return Snapshot{}, fmt.Errorf("decode encrypted file: %w", err) + } + if err := validateSnapshot(snapshot); err != nil { + return Snapshot{}, fmt.Errorf("validate encrypted file: %w", err) + } + return cloneSnapshot(snapshot), nil +} + +func (s *Store) save(snapshot Snapshot) error { + if err := validateSnapshot(snapshot); err != nil { + return err + } + var encrypted bytes.Buffer + writer, err := age.Encrypt(&encrypted, s.recipient) + if err != nil { + return fmt.Errorf("initialize encryption: %w", err) + } + encoder := json.NewEncoder(writer) + encoder.SetEscapeHTML(false) + if err := encoder.Encode(snapshot); err != nil { + return fmt.Errorf("encode encrypted file: %w", err) + } + if err := writer.Close(); err != nil { + return fmt.Errorf("finish encryption: %w", err) + } + if err := writeFileAtomic(s.path, encrypted.Bytes(), 0o600); err != nil { + return fmt.Errorf("replace encrypted file: %w", err) + } + return nil +} + +func validateSnapshot(snapshot Snapshot) error { + if snapshot.Version != documentVersion { + return fmt.Errorf("unsupported document version %d", snapshot.Version) + } + if len(snapshot.VaultID) != 16 { + return errors.New("vault ID must be 16 hexadecimal characters") + } + if _, err := hex.DecodeString(snapshot.VaultID); err != nil { + return errors.New("vault ID must be hexadecimal") + } + if snapshot.Secrets == nil { + return errors.New("secrets map is required") + } + for name, record := range snapshot.Secrets { + if !barkfile.ValidSecretReference(name) { + return fmt.Errorf("invalid secret name %q", name) + } + if len(record.Value) == 0 || len(record.Value) > maxSecretBytes { + return fmt.Errorf("secret %q has invalid value length", name) + } + if len(record.Revision) != 32 { + return fmt.Errorf("secret %q has invalid revision", name) + } + if _, err := hex.DecodeString(record.Revision); err != nil { + return fmt.Errorf("secret %q has invalid revision", name) + } + } + return nil +} + +func cloneSnapshot(snapshot Snapshot) Snapshot { + clone := Snapshot{Version: snapshot.Version, VaultID: snapshot.VaultID, Secrets: make(map[string]Record, len(snapshot.Secrets))} + for name, record := range snapshot.Secrets { + record.Value = append([]byte(nil), record.Value...) + clone.Secrets[name] = record + } + return clone +} + +func randomHex(bytesCount int) (string, error) { + value := make([]byte, bytesCount) + if _, err := rand.Read(value); err != nil { + return "", fmt.Errorf("generate random identifier: %w", err) + } + return hex.EncodeToString(value), nil +} + +func writeFileAtomic(path string, contents []byte, mode os.FileMode) error { + directory := filepath.Dir(path) + if err := os.MkdirAll(directory, 0o700); err != nil { + return err + } + temporary, err := os.CreateTemp(directory, ".treatvault-*") + if err != nil { + return err + } + temporaryPath := temporary.Name() + defer os.Remove(temporaryPath) + if err := temporary.Chmod(mode); err != nil { + temporary.Close() + return err + } + if _, err := temporary.Write(contents); err != nil { + temporary.Close() + return err + } + if err := temporary.Sync(); err != nil { + temporary.Close() + return err + } + if err := temporary.Close(); err != nil { + return err + } + if err := os.Rename(temporaryPath, path); err != nil { + return err + } + directoryHandle, err := os.Open(directory) + if err != nil { + return err + } + defer directoryHandle.Close() + return directoryHandle.Sync() +} diff --git a/internal/vault/store_test.go b/internal/vault/store_test.go new file mode 100644 index 0000000..22b169e --- /dev/null +++ b/internal/vault/store_test.go @@ -0,0 +1,108 @@ +package vault + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "filippo.io/age" +) + +func TestStoreEncryptsUpdatesAndDeletesSecrets(t *testing.T) { + identity, err := age.GenerateX25519Identity() + if err != nil { + t.Fatal(err) + } + path := filepath.Join(t.TempDir(), "vault", "secrets.age") + if err := Initialize(path, identity); err != nil { + t.Fatal(err) + } + store := New(path, identity) + first, err := store.Set("database_password", []byte("correct horse battery staple")) + if err != nil { + t.Fatal(err) + } + record := first.Secrets["database_password"] + if record.Revision == "" || string(record.Value) != "correct horse battery staple" { + t.Fatalf("record = %#v", record) + } + encrypted, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if bytes.Contains(encrypted, []byte("correct horse battery staple")) || bytes.Contains(encrypted, []byte("database_password")) { + t.Fatal("encrypted file contains plaintext secret material") + } + + unchanged, err := store.Set("database_password", []byte("correct horse battery staple")) + if err != nil { + t.Fatal(err) + } + if unchanged.Secrets["database_password"].Revision != record.Revision { + t.Fatal("setting the same value changed its revision") + } + rotated, err := store.Set("database_password", []byte("new value")) + if err != nil { + t.Fatal(err) + } + if rotated.Secrets["database_password"].Revision == record.Revision { + t.Fatal("rotating a value did not change its revision") + } + if _, err := store.Delete("database_password"); err != nil { + t.Fatal(err) + } + loaded, err := store.Load() + if err != nil { + t.Fatal(err) + } + if len(loaded.Secrets) != 0 { + t.Fatalf("secrets = %#v", loaded.Secrets) + } +} + +func TestStoreRejectsWrongIdentityAndInvalidValues(t *testing.T) { + identity, err := age.GenerateX25519Identity() + if err != nil { + t.Fatal(err) + } + path := filepath.Join(t.TempDir(), "secrets.age") + if err := Initialize(path, identity); err != nil { + t.Fatal(err) + } + store := New(path, identity) + if _, err := store.Set("../unsafe", []byte("value")); err == nil { + t.Fatal("Set() accepted unsafe name") + } + if _, err := store.Set("empty", nil); err == nil { + t.Fatal("Set() accepted empty value") + } + wrongIdentity, err := age.GenerateX25519Identity() + if err != nil { + t.Fatal(err) + } + if _, err := New(path, wrongIdentity).Load(); err == nil { + t.Fatal("Load() succeeded with wrong identity") + } +} + +func TestGenerateIdentityDoesNotOverwrite(t *testing.T) { + path := filepath.Join(t.TempDir(), "identity.txt") + recipient, err := GenerateIdentity(path) + if err != nil { + t.Fatal(err) + } + if recipient == "" { + t.Fatal("recipient is empty") + } + if _, err := GenerateIdentity(path); err == nil { + t.Fatal("GenerateIdentity() overwrote existing identity") + } + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm() != 0o600 { + t.Fatalf("identity permissions = %o", info.Mode().Perm()) + } +} diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/ui/bun.lock b/ui/bun.lock new file mode 100644 index 0000000..dbdaaba --- /dev/null +++ b/ui/bun.lock @@ -0,0 +1,350 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "@barkstack/treatvault-ui", + "dependencies": { + "svelte": "^5.38.10", + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.2.0", + "jsdom": "^27.0.0", + "typescript": "^5.9.2", + "vite": "^7.1.5", + "vitest": "^3.2.4", + }, + }, + }, + "packages": { + "@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.2", "", { "dependencies": { "@csstools/css-calc": "^3.0.0", "@csstools/css-color-parser": "^4.0.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.5" } }, "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg=="], + + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.8.1", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.6" } }, "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ=="], + + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@6.1.1", "", {}, "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w=="], + + "@csstools/css-calc": ["@csstools/css-calc@3.4.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.2.3", "", { "dependencies": { "@csstools/color-helpers": "^6.1.1", "@csstools/css-calc": "^3.4.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], + + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.14", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], + + "@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.3", "", { "os": "android", "cpu": "arm" }, "sha512-w3Jnvi1ocaVm/c7yVPpfB98XeSRBMyzp6njL5MVVbGyXjpmUkN+s6Hp4t0PqhGCCaI1ZHMKXt/w0lA1RCaLVcw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.3", "", { "os": "android", "cpu": "arm64" }, "sha512-uI/ESiaIbbRYAEhzy8PCUWDp1hB0bjAqM06mW9flOoNO4Q8DQpeoREhBR5Hegfl+wpXiguyJv6XSPzEN7OxyHQ=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oxhrd1jmXLwWZ83eQYDXxuqRdkqkzrjR3JobKeuUyfdNZo11FuQIvqEOZhyIT7OBHxXoGslDDjN0cQcM6T0TqQ=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-7/YiIMghVE8DrxKvNdorAaJVdriOFgOIpdStnPx8ppx5zfTwC3jBCSEAIzB7JD5404m65THl6H93UTTVUvypmg=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GXFZRRoMAytaI5z6N3Zhfw0WL18Q0M8r95D5hlC4GqE/lGk8pbSJNUBoOWDfbm6dTciqHj2nU87tI5f6XhQiOg=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-77W+8X3ddYgPxUpB8nZFQs2Mq+wc4HVlcSRtApXLjYBcnPMkttrSnU8VwKQjeWYhMsITHFs5cWBQ8vz1Q+5RHQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.3", "", { "os": "linux", "cpu": "arm" }, "sha512-FVkwK+iUC+mq+GipVK46rRVticfAPtvPUNlqlGXUDxdVk/UGjQiiiUVPUrEXdSpU2ufU0XxLGyTqDtBidDOVmg=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.3", "", { "os": "linux", "cpu": "arm" }, "sha512-+aGU1t3398yQOVj1Bz8o3e+KtswxAPvO+mtxtNdfXYMkXIHu7XhhkCD7/DEH9q8tF8uhDnMWvfpUKI8y1sZJsg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-cR0kjpRXR2KJ2oQK8E2KTPtphs+b9hZ8IhTZubNryt/RsqgdOZBQ2Zq0q5UedtiIi0rs3jVhJh55RE1ZHUVGUA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-y1RYi4Q3/9ByVWSSt9kX2ustE0B7kFYbJ6zZdVZVyqopZs3yhCTwRfrjIX4vezUJInma/Gs6BOFDJg7yZmJ0IQ=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.3", "", { "os": "linux", "cpu": "none" }, "sha512-DNhEA5viIj3Z5bZLE4z4oV8N5ozWqDwyt7T6KG7VdLDJ0nW+rNOYlphBl4/3HQkK75qipPLsVOfStHHOwN9WSg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.3", "", { "os": "linux", "cpu": "none" }, "sha512-17gQCqrIpXBX2Cmi9/TygnVOqGbzsba/iaqcYSL8FY7lNugg+7AiYNs5c5nKWD+NRQha36Sa0CqkJqH4XVHwnQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-6LwVnZRIyINpdku/yOcI8Tm9YqLmhHK5emmlOOnW9tO0SYEm1FmKPcsSAGp0NBlqR2P04xaND4jvN6sTHqhq8A=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-xMUqkTXlEUtI/p5AAukMwBRr1enU3efsTeF+bskeFfk8t1C9rcC8sLREcZXmTfAXEbvRdJVSonVJez3TMlbR3w=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.3", "", { "os": "linux", "cpu": "none" }, "sha512-S3E94co9F9WRRqEaUoQZ38K1gCz6KiM+nL7/3ijq7fDGF3OznjS5TasgYITlvl27GQKtu4lOAOsr5MFwkijvOA=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.3", "", { "os": "linux", "cpu": "none" }, "sha512-1QtRDwG42x5BJI3s9mxu5rEjDnfbSnk20HQ9/ylTAYnSwYwxMVb+Vgu34wzzTQ7ogqBybebgQNUDAvZVQ38DbA=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-BQhejF6ZXOpxbngiNTP12GCGQeaDVL2QXGeBVViKIYzFHM5RKxTxwUMB1fr1BeNFphFMpnRqC5QSXFSa4z6UQw=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SXagRwnI2Wlwlitllu59UK/nGVbD1CKPcNqDplHwIC4BqJcpXFjD32d1R/RbuISa95HdQrZM3/7v4bKiowFaLA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.3", "", { "os": "linux", "cpu": "x64" }, "sha512-2IPozoEALRCziGqE8O9KMK60PMu5TS1huv4fwoeCexj+WjmcwFtX9CTOVbfXCUqcELAubEwRFPYlzb/WvwY2HQ=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-AoxqosUHT9IX54hFn2TiN6A7d6ZKTtE6pd2bqWtqkkNJ6HJGaU6FRouGX8L1O7R/ZwsnCnpQrHzb4pDEx+UHRQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.3", "", { "os": "none", "cpu": "arm64" }, "sha512-d+CaftKgmkFBzCwezMqqy1d0QNNYugqLCMcYVQWBy5SS2YfeMP8Q8ripkgx9O8IyBXXLHrJ+aaCV4U96usv6Yg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-xXlDF6nR1eOuXbdDy5Hl5fmtY7teUDevF/k0O7IPoZe4Tpmdv+lgdE5JRsnhQtt37ql9P0VF2kAN9a0OCZdo+Q=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-YtXAgLN+JP7Ay6qG3eWhc7IHMQPzLc8r3uvhAvlJIoCz/4Q32+Bl9Fmnywidh8v1GOIMmymjovfqY9ETAtysvA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.3", "", { "os": "win32", "cpu": "x64" }, "sha512-WuWtSJRNo549vzcfZyEgfqb6zeSgn1F+UE5kQ+BCjzz0W4MGCjntUHkZVc1VRuAM7+ULaSyhiPxD1spyewFvkQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.3", "", { "os": "win32", "cpu": "x64" }, "sha512-+lIKX7O0+IGe7WuhATaAMMeT7B76vfhXH/l9wLQL+nvyhbw2ohYCKIdWL56JfDu75CWt5oKRP4QFH/jkMtBquA=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="], + + "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@vitest/expect": ["@vitest/expect@3.2.7", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.7", "@vitest/utils": "3.2.7", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w=="], + + "@vitest/mocker": ["@vitest/mocker@3.2.7", "", { "dependencies": { "@vitest/spy": "3.2.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.7", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA=="], + + "@vitest/runner": ["@vitest/runner@3.2.7", "", { "dependencies": { "@vitest/utils": "3.2.7", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA=="], + + "@vitest/snapshot": ["@vitest/snapshot@3.2.7", "", { "dependencies": { "@vitest/pretty-format": "3.2.7", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g=="], + + "@vitest/spy": ["@vitest/spy@3.2.7", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ=="], + + "@vitest/utils": ["@vitest/utils@3.2.7", "", { "dependencies": { "@vitest/pretty-format": "3.2.7", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw=="], + + "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "bidi-js": ["bidi-js@1.1.0", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + + "cssstyle": ["cssstyle@5.3.7", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ=="], + + "data-urls": ["data-urls@6.0.1", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^15.1.0" } }, "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "devalue": ["devalue@5.9.2", "", {}, "sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w=="], + + "entities": ["entities@8.1.0", "", {}, "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "esrap": ["esrap@2.3.7", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-n2nf7fZR3c9yXf0BPEuHuXqT+KW0SJVj4cN5FMEkpCZ3scLjOQWpiccyCxVzCC2q1wubTghuEGzngJY/7Ah0Ow=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "jsdom": ["jsdom@27.4.0", "", { "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", "@exodus/bytes": "^1.6.0", "cssstyle": "^5.3.4", "data-urls": "^6.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.0", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.1.0", "ws": "^8.18.3", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + + "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="], + + "obug": ["obug@2.2.1", "", {}, "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q=="], + + "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "rollup": ["rollup@4.63.3", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.3", "@rollup/rollup-android-arm64": "4.63.3", "@rollup/rollup-darwin-arm64": "4.63.3", "@rollup/rollup-darwin-x64": "4.63.3", "@rollup/rollup-freebsd-arm64": "4.63.3", "@rollup/rollup-freebsd-x64": "4.63.3", "@rollup/rollup-linux-arm-gnueabihf": "4.63.3", "@rollup/rollup-linux-arm-musleabihf": "4.63.3", "@rollup/rollup-linux-arm64-gnu": "4.63.3", "@rollup/rollup-linux-arm64-musl": "4.63.3", "@rollup/rollup-linux-loong64-gnu": "4.63.3", "@rollup/rollup-linux-loong64-musl": "4.63.3", "@rollup/rollup-linux-ppc64-gnu": "4.63.3", "@rollup/rollup-linux-ppc64-musl": "4.63.3", "@rollup/rollup-linux-riscv64-gnu": "4.63.3", "@rollup/rollup-linux-riscv64-musl": "4.63.3", "@rollup/rollup-linux-s390x-gnu": "4.63.3", "@rollup/rollup-linux-x64-gnu": "4.63.3", "@rollup/rollup-linux-x64-musl": "4.63.3", "@rollup/rollup-openbsd-x64": "4.63.3", "@rollup/rollup-openharmony-arm64": "4.63.3", "@rollup/rollup-win32-arm64-msvc": "4.63.3", "@rollup/rollup-win32-ia32-msvc": "4.63.3", "@rollup/rollup-win32-x64-gnu": "4.63.3", "@rollup/rollup-win32-x64-msvc": "4.63.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-1i2XreiAoMMXuPGD6Msj2xWrMMkHojNRKivInxGQcg7/1KuPuYlfUutLyh4drnOxUTHX9cHI4wFoat8D/NKaBw=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + + "svelte": ["svelte@5.57.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@4.0.6", "", {}, "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg=="], + + "tldts": ["tldts@7.4.13", "", { "dependencies": { "tldts-core": "^7.4.13" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-iHtaIWWIbMDkCeJdTBzZFGgbluE5J+oHlb2g7+oAz1S1gpuVpabRZdQyd471Vl8UUkcz2vXSL8xZH2kyCe8tfA=="], + + "tldts-core": ["tldts-core@7.4.13", "", {}, "sha512-mbYsrih5FRtGxs3Usvl/PqwJsNpp+jsmrdFviiK02teHDG0/HebBG/pqCylje3kzgXYzuLoHJF/0mz9W53t8Xg=="], + + "tough-cookie": ["tough-cookie@6.0.2", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA=="], + + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="], + + "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], + + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], + + "vitest": ["vitest@3.2.7", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.7", "@vitest/mocker": "3.2.7", "@vitest/pretty-format": "^3.2.7", "@vitest/runner": "3.2.7", "@vitest/snapshot": "3.2.7", "@vitest/spy": "3.2.7", "@vitest/utils": "3.2.7", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.7", "@vitest/ui": "3.2.7", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + + "whatwg-url": ["whatwg-url@15.1.0", "", { "dependencies": { "tr46": "^6.0.0", "webidl-conversions": "^8.0.0" } }, "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "zimmerframe": ["zimmerframe@1.1.5", "", {}, "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA=="], + + "data-urls/whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..3f156b7 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,20 @@ +{ + "name": "@barkstack/treatvault-ui", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "test": "vitest run" + }, + "dependencies": { + "svelte": "^5.38.10" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.2.0", + "jsdom": "^27.0.0", + "typescript": "^5.9.2", + "vite": "^7.1.5", + "vitest": "^3.2.4" + } +} diff --git a/ui/src/TreatVault.svelte b/ui/src/TreatVault.svelte new file mode 100644 index 0000000..958c8e4 --- /dev/null +++ b/ui/src/TreatVault.svelte @@ -0,0 +1,226 @@ + + + + +
+
+
+ Secret management +

TreatVault

+

Namespaced secrets, encrypted at rest, synced to Docker Swarm.

+
+
+
Sync state{error ? 'Degraded' : (status?.state ?? 'Connecting')}
+
+
+ + {#if route !== '/'} + + {/if} + + {#if loading} +
+ {:else if error} + + {:else} + {#if status?.error} + + {/if} + +
+
Add secret

New namespaced secret

+
+ + + +
+

Values are sent to TreatVault and never displayed again. The encrypted file is the source of truth.

+
+ +
+
Vault contents

{route.startsWith('/audit') ? 'All managed secrets' : 'Managed secrets'}

{#if route === '/'}{/if}
+ {#if !status?.secrets.length} +
No secrets are stored yet.
+ {:else} +
+
NameSyncConsumersActions
+ {#each status.secrets as secret (secret.name)} +
+ {secret.name} + {secret.synced ? 'Synced' : 'Pending'} + {secret.inUse ? 'In use' : 'Unreferenced'} + + {#if rotateTarget === secret.name} + + + + {:else} + + + {/if} + +
+ {/each} +
+ {/if} +
+ {/if} +
+ + diff --git a/ui/src/entry.ts b/ui/src/entry.ts new file mode 100644 index 0000000..9bc4843 --- /dev/null +++ b/ui/src/entry.ts @@ -0,0 +1 @@ +import './TreatVault.svelte'; diff --git a/ui/src/events.test.ts b/ui/src/events.test.ts new file mode 100644 index 0000000..db9bdad --- /dev/null +++ b/ui/src/events.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it, vi } from 'vitest'; +import { dispatchConfirm, dispatchNavigate, dispatchNotify } from './events'; + +describe('Barkstack host events', () => { + it('requests host-owned navigation with a composed custom event', () => { + const target = new EventTarget(); + const listener = vi.fn(); + target.addEventListener('barkstack:navigate', listener); + dispatchNavigate(target, '/treatvault/audit'); + const event = listener.mock.calls[0][0] as CustomEvent; + expect(event.detail).toEqual({ path: '/treatvault/audit' }); + expect(event.composed).toBe(true); + }); + + it('requests a global notification', () => { + const target = new EventTarget(); + let detail: unknown; + target.addEventListener('barkstack:notify', (event) => { detail = (event as CustomEvent).detail; }); + dispatchNotify(target, 'warning', 'Rotation failed'); + expect(detail).toEqual({ level: 'warning', message: 'Rotation failed' }); + }); + + it('requests confirmation with a resolve callback', () => { + const target = new EventTarget(); + let detail: { message: string; resolve: (confirmed: boolean) => void } | undefined; + target.addEventListener('barkstack:confirm', (event) => { detail = (event as CustomEvent).detail; }); + const resolve = vi.fn(); + dispatchConfirm(target, 'Delete secret?', resolve); + expect(detail?.message).toBe('Delete secret?'); + detail?.resolve(true); + expect(resolve).toHaveBeenCalledWith(true); + }); +}); diff --git a/ui/src/events.ts b/ui/src/events.ts new file mode 100644 index 0000000..d634d37 --- /dev/null +++ b/ui/src/events.ts @@ -0,0 +1,23 @@ +export function dispatchNavigate(target: EventTarget, path: string): void { + target.dispatchEvent(new CustomEvent('barkstack:navigate', { + bubbles: true, + composed: true, + detail: { path } + })); +} + +export function dispatchNotify(target: EventTarget, level: 'success' | 'info' | 'warning' | 'error', message: string): void { + target.dispatchEvent(new CustomEvent('barkstack:notify', { + bubbles: true, + composed: true, + detail: { level, message } + })); +} + +export function dispatchConfirm(target: EventTarget, message: string, resolve: (confirmed: boolean) => void): void { + target.dispatchEvent(new CustomEvent('barkstack:confirm', { + bubbles: true, + composed: true, + detail: { message, resolve } + })); +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..5df25e9 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2022" + }, + "include": ["src/**/*.ts", "src/**/*.svelte", "vite.config.ts"] +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts new file mode 100644 index 0000000..5f362ff --- /dev/null +++ b/ui/vite.config.ts @@ -0,0 +1,18 @@ +import { svelte } from '@sveltejs/vite-plugin-svelte'; +import { defineConfig } from 'vite'; +import { resolve } from 'node:path'; + +export default defineConfig({ + plugins: [svelte({ compilerOptions: { customElement: true } })], + build: { + lib: { + entry: resolve(__dirname, 'src/entry.ts'), + formats: ['es'], + fileName: () => 'assets/entry.js' + }, + outDir: '../internal/adminui/dist', + emptyOutDir: true, + target: 'es2022' + }, + test: { environment: 'jsdom' } +});