From 32acb018d81e5a7efb23f2551112038b56b0576b Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Fri, 25 Sep 2026 20:22:24 -0700 Subject: [PATCH 01/16] Add one-command local Git demo Co-authored-by: Codex --- Makefile | 5 +- README.md | 3 +- examples/git/README.md | 85 ++++++--------------- scripts/run-git-local.sh | 154 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 64 deletions(-) create mode 100755 scripts/run-git-local.sh diff --git a/Makefile b/Makefile index 5e8a4a2..b21e5b3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: check test bench minio-test minio-performance gc-acceptance git-check git-build git-spin-config-test wasi-credential-test git-provider-test git-qualification-tools-test +.PHONY: check test bench minio-test minio-performance gc-acceptance git-check git-build git-local git-spin-config-test wasi-credential-test git-provider-test git-qualification-tools-test check: cargo fmt --all --check @@ -48,6 +48,9 @@ git-build: $(MAKE) -C $(GIT_EXAMPLE_DIR) build cd $(GIT_EXAMPLE_DIR) && wac plug --plug ../wal-component/target/wasm32-wasip2/release/object_log_component.wasm main.wasm -o git.wasm +git-local: + ./scripts/run-git-local.sh + git-spin-config-test: git-build ./scripts/test-git-spin-config.sh diff --git a/README.md b/README.md index e47bda8..6738a24 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,8 @@ host. Spin dependencies remain outside the core and KV workspaces. [AWS host](https://github.com/carsonfarmer/object-log/blob/main/examples/git/qualification/aws/HOSTING.md) supplies HTTPS, instance-role credentials, and a worker that schedules those endpoints. - Use the provider suite to qualify the intended storage, client and host limits. + Run `make git-local` for a disposable local service, or use the provider suite + to qualify the intended storage, client and host limits. The core library has no Git, Spin, or serverless-runtime dependency. diff --git a/examples/git/README.md b/examples/git/README.md index f3aac85..3ac25d4 100644 --- a/examples/git/README.md +++ b/examples/git/README.md @@ -23,84 +23,40 @@ service over HTTPS. Run the provider suite for your intended workload and host capacity before deployment. It is pre-release: use a fresh object-store prefix when changing incompatible revisions. -## Build +## Setup Install Go 1.27.1, the repository's pinned Rust toolchain, Spin CLI v4.1.0, -`wac` v0.11.0, MinIO, -and the AWS CLI. For a pinned MinIO container or a native source build, see +`wac` v0.11.0, the AWS CLI, and either MinIO or Docker. +For a pinned MinIO container or a native source build, see [the contributor setup](../../CONTRIBUTING.md). Then run from the repository root: ```sh rustup target add wasm32-wasip2 -make git-check -make git-build ``` -`git-build` creates the Go component, builds the Rust WAL component, and composes -both into `examples/git/git.wasm`. Spin loads that final file. -The first build also compiles the pinned component build tool; later builds use -the cached binary. +The first build compiles the pinned component build tool; later builds use +the cached binary. `make git-build` builds and composes the Go and Rust WAL +components into `examples/git/git.wasm` without starting the service. ## Run locally -Start an isolated MinIO instance in one terminal using the native binary from -the contributor setup. The command prints its unique data directory; remove -that directory after stopping MinIO: - -```sh -minio_data="$(mktemp -d "${TMPDIR:-/tmp}/object-log-git-minio.XXXXXX")" -echo "MinIO data: $minio_data" -minio_binary="${OBJECT_LOG_MINIO_BINARY:-$(go env GOPATH)/bin/minio}" -MINIO_ROOT_USER=objectlog MINIO_ROOT_PASSWORD=local-test-secret \ - "$minio_binary" server "$minio_data" --address 127.0.0.1:19090 -``` - -In another terminal, create the bucket and a protected Spin variables file. -Each run gets a new WAL prefix: - -```sh -local_config="$(mktemp -d "${TMPDIR:-/tmp}/object-log-git-config.XXXXXX")" -AWS_ACCESS_KEY_ID=objectlog AWS_SECRET_ACCESS_KEY=local-test-secret \ - AWS_DEFAULT_REGION=us-east-1 aws --endpoint-url http://127.0.0.1:19090 \ - s3api create-bucket --bucket wal-proof -test_id="local-$(date -u +%Y%m%dT%H%M%SZ)-$$" -( - umask 077 - cat >"$local_config/variables.toml" <&1 | tee /tmp/object-log-git-spin.log -``` - -Before sending Git traffic, validate the same MinIO settings from another -terminal. A failed probe returns HTTP 503; fix the backend before serving: +With the tools above installed, run from the repository root: ```sh -curl --fail-with-body -u git:local-git-password \ - -X POST http://127.0.0.1:19100/_validate_backend +make git-local ``` -The repositories are available at: +This builds the service, starts isolated MinIO and Spin instances, creates the +bucket, and validates storage before printing the Git URLs. It uses a native +MinIO binary when available, otherwise the pinned Docker image. No AWS account +is involved. The default URLs are: ```text http://127.0.0.1:19100/sha1.git http://127.0.0.1:19100/sha256.git ``` -Initialize a repository with its first push. Enter `git` and the local password +Initialize a repository with its first push. Enter `git` and `local-git-password` when Git prompts. Reads never create repositories: ```sh @@ -114,8 +70,11 @@ cd .. git clone http://127.0.0.1:19100/sha256.git cloned-demo ``` -When finished, stop Spin and MinIO, then remove `"$local_config"` and the -printed MinIO data directory. +Press Ctrl-C in the service terminal to stop Spin and MinIO and remove all demo +data. Set `OBJECT_LOG_GIT_LOCAL_PORT` to choose another loopback port. +To keep data or use an existing backend, build with `make git-build`, configure +the variables below, and run Spin directly. Call `POST /_validate_backend` with +administrator credentials before serving traffic. The local `password` mode uses HTTP Basic authentication; the username is ignored. An empty password is a configuration error. Anonymous access requires @@ -126,7 +85,8 @@ rewrite branches. ## Configuration -All values are Spin variables. The defaults target the local MinIO setup above. +All values are Spin variables. The local helper fills the endpoint and +credentials for its disposable MinIO instance. | Variable | Default | Purpose | | --- | --- | --- | @@ -248,11 +208,12 @@ and restart it before the first suite run or a rerun: ```sh GIT_PROBE_URL=http://127.0.0.1:19100 \ GIT_PROBE_PASSWORD=local-git-password \ -GIT_PROBE_LOG=/tmp/object-log-git-spin.log make git-provider-test +GIT_PROBE_LOG=/path/to/printed/spin.log make git-provider-test ``` Set `GIT_PROBE_PASSWORD` and `GIT_PROBE_BRANCH` when those values are configured. -`GIT_PROBE_LOG` supplies per-request storage counters when Spin omits HTTP trailers. +Use the Spin log path printed by `make git-local` for `GIT_PROBE_LOG`; it supplies +per-request storage counters when Spin omits HTTP trailers. The provider suite uses installed Git as an independent protocol and integrity oracle. Larger opt-in cases are controlled by these environment variables: diff --git a/scripts/run-git-local.sh b/scripts/run-git-local.sh new file mode 100755 index 0000000..5c01e7d --- /dev/null +++ b/scripts/run-git-local.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd "$(dirname "$0")/.." && pwd)" +for tool in make spin aws curl python3; do + command -v "$tool" >/dev/null || { echo "Install $tool before running the Git demo." >&2; exit 2; } +done + +# Build before starting disposable services, so a failed build leaves no data. +make -C "$root" git-build + +demo_dir="$(mktemp -d "${TMPDIR:-/tmp}/object-log-git-local.XXXXXX")" +minio_pid="" +spin_pid="" +container="" +cleanup() { + local status=$? + trap - EXIT INT TERM + if [[ -n "$spin_pid" ]]; then + kill "$spin_pid" 2>/dev/null || true + wait "$spin_pid" 2>/dev/null || true + fi + if [[ -n "$minio_pid" ]]; then + kill "$minio_pid" 2>/dev/null || true + wait "$minio_pid" 2>/dev/null || true + fi + if [[ -n "$container" ]]; then + docker stop "$container" >/dev/null 2>&1 || true + fi + rm -r -- "$demo_dir" + exit "$status" +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +git_port="${OBJECT_LOG_GIT_LOCAL_PORT:-19100}" +python3 - "$git_port" <<'PY' +import socket +import sys + +with socket.socket() as listener: + try: + port = int(sys.argv[1]) + if not 1 <= port <= 65535: + raise ValueError("expected an integer from 1 to 65535") + listener.bind(("127.0.0.1", port)) + except (OSError, ValueError) as error: + raise SystemExit(f"Git port {sys.argv[1]} is unavailable: {error}") +PY + +access_key="objectlog" +secret_key="local-test-secret" +bucket="wal-proof" +minio_binary="${OBJECT_LOG_MINIO_BINARY:-$(command -v minio || true)}" +if [[ -z "$minio_binary" && -x "$(go env GOPATH)/bin/minio" ]]; then + minio_binary="$(go env GOPATH)/bin/minio" +fi +if [[ -n "$minio_binary" ]]; then + [[ -x "$minio_binary" ]] || { echo "MinIO binary is not executable: $minio_binary" >&2; exit 2; } + minio_port="$(python3 - <<'PY' +import socket + +with socket.socket() as listener: + listener.bind(("127.0.0.1", 0)) + print(listener.getsockname()[1]) +PY +)" + endpoint="http://127.0.0.1:$minio_port" + MINIO_ROOT_USER="$access_key" MINIO_ROOT_PASSWORD="$secret_key" MINIO_BROWSER=off \ + "$minio_binary" server "$demo_dir/minio" --address "127.0.0.1:$minio_port" \ + >"$demo_dir/minio.log" 2>&1 & + minio_pid=$! +else + command -v docker >/dev/null || { echo "Install MinIO or Docker before running the Git demo." >&2; exit 2; } + docker info >/dev/null + container="object-log-git-local-$$" + docker run --detach --rm --name "$container" --publish 127.0.0.1::9000 \ + --env "MINIO_ROOT_USER=$access_key" --env "MINIO_ROOT_PASSWORD=$secret_key" \ + 'minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e' \ + server /data >/dev/null + endpoint="http://$(docker port "$container" 9000/tcp)" +fi + +ready=0 +for _ in {1..60}; do + if curl --connect-timeout 1 --max-time 2 --fail --silent "$endpoint/minio/health/ready" >/dev/null; then + ready=1 + break + fi + if [[ -n "$minio_pid" ]] && ! kill -0 "$minio_pid" 2>/dev/null; then break; fi + sleep 1 +done +if [[ "$ready" != 1 ]]; then + if [[ -n "$minio_pid" ]]; then cat "$demo_dir/minio.log" >&2; else docker logs "$container" >&2; fi + echo "MinIO did not start." >&2 + exit 1 +fi + +AWS_ACCESS_KEY_ID="$access_key" AWS_SECRET_ACCESS_KEY="$secret_key" \ + AWS_SESSION_TOKEN="" AWS_SECURITY_TOKEN="" \ + AWS_DEFAULT_REGION=us-east-1 aws --no-cli-pager --endpoint-url "$endpoint" \ + s3api create-bucket --bucket "$bucket" >/dev/null + +cat >"$demo_dir/variables.toml" <"$demo_dir/spin.log" 2>&1 & +spin_pid=$! + +validated=0 +for _ in {1..60}; do + if curl --connect-timeout 1 --max-time 5 --fail --silent \ + --user git:local-git-password --request POST \ + "http://127.0.0.1:$git_port/_validate_backend" >/dev/null; then + validated=1 + break + fi + if ! kill -0 "$spin_pid" 2>/dev/null; then break; fi + sleep 1 +done +if [[ "$validated" != 1 ]]; then + cat "$demo_dir/spin.log" >&2 + echo "Spin did not validate its MinIO backend." >&2 + exit 1 +fi + +cat < README.md + git add README.md && git commit -m 'Initial commit' + git push http://127.0.0.1:$git_port/sha256.git main + +When prompted, use username git and password local-git-password. +Press Ctrl-C here to stop Spin and MinIO and remove their disposable data. +EOF +wait "$spin_pid" From 429cbf81f1c179405acd36814d2e0f2741df33e4 Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Fri, 25 Sep 2026 20:35:56 -0700 Subject: [PATCH 02/16] Allow local demo to select an optional Spin manifest Co-authored-by: Codex --- scripts/run-git-local.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/run-git-local.sh b/scripts/run-git-local.sh index 5c01e7d..0639b39 100755 --- a/scripts/run-git-local.sh +++ b/scripts/run-git-local.sh @@ -114,7 +114,7 @@ git_password = "local-git-password" EOF chmod 600 "$demo_dir/variables.toml" -(cd "$root/examples/git" && spin up --listen "127.0.0.1:$git_port" \ +(cd "$root/examples/git" && spin up -f "${OBJECT_LOG_GIT_LOCAL_MANIFEST:-spin.toml}" --listen "127.0.0.1:$git_port" \ --variable "@$demo_dir/variables.toml") >"$demo_dir/spin.log" 2>&1 & spin_pid=$! From 478439118e5a877376cdb45e608ddd96a686eb31 Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Fri, 25 Sep 2026 20:38:47 -0700 Subject: [PATCH 03/16] Add a minimal TypeScript Spin repository viewer Co-authored-by: Codex --- Makefile | 2 +- examples/git/.gitignore | 2 + examples/git/browse.go | 216 ++ examples/git/browse_test.go | 207 ++ examples/git/main.go | 5 +- examples/git/repositories.go | 9 +- examples/git/viewer/.gitignore | 3 + examples/git/viewer/README.md | 79 + examples/git/viewer/build.mjs | 50 + examples/git/viewer/package-lock.json | 3662 +++++++++++++++++++++++++ examples/git/viewer/package.json | 26 + examples/git/viewer/src/client.ts | 301 ++ examples/git/viewer/src/index.ts | 45 + examples/git/viewer/src/page.html | 92 + examples/git/viewer/src/style.css | 381 +++ examples/git/viewer/tsconfig.json | 12 + 16 files changed, 5087 insertions(+), 5 deletions(-) create mode 100644 examples/git/browse.go create mode 100644 examples/git/browse_test.go create mode 100644 examples/git/viewer/.gitignore create mode 100644 examples/git/viewer/README.md create mode 100644 examples/git/viewer/build.mjs create mode 100644 examples/git/viewer/package-lock.json create mode 100644 examples/git/viewer/package.json create mode 100644 examples/git/viewer/src/client.ts create mode 100644 examples/git/viewer/src/index.ts create mode 100644 examples/git/viewer/src/page.html create mode 100644 examples/git/viewer/src/style.css create mode 100644 examples/git/viewer/tsconfig.json diff --git a/Makefile b/Makefile index 5e8a4a2..777ec04 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ gc-acceptance: GIT_EXAMPLE_DIR = examples/git WAL_COMPONENT = examples/wal-component/Cargo.toml -GIT_TEST_FILES = access.go access_test.go auth.go auth_test.go repositories.go repositories_test.go imports.go imports_test.go receive_transport.go receive_transport_test.go request_config.go request_config_test.go import_pack.go import_pack_test.go import_pack_lifecycle_test.go limits.go limits_test.go index.go index_test.go codec.go codec_test.go delta.go delta_test.go read_retry.go read_retry_test.go retention.go retention_test.go retry.go retry_test.go fetch_policy.go fetch_policy_test.go validate_objects.go validate_objects_test.go validate.go validate_refname.go validate_refname_test.go validate_native_test.go +GIT_TEST_FILES = browse.go browse_test.go access.go access_test.go auth.go auth_test.go repositories.go repositories_test.go imports.go imports_test.go receive_transport.go receive_transport_test.go request_config.go request_config_test.go import_pack.go import_pack_test.go import_pack_lifecycle_test.go limits.go limits_test.go index.go index_test.go codec.go codec_test.go delta.go delta_test.go read_retry.go read_retry_test.go retention.go retention_test.go retry.go retry_test.go fetch_policy.go fetch_policy_test.go validate_objects.go validate_objects_test.go validate.go validate_refname.go validate_refname_test.go validate_native_test.go git-check: git-qualification-tools-test test -z "$$(gofmt -l $(GIT_EXAMPLE_DIR)/*.go $(GIT_EXAMPLE_DIR)/tests/*.go)" diff --git a/examples/git/.gitignore b/examples/git/.gitignore index b87c806..74417da 100644 --- a/examples/git/.gitignore +++ b/examples/git/.gitignore @@ -4,3 +4,5 @@ /git.wasm /.spin/ /.componentize/ + +/spin.viewer.toml diff --git a/examples/git/browse.go b/examples/git/browse.go new file mode 100644 index 0000000..504781d --- /dev/null +++ b/examples/git/browse.go @@ -0,0 +1,216 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + "slices" + "strings" + "unicode/utf8" + + "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/plumbing/filemode" + "github.com/go-git/go-git/v6/plumbing/object" + "github.com/go-git/go-git/v6/plumbing/storer" +) + +const browseFileBytes = 256 << 10 + +var errBrowseNotFound = errors.New("branch or path not found") + +type browseView struct { + Branch string `json:"branch"` + Branches []string `json:"branches"` + Path string `json:"path"` + History []browseCommit `json:"history"` + Entries []browseEntry `json:"entries"` + More bool `json:"more"` + File *browseFile `json:"file,omitempty"` +} + +type browseCommit struct { + ID string `json:"id"` + Title string `json:"title"` + Author string `json:"author"` + Date string `json:"date"` +} + +type browseEntry struct { + Name string `json:"name"` + Kind string `json:"kind"` + ID string `json:"id"` +} + +type browseFile struct { + ID string `json:"id"` + Size int64 `json:"size"` + State string `json:"state"` + Text string `json:"text,omitempty"` +} + +func serveBrowse(w http.ResponseWriter, r *http.Request, s *store) error { + query, err := url.ParseQuery(r.URL.RawQuery) + if err != nil || !validBrowseQuery(query) { + http.Error(w, "invalid browse query", http.StatusBadRequest) + return nil + } + view, err := browseSnapshot(s, s.meta, query) + if errors.Is(err, errBrowseNotFound) { + http.NotFound(w, r) + return nil + } + if err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("X-Content-Type-Options", "nosniff") + return json.NewEncoder(w).Encode(view) +} + +func validBrowseQuery(query url.Values) bool { + for name, values := range query { + if (name != "ref" && name != "path") || len(values) != 1 || len(values[0]) > 4096 { + return false + } + } + path := query.Get("path") + if path == "" { + return true + } + parts := strings.Split(path, "/") + if len(parts) > 64 || !utf8.ValidString(path) { + return false + } + for _, part := range parts { + if part == "" || part == "." || part == ".." || strings.ContainsRune(part, 0) { + return false + } + } + return true +} + +// Only configured branch tips are selectable. Object IDs cannot expose objects +// left behind by deleted refs, and paths are traversed within that tip's tree. +func browseSnapshot(s storer.EncodedObjectStorer, meta rootMeta, query url.Values) (*browseView, error) { + view := &browseView{ + Branch: query.Get("ref"), Path: query.Get("path"), + Branches: []string{}, History: []browseCommit{}, Entries: []browseEntry{}, + } + for ref := range meta.Refs { + if strings.HasPrefix(ref, "refs/heads/") { + view.Branches = append(view.Branches, ref) + } + } + slices.Sort(view.Branches) + if view.Branch == "" { + view.Branch = meta.Head + if _, exists := meta.Refs[view.Branch]; !exists && len(view.Branches) > 0 { + view.Branch = view.Branches[0] + } + } + if len(view.Branches) == 0 && query.Get("ref") == "" && view.Path == "" { + return view, nil + } + id, exists := meta.Refs[view.Branch] + if !exists || !slices.Contains(view.Branches, view.Branch) { + return nil, errBrowseNotFound + } + commit, err := object.GetCommit(s, plumbing.NewHash(id)) + if err != nil { + return nil, err + } + current := commit + for i := range 8 { + title := strings.SplitN(current.Message, "\n", 2)[0] + view.History = append(view.History, browseCommit{ + ID: current.Hash.String(), Title: strings.ToValidUTF8(title[:min(len(title), 512)], "�"), + Author: current.Author.Name[:min(len(current.Author.Name), 512)], Date: current.Author.When.Format("2006-01-02T15:04:05Z07:00"), + }) + if len(current.ParentHashes) == 0 || i == 7 { + break + } + current, err = object.GetCommit(s, current.ParentHashes[0]) + if err != nil { + return nil, err + } + } + tree, err := object.GetTree(s, commit.TreeHash) + if err != nil { + return nil, err + } + if view.Path != "" { + parts := strings.Split(view.Path, "/") + for i, part := range parts { + index := slices.IndexFunc(tree.Entries, func(e object.TreeEntry) bool { return e.Name == part }) + if index < 0 { + return nil, errBrowseNotFound + } + entry := tree.Entries[index] + if entry.Mode != filemode.Dir { + if i != len(parts)-1 { + return nil, errBrowseNotFound + } + view.File, err = browseBlob(s, entry) + return view, err + } + tree, err = object.GetTree(s, entry.Hash) + if err != nil { + return nil, err + } + } + } + view.More = len(tree.Entries) > 500 + for _, entry := range tree.Entries[:min(len(tree.Entries), 500)] { + view.Entries = append(view.Entries, browseEntry{Name: entry.Name, Kind: browseKind(entry.Mode), ID: entry.Hash.String()}) + } + return view, nil +} + +func browseKind(mode filemode.FileMode) string { + switch mode { + case filemode.Dir: + return "directory" + case filemode.Submodule: + return "submodule" + default: + return "file" + } +} + +func browseBlob(s storer.EncodedObjectStorer, entry object.TreeEntry) (*browseFile, error) { + file := &browseFile{ID: entry.Hash.String(), State: "submodule"} + if entry.Mode == filemode.Submodule { + return file, nil + } + blob, err := object.GetBlob(s, entry.Hash) + if err != nil { + return nil, err + } + file.Size = blob.Size + file.State = "large" + if blob.Size > browseFileBytes { + return file, nil + } + r, err := blob.Reader() + if err != nil { + return nil, err + } + data, err := io.ReadAll(io.LimitReader(r, browseFileBytes+1)) + err = errors.Join(err, r.Close()) + if err != nil { + return nil, err + } + if len(data) > browseFileBytes { + return file, nil + } + file.State = "binary" + if utf8.Valid(data) && !bytes.ContainsRune(data, 0) { + file.State = "text" + file.Text = string(data) + } + return file, nil +} diff --git a/examples/git/browse_test.go b/examples/git/browse_test.go new file mode 100644 index 0000000..0de525e --- /dev/null +++ b/examples/git/browse_test.go @@ -0,0 +1,207 @@ +//go:build git_native_test + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/plumbing/filemode" + "github.com/go-git/go-git/v6/plumbing/format/config" + "github.com/go-git/go-git/v6/plumbing/object" + "github.com/go-git/go-git/v6/plumbing/storer" + "github.com/go-git/go-git/v6/storage/memory" +) + +func browseFixture(t *testing.T, format config.ObjectFormat) *store { + t.Helper() + s := &store{Storage: memory.NewStorage(memory.WithObjectFormat(format))} + blob := func(text string) plumbing.Hash { + o := s.NewEncodedObject() + o.SetType(plumbing.BlobObject) + w, _ := o.Writer() + _, _ = w.Write([]byte(text)) + _ = w.Close() + id, err := s.SetEncodedObject(o) + if err != nil { + t.Fatal(err) + } + return id + } + put := func(value interface { + Encode(plumbing.EncodedObject) error + }) plumbing.Hash { + o := s.NewEncodedObject() + if err := value.Encode(o); err != nil { + t.Fatal(err) + } + id, err := s.SetEncodedObject(o) + if err != nil { + t.Fatal(err) + } + return id + } + subtree := put(&object.Tree{Entries: []object.TreeEntry{ + {Name: ".txt", Mode: filemode.Regular, Hash: blob("\n")}, + }}) + tree := put(&object.Tree{Entries: []object.TreeEntry{ + {Name: "binary", Mode: filemode.Regular, Hash: blob("a\x00b")}, + {Name: "docs", Mode: filemode.Dir, Hash: subtree}, + {Name: "large", Mode: filemode.Regular, Hash: blob(strings.Repeat("x", browseFileBytes+1))}, + {Name: "link", Mode: filemode.Symlink, Hash: blob("docs/.txt")}, + }}) + var tip plumbing.Hash + for i := range 10 { + commit := &object.Commit{TreeHash: tree, Message: "Commit " + string(rune('A'+i)) + "\nDetails"} + if i != 0 { + commit.ParentHashes = []plumbing.Hash{tip} + } + tip = put(commit) + } + s.meta = rootMeta{Format: format, Head: "refs/heads/main", Refs: map[string]string{ + "refs/heads/main": tip.String(), "refs/tags/v1": tip.String(), + }} + return s +} + +func TestBrowseSnapshotUsesBranchTreeAndBoundsHistory(t *testing.T) { + for _, format := range []config.ObjectFormat{config.SHA1, config.SHA256} { + t.Run(format.String(), func(t *testing.T) { + s := browseFixture(t, format) + view, err := browseSnapshot(s, s.meta, url.Values{}) + if err != nil { + t.Fatal(err) + } + if len(view.Branches) != 1 || len(view.History) != 8 || view.History[0].Title != "Commit J" { + t.Fatalf("unexpected branch/history: %+v", view) + } + if len(view.Entries) != 4 || len(view.History[0].ID) != format.HexSize() { + t.Fatalf("wrong tree or hash width: %+v", view) + } + for _, ref := range []string{"refs/tags/v1", view.History[0].ID, "refs/heads/missing"} { + _, err := browseSnapshot(s, s.meta, url.Values{"ref": {ref}}) + if !errors.Is(err, errBrowseNotFound) { + t.Fatalf("unselectable ref %q: %v", ref, err) + } + } + for _, path := range []string{"missing", "binary/child"} { + _, err := browseSnapshot(s, s.meta, url.Values{"path": {path}}) + if !errors.Is(err, errBrowseNotFound) { + t.Fatalf("missing path %q: %v", path, err) + } + } + }) + } +} + +func TestBrowseTextBinaryLargeAndSymlink(t *testing.T) { + s := browseFixture(t, config.SHA256) + for _, test := range []struct{ path, state, text string }{ + {path: "docs/.txt", state: "text", text: "\n"}, + {path: "binary", state: "binary"}, + {path: "large", state: "large"}, + {path: "link", state: "text", text: "docs/.txt"}, + } { + view, err := browseSnapshot(s, s.meta, url.Values{"path": {test.path}}) + if err != nil || view.File == nil || view.File.State != test.state || view.File.Text != test.text { + t.Fatalf("path %q: view=%+v err=%v", test.path, view, err) + } + } +} + +func TestBrowseRejectsAmbiguousQueriesAndUsesReadPermissions(t *testing.T) { + s := browseFixture(t, config.SHA1) + for _, query := range []string{"ref=a&ref=b", "path=../secret", "path=a//b", "path=/etc", "path=a/.", "path=%ZZ", "other=x"} { + w := httptest.NewRecorder() + err := serveBrowse(w, httptest.NewRequest(http.MethodGet, "/team/alpha.git/_browse?"+query, nil), s) + if err != nil || w.Code != http.StatusBadRequest { + t.Fatalf("query %q: status=%d err=%v", query, w.Code, err) + } + } + route, err := resolveRepository(repositoriesForTest(t), httptest.NewRequest(http.MethodGet, "/team/alpha.git/_browse?path=docs", nil)) + if err != nil || route.Action != gitRead || route.Method != http.MethodGet { + t.Fatalf("wrong authorization route: %+v %v", route, err) + } + _, err = resolveRepository(repositoriesForTest(t), httptest.NewRequest(http.MethodPost, "/team/alpha.git/_browse", nil)) + if !errors.Is(err, errRepositoryMethod) { + t.Fatalf("browse accepted a mutation method: %v", err) + } + w := httptest.NewRecorder() + if err := serveBrowse(w, httptest.NewRequest(http.MethodGet, "/team/alpha.git/_browse", nil), s); err != nil { + t.Fatal(err) + } + var view browseView + if err := json.Unmarshal(w.Body.Bytes(), &view); err != nil || w.Header().Get("Cache-Control") != "no-store" { + t.Fatalf("invalid uncached JSON: %v", err) + } +} + +func TestBrowseEscapedNamesAndDirectoryLimit(t *testing.T) { + s := browseFixture(t, config.SHA256) + commit, err := object.GetCommit(s, plumbing.NewHash(s.meta.Refs[s.meta.Head])) + if err != nil { + t.Fatal(err) + } + tree, err := object.GetTree(s, commit.TreeHash) + if err != nil { + t.Fatal(err) + } + blob := tree.Entries[0].Hash + name := "space #?.café\\file" + entries := []object.TreeEntry{} + for i := range 501 { + entries = append(entries, object.TreeEntry{Name: fmt.Sprintf("file%04d", i), Mode: filemode.Regular, Hash: blob}) + } + entries = append(entries, object.TreeEntry{Name: name, Mode: filemode.Regular, Hash: blob}) + o := s.NewEncodedObject() + if err := (&object.Tree{Entries: entries}).Encode(o); err != nil { + t.Fatal(err) + } + treeID, err := s.SetEncodedObject(o) + if err != nil { + t.Fatal(err) + } + o = s.NewEncodedObject() + if err := (&object.Commit{TreeHash: treeID}).Encode(o); err != nil { + t.Fatal(err) + } + tip, err := s.SetEncodedObject(o) + if err != nil { + t.Fatal(err) + } + ref := "refs/heads/feature/100%" + s.meta.Refs[ref] = tip.String() + view, err := browseSnapshot(s, s.meta, url.Values{"ref": {ref}}) + if err != nil || !view.More || len(view.Entries) != 500 { + t.Fatalf("directory bound: view=%+v err=%v", view, err) + } + query := url.Values{"ref": {ref}, "path": {name}} + w := httptest.NewRecorder() + if err := serveBrowse(w, httptest.NewRequest(http.MethodGet, "/showcase.git/_browse?"+query.Encode(), nil), s); err != nil { + t.Fatal(err) + } + if w.Code != http.StatusOK { + t.Fatalf("escaped path did not round trip: status=%d body=%s", w.Code, w.Body) + } +} + +type failingBrowseObjects struct{ storer.EncodedObjectStorer } + +func (failingBrowseObjects) EncodedObject(plumbing.ObjectType, plumbing.Hash) (plumbing.EncodedObject, error) { + return nil, errExpired +} + +func TestBrowseStorageFailuresRemainRetryable(t *testing.T) { + s := browseFixture(t, config.SHA1) + _, err := browseSnapshot(failingBrowseObjects{EncodedObjectStorer: s}, s.meta, url.Values{}) + if !errors.Is(err, errExpired) { + t.Fatalf("storage failure lost: %v", err) + } +} diff --git a/examples/git/main.go b/examples/git/main.go index 2be44cc..26f9060 100644 --- a/examples/git/main.go +++ b/examples/git/main.go @@ -202,7 +202,7 @@ func serve(response http.ResponseWriter, r *http.Request) { } return err } - if service == transport.UploadPackService { + if service == transport.UploadPackService || service == "_browse" { e = retryRead(w, r, refresh, func(attempt *readResponse, request *http.Request) error { retain, release := sessionRetention(session) return retained(r.Context(), retain, release, func() error { @@ -215,6 +215,9 @@ func serve(response http.ResponseWriter, r *http.Request) { return errLogMissing } attempt.failure = &s.failure + if service == "_browse" { + return serveBrowse(attempt, request, s) + } if request.Method == http.MethodPost { var body io.Reader = request.Body if request.Header.Get("Content-Encoding") == "gzip" { diff --git a/examples/git/repositories.go b/examples/git/repositories.go index aa4a317..084dc30 100644 --- a/examples/git/repositories.go +++ b/examples/git/repositories.go @@ -183,6 +183,7 @@ func resolveRepository(repositories map[string]repositoryConfig, r *http.Request } for _, service := range []string{ "info/refs", transport.UploadPackService, transport.ReceivePackService, + "_browse", "maintenance", "collect", "prune-invalid-refs", "recover-retentions-after-drain", } { suffix := "/" + service @@ -198,7 +199,9 @@ func resolveRepository(repositories map[string]repositoryConfig, r *http.Request } route.Repository = repository route.Method = http.MethodPost - if route.Service == "info/refs" { + if route.Service == "_browse" { + route.Method = http.MethodGet + } else if route.Service == "info/refs" { query, err := url.ParseQuery(r.URL.RawQuery) services := query["service"] if err != nil || len(query) != 1 || len(services) != 1 { @@ -209,11 +212,11 @@ func resolveRepository(repositories map[string]repositoryConfig, r *http.Request if route.Service != transport.UploadPackService && route.Service != transport.ReceivePackService { return repositoryRoute{}, errRepositoryNotFound } - } else if r.URL.RawQuery != "" || r.URL.ForceQuery { + } else if route.Service != "_browse" && (r.URL.RawQuery != "" || r.URL.ForceQuery) { return repositoryRoute{}, errRepositoryNotFound } switch route.Service { - case transport.UploadPackService: + case transport.UploadPackService, "_browse": route.Action = gitRead case transport.ReceivePackService: route.Action = gitWrite diff --git a/examples/git/viewer/.gitignore b/examples/git/viewer/.gitignore new file mode 100644 index 0000000..cc3b8ba --- /dev/null +++ b/examples/git/viewer/.gitignore @@ -0,0 +1,3 @@ +/node_modules/ +/build/ +/dist/ diff --git a/examples/git/viewer/README.md b/examples/git/viewer/README.md new file mode 100644 index 0000000..0426cfa --- /dev/null +++ b/examples/git/viewer/README.md @@ -0,0 +1,79 @@ +# Repository explorer + +A small read-only repository UI implemented in TypeScript as its own Spin +component. It browses branches, directories and text files, and shows the eight +most recent commits along the first parent. The browser uses no framework or +third-party runtime library. + +## Run + +Install the [Git example prerequisites](../README.md) plus Node.js 24 or later. +Build the optional viewer and start both components from a checkout: + +```sh +cd examples/git/viewer +npm ci +npm run check +npm run build +cd ../../.. +OBJECT_LOG_GIT_LOCAL_MANIFEST=spin.viewer.toml make git-local +``` + +The starter builds Git, starts an isolated MinIO backend, validates it, and +prints the Git URLs and password. Push a repository, then open: + +```text +http://127.0.0.1:19100/browse?repo=sha256.git +``` + +For an existing backend, build Git with `make git-build` and pass +`-f spin.viewer.toml` to the Git guide's normal `spin up` command, using the same +protected variables file. + +The `/browse` landing page also accepts a repository path such as +`team/project.git`. Sign in with the Git password or a Cognito access token. +The page keeps the credential in memory and clears it on reload; it never puts +it in a URL, cookie, or browser storage. Directory links preserve the selected +branch, and browser Back/Forward works. + +`npm run build` creates `viewer/dist/viewer.wasm` and the ignored +`examples/git/spin.viewer.toml`. That manifest copies the authoritative +`spin.toml` and adds three exact UI/asset routes. Rebuild the viewer after +changing the base manifest. The normal Git build and manifest require no Node +installation. A hosted reverse proxy must forward those exact viewer routes +and `//_browse` through its existing backend admission rules. + +## Data and limits + +The TypeScript component serves only the page and its assets. It receives no +storage credentials, has no outbound hosts, and does not read the WAL. The +existing Git component supplies a narrow `GET //_browse` adapter +because Git smart HTTP has no tree or file browsing API. That adapter uses the +same read authorization, existing repository open, reader retention, and retry +path as fetch. The core log is unchanged. + +Only branch tips are selectable. Tags and arbitrary object IDs are not exposed. +The prototype returns up to 500 entries in one directory, eight first-parent +commits, and UTF-8 text previews up to 256 KiB. Binary and larger files show a +summary; symlinks show their stored target without following it. Submodules +show their commit ID. Commit titles are bounded to 512 bytes. Paths are bounded +to 64 segments and 4096 UTF-8 bytes. File content is rendered as plain text. + +Run the adapter tests and Git gates from the repository root: + +```sh +make git-check +``` + +Check the UI source and locked build dependencies from this directory: + +```sh +npm run format:check +npm run check +npm audit +npm run build +``` + +The build pins `componentize-js` consistently across Spin's compiler packages +and explicitly supplies its preview shim. All compiler packages run at build +time; the resulting component needs only Spin at runtime. diff --git a/examples/git/viewer/build.mjs b/examples/git/viewer/build.mjs new file mode 100644 index 0000000..491a726 --- /dev/null +++ b/examples/git/viewer/build.mjs @@ -0,0 +1,50 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { build } from "esbuild"; +import { SpinEsbuildPlugin } from "@spinframework/build-tools/plugins/esbuild"; + +const client = await build({ + entryPoints: ["src/client.ts"], + bundle: true, + format: "esm", + write: false, + minify: true, +}); +await build({ + entryPoints: ["src/index.ts"], + outfile: "build/component.js", + bundle: true, + format: "esm", + platform: "browser", + define: { + CLIENT_SCRIPT: JSON.stringify(client.outputFiles[0].text), + STYLES: JSON.stringify(await readFile("src/style.css", "utf8")), + PAGE: JSON.stringify(await readFile("src/page.html", "utf8")), + }, + plugins: [ + await SpinEsbuildPlugin({ componentize: { output: "dist/viewer.wasm" } }), + ], +}); + +const manifest = await readFile("../spin.toml", "utf8"); +await writeFile( + "../spin.viewer.toml", + manifest + + ` +# Generated by viewer/build.mjs; rebuild after changing spin.toml. +[[trigger.http]] +route = "/browse" +component = "viewer" + +[[trigger.http]] +route = "/browse/assets/client.js" +component = "viewer" + +[[trigger.http]] +route = "/browse/assets/style.css" +component = "viewer" + +[component.viewer] +source = "viewer/dist/viewer.wasm" +allowed_outbound_hosts = [] +`, +); diff --git a/examples/git/viewer/package-lock.json b/examples/git/viewer/package-lock.json new file mode 100644 index 0000000..b5dfa62 --- /dev/null +++ b/examples/git/viewer/package-lock.json @@ -0,0 +1,3662 @@ +{ + "name": "object-log-repo-viewer", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "object-log-repo-viewer", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@spinframework/wasi-http-proxy": "2.0.0" + }, + "devDependencies": { + "@bytecodealliance/preview2-shim": "0.26.0", + "@spinframework/build-tools": "2.1.1", + "esbuild": "0.28.2", + "prettier": "3.8.1", + "typescript": "7.0.2" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@andreiltd/componentize-qjs-binding-darwin-arm64": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-darwin-arm64/-/componentize-qjs-binding-darwin-arm64-0.4.5.tgz", + "integrity": "sha512-CW+ukVP778x2NAHAC1FvXb5KVSXmUlUQxhQTFtF0mAqqOVnYrQgy4DDNUa48EGthwtLdgLT1+eE71qmQiyMEqg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@andreiltd/componentize-qjs-binding-darwin-x64": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-darwin-x64/-/componentize-qjs-binding-darwin-x64-0.4.5.tgz", + "integrity": "sha512-37uz97pCEuXNfoVuZMBbI6xUirSndxJyMzPa4UCQb9nJdZhzRujmrU79yiFl+5nDYQxcCjthhBVEKU3OxJNAdg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@andreiltd/componentize-qjs-binding-linux-arm64-gnu": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-linux-arm64-gnu/-/componentize-qjs-binding-linux-arm64-gnu-0.4.5.tgz", + "integrity": "sha512-L5S0C6qIx3f+DnwN42HnDuMhV8NgfqaY+J1PaJAw468Ip/JN+h67tVAp4wUvs8xgTqj6UnGSBbSZVGYifK2XtA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@andreiltd/componentize-qjs-binding-linux-arm64-musl": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-linux-arm64-musl/-/componentize-qjs-binding-linux-arm64-musl-0.4.5.tgz", + "integrity": "sha512-bBaq8/tvQ6HweqBLKWkE5tsPEMf7g3l5EGtka3Z7AFDUOtFzkAkG67nW1OYiYT6/0pjwDYw7Mvya45f79PG28g==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@andreiltd/componentize-qjs-binding-linux-x64-gnu": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-linux-x64-gnu/-/componentize-qjs-binding-linux-x64-gnu-0.4.5.tgz", + "integrity": "sha512-Xec4O4sy/xHlhPPhf7GAV41D+AT7f9vxxjBpE7lUIElZNhX/kc+Ty6AQG50xbHsT5Mi4XQvSmtJJqhnB6xi8Vw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@andreiltd/componentize-qjs-binding-linux-x64-musl": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-linux-x64-musl/-/componentize-qjs-binding-linux-x64-musl-0.4.5.tgz", + "integrity": "sha512-62+PgJ+tJOjgNvXzRuz3s76tPBaExCodeVhDIIXkGI/PNbulqTHiyYXRw4O3UX6/CO71Nux8onqnXxQkcXBlvg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@andreiltd/componentize-qjs-binding-win32-arm64-msvc": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-win32-arm64-msvc/-/componentize-qjs-binding-win32-arm64-msvc-0.4.5.tgz", + "integrity": "sha512-OZO/omsMQFpw3wJJs3XFN+iEu2ZQCWMK8BxkhmXVm1sddEHtgSTY2sT/zq69VZeXjC0zgN1HzlxHbNBq0OkcOQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@andreiltd/componentize-qjs-binding-win32-x64-msvc": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-win32-x64-msvc/-/componentize-qjs-binding-win32-x64-msvc-0.4.5.tgz", + "integrity": "sha512-IlhlcNOEFky46ypfiD6l8MS45zosfhKWUclr10evQFw5Cpky+pji2RAGJRnBXdLQvH4Q3bZr6KYlIhxmtGRhkw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@bytecodealliance/componentize-js": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/componentize-js/-/componentize-js-0.23.0.tgz", + "integrity": "sha512-Qc2+lrwJHU7lq4CBMyVVYE8ASyUFeegMYdRnMVG0wNpNiATzGDF/mWpjDNTeVZeT8L9chhK3vuLaQE9FVANvfw==", + "workspaces": [ + "." + ], + "dependencies": { + "@bytecodealliance/jco": "^1.15.1", + "@bytecodealliance/weval": "^0.5.0", + "@bytecodealliance/wizer": "^10.0.0", + "es-module-lexer": "^1.6.0", + "oxc-parser": "^0.76.0" + }, + "bin": { + "componentize-js": "src/cli.js" + } + }, + "node_modules/@bytecodealliance/componentize-js-0-19-3": { + "name": "@bytecodealliance/componentize-js", + "version": "0.19.3", + "resolved": "https://registry.npmjs.org/@bytecodealliance/componentize-js/-/componentize-js-0.19.3.tgz", + "integrity": "sha512-ju7Y4WeF0B9uMkSPHJgmT6ouEfSwbe9M1uR/YOnYZjBpxJjH9qzxIkJg/kf8NycVDyFJ2/lscmJ1E1uPiDQVRQ==", + "workspaces": [ + "." + ], + "dependencies": { + "@bytecodealliance/jco": "^1.15.1", + "@bytecodealliance/wizer": "^10.0.0", + "es-module-lexer": "^1.6.0", + "oxc-parser": "^0.76.0" + }, + "bin": { + "componentize-js": "src/cli.js" + } + }, + "node_modules/@bytecodealliance/jco": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco/-/jco-1.35.0.tgz", + "integrity": "sha512-O53cWIMm/M1SpwLxKRicvAMDnzWf4zWs6I6qFZIGbn3HUovDMCCe9blkGxxYuyocHjVf/cXgNmlMu/x7Tyi77A==", + "license": "(Apache-2.0 WITH LLVM-exception)", + "dependencies": { + "@bytecodealliance/componentize-js": "^0.22.0", + "@bytecodealliance/componentize-js-0-19-3": "npm:@bytecodealliance/componentize-js@^0.19.3", + "@bytecodealliance/jco-std": "^0.5.0", + "@bytecodealliance/jco-transpile": "^0.15.0", + "@bytecodealliance/preview2-shim": "^0.26.0", + "binaryen": "^130.0.0", + "commander": "^14", + "componentize-qjs": "^0.4.5", + "mkdirp": "^3", + "regexpu-core": "6.4.0", + "rolldown": "^1.2.4", + "typescript": "7.0.2", + "typescript-compiler-api": "npm:typescript@6.0.3", + "unenv": "2.0.0-rc.24" + }, + "bin": { + "jco": "dist/jco.js" + } + }, + "node_modules/@bytecodealliance/jco-node-fs": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs/-/jco-node-fs-0.3.2.tgz", + "integrity": "sha512-yxvqQreNcOQdMsEKZomkSqThtkxrAz3f1Cmy8BsV0UHi9saa2/D1KDB14IAXPYwDq/maS6lCzS+MuMQx6QkUgA==", + "license": "(Apache-2.0 WITH LLVM-exception)", + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@bytecodealliance/jco-node-fs-darwin-arm64": "0.3.2", + "@bytecodealliance/jco-node-fs-darwin-x64": "0.3.2", + "@bytecodealliance/jco-node-fs-linux-arm64-gnu": "0.3.2", + "@bytecodealliance/jco-node-fs-linux-arm64-musl": "0.3.2", + "@bytecodealliance/jco-node-fs-linux-x64-gnu": "0.3.2", + "@bytecodealliance/jco-node-fs-linux-x64-musl": "0.3.2", + "@bytecodealliance/jco-node-fs-win32-arm64-msvc": "0.3.2", + "@bytecodealliance/jco-node-fs-win32-x64-msvc": "0.3.2" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-darwin-arm64": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-darwin-arm64/-/jco-node-fs-darwin-arm64-0.3.2.tgz", + "integrity": "sha512-WXQq1tRVUWhbK+ImH0G8Kni2AR0sdokLf/KyXRw5P6n4A9ZbgMHOuPfN/PEsYgw4eosuTAonutK0xILk/hA81A==", + "cpu": [ + "arm64" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-darwin-x64": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-darwin-x64/-/jco-node-fs-darwin-x64-0.3.2.tgz", + "integrity": "sha512-Fy/OjHnto7hNE5ksewVkmAMXG/uXyj+RAKFZWQFTzFkp73UyT5vuPjKq3nvx9KolUGWBG37BudEMb4ZAoD9TbQ==", + "cpu": [ + "x64" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-linux-arm64-gnu": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-linux-arm64-gnu/-/jco-node-fs-linux-arm64-gnu-0.3.2.tgz", + "integrity": "sha512-OafRtRu3tfOFQARfkia/LDEqSo2R3yAL1xPwzf4RcRW6JqkOY7qKWsUS5+LPVo/aFaSkMX7F7bdFGpFPmcoZJg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-linux-arm64-musl": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-linux-arm64-musl/-/jco-node-fs-linux-arm64-musl-0.3.2.tgz", + "integrity": "sha512-JblDiwD7+Y+w0d/tjaNDX4QdGvh5lyuDc+S/jDeT7sYA1caA9yiwIjLa8PC/lm2xudqSxD+Cuy4AosXvl+qI0g==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-linux-x64-gnu": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-linux-x64-gnu/-/jco-node-fs-linux-x64-gnu-0.3.2.tgz", + "integrity": "sha512-6FzmHoLOsa6b4X9AuFLsvlpNF66Dt4sRRftZHaT1I1pYzCWrJkkpgNJhRXxf67ZPmOwBZJT4dan/g752Rx4xRw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-linux-x64-musl": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-linux-x64-musl/-/jco-node-fs-linux-x64-musl-0.3.2.tgz", + "integrity": "sha512-SbAJG2PDV8YAHBESHU9sVSckOitJyvlezdF4CGP5/mOp01JnijVkLVXzGK62yVSfa5TpAybbSd1rk+v36NAW/Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-win32-arm64-msvc": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-win32-arm64-msvc/-/jco-node-fs-win32-arm64-msvc-0.3.2.tgz", + "integrity": "sha512-nUWYdFysRQJGsIOCk0KAYN2smkNYiHG7rZ7m+0ZXqP+5wqWxKEjkDsORwXiBjdaWFLVww0anZ3SBxrju6P7hXQ==", + "cpu": [ + "arm64" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-win32-x64-msvc": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-win32-x64-msvc/-/jco-node-fs-win32-x64-msvc-0.3.2.tgz", + "integrity": "sha512-RsPj2t8TvltsMWK7/Z7JcVC/IWPMi07/AJkNLiNTCuGTdT/sseqTThJJwhuz3sjFw7dJdxA+bSGT+23y/KT0fw==", + "cpu": [ + "x64" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-std": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-std/-/jco-std-0.5.0.tgz", + "integrity": "sha512-TLklLNHUbCtXH9nRDDhZuk8rPYW0kd5R0ClvwpIXCBOyESHZGIGU1y7V47Uggz3i0h5WG3+srOg3aKfyZT4IIg==", + "license": "(Apache-2.0 WITH LLVM-exception)", + "dependencies": { + "acorn": "8.17.0", + "acorn-walk": "8.3.5", + "minimatch": "10.2.6", + "punycode": "2.3.1", + "readable-stream": "4.7.0", + "urlpattern-polyfill": "10.1.0", + "whatwg-url": "14.2.0" + }, + "peerDependencies": { + "@bytecodealliance/preview2-shim": "^0.25.0" + }, + "peerDependenciesMeta": { + "@bytecodealliance/preview2-shim": { + "optional": true + } + } + }, + "node_modules/@bytecodealliance/jco-transpile": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-transpile/-/jco-transpile-0.15.0.tgz", + "integrity": "sha512-MFftJ81B9TtHbvgCRZQod3jfgl62YvJszxsNdfx9IVNewxlX9q7U4Wd+w6gJzw+o9IjEGWoyg/BFpb/RnaJvlA==", + "license": "(Apache-2.0 WITH LLVM-exception)", + "dependencies": { + "@bytecodealliance/preview2-shim": "^0.26.0", + "@bytecodealliance/preview3-shim": "^0.7.0", + "binaryen": "^130.0.0", + "oxc-minify": "^0.136.0" + } + }, + "node_modules/@bytecodealliance/preview2-shim": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.26.0.tgz", + "integrity": "sha512-oYekIJYroM1yJygziKpuMGmL2XDKZ4XspkdxfKCQqF/E0tVkCxyRRT+gFP0dFzhrSL8oh6obRCv84bBf1sYmAA==", + "license": "(Apache-2.0 WITH LLVM-exception)", + "dependencies": { + "@bytecodealliance/jco-node-fs": "0.3.2" + } + }, + "node_modules/@bytecodealliance/preview3-shim": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/preview3-shim/-/preview3-shim-0.7.0.tgz", + "integrity": "sha512-5bBAE1nKSIT6ZyWoyLQwV1VjJciAdgLk7hbMeO3y5mLJhrkBL9egE48x5IfvU1ubMsQsKWFrP0jle7ow0b5I6w==", + "license": "(Apache-2.0 WITH LLVM-exception)", + "dependencies": { + "@bytecodealliance/jco-node-fs": "0.3.2", + "@bytecodealliance/preview2-shim": "^0.26.0" + } + }, + "node_modules/@bytecodealliance/weval": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/weval/-/weval-0.5.0.tgz", + "integrity": "sha512-lIR6+enn52gcdMQM8C3YaNoUlzm90TRkJrIgiVky/hED81ezDjhnq+6Z4w0eSFf9C6b2nmdKUFwbp6q1PhiuIQ==", + "license": "Apache-2.0", + "dependencies": { + "@napi-rs/lzma": "^1.1.2", + "fflate": "^0.8.3", + "tar": "^7.5.19" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@bytecodealliance/wizer": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer/-/wizer-10.0.0.tgz", + "integrity": "sha512-ziWmovyu1jQl9TsKlfC2bwuUZwxVPFHlX4fOqTzxhgS76jITIo45nzODEwPgU+jjmOr8F3YX2V2wAChC5NKujg==", + "license": "Apache-2.0", + "bin": { + "wizer": "wizer.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@bytecodealliance/wizer-darwin-arm64": "10.0.0", + "@bytecodealliance/wizer-darwin-x64": "10.0.0", + "@bytecodealliance/wizer-linux-arm64": "10.0.0", + "@bytecodealliance/wizer-linux-s390x": "10.0.0", + "@bytecodealliance/wizer-linux-x64": "10.0.0", + "@bytecodealliance/wizer-win32-x64": "10.0.0" + } + }, + "node_modules/@bytecodealliance/wizer-darwin-arm64": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-darwin-arm64/-/wizer-darwin-arm64-10.0.0.tgz", + "integrity": "sha512-dhZTWel+xccGTKSJtI9A7oM4yyP20FWflsT+AoqkOqkCY7kCNrj4tmMtZ6GXZFRDkrPY5+EnOh62sfShEibAMA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "wizer-darwin-arm64": "wizer" + } + }, + "node_modules/@bytecodealliance/wizer-darwin-x64": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-darwin-x64/-/wizer-darwin-x64-10.0.0.tgz", + "integrity": "sha512-r/LUIZw6Q3Hf4htd46mD+EBxfwjBkxVIrTM1r+B2pTCddoBYQnKVdVsI4UFyy7NoBxzEg8F8BwmTNoSLmFRjpw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "wizer-darwin-x64": "wizer" + } + }, + "node_modules/@bytecodealliance/wizer-linux-arm64": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-linux-arm64/-/wizer-linux-arm64-10.0.0.tgz", + "integrity": "sha512-pGSfFWXzeTqHm6z1PtVaEn+7Fm3QGC8YnHrzBV4sQDVS3N1NwmuHZAc8kslmlFPNdu61ycEvdOsSgCny8JPQvg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "wizer-linux-arm64": "wizer" + } + }, + "node_modules/@bytecodealliance/wizer-linux-s390x": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-linux-s390x/-/wizer-linux-s390x-10.0.0.tgz", + "integrity": "sha512-O8vHxRTAdb1lUnVXMIMTcp/9q4pq1D4iIKigJCipg2JN15taV9uFAWh0fO88wylXwuSlO7dOE1AwQl54fMKXQg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "wizer-linux-s390x": "wizer" + } + }, + "node_modules/@bytecodealliance/wizer-linux-x64": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-linux-x64/-/wizer-linux-x64-10.0.0.tgz", + "integrity": "sha512-fJtM1sy43FBMnp+xpapFX6U1YdTBKA/1T4CYfG/qeE8jn0SXk2EuiYoY/EnC2uyNy9hjTrvfdYO5n4MXW0EIdQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "wizer-linux-x64": "wizer" + } + }, + "node_modules/@bytecodealliance/wizer-win32-x64": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-win32-x64/-/wizer-win32-x64-10.0.0.tgz", + "integrity": "sha512-55BPLfGT7iT7gH5M69NpTM16QknJZ7OxJ0z73VOEoeGA9CT8QPKMRzFKsPIvLs+W8G28fdudFA94nElrdkp3Kg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "bin": { + "wizer-win32-x64": "wizer" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma/-/lzma-1.5.1.tgz", + "integrity": "sha512-sgOZ89+y8cDbY+3WbzR8CtIhCuFRWotZ9/2PjPVDJHz6np5KFTAev0DrwiyTJTgFsCRDhfGlbmhMgyhHbWdZ6g==", + "license": "MIT", + "engines": { + "node": "^22.20 || ^24.12 || >=25" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/lzma-android-arm-eabi": "1.5.1", + "@napi-rs/lzma-android-arm64": "1.5.1", + "@napi-rs/lzma-darwin-arm64": "1.5.1", + "@napi-rs/lzma-darwin-x64": "1.5.1", + "@napi-rs/lzma-freebsd-x64": "1.5.1", + "@napi-rs/lzma-linux-arm-gnueabihf": "1.5.1", + "@napi-rs/lzma-linux-arm64-gnu": "1.5.1", + "@napi-rs/lzma-linux-arm64-musl": "1.5.1", + "@napi-rs/lzma-linux-ppc64-gnu": "1.5.1", + "@napi-rs/lzma-linux-riscv64-gnu": "1.5.1", + "@napi-rs/lzma-linux-s390x-gnu": "1.5.1", + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@napi-rs/lzma-linux-x64-musl": "1.5.1", + "@napi-rs/lzma-wasm32-wasi": "1.5.1", + "@napi-rs/lzma-win32-arm64-msvc": "1.5.1", + "@napi-rs/lzma-win32-ia32-msvc": "1.5.1", + "@napi-rs/lzma-win32-x64-msvc": "1.5.1" + } + }, + "node_modules/@napi-rs/lzma-android-arm-eabi": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-android-arm-eabi/-/lzma-android-arm-eabi-1.5.1.tgz", + "integrity": "sha512-sahBe4ko2Z69NPTddaX6ZgbQZu9SDoITxw1S3dWl1gAGynZG34qHHCT8UaUMFxf3h3zMhCJjEzz4basaBxiTuQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-android-arm64": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-android-arm64/-/lzma-android-arm64-1.5.1.tgz", + "integrity": "sha512-7tkQAJJuBHxAxiEBNFgSTpvrtGpbwZYYJUSOmGEK3OfbdbNeoT2rdBxpM/gY1s+itEVbtOSlpaRPPG19MnwOzA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-darwin-arm64": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-darwin-arm64/-/lzma-darwin-arm64-1.5.1.tgz", + "integrity": "sha512-XWX8gtF+GHGk3nH3Wm3QUZNcxw9QHsFVZz3MzVLhWWHhceede1J4/vD+3dj3E1iKB9G6mualaZxOoD08R3E+7g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-darwin-x64": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-darwin-x64/-/lzma-darwin-x64-1.5.1.tgz", + "integrity": "sha512-CfsqUpMTI1z8enrA/b+GcHM6YDI8D0kqCiqPYEnst4rbOABQ9KZ92ybTTNnlnZ7A017WoMZKUEWc36KXDwi0xg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-freebsd-x64": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-freebsd-x64/-/lzma-freebsd-x64-1.5.1.tgz", + "integrity": "sha512-bTyNfg90FXIgE61U7l14aMmVOqRQ6AyP5JMT3jmCStaZI18apLNPdzZ8i7yqxZfKvRMVfPjE2brXIw27c+RRgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-arm-gnueabihf": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm-gnueabihf/-/lzma-linux-arm-gnueabihf-1.5.1.tgz", + "integrity": "sha512-vNE+D8nrw+eOkBsdKCsmDhowDV3pIMKXEhedvXfbgrWbrO7GlZJH+RXL+X+RYLxGwi8Ym61ZMt15sIOnNmh9Sw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-arm64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm64-gnu/-/lzma-linux-arm64-gnu-1.5.1.tgz", + "integrity": "sha512-csUem4WgoKGTprv/pOPm9UIWbb+hrfUwYXefpTHPAEGVFLl5behEFabisJ7FtihCa3yG2Efcl+yw25rlhhrIYw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-arm64-musl": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm64-musl/-/lzma-linux-arm64-musl-1.5.1.tgz", + "integrity": "sha512-kB/xhlVN1eLvVmDJSKZEjp5Gg2xDYexNrB5jwpSMbOkeGS6N9AasByPBg5VqCpMYC+zZi7DM458DRhtWYhqXTQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-ppc64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-ppc64-gnu/-/lzma-linux-ppc64-gnu-1.5.1.tgz", + "integrity": "sha512-s28RW0W1yBWQc1nbPdF7tp14koqslY3ZWLVI8uaanX292Dc6ezd4NPVwxEoCNBVON/oD7BmUbWGtyFvmm7dQ5A==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-riscv64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-riscv64-gnu/-/lzma-linux-riscv64-gnu-1.5.1.tgz", + "integrity": "sha512-+lGNwYlIN14YPMTNvYtIJJqHFevDTd6Juw/1NmXbWx/iRd/LLrjhlM/yluMX6pxs6NkOGsuuEXJJrbbEUS59OQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-s390x-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-s390x-gnu/-/lzma-linux-s390x-gnu-1.5.1.tgz", + "integrity": "sha512-PB44FFWWFrLeQowhcep1hPD1YcLqKlnnY60RMU74qrxTlr4YGEyzeMItJqh2uivBfv9kQScOF/B0J9+Vab/oyw==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-musl": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-musl/-/lzma-linux-x64-musl-1.5.1.tgz", + "integrity": "sha512-I3nsYrWtrW9JpeCr+mkJIVDt0HY3m6qVUBs5vTtoIvJQxwqf1PBXSy5IS7T53ksQFH2kd2UX8rLxJ7B4WISpZg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-wasm32-wasi": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-wasm32-wasi/-/lzma-wasm32-wasi-1.5.1.tgz", + "integrity": "sha512-gy3wwPBa6+XEyA4fUzq6CClrXA1ajXjuVf5zbnHytJRgoHznj+mvpU3+co2fxXwqTCmIpn6KrzqH5bRDztBPhA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-win32-arm64-msvc": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-arm64-msvc/-/lzma-win32-arm64-msvc-1.5.1.tgz", + "integrity": "sha512-dK+huOsHiyH6oJjij+cnjqFCakk2HgWmpI12Xm4pLUyPphe4ebYoJBgehaNAxprmjFqBQ7nL95YPVz9BHyqmPg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-win32-ia32-msvc": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-ia32-msvc/-/lzma-win32-ia32-msvc-1.5.1.tgz", + "integrity": "sha512-dGE8L+0EQ+GyU9ap9InqB/t/PmPG/bLj918q7OsJ29FuTdn8fK4OX3U4IQZhylHIA+/dQ/SXJk5n4yfah2XVvA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-win32-x64-msvc": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-x64-msvc/-/lzma-win32-x64-msvc-1.5.1.tgz", + "integrity": "sha512-EKW4t/iqdCT/xnd5t9oXLvVER/PMNAWXKqUAl3fgvUcOILeZIIht77/dVnfFcc9htA/DCBXC/6YQWdW+LusjFA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz", + "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@oxc-minify/binding-android-arm-eabi": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm-eabi/-/binding-android-arm-eabi-0.136.0.tgz", + "integrity": "sha512-gk1jzky1vLeOcK7w9Ib2RT/UooZ7O21s8YSeXCsy4tzsmguKb3Cbsv/Ra2fKhMp4mKeKCzvC1Xa41OfSBWfWdQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-android-arm64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm64/-/binding-android-arm64-0.136.0.tgz", + "integrity": "sha512-5GIYanqud/ohXEcg2dsdn0+eSKXqclw5qlRUkvSM1sKEa4ZQ7hqOk8XPJBNPh3LXXw+Qw85B6jkmOFLKsCHp5w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-darwin-arm64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-darwin-arm64/-/binding-darwin-arm64-0.136.0.tgz", + "integrity": "sha512-QAMpHKXWOoMCTenSZjQjVT5YmMooujlCKAf1T3PHFl2LuaA6ukQFXba0bgSLgAxRrAeX/itkwi4lzVQXtYmnRg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-darwin-x64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-darwin-x64/-/binding-darwin-x64-0.136.0.tgz", + "integrity": "sha512-rDnoPj8hztiU8C1VNKuzujh7SgTJlgiUvUCiWQjTnVSLsABsLW9ZWd1l1Ji0VN3UW39wGpptgvahIWTrHnGH/A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-freebsd-x64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-freebsd-x64/-/binding-freebsd-x64-0.136.0.tgz", + "integrity": "sha512-9fGSgP7uyPLCTjMx9z3AUV4UjnfOVoGTH5JzamLegPwGLJ97Gr1jht1437CRk8ZNabGmxQR6qNn3e+Tox2lswg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-linux-arm-gnueabihf": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.136.0.tgz", + "integrity": "sha512-k0QvZS+x6ROI4O5Ikqn9dDU2dmkEJ46Yh6baZJI/eUk/a1k1GOs9/vyXItLZrKlwYrSgdEx2BFXbKgyd1NyRPw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-linux-arm-musleabihf": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.136.0.tgz", + "integrity": "sha512-iRFxUc8ubotVfuyq3e4LPGbHrjvWd1/B3499a55GueMLc1EDJ0fHzNXJiCx2eMQGfzHtHodjmvrJ2BSvSXBNhQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-linux-arm64-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.136.0.tgz", + "integrity": "sha512-hbKxNCXJsULrb6EyG5SumzJKuPfP8Y0UQ/TElRgXVUhyyPf6cBPlF8HMq+MAylF98ACBzV8jBp2Mm62lJyTo+w==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-linux-arm64-musl": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.136.0.tgz", + "integrity": "sha512-T+u/CGKW0UXemJeVux4ix6rK+ug3bQCUEbu5hFlX9K61KQNd44rOcrM15mDK99L7USaXwmiwHeIkEDvmmyDMSA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-linux-ppc64-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.136.0.tgz", + "integrity": "sha512-WNPTN2F448tb49BoqjV9IMRzZWa0WB8GHzrB+bnxKQPOyzz07T6rmDKrr4Hjl81ADGjDYSsr1fgXcjyLQFdMdw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-linux-riscv64-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.136.0.tgz", + "integrity": "sha512-MvcCfu8OeabrmNN9wqhh8Gqizg68RQ3iOvXBJmlLMy5p1PC6az9bAwqOk98lsCmKI4I5LpULekOlfxRYh3o5Kw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-linux-riscv64-musl": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.136.0.tgz", + "integrity": "sha512-rwJXFUFFA0egAULtOOgfW8YhhLhtWrr2qFxftvZ7/U6Xtxiev8i+eCwSNW1RpHiX74QMsJVpRE3ykoefqb1MtQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-linux-s390x-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.136.0.tgz", + "integrity": "sha512-qizAxHmj7VHm5/D5qiIlvzxAS2tlmadDOJ5XFI7zx/wfnk/+fdk5iVaHOv+C4tDE/4wLQyMoBYVIs6R1LeN91w==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-linux-x64-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.136.0.tgz", + "integrity": "sha512-gcyLAad0UblkEhGjVfx5Mwq63OSx8r04IbcfwzpZYpHisO/vioMsrs4GkDsQZ2Ejc8cdUk6YMIrf9FwNBzh+Dw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-linux-x64-musl": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-x64-musl/-/binding-linux-x64-musl-0.136.0.tgz", + "integrity": "sha512-QAK6kwBjljX6luYzeppVdRP3gTaFNFfp9iS/V0cCvU43wUZay5xFXBgmzBHYrzZV2MhzURNnXfEZRo9poAxyvA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-openharmony-arm64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-openharmony-arm64/-/binding-openharmony-arm64-0.136.0.tgz", + "integrity": "sha512-nIGxkTOnN9GwslmooxbZ3Ti2RgbX9QLHy0X9uO0pslOT+4dsNVUawBU/6r4M5cWM+ZdRD2tOF4V+n0l8tS59nQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-wasm32-wasi": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-wasm32-wasi/-/binding-wasm32-wasi-0.136.0.tgz", + "integrity": "sha512-YB3G8AHT6PpDS8/DfjlJrQjywj+/6gCkNBcAJdYzcYbQRIZE6SaTNowiGuZPR1m1jOl1WjcjefniKSmVxzhvRA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-minify/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-minify/binding-win32-arm64-msvc": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.136.0.tgz", + "integrity": "sha512-UVRH3BlNc2rmla+sbSd5xTF/u+esCqZS6VSWPolZybcBh3TlVe1Sf5MOuHQWUaFoyv5uFOvee4hOfS/14nPvZw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-win32-ia32-msvc": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.136.0.tgz", + "integrity": "sha512-zFGkUd82jci0E+isc2O36lVmmXVUDXyEMN2RYIyez+MBNqhdjPLIGx62FCsrJtLKeuiPf/8XkFv977aUtndUng==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-minify/binding-win32-x64-msvc": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.136.0.tgz", + "integrity": "sha512-VHgPwd5xibC9UYxBeq7nURTjY+wxhdumllij6JaCIUVfD9IRcrA66SVlJ5fPNoUXcRp8ZXhJfoIZwVz9wAEGJw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.76.0.tgz", + "integrity": "sha512-1XJW/16CDmF5bHE7LAyPPmEEVnxSadDgdJz+xiLqBrmC4lfAeuAfRw3HlOygcPGr+AJsbD4Z5sFJMkwjbSZlQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.76.0.tgz", + "integrity": "sha512-yoQwSom8xsB+JdGsPUU0xxmxLKiF2kdlrK7I56WtGKZilixuBf/TmOwNYJYLRWkBoW5l2/pDZOhBm2luwmLiLw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.76.0.tgz", + "integrity": "sha512-uRIopPLvr3pf2Xj7f5LKyCuqzIU6zOS+zEIR8UDYhcgJyZHnvBkfrYnfcztyIcrGdQehrFUi3uplmI09E7RdiQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.76.0.tgz", + "integrity": "sha512-a0EOFvnOd2FqmDSvH6uWLROSlU6KV/JDKbsYDA/zRLyKcG6HCsmFnPsp8iV7/xr9WMbNgyJi6R5IMpePQlUq7Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.76.0.tgz", + "integrity": "sha512-ikRYDHL3fOdZwfJKmcdqjlLgkeNZ3Ez0qM8wAev5zlHZ+lY/Ig7qG5SCqPlvuTu+nNQ6zrFFaKvvt69EBKXU/g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.76.0.tgz", + "integrity": "sha512-dtRv5J5MRCLR7x39K8ufIIW4svIc7gYFUaI0YFXmmeOBhK/K2t/CkguPnDroKtsmXIPHDRtmJ1JJYzNcgJl6Wg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.76.0.tgz", + "integrity": "sha512-IE4iiiggFH2snagQxHrY5bv6dDpRMMat+vdlMN/ibonA65eOmRLp8VLTXnDiNrcla/itJ1L9qGABHNKU+SnE8g==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.76.0.tgz", + "integrity": "sha512-wi9zQPMDHrBuRuT7Iurfidc9qlZh7cKa5vfYzOWNBCaqJdgxmNOFzvYen02wVUxSWGKhpiPHxrPX0jdRyJ8Npg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.76.0.tgz", + "integrity": "sha512-0tqqu1pqPee2lLGY8vtYlX1L415fFn89e0a3yp4q5N9f03j1rRs0R31qesTm3bt/UK8HYjECZ+56FCVPs2MEMQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.76.0.tgz", + "integrity": "sha512-y36Hh1a5TA+oIGtlc8lT7N9vdHXBlhBetQJW0p457KbiVQ7jF7AZkaPWhESkjHWAsTVKD2OjCa9ZqfaqhSI0FQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.76.0.tgz", + "integrity": "sha512-7/acaG9htovp3gp/J0kHgbItQTuHctl+rbqPPqZ9DRBYTz8iV8kv3QN8t8Or8i/hOmOjfZp9McDoSU1duoR4/A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.76.0.tgz", + "integrity": "sha512-AxFt0reY6Q2rfudABmMTFGR8tFFr58NlH2rRBQgcj+F+iEwgJ+jMwAPhXd2y1I2zaI8GspuahedUYQinqxWqjA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.76.0.tgz", + "integrity": "sha512-wHdkHdhf6AWBoO8vs5cpoR6zEFY1rB+fXWtq6j/xb9j/lu1evlujRVMkh8IM/M/pOUIrNkna3nzST/mRImiveQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.76.0.tgz", + "integrity": "sha512-G7ZlEWcb2hNwCK3qalzqJoyB6HaTigQ/GEa7CU8sAJ/WwMdG/NnPqiC9IqpEAEy1ARSo4XMALfKbKNuqbSs5mg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.76.0.tgz", + "integrity": "sha512-0jLzzmnu8/mqNhKBnNS2lFUbPEzRdj5ReiZwHGHpjma0+ullmmwP2AqSEqx3ssHDK9CpcEMdKOK2LsbCfhHKIA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.76.0.tgz", + "integrity": "sha512-CH3THIrSViKal8yV/Wh3FK0pFhp40nzW1MUDCik9fNuid2D/7JJXKJnfFOAvMxInGXDlvmgT6ACAzrl47TqzkQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", + "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", + "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", + "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", + "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", + "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", + "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", + "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", + "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", + "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", + "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", + "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", + "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", + "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", + "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", + "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@spinframework/build-tools": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@spinframework/build-tools/-/build-tools-2.1.1.tgz", + "integrity": "sha512-VF4NRtkqW0vUv3MzgwbyZKzG4qC1D2DrmFbr+P3uJPLq0stuLqcuASqq3LzrxTbTnpAeeswn8JhEe+wPbH1Lng==", + "license": "Apache-2.0 WITH LLVM-exception", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bytecodealliance/componentize-js": "^0.23.0", + "@bytecodealliance/jco": "^1.17.9", + "acorn-walk": "^8.3.4", + "acron": "^1.0.5", + "magic-string": "^0.30.17", + "regexpu-core": "^6.2.0", + "yargs": "^18.0.0" + }, + "bin": { + "j2w": "dist/index.js" + } + }, + "node_modules/@spinframework/wasi-http-proxy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@spinframework/wasi-http-proxy/-/wasi-http-proxy-2.0.0.tgz", + "integrity": "sha512-nMW4QcueYajZ4fqcC3gTbfWuJv3hm8oc6sJfc5FI4y1hEOH1K7ciV6g+9R3EEmQl8GjeZhI5rdQ9Anz3N4NVqQ==", + "license": " Apache-2.0 WITH LLVM-exception", + "peerDependencies": { + "@spinframework/build-tools": "^2.0.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acron": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/acron/-/acron-1.0.5.tgz", + "integrity": "sha512-Jm3uey9PSUl5k9RsjBSfDJG35nG0vd9B4FUCV1J/NeVHDQ5HJPVpAhmGrPYtPKh0gLhEUIEy4DKqBy/p60B+4Q==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.11" + }, + "bin": { + "acron": "cli.js" + } + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/binaryen": { + "version": "130.0.0", + "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-130.0.0.tgz", + "integrity": "sha512-XDrb+zql0RbFPKgj7MuH9zOc78R3Fa/P/VSGnnpdwYsvNZPWjcMYMdAkKCOQEL2A7yqgjSMTDRFp6gfSDW+/QQ==", + "license": "Apache-2.0", + "bin": { + "wasm-as": "bin/wasm-as", + "wasm-ctor-eval": "bin/wasm-ctor-eval", + "wasm-dis": "bin/wasm-dis", + "wasm-merge": "bin/wasm-merge", + "wasm-metadce": "bin/wasm-metadce", + "wasm-opt": "bin/wasm-opt", + "wasm-reduce": "bin/wasm-reduce", + "wasm-shell": "bin/wasm-shell", + "wasm2js": "bin/wasm2js" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/componentize-qjs": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/componentize-qjs/-/componentize-qjs-0.4.5.tgz", + "integrity": "sha512-/Brt+/Ps7DQ1mYuUsDISTQTFSaZfCSoRHkHnh9Dm5WHCO6arc/vzVdD8TXssfyNex2Iv3UR8NHdahsqABPDh0w==", + "license": "Apache-2.0", + "bin": { + "componentize-qjs": "bin/componentize-qjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@andreiltd/componentize-qjs-binding-darwin-arm64": "0.4.5", + "@andreiltd/componentize-qjs-binding-darwin-x64": "0.4.5", + "@andreiltd/componentize-qjs-binding-linux-arm64-gnu": "0.4.5", + "@andreiltd/componentize-qjs-binding-linux-arm64-musl": "0.4.5", + "@andreiltd/componentize-qjs-binding-linux-x64-gnu": "0.4.5", + "@andreiltd/componentize-qjs-binding-linux-x64-musl": "0.4.5", + "@andreiltd/componentize-qjs-binding-win32-arm64-msvc": "0.4.5", + "@andreiltd/componentize-qjs-binding-win32-x64-msvc": "0.4.5" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "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" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/oxc-minify": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/oxc-minify/-/oxc-minify-0.136.0.tgz", + "integrity": "sha512-u9UvPFTYVwQ/i1kpp/c20S/SbvLaLV0Ngzyhgi0YV4B1SlxM7RgnnePl4vY9N3Z9sTRcozL1MRStI3PlUN9RFA==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-minify/binding-android-arm-eabi": "0.136.0", + "@oxc-minify/binding-android-arm64": "0.136.0", + "@oxc-minify/binding-darwin-arm64": "0.136.0", + "@oxc-minify/binding-darwin-x64": "0.136.0", + "@oxc-minify/binding-freebsd-x64": "0.136.0", + "@oxc-minify/binding-linux-arm-gnueabihf": "0.136.0", + "@oxc-minify/binding-linux-arm-musleabihf": "0.136.0", + "@oxc-minify/binding-linux-arm64-gnu": "0.136.0", + "@oxc-minify/binding-linux-arm64-musl": "0.136.0", + "@oxc-minify/binding-linux-ppc64-gnu": "0.136.0", + "@oxc-minify/binding-linux-riscv64-gnu": "0.136.0", + "@oxc-minify/binding-linux-riscv64-musl": "0.136.0", + "@oxc-minify/binding-linux-s390x-gnu": "0.136.0", + "@oxc-minify/binding-linux-x64-gnu": "0.136.0", + "@oxc-minify/binding-linux-x64-musl": "0.136.0", + "@oxc-minify/binding-openharmony-arm64": "0.136.0", + "@oxc-minify/binding-wasm32-wasi": "0.136.0", + "@oxc-minify/binding-win32-arm64-msvc": "0.136.0", + "@oxc-minify/binding-win32-ia32-msvc": "0.136.0", + "@oxc-minify/binding-win32-x64-msvc": "0.136.0" + } + }, + "node_modules/oxc-parser": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.76.0.tgz", + "integrity": "sha512-l98B2e9evuhES7zN99rb1QGhbzx25829TJFaKi2j0ib3/K/G5z1FdGYz6HZkrU3U8jdH7v2FC8mX1j2l9JrOUg==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.76.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm64": "0.76.0", + "@oxc-parser/binding-darwin-arm64": "0.76.0", + "@oxc-parser/binding-darwin-x64": "0.76.0", + "@oxc-parser/binding-freebsd-x64": "0.76.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.76.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.76.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.76.0", + "@oxc-parser/binding-linux-arm64-musl": "0.76.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.76.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.76.0", + "@oxc-parser/binding-linux-x64-gnu": "0.76.0", + "@oxc-parser/binding-linux-x64-musl": "0.76.0", + "@oxc-parser/binding-wasm32-wasi": "0.76.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.76.0", + "@oxc-parser/binding-win32-x64-msvc": "0.76.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.3.tgz", + "integrity": "sha512-ycwFAS14Jw4mppvmK4GR/J6u3WpWpjkEApehuHtLc/8VpPNpDMbQ4WjqwplXifGeyKOzHSFLmSPqzksDQE2Sfg==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/rolldown": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", + "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.151.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.11", + "@rolldown/binding-android-arm64": "1.2.11", + "@rolldown/binding-darwin-arm64": "1.2.11", + "@rolldown/binding-darwin-x64": "1.2.11", + "@rolldown/binding-freebsd-x64": "1.2.11", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", + "@rolldown/binding-linux-arm64-gnu": "1.2.11", + "@rolldown/binding-linux-arm64-musl": "1.2.11", + "@rolldown/binding-linux-ppc64-gnu": "1.2.11", + "@rolldown/binding-linux-s390x-gnu": "1.2.11", + "@rolldown/binding-linux-x64-gnu": "1.2.11", + "@rolldown/binding-linux-x64-musl": "1.2.11", + "@rolldown/binding-openharmony-arm64": "1.2.11", + "@rolldown/binding-win32-arm64-msvc": "1.2.11", + "@rolldown/binding-win32-x64-msvc": "1.2.11" + } + }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/typescript-compiler-api": { + "name": "typescript", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yargs": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + } + } +} diff --git a/examples/git/viewer/package.json b/examples/git/viewer/package.json new file mode 100644 index 0000000..f8ec508 --- /dev/null +++ b/examples/git/viewer/package.json @@ -0,0 +1,26 @@ +{ + "name": "object-log-repo-viewer", + "private": true, + "version": "0.1.0", + "type": "module", + "license": "Apache-2.0", + "scripts": { + "build": "node build.mjs", + "check": "tsc --noEmit", + "format": "prettier --write src build.mjs", + "format:check": "prettier --check src build.mjs" + }, + "devDependencies": { + "@bytecodealliance/preview2-shim": "0.26.0", + "@spinframework/build-tools": "2.1.1", + "esbuild": "0.28.2", + "prettier": "3.8.1", + "typescript": "7.0.2" + }, + "overrides": { + "@bytecodealliance/componentize-js": "0.23.0" + }, + "dependencies": { + "@spinframework/wasi-http-proxy": "2.0.0" + } +} diff --git a/examples/git/viewer/src/client.ts b/examples/git/viewer/src/client.ts new file mode 100644 index 0000000..a574e37 --- /dev/null +++ b/examples/git/viewer/src/client.ts @@ -0,0 +1,301 @@ +export {}; + +interface Commit { + id: string; + title: string; + author: string; + date: string; +} +interface Entry { + name: string; + kind: string; + id: string; +} +interface Snapshot { + branch: string; + branches: string[]; + path: string; + history: Commit[]; + entries: Entry[]; + more: boolean; + file?: { id: string; size: number; state: string; text?: string }; +} + +function get(id: string): T { + return document.getElementById(id) as T; +} +function node( + tag: K, + text = "", + className = "", +): HTMLElementTagNameMap[K] { + const element = document.createElement(tag); + element.textContent = text; + element.className = className; + return element; +} +function icon(kind: string): SVGSVGElement { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("viewBox", "0 0 16 16"); + svg.setAttribute("aria-hidden", "true"); + const path = document.createElementNS(svg.namespaceURI, "path"); + path.setAttribute( + "d", + kind === "directory" + ? "M1 2h5l2 2h7v9H1Z" + : "M3 1h6l4 4v10H3Zm6 1v4h3M5 9h6M5 12h6", + ); + if (kind === "directory") svg.classList.add("directory-icon"); + svg.append(path); + return svg; +} +function validRepository(name: string): boolean { + return ( + name.endsWith(".git") && + !/[\s%?#\\]/.test(name) && + name + .split("/") + .every((part) => part !== "" && part !== "." && part !== "..") && + new URL("/" + name, location.origin).pathname === "/" + name + ); +} + +const repository = new URLSearchParams(location.search).get("repo") ?? ""; +const statusMessage = get("status"); +const auth = get("auth"); +const content = get("content"); +const branch = get("branch"); +let authorization = ""; +let pending: AbortController | undefined; + +function link(label: string, path: string): HTMLAnchorElement { + const anchor = node("a", label); + const query = new URLSearchParams(location.search); + query.set("path", path); + anchor.href = "?" + query.toString(); + anchor.addEventListener("click", (event) => { + if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) + return; + event.preventDefault(); + history.pushState(null, "", anchor.href); + void load(); + }); + return anchor; +} +function date(value: string): string { + return new Date(value).toLocaleDateString(undefined, { + month: "short", + day: "numeric", + year: "numeric", + }); +} +function render(view: Snapshot): void { + branch.replaceChildren(); + for (const ref of view.branches) { + const option = node("option", ref.replace(/^refs\/heads\//, "")); + option.value = ref; + branch.append(option); + } + branch.value = view.branch; + branch.disabled = view.branches.length === 0; + get("branch-count").textContent = + `${view.branches.length} ${view.branches.length === 1 ? "branch" : "branches"}`; + get("commit-count").textContent = String(view.history.length); + const breadcrumbs = get("breadcrumbs"); + breadcrumbs.replaceChildren(link(repository.replace(/\.git$/, ""), "")); + const parts = view.path ? view.path.split("/") : []; + parts.forEach((part, i) => + breadcrumbs.append( + node("span", "/", "muted"), + link(part, parts.slice(0, i + 1).join("/")), + ), + ); + const tip = get("tip"); + tip.replaceChildren(); + const latest = view.history[0]; + tip.hidden = !latest; + if (latest) + tip.append( + node("span", latest.author.slice(0, 1).toUpperCase() || "G", "avatar"), + node("span", latest.title || "Untitled commit", "title"), + node("span", latest.id.slice(0, 7), "hash"), + ); + const files = get("files"); + files.replaceChildren(); + if (view.file) { + const header = node("div", "", "file-header"); + header.append( + node("span", `${view.file.size.toLocaleString()} bytes`, "muted"), + node("span", view.file.id.slice(0, 12), "hash"), + ); + files.append(header); + const messages: Record = { + binary: "Binary file · preview unavailable", + large: "This file exceeds the 256 KiB preview limit.", + submodule: "Submodule · " + view.file.id, + }; + files.append( + view.file.state === "text" + ? node("pre", view.file.text ?? "") + : node( + "div", + messages[view.file.state] ?? "Preview unavailable", + "empty", + ), + ); + } else if (!view.history.length) { + files.append( + node( + "div", + "This repository is empty. Push a branch to start exploring.", + "empty", + ), + ); + } else { + const entries = [...view.entries].sort( + (a, b) => + Number(b.kind === "directory") - Number(a.kind === "directory") || + a.name.localeCompare(b.name), + ); + for (const entry of entries) { + const row = node("div", "", "file-row"); + row.append( + icon(entry.kind), + link(entry.name, [...parts, entry.name].join("/")), + node("span", entry.id.slice(0, 7), "hash"), + ); + files.append(row); + } + if (!entries.length) + files.append(node("div", "This directory is empty.", "empty")); + if (view.more) + files.append(node("div", "Showing the first 500 entries.", "empty")); + } + const commits = get("history"); + commits.replaceChildren(); + for (const commit of view.history) { + const row = node("div", "", "history-row"); + const info = node("div", "", "history-info"); + info.append( + node("div", commit.title || "Untitled commit", "history-title"), + node( + "div", + `${commit.author || "Unknown author"} committed on ${date(commit.date)}`, + "muted", + ), + ); + row.append( + node("span", commit.author.slice(0, 1).toUpperCase() || "G", "avatar"), + info, + node("span", commit.id.slice(0, 7), "hash"), + ); + commits.append(row); + } + if (!view.history.length) + commits.append(node("div", "No commits yet.", "empty")); +} + +async function load(): Promise { + pending?.abort(); + const request = new AbortController(); + pending = request; + statusMessage.hidden = false; + statusMessage.textContent = "Loading repository…"; + auth.hidden = true; + content.hidden = true; + try { + const query = new URLSearchParams(location.search); + query.delete("repo"); + const response = await fetch(`/${repository}/_browse?${query}`, { + headers: authorization ? { Authorization: authorization } : {}, + credentials: "omit", + cache: "no-store", + signal: request.signal, + }); + if (response.status === 401) { + statusMessage.textContent = "Authentication required."; + auth.hidden = false; + get("credential").focus(); + return; + } + if (!response.ok) { + const messages: Record = { + 403: "This credential does not have read access.", + 404: "Repository or path not found. Check the path and push a branch before browsing.", + 400: "This branch or path query is invalid.", + }; + throw new Error( + messages[response.status] ?? + "Repository unavailable. Try refreshing this page.", + ); + } + const view = (await response.json()) as Snapshot; + if (pending !== request) return; + render(view); + statusMessage.hidden = true; + content.hidden = false; + } catch (error) { + if (request.signal.aborted) return; + statusMessage.textContent = + error instanceof Error ? error.message : "Repository unavailable."; + auth.hidden = false; + } +} + +auth.addEventListener("submit", (event) => { + event.preventDefault(); + const field = get("credential"); + const credential = field.value; + field.value = ""; + const bytes = new TextEncoder().encode("git:" + credential); + authorization = + get("auth-mode").value === "bearer" + ? "Bearer " + credential + : "Basic " + + btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join("")); + void load(); +}); +branch.addEventListener("change", () => { + const query = new URLSearchParams({ repo: repository, ref: branch.value }); + history.pushState(null, "", "?" + query); + void load(); +}); +for (const name of ["code", "commits"]) { + get(name + "-tab").addEventListener("click", () => { + for (const panel of ["code", "commits"]) { + const active = panel === name; + get(panel + "-view").hidden = !active; + get(panel + "-tab").classList.toggle("active", active); + get(panel + "-tab").setAttribute("aria-pressed", String(active)); + } + }); +} +get("open-repo").addEventListener("submit", (event) => { + event.preventDefault(); + const name = get("repo-name").value.trim(); + if (validRepository(name)) + location.href = "/browse?" + new URLSearchParams({ repo: name }); + else + statusMessage.textContent = + "Enter a repository path such as team/project.git."; +}); +get("copy-url").addEventListener("click", async () => { + try { + await navigator.clipboard.writeText( + get("clone-url").value, + ); + get("copy-url").textContent = "Copied"; + } catch { + get("clone-url").select(); + } +}); +addEventListener("popstate", () => void load()); +if (validRepository(repository)) { + get("repository").textContent = repository.replace(/\.git$/, ""); + document.title = repository + " · object-log"; + get("clone-url").value = location.origin + "/" + repository; + void load(); +} else { + get("open-repo").hidden = false; + statusMessage.textContent = "Enter the path of a configured Git repository."; +} diff --git a/examples/git/viewer/src/index.ts b/examples/git/viewer/src/index.ts new file mode 100644 index 0000000..971ed4a --- /dev/null +++ b/examples/git/viewer/src/index.ts @@ -0,0 +1,45 @@ +export {}; + +declare const CLIENT_SCRIPT: string; +declare const STYLES: string; +declare const PAGE: string; + +function handle(request: Request): Response { + if (request.method !== "GET" && request.method !== "HEAD") { + return new Response("Method not allowed", { + status: 405, + headers: { Allow: "GET, HEAD" }, + }); + } + const path = new URL(request.url).pathname; + if ( + path !== "/browse" && + path !== "/browse/assets/client.js" && + path !== "/browse/assets/style.css" + ) { + return new Response("Not found", { status: 404 }); + } + const assets: Record = { + "/browse/assets/client.js": [ + CLIENT_SCRIPT, + "text/javascript; charset=utf-8", + ], + "/browse/assets/style.css": [STYLES, "text/css; charset=utf-8"], + }; + const [body, type] = assets[path] ?? [PAGE, "text/html; charset=utf-8"]; + return new Response(request.method === "HEAD" ? null : body, { + headers: { + "Content-Type": type, + "Cache-Control": "no-cache", + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "no-referrer", + "Content-Security-Policy": + "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'", + }, + }); +} + +addEventListener("fetch", (event: Event) => { + const request = event as FetchEvent; + request.respondWith(handle(request.request)); +}); diff --git a/examples/git/viewer/src/page.html b/examples/git/viewer/src/page.html new file mode 100644 index 0000000..7606580 --- /dev/null +++ b/examples/git/viewer/src/page.html @@ -0,0 +1,92 @@ + + + + + + Repository · object-log + + + + +
+ object-logRepository explorer +
+
+
+

Repository

+ Read only +
+ +

Loading repository…

+ + +
+
A small window into Git on object storage.
+ + diff --git a/examples/git/viewer/src/style.css b/examples/git/viewer/src/style.css new file mode 100644 index 0000000..10c169e --- /dev/null +++ b/examples/git/viewer/src/style.css @@ -0,0 +1,381 @@ +:root { + color-scheme: light; + font: + 14px/1.5 -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; + color: #1f2328; + background: #fff; +} +* { + box-sizing: border-box; +} +body { + margin: 0; +} +a { + color: #0969da; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +button, +input, +select, +summary { + font: inherit; +} +button, +select, +summary { + cursor: pointer; +} +button, +input, +select, +summary { + border: 1px solid #d0d7de; + border-radius: 6px; + padding: 6px 12px; + background: #f6f8fa; + color: inherit; +} +input { + background: #fff; + min-width: 0; +} +button:hover, +summary:hover { + background: #eef1f4; +} +:focus-visible { + outline: 2px solid #0969da; + outline-offset: 2px; +} +[hidden] { + display: none !important; +} +.topbar { + height: 64px; + padding: 0 32px; + display: flex; + align-items: center; + gap: 24px; + background: #f6f8fa; + border-bottom: 1px solid #d8dee4; +} +.brand { + color: #1f2328; + font-size: 16px; + font-weight: 650; + display: flex; + align-items: center; + gap: 10px; +} +.mark { + font-size: 30px; + line-height: 1; +} +.topbar-note { + color: #656d76; + font-size: 13px; +} +main { + max-width: 1120px; + margin: 0 auto; + padding: 32px 32px 64px; +} +.heading { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 24px; +} +h1 { + font-size: 22px; + font-weight: 550; + margin: 0; + color: #0969da; + overflow-wrap: anywhere; +} +h2 { + font-size: 16px; + margin: 0 0 16px; +} +.badge { + border: 1px solid #d0d7de; + border-radius: 20px; + font-size: 12px; + color: #656d76; + padding: 1px 8px; + white-space: nowrap; +} +.muted { + color: #656d76; + font-size: 13px; +} +.panel { + border: 1px solid #d0d7de; + border-radius: 6px; + overflow: hidden; +} +.tabs { + display: flex; + gap: 8px; + border-bottom: 1px solid #d8dee4; + margin-bottom: 20px; +} +.tab { + border: none; + background: transparent; + border-radius: 0; + padding: 10px 16px; + border-bottom: 2px solid transparent; +} +.tab.active { + font-weight: 600; + border-bottom-color: #fd8c73; +} +.count { + margin-left: 6px; + padding: 0 6px; + border-radius: 12px; + background: #eaeef2; + font-size: 12px; +} +.toolbar { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 20px; +} +.toolbar select { + max-width: 55%; + font-weight: 550; +} +.primary { + color: white; + background: #1f883d; + border-color: #1f883d; + font-weight: 550; +} +.primary:hover { + background: #1a7f37; +} +.clone { + position: relative; + margin-left: auto; +} +.clone summary { + list-style: none; +} +.clone-box { + position: absolute; + top: 42px; + right: 0; + background: white; + border: 1px solid #d0d7de; + padding: 16px; + border-radius: 6px; + width: 340px; + box-shadow: 0 8px 24px #8c959f33; + z-index: 2; +} +.clone-box input { + width: 100%; + margin: 8px 0; + font: + 12px ui-monospace, + monospace; +} +.breadcrumbs { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 0 0 12px; + font-weight: 550; +} +.commit-tip { + display: flex; + align-items: center; + gap: 12px; + background: #f6f8fa; + border: 1px solid #d0d7de; + border-radius: 6px 6px 0 0; + padding: 14px 16px; +} +.commit-tip + .panel { + border-top: 0; + border-radius: 0 0 6px 6px; +} +.avatar { + width: 26px; + height: 26px; + flex: 0 0 26px; + border-radius: 50%; + background: #ddf4ff; + color: #0969da; + text-align: center; + line-height: 26px; + font-weight: 600; +} +.title { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.hash { + color: #656d76; + font: + 12px ui-monospace, + SFMono-Regular, + monospace; +} +.file-row, +.history-row { + display: flex; + align-items: center; + gap: 12px; + padding: 11px 16px; + border-bottom: 1px solid #d8dee4; +} +.file-row:last-child, +.history-row:last-child { + border-bottom: none; +} +.file-row:hover { + background: #f6f8fa; +} +.file-row a { + flex: 1; + min-width: 0; + overflow-wrap: anywhere; + color: #1f2328; +} +.file-row a:hover { + color: #0969da; +} +svg { + width: 16px; + height: 16px; + flex-shrink: 0; + fill: #656d76; +} +.directory-icon { + fill: #54aeff; +} +.history-row { + align-items: flex-start; + padding: 16px; +} +.history-info { + flex: 1; + min-width: 0; +} +.history-title { + font-weight: 600; + margin-bottom: 4px; + overflow-wrap: anywhere; +} +.file-header { + padding: 12px 16px; + background: #f6f8fa; + border-bottom: 1px solid #d8dee4; + display: flex; + gap: 12px; + align-items: center; +} +pre { + font: + 12px/1.65 ui-monospace, + SFMono-Regular, + Consolas, + monospace; + margin: 0; + padding: 20px; + overflow: auto; + tab-size: 4; +} +.empty { + padding: 40px 24px; + text-align: center; + color: #656d76; +} +.open-form, +.auth-form { + padding: 24px; + max-width: 520px; + margin: 24px 0; +} +.auth-form { + display: flex; + flex-direction: column; + gap: 8px; +} +.auth-form button { + margin-top: 8px; + align-self: flex-start; +} +.auth-form p { + margin-bottom: 0; +} +.form-row { + display: flex; + gap: 8px; + margin-top: 8px; +} +.form-row input { + flex: 1; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} +footer { + text-align: center; + color: #8c959f; + font-size: 12px; + padding: 24px; + border-top: 1px solid #eaeef2; +} +@media (max-width: 600px) { + .topbar { + padding: 0 20px; + } + .topbar-note { + display: none; + } + main { + padding: 24px 16px 40px; + } + .heading { + align-items: flex-start; + } + h1 { + font-size: 19px; + } + .toolbar { + gap: 10px; + } + #branch-count, + .commit-tip .hash { + display: none; + } + .clone-box { + width: min(340px, calc(100vw - 32px)); + } + .form-row { + flex-direction: column; + } +} diff --git a/examples/git/viewer/tsconfig.json b/examples/git/viewer/tsconfig.json new file mode 100644 index 0000000..cf18cce --- /dev/null +++ b/examples/git/viewer/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true, + "lib": ["ES2022", "DOM", "WebWorker"], + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} From 4b756131895567b04531f609cb2c75d31706512b Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Fri, 25 Sep 2026 21:36:51 -0700 Subject: [PATCH 04/16] Simplify repository viewer with Bun, Preact, and Biome Co-authored-by: Codex --- .github/workflows/ci.yml | 14 + examples/git/viewer/README.md | 96 +- examples/git/viewer/biome.json | 6 + examples/git/viewer/build.mjs | 50 - examples/git/viewer/build.ts | 29 + examples/git/viewer/bun.lock | 466 ++++ examples/git/viewer/package-lock.json | 3662 ------------------------- examples/git/viewer/package.json | 15 +- examples/git/viewer/src/client.ts | 301 -- examples/git/viewer/src/client.tsx | 347 +++ examples/git/viewer/src/index.ts | 5 +- examples/git/viewer/src/page.html | 83 +- examples/git/viewer/src/style.css | 339 +-- examples/git/viewer/tsconfig.json | 7 +- 14 files changed, 1002 insertions(+), 4418 deletions(-) create mode 100644 examples/git/viewer/biome.json delete mode 100644 examples/git/viewer/build.mjs create mode 100644 examples/git/viewer/build.ts create mode 100644 examples/git/viewer/bun.lock delete mode 100644 examples/git/viewer/package-lock.json delete mode 100644 examples/git/viewer/src/client.ts create mode 100644 examples/git/viewer/src/client.tsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fc729c..f80a77e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,20 @@ jobs: - name: Test composed WASI credentials run: make wasi-credential-test + repo-viewer: + runs-on: ubuntu-latest + timeout-minutes: 10 + defaults: + run: + working-directory: examples/git/viewer + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: "1.3.13" + - name: Lint, typecheck, and build viewer + run: bun install --frozen-lockfile && bun run check && bun run build + spin-key-value: runs-on: ubuntu-latest timeout-minutes: 30 diff --git a/examples/git/viewer/README.md b/examples/git/viewer/README.md index 0426cfa..868d7c5 100644 --- a/examples/git/viewer/README.md +++ b/examples/git/viewer/README.md @@ -1,79 +1,51 @@ # Repository explorer -A small read-only repository UI implemented in TypeScript as its own Spin -component. It browses branches, directories and text files, and shows the eight -most recent commits along the first parent. The browser uses no framework or -third-party runtime library. +A small read-only UI for the Git example, built with TypeScript and Preact. It +browses branches, directories, text files and recent first-parent commits. A +separate Spin component serves the page; the existing Git service supplies the +data through its read authorization, retention and retry path. The WAL is +unchanged. -## Run +## Run locally -Install the [Git example prerequisites](../README.md) plus Node.js 24 or later. -Build the optional viewer and start both components from a checkout: +Install the [Git prerequisites](../README.md) and [Bun](https://bun.com/). +From the repository root: ```sh cd examples/git/viewer -npm ci -npm run check -npm run build +bun install --frozen-lockfile +bun run check +bun run build cd ../../.. OBJECT_LOG_GIT_LOCAL_MANIFEST=spin.viewer.toml make git-local ``` -The starter builds Git, starts an isolated MinIO backend, validates it, and -prints the Git URLs and password. Push a repository, then open: +Push to a printed Git URL, then open +`http://127.0.0.1:19100/browse?repo=sha256.git` and enter the printed password. +`/browse` also accepts a configured path such as `team/project.git`. Cognito +access tokens work with the same repository permissions. Credentials stay in +page memory and clear on reload. Branch and path selections stay in the URL; +Back/Forward and ordinary modified link clicks work. -```text -http://127.0.0.1:19100/browse?repo=sha256.git -``` - -For an existing backend, build Git with `make git-build` and pass -`-f spin.viewer.toml` to the Git guide's normal `spin up` command, using the same -protected variables file. - -The `/browse` landing page also accepts a repository path such as -`team/project.git`. Sign in with the Git password or a Cognito access token. -The page keeps the credential in memory and clears it on reload; it never puts -it in a URL, cookie, or browser storage. Directory links preserve the selected -branch, and browser Back/Forward works. +For an existing backend, use `spin up -f spin.viewer.toml` from `examples/git` +with your protected variables file. Hosted proxies must forward the three exact +viewer routes and `//_browse` to the service. -`npm run build` creates `viewer/dist/viewer.wasm` and the ignored -`examples/git/spin.viewer.toml`. That manifest copies the authoritative -`spin.toml` and adds three exact UI/asset routes. Rebuild the viewer after -changing the base manifest. The normal Git build and manifest require no Node -installation. A hosted reverse proxy must forward those exact viewer routes -and `//_browse` through its existing backend admission rules. +## Build and checks -## Data and limits +Bun bundles the browser and static handler; Spin's standard `j2w` compiler makes +the WASI component. `bun run check` runs Biome linting/formatting and strict +TypeScript checks. `bun run format` applies formatting and safe lint fixes. CI +checks and builds the viewer with the frozen Bun lockfile. -The TypeScript component serves only the page and its assets. It receives no -storage credentials, has no outbound hosts, and does not read the WAL. The -existing Git component supplies a narrow `GET //_browse` adapter -because Git smart HTTP has no tree or file browsing API. That adapter uses the -same read authorization, existing repository open, reader retention, and retry -path as fetch. The core log is unchanged. +The build generates `viewer/dist/viewer.wasm` and `spin.viewer.toml` from the +existing manifest. Rebuild after changing `spin.toml`. Normal Git builds do not +require Bun. The component has no storage credentials or outbound hosts. -Only branch tips are selectable. Tags and arbitrary object IDs are not exposed. -The prototype returns up to 500 entries in one directory, eight first-parent -commits, and UTF-8 text previews up to 256 KiB. Binary and larger files show a -summary; symlinks show their stored target without following it. Submodules -show their commit ID. Commit titles are bounded to 512 bytes. Paths are bounded -to 64 segments and 4096 UTF-8 bytes. File content is rendered as plain text. - -Run the adapter tests and Git gates from the repository root: - -```sh -make git-check -``` - -Check the UI source and locked build dependencies from this directory: - -```sh -npm run format:check -npm run check -npm audit -npm run build -``` +## Scope -The build pins `componentize-js` consistently across Spin's compiler packages -and explicitly supplies its preview shim. All compiler packages run at build -time; the resulting component needs only Spin at runtime. +Only branch tips are selectable. The UI shows eight first-parent commits, +500 entries per directory and UTF-8 text previews up to 256 KiB. Binary and +larger files show a summary. Symlinks show their stored target; submodules show +their commit ID. Names and file content render as text. The adapter's tests are +included in `make git-check`. diff --git a/examples/git/viewer/biome.json b/examples/git/viewer/biome.json new file mode 100644 index 0000000..b053781 --- /dev/null +++ b/examples/git/viewer/biome.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.14/schema.json", + "files": { "includes": ["src/**", "build.ts", "*.json"] }, + "formatter": { "indentStyle": "space", "lineWidth": 100 }, + "linter": { "rules": { "preset": "recommended" } } +} diff --git a/examples/git/viewer/build.mjs b/examples/git/viewer/build.mjs deleted file mode 100644 index 491a726..0000000 --- a/examples/git/viewer/build.mjs +++ /dev/null @@ -1,50 +0,0 @@ -import { readFile, writeFile } from "node:fs/promises"; -import { build } from "esbuild"; -import { SpinEsbuildPlugin } from "@spinframework/build-tools/plugins/esbuild"; - -const client = await build({ - entryPoints: ["src/client.ts"], - bundle: true, - format: "esm", - write: false, - minify: true, -}); -await build({ - entryPoints: ["src/index.ts"], - outfile: "build/component.js", - bundle: true, - format: "esm", - platform: "browser", - define: { - CLIENT_SCRIPT: JSON.stringify(client.outputFiles[0].text), - STYLES: JSON.stringify(await readFile("src/style.css", "utf8")), - PAGE: JSON.stringify(await readFile("src/page.html", "utf8")), - }, - plugins: [ - await SpinEsbuildPlugin({ componentize: { output: "dist/viewer.wasm" } }), - ], -}); - -const manifest = await readFile("../spin.toml", "utf8"); -await writeFile( - "../spin.viewer.toml", - manifest + - ` -# Generated by viewer/build.mjs; rebuild after changing spin.toml. -[[trigger.http]] -route = "/browse" -component = "viewer" - -[[trigger.http]] -route = "/browse/assets/client.js" -component = "viewer" - -[[trigger.http]] -route = "/browse/assets/style.css" -component = "viewer" - -[component.viewer] -source = "viewer/dist/viewer.wasm" -allowed_outbound_hosts = [] -`, -); diff --git a/examples/git/viewer/build.ts b/examples/git/viewer/build.ts new file mode 100644 index 0000000..ea958c7 --- /dev/null +++ b/examples/git/viewer/build.ts @@ -0,0 +1,29 @@ +const client = await Bun.build({ + entrypoints: ["src/client.tsx"], + target: "browser", + minify: true, +}); +if (!client.success) throw new AggregateError(client.logs, "Browser build failed"); +const component = await Bun.build({ + entrypoints: ["src/index.ts"], + target: "browser", + outdir: "build", + naming: "component.js", + define: { + CLIENT_SCRIPT: JSON.stringify(await client.outputs[0].text()), + STYLES: JSON.stringify(await Bun.file("src/style.css").text()), + PAGE: JSON.stringify(await Bun.file("src/page.html").text()), + }, +}); +if (!component.success) throw new AggregateError(component.logs, "Spin bundle failed"); +const routes = ["/browse", "/browse/assets/client.js", "/browse/assets/style.css"]; +await Bun.write( + "../spin.viewer.toml", + (await Bun.file("../spin.toml").text()) + + routes + .map((route) => `\n[[trigger.http]]\nroute = "${route}"\ncomponent = "viewer"\n`) + .join("") + + '\n[component.viewer]\nsource = "viewer/dist/viewer.wasm"\nallowed_outbound_hosts = []\n', +); + +export {}; diff --git a/examples/git/viewer/bun.lock b/examples/git/viewer/bun.lock new file mode 100644 index 0000000..6b7be7d --- /dev/null +++ b/examples/git/viewer/bun.lock @@ -0,0 +1,466 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "object-log-repo-viewer", + "dependencies": { + "@spinframework/wasi-http-proxy": "2.0.0", + "preact": "10.29.8", + }, + "devDependencies": { + "@biomejs/biome": "2.5.14", + "@bytecodealliance/preview2-shim": "0.26.0", + "@spinframework/build-tools": "2.1.1", + "@types/bun": "1.3.13", + "typescript": "7.0.2", + }, + }, + }, + "overrides": { + "@bytecodealliance/componentize-js": "0.23.0", + }, + "packages": { + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@andreiltd/componentize-qjs-binding-darwin-arm64": ["@andreiltd/componentize-qjs-binding-darwin-arm64@0.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CW+ukVP778x2NAHAC1FvXb5KVSXmUlUQxhQTFtF0mAqqOVnYrQgy4DDNUa48EGthwtLdgLT1+eE71qmQiyMEqg=="], + + "@andreiltd/componentize-qjs-binding-darwin-x64": ["@andreiltd/componentize-qjs-binding-darwin-x64@0.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-37uz97pCEuXNfoVuZMBbI6xUirSndxJyMzPa4UCQb9nJdZhzRujmrU79yiFl+5nDYQxcCjthhBVEKU3OxJNAdg=="], + + "@andreiltd/componentize-qjs-binding-linux-arm64-gnu": ["@andreiltd/componentize-qjs-binding-linux-arm64-gnu@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-L5S0C6qIx3f+DnwN42HnDuMhV8NgfqaY+J1PaJAw468Ip/JN+h67tVAp4wUvs8xgTqj6UnGSBbSZVGYifK2XtA=="], + + "@andreiltd/componentize-qjs-binding-linux-arm64-musl": ["@andreiltd/componentize-qjs-binding-linux-arm64-musl@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-bBaq8/tvQ6HweqBLKWkE5tsPEMf7g3l5EGtka3Z7AFDUOtFzkAkG67nW1OYiYT6/0pjwDYw7Mvya45f79PG28g=="], + + "@andreiltd/componentize-qjs-binding-linux-x64-gnu": ["@andreiltd/componentize-qjs-binding-linux-x64-gnu@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Xec4O4sy/xHlhPPhf7GAV41D+AT7f9vxxjBpE7lUIElZNhX/kc+Ty6AQG50xbHsT5Mi4XQvSmtJJqhnB6xi8Vw=="], + + "@andreiltd/componentize-qjs-binding-linux-x64-musl": ["@andreiltd/componentize-qjs-binding-linux-x64-musl@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-62+PgJ+tJOjgNvXzRuz3s76tPBaExCodeVhDIIXkGI/PNbulqTHiyYXRw4O3UX6/CO71Nux8onqnXxQkcXBlvg=="], + + "@andreiltd/componentize-qjs-binding-win32-arm64-msvc": ["@andreiltd/componentize-qjs-binding-win32-arm64-msvc@0.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-OZO/omsMQFpw3wJJs3XFN+iEu2ZQCWMK8BxkhmXVm1sddEHtgSTY2sT/zq69VZeXjC0zgN1HzlxHbNBq0OkcOQ=="], + + "@andreiltd/componentize-qjs-binding-win32-x64-msvc": ["@andreiltd/componentize-qjs-binding-win32-x64-msvc@0.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-IlhlcNOEFky46ypfiD6l8MS45zosfhKWUclr10evQFw5Cpky+pji2RAGJRnBXdLQvH4Q3bZr6KYlIhxmtGRhkw=="], + + "@biomejs/biome": ["@biomejs/biome@2.5.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.14", "@biomejs/cli-darwin-x64": "2.5.14", "@biomejs/cli-linux-arm64": "2.5.14", "@biomejs/cli-linux-arm64-musl": "2.5.14", "@biomejs/cli-linux-x64": "2.5.14", "@biomejs/cli-linux-x64-musl": "2.5.14", "@biomejs/cli-win32-arm64": "2.5.14", "@biomejs/cli-win32-x64": "2.5.14" }, "bin": { "biome": "bin/biome" } }, "sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.14", "", { "os": "win32", "cpu": "x64" }, "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ=="], + + "@bytecodealliance/componentize-js": ["@bytecodealliance/componentize-js@0.23.0", "", { "dependencies": { "@bytecodealliance/jco": "^1.15.1", "@bytecodealliance/weval": "^0.5.0", "@bytecodealliance/wizer": "^10.0.0", "es-module-lexer": "^1.6.0", "oxc-parser": "^0.76.0" }, "bin": { "componentize-js": "src/cli.js" } }, "sha512-Qc2+lrwJHU7lq4CBMyVVYE8ASyUFeegMYdRnMVG0wNpNiATzGDF/mWpjDNTeVZeT8L9chhK3vuLaQE9FVANvfw=="], + + "@bytecodealliance/componentize-js-0-19-3": ["@bytecodealliance/componentize-js@0.19.3", "", { "dependencies": { "@bytecodealliance/jco": "^1.15.1", "@bytecodealliance/wizer": "^10.0.0", "es-module-lexer": "^1.6.0", "oxc-parser": "^0.76.0" }, "bin": { "componentize-js": "src/cli.js" } }, "sha512-ju7Y4WeF0B9uMkSPHJgmT6ouEfSwbe9M1uR/YOnYZjBpxJjH9qzxIkJg/kf8NycVDyFJ2/lscmJ1E1uPiDQVRQ=="], + + "@bytecodealliance/jco": ["@bytecodealliance/jco@1.35.0", "", { "dependencies": { "@bytecodealliance/componentize-js": "^0.22.0", "@bytecodealliance/componentize-js-0-19-3": "npm:@bytecodealliance/componentize-js@^0.19.3", "@bytecodealliance/jco-std": "^0.5.0", "@bytecodealliance/jco-transpile": "^0.15.0", "@bytecodealliance/preview2-shim": "^0.26.0", "binaryen": "^130.0.0", "commander": "^14", "componentize-qjs": "^0.4.5", "mkdirp": "^3", "regexpu-core": "6.4.0", "rolldown": "^1.2.4", "typescript": "7.0.2", "typescript-compiler-api": "npm:typescript@6.0.3", "unenv": "2.0.0-rc.24" }, "bin": { "jco": "dist/jco.js" } }, "sha512-O53cWIMm/M1SpwLxKRicvAMDnzWf4zWs6I6qFZIGbn3HUovDMCCe9blkGxxYuyocHjVf/cXgNmlMu/x7Tyi77A=="], + + "@bytecodealliance/jco-node-fs": ["@bytecodealliance/jco-node-fs@0.3.2", "", { "optionalDependencies": { "@bytecodealliance/jco-node-fs-darwin-arm64": "0.3.2", "@bytecodealliance/jco-node-fs-darwin-x64": "0.3.2", "@bytecodealliance/jco-node-fs-linux-arm64-gnu": "0.3.2", "@bytecodealliance/jco-node-fs-linux-arm64-musl": "0.3.2", "@bytecodealliance/jco-node-fs-linux-x64-gnu": "0.3.2", "@bytecodealliance/jco-node-fs-linux-x64-musl": "0.3.2", "@bytecodealliance/jco-node-fs-win32-arm64-msvc": "0.3.2", "@bytecodealliance/jco-node-fs-win32-x64-msvc": "0.3.2" } }, "sha512-yxvqQreNcOQdMsEKZomkSqThtkxrAz3f1Cmy8BsV0UHi9saa2/D1KDB14IAXPYwDq/maS6lCzS+MuMQx6QkUgA=="], + + "@bytecodealliance/jco-node-fs-darwin-arm64": ["@bytecodealliance/jco-node-fs-darwin-arm64@0.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WXQq1tRVUWhbK+ImH0G8Kni2AR0sdokLf/KyXRw5P6n4A9ZbgMHOuPfN/PEsYgw4eosuTAonutK0xILk/hA81A=="], + + "@bytecodealliance/jco-node-fs-darwin-x64": ["@bytecodealliance/jco-node-fs-darwin-x64@0.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Fy/OjHnto7hNE5ksewVkmAMXG/uXyj+RAKFZWQFTzFkp73UyT5vuPjKq3nvx9KolUGWBG37BudEMb4ZAoD9TbQ=="], + + "@bytecodealliance/jco-node-fs-linux-arm64-gnu": ["@bytecodealliance/jco-node-fs-linux-arm64-gnu@0.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-OafRtRu3tfOFQARfkia/LDEqSo2R3yAL1xPwzf4RcRW6JqkOY7qKWsUS5+LPVo/aFaSkMX7F7bdFGpFPmcoZJg=="], + + "@bytecodealliance/jco-node-fs-linux-arm64-musl": ["@bytecodealliance/jco-node-fs-linux-arm64-musl@0.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-JblDiwD7+Y+w0d/tjaNDX4QdGvh5lyuDc+S/jDeT7sYA1caA9yiwIjLa8PC/lm2xudqSxD+Cuy4AosXvl+qI0g=="], + + "@bytecodealliance/jco-node-fs-linux-x64-gnu": ["@bytecodealliance/jco-node-fs-linux-x64-gnu@0.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-6FzmHoLOsa6b4X9AuFLsvlpNF66Dt4sRRftZHaT1I1pYzCWrJkkpgNJhRXxf67ZPmOwBZJT4dan/g752Rx4xRw=="], + + "@bytecodealliance/jco-node-fs-linux-x64-musl": ["@bytecodealliance/jco-node-fs-linux-x64-musl@0.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-SbAJG2PDV8YAHBESHU9sVSckOitJyvlezdF4CGP5/mOp01JnijVkLVXzGK62yVSfa5TpAybbSd1rk+v36NAW/Q=="], + + "@bytecodealliance/jco-node-fs-win32-arm64-msvc": ["@bytecodealliance/jco-node-fs-win32-arm64-msvc@0.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-nUWYdFysRQJGsIOCk0KAYN2smkNYiHG7rZ7m+0ZXqP+5wqWxKEjkDsORwXiBjdaWFLVww0anZ3SBxrju6P7hXQ=="], + + "@bytecodealliance/jco-node-fs-win32-x64-msvc": ["@bytecodealliance/jco-node-fs-win32-x64-msvc@0.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-RsPj2t8TvltsMWK7/Z7JcVC/IWPMi07/AJkNLiNTCuGTdT/sseqTThJJwhuz3sjFw7dJdxA+bSGT+23y/KT0fw=="], + + "@bytecodealliance/jco-std": ["@bytecodealliance/jco-std@0.5.0", "", { "dependencies": { "acorn": "8.17.0", "acorn-walk": "8.3.5", "minimatch": "10.2.6", "punycode": "2.3.1", "readable-stream": "4.7.0", "urlpattern-polyfill": "10.1.0", "whatwg-url": "14.2.0" }, "peerDependencies": { "@bytecodealliance/preview2-shim": "^0.25.0" } }, "sha512-TLklLNHUbCtXH9nRDDhZuk8rPYW0kd5R0ClvwpIXCBOyESHZGIGU1y7V47Uggz3i0h5WG3+srOg3aKfyZT4IIg=="], + + "@bytecodealliance/jco-transpile": ["@bytecodealliance/jco-transpile@0.15.0", "", { "dependencies": { "@bytecodealliance/preview2-shim": "^0.26.0", "@bytecodealliance/preview3-shim": "^0.7.0", "binaryen": "^130.0.0", "oxc-minify": "^0.136.0" } }, "sha512-MFftJ81B9TtHbvgCRZQod3jfgl62YvJszxsNdfx9IVNewxlX9q7U4Wd+w6gJzw+o9IjEGWoyg/BFpb/RnaJvlA=="], + + "@bytecodealliance/preview2-shim": ["@bytecodealliance/preview2-shim@0.26.0", "", { "dependencies": { "@bytecodealliance/jco-node-fs": "0.3.2" } }, "sha512-oYekIJYroM1yJygziKpuMGmL2XDKZ4XspkdxfKCQqF/E0tVkCxyRRT+gFP0dFzhrSL8oh6obRCv84bBf1sYmAA=="], + + "@bytecodealliance/preview3-shim": ["@bytecodealliance/preview3-shim@0.7.0", "", { "dependencies": { "@bytecodealliance/jco-node-fs": "0.3.2", "@bytecodealliance/preview2-shim": "^0.26.0" } }, "sha512-5bBAE1nKSIT6ZyWoyLQwV1VjJciAdgLk7hbMeO3y5mLJhrkBL9egE48x5IfvU1ubMsQsKWFrP0jle7ow0b5I6w=="], + + "@bytecodealliance/weval": ["@bytecodealliance/weval@0.5.0", "", { "dependencies": { "@napi-rs/lzma": "^1.1.2", "fflate": "^0.8.3", "tar": "^7.5.19" } }, "sha512-lIR6+enn52gcdMQM8C3YaNoUlzm90TRkJrIgiVky/hED81ezDjhnq+6Z4w0eSFf9C6b2nmdKUFwbp6q1PhiuIQ=="], + + "@bytecodealliance/wizer": ["@bytecodealliance/wizer@10.0.0", "", { "optionalDependencies": { "@bytecodealliance/wizer-darwin-arm64": "10.0.0", "@bytecodealliance/wizer-darwin-x64": "10.0.0", "@bytecodealliance/wizer-linux-arm64": "10.0.0", "@bytecodealliance/wizer-linux-s390x": "10.0.0", "@bytecodealliance/wizer-linux-x64": "10.0.0", "@bytecodealliance/wizer-win32-x64": "10.0.0" }, "bin": { "wizer": "wizer.js" } }, "sha512-ziWmovyu1jQl9TsKlfC2bwuUZwxVPFHlX4fOqTzxhgS76jITIo45nzODEwPgU+jjmOr8F3YX2V2wAChC5NKujg=="], + + "@bytecodealliance/wizer-darwin-arm64": ["@bytecodealliance/wizer-darwin-arm64@10.0.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "wizer-darwin-arm64": "wizer" } }, "sha512-dhZTWel+xccGTKSJtI9A7oM4yyP20FWflsT+AoqkOqkCY7kCNrj4tmMtZ6GXZFRDkrPY5+EnOh62sfShEibAMA=="], + + "@bytecodealliance/wizer-darwin-x64": ["@bytecodealliance/wizer-darwin-x64@10.0.0", "", { "os": "darwin", "cpu": "x64", "bin": { "wizer-darwin-x64": "wizer" } }, "sha512-r/LUIZw6Q3Hf4htd46mD+EBxfwjBkxVIrTM1r+B2pTCddoBYQnKVdVsI4UFyy7NoBxzEg8F8BwmTNoSLmFRjpw=="], + + "@bytecodealliance/wizer-linux-arm64": ["@bytecodealliance/wizer-linux-arm64@10.0.0", "", { "os": "linux", "cpu": "arm64", "bin": { "wizer-linux-arm64": "wizer" } }, "sha512-pGSfFWXzeTqHm6z1PtVaEn+7Fm3QGC8YnHrzBV4sQDVS3N1NwmuHZAc8kslmlFPNdu61ycEvdOsSgCny8JPQvg=="], + + "@bytecodealliance/wizer-linux-s390x": ["@bytecodealliance/wizer-linux-s390x@10.0.0", "", { "os": "linux", "cpu": "s390x", "bin": { "wizer-linux-s390x": "wizer" } }, "sha512-O8vHxRTAdb1lUnVXMIMTcp/9q4pq1D4iIKigJCipg2JN15taV9uFAWh0fO88wylXwuSlO7dOE1AwQl54fMKXQg=="], + + "@bytecodealliance/wizer-linux-x64": ["@bytecodealliance/wizer-linux-x64@10.0.0", "", { "os": "linux", "cpu": "x64", "bin": { "wizer-linux-x64": "wizer" } }, "sha512-fJtM1sy43FBMnp+xpapFX6U1YdTBKA/1T4CYfG/qeE8jn0SXk2EuiYoY/EnC2uyNy9hjTrvfdYO5n4MXW0EIdQ=="], + + "@bytecodealliance/wizer-win32-x64": ["@bytecodealliance/wizer-win32-x64@10.0.0", "", { "os": "win32", "cpu": "x64", "bin": { "wizer-win32-x64": "wizer" } }, "sha512-55BPLfGT7iT7gH5M69NpTM16QknJZ7OxJ0z73VOEoeGA9CT8QPKMRzFKsPIvLs+W8G28fdudFA94nElrdkp3Kg=="], + + "@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@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/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": ["@napi-rs/lzma@1.5.1", "", { "optionalDependencies": { "@napi-rs/lzma-android-arm-eabi": "1.5.1", "@napi-rs/lzma-android-arm64": "1.5.1", "@napi-rs/lzma-darwin-arm64": "1.5.1", "@napi-rs/lzma-darwin-x64": "1.5.1", "@napi-rs/lzma-freebsd-x64": "1.5.1", "@napi-rs/lzma-linux-arm-gnueabihf": "1.5.1", "@napi-rs/lzma-linux-arm64-gnu": "1.5.1", "@napi-rs/lzma-linux-arm64-musl": "1.5.1", "@napi-rs/lzma-linux-ppc64-gnu": "1.5.1", "@napi-rs/lzma-linux-riscv64-gnu": "1.5.1", "@napi-rs/lzma-linux-s390x-gnu": "1.5.1", "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@napi-rs/lzma-linux-x64-musl": "1.5.1", "@napi-rs/lzma-wasm32-wasi": "1.5.1", "@napi-rs/lzma-win32-arm64-msvc": "1.5.1", "@napi-rs/lzma-win32-ia32-msvc": "1.5.1", "@napi-rs/lzma-win32-x64-msvc": "1.5.1" } }, "sha512-sgOZ89+y8cDbY+3WbzR8CtIhCuFRWotZ9/2PjPVDJHz6np5KFTAev0DrwiyTJTgFsCRDhfGlbmhMgyhHbWdZ6g=="], + + "@napi-rs/lzma-android-arm-eabi": ["@napi-rs/lzma-android-arm-eabi@1.5.1", "", { "os": "android", "cpu": "arm" }, "sha512-sahBe4ko2Z69NPTddaX6ZgbQZu9SDoITxw1S3dWl1gAGynZG34qHHCT8UaUMFxf3h3zMhCJjEzz4basaBxiTuQ=="], + + "@napi-rs/lzma-android-arm64": ["@napi-rs/lzma-android-arm64@1.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-7tkQAJJuBHxAxiEBNFgSTpvrtGpbwZYYJUSOmGEK3OfbdbNeoT2rdBxpM/gY1s+itEVbtOSlpaRPPG19MnwOzA=="], + + "@napi-rs/lzma-darwin-arm64": ["@napi-rs/lzma-darwin-arm64@1.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XWX8gtF+GHGk3nH3Wm3QUZNcxw9QHsFVZz3MzVLhWWHhceede1J4/vD+3dj3E1iKB9G6mualaZxOoD08R3E+7g=="], + + "@napi-rs/lzma-darwin-x64": ["@napi-rs/lzma-darwin-x64@1.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-CfsqUpMTI1z8enrA/b+GcHM6YDI8D0kqCiqPYEnst4rbOABQ9KZ92ybTTNnlnZ7A017WoMZKUEWc36KXDwi0xg=="], + + "@napi-rs/lzma-freebsd-x64": ["@napi-rs/lzma-freebsd-x64@1.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-bTyNfg90FXIgE61U7l14aMmVOqRQ6AyP5JMT3jmCStaZI18apLNPdzZ8i7yqxZfKvRMVfPjE2brXIw27c+RRgA=="], + + "@napi-rs/lzma-linux-arm-gnueabihf": ["@napi-rs/lzma-linux-arm-gnueabihf@1.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-vNE+D8nrw+eOkBsdKCsmDhowDV3pIMKXEhedvXfbgrWbrO7GlZJH+RXL+X+RYLxGwi8Ym61ZMt15sIOnNmh9Sw=="], + + "@napi-rs/lzma-linux-arm64-gnu": ["@napi-rs/lzma-linux-arm64-gnu@1.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-csUem4WgoKGTprv/pOPm9UIWbb+hrfUwYXefpTHPAEGVFLl5behEFabisJ7FtihCa3yG2Efcl+yw25rlhhrIYw=="], + + "@napi-rs/lzma-linux-arm64-musl": ["@napi-rs/lzma-linux-arm64-musl@1.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-kB/xhlVN1eLvVmDJSKZEjp5Gg2xDYexNrB5jwpSMbOkeGS6N9AasByPBg5VqCpMYC+zZi7DM458DRhtWYhqXTQ=="], + + "@napi-rs/lzma-linux-ppc64-gnu": ["@napi-rs/lzma-linux-ppc64-gnu@1.5.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-s28RW0W1yBWQc1nbPdF7tp14koqslY3ZWLVI8uaanX292Dc6ezd4NPVwxEoCNBVON/oD7BmUbWGtyFvmm7dQ5A=="], + + "@napi-rs/lzma-linux-riscv64-gnu": ["@napi-rs/lzma-linux-riscv64-gnu@1.5.1", "", { "os": "linux", "cpu": "none" }, "sha512-+lGNwYlIN14YPMTNvYtIJJqHFevDTd6Juw/1NmXbWx/iRd/LLrjhlM/yluMX6pxs6NkOGsuuEXJJrbbEUS59OQ=="], + + "@napi-rs/lzma-linux-s390x-gnu": ["@napi-rs/lzma-linux-s390x-gnu@1.5.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-PB44FFWWFrLeQowhcep1hPD1YcLqKlnnY60RMU74qrxTlr4YGEyzeMItJqh2uivBfv9kQScOF/B0J9+Vab/oyw=="], + + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + + "@napi-rs/lzma-linux-x64-musl": ["@napi-rs/lzma-linux-x64-musl@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-I3nsYrWtrW9JpeCr+mkJIVDt0HY3m6qVUBs5vTtoIvJQxwqf1PBXSy5IS7T53ksQFH2kd2UX8rLxJ7B4WISpZg=="], + + "@napi-rs/lzma-wasm32-wasi": ["@napi-rs/lzma-wasm32-wasi@1.5.1", "", { "dependencies": { "@emnapi/core": "1.11.2", "@emnapi/runtime": "1.11.2", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-gy3wwPBa6+XEyA4fUzq6CClrXA1ajXjuVf5zbnHytJRgoHznj+mvpU3+co2fxXwqTCmIpn6KrzqH5bRDztBPhA=="], + + "@napi-rs/lzma-win32-arm64-msvc": ["@napi-rs/lzma-win32-arm64-msvc@1.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-dK+huOsHiyH6oJjij+cnjqFCakk2HgWmpI12Xm4pLUyPphe4ebYoJBgehaNAxprmjFqBQ7nL95YPVz9BHyqmPg=="], + + "@napi-rs/lzma-win32-ia32-msvc": ["@napi-rs/lzma-win32-ia32-msvc@1.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-dGE8L+0EQ+GyU9ap9InqB/t/PmPG/bLj918q7OsJ29FuTdn8fK4OX3U4IQZhylHIA+/dQ/SXJk5n4yfah2XVvA=="], + + "@napi-rs/lzma-win32-x64-msvc": ["@napi-rs/lzma-win32-x64-msvc@1.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-EKW4t/iqdCT/xnd5t9oXLvVER/PMNAWXKqUAl3fgvUcOILeZIIht77/dVnfFcc9htA/DCBXC/6YQWdW+LusjFA=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + + "@oxc-minify/binding-android-arm-eabi": ["@oxc-minify/binding-android-arm-eabi@0.136.0", "", { "os": "android", "cpu": "arm" }, "sha512-gk1jzky1vLeOcK7w9Ib2RT/UooZ7O21s8YSeXCsy4tzsmguKb3Cbsv/Ra2fKhMp4mKeKCzvC1Xa41OfSBWfWdQ=="], + + "@oxc-minify/binding-android-arm64": ["@oxc-minify/binding-android-arm64@0.136.0", "", { "os": "android", "cpu": "arm64" }, "sha512-5GIYanqud/ohXEcg2dsdn0+eSKXqclw5qlRUkvSM1sKEa4ZQ7hqOk8XPJBNPh3LXXw+Qw85B6jkmOFLKsCHp5w=="], + + "@oxc-minify/binding-darwin-arm64": ["@oxc-minify/binding-darwin-arm64@0.136.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QAMpHKXWOoMCTenSZjQjVT5YmMooujlCKAf1T3PHFl2LuaA6ukQFXba0bgSLgAxRrAeX/itkwi4lzVQXtYmnRg=="], + + "@oxc-minify/binding-darwin-x64": ["@oxc-minify/binding-darwin-x64@0.136.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDnoPj8hztiU8C1VNKuzujh7SgTJlgiUvUCiWQjTnVSLsABsLW9ZWd1l1Ji0VN3UW39wGpptgvahIWTrHnGH/A=="], + + "@oxc-minify/binding-freebsd-x64": ["@oxc-minify/binding-freebsd-x64@0.136.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-9fGSgP7uyPLCTjMx9z3AUV4UjnfOVoGTH5JzamLegPwGLJ97Gr1jht1437CRk8ZNabGmxQR6qNn3e+Tox2lswg=="], + + "@oxc-minify/binding-linux-arm-gnueabihf": ["@oxc-minify/binding-linux-arm-gnueabihf@0.136.0", "", { "os": "linux", "cpu": "arm" }, "sha512-k0QvZS+x6ROI4O5Ikqn9dDU2dmkEJ46Yh6baZJI/eUk/a1k1GOs9/vyXItLZrKlwYrSgdEx2BFXbKgyd1NyRPw=="], + + "@oxc-minify/binding-linux-arm-musleabihf": ["@oxc-minify/binding-linux-arm-musleabihf@0.136.0", "", { "os": "linux", "cpu": "arm" }, "sha512-iRFxUc8ubotVfuyq3e4LPGbHrjvWd1/B3499a55GueMLc1EDJ0fHzNXJiCx2eMQGfzHtHodjmvrJ2BSvSXBNhQ=="], + + "@oxc-minify/binding-linux-arm64-gnu": ["@oxc-minify/binding-linux-arm64-gnu@0.136.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-hbKxNCXJsULrb6EyG5SumzJKuPfP8Y0UQ/TElRgXVUhyyPf6cBPlF8HMq+MAylF98ACBzV8jBp2Mm62lJyTo+w=="], + + "@oxc-minify/binding-linux-arm64-musl": ["@oxc-minify/binding-linux-arm64-musl@0.136.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-T+u/CGKW0UXemJeVux4ix6rK+ug3bQCUEbu5hFlX9K61KQNd44rOcrM15mDK99L7USaXwmiwHeIkEDvmmyDMSA=="], + + "@oxc-minify/binding-linux-ppc64-gnu": ["@oxc-minify/binding-linux-ppc64-gnu@0.136.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WNPTN2F448tb49BoqjV9IMRzZWa0WB8GHzrB+bnxKQPOyzz07T6rmDKrr4Hjl81ADGjDYSsr1fgXcjyLQFdMdw=="], + + "@oxc-minify/binding-linux-riscv64-gnu": ["@oxc-minify/binding-linux-riscv64-gnu@0.136.0", "", { "os": "linux", "cpu": "none" }, "sha512-MvcCfu8OeabrmNN9wqhh8Gqizg68RQ3iOvXBJmlLMy5p1PC6az9bAwqOk98lsCmKI4I5LpULekOlfxRYh3o5Kw=="], + + "@oxc-minify/binding-linux-riscv64-musl": ["@oxc-minify/binding-linux-riscv64-musl@0.136.0", "", { "os": "linux", "cpu": "none" }, "sha512-rwJXFUFFA0egAULtOOgfW8YhhLhtWrr2qFxftvZ7/U6Xtxiev8i+eCwSNW1RpHiX74QMsJVpRE3ykoefqb1MtQ=="], + + "@oxc-minify/binding-linux-s390x-gnu": ["@oxc-minify/binding-linux-s390x-gnu@0.136.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-qizAxHmj7VHm5/D5qiIlvzxAS2tlmadDOJ5XFI7zx/wfnk/+fdk5iVaHOv+C4tDE/4wLQyMoBYVIs6R1LeN91w=="], + + "@oxc-minify/binding-linux-x64-gnu": ["@oxc-minify/binding-linux-x64-gnu@0.136.0", "", { "os": "linux", "cpu": "x64" }, "sha512-gcyLAad0UblkEhGjVfx5Mwq63OSx8r04IbcfwzpZYpHisO/vioMsrs4GkDsQZ2Ejc8cdUk6YMIrf9FwNBzh+Dw=="], + + "@oxc-minify/binding-linux-x64-musl": ["@oxc-minify/binding-linux-x64-musl@0.136.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QAK6kwBjljX6luYzeppVdRP3gTaFNFfp9iS/V0cCvU43wUZay5xFXBgmzBHYrzZV2MhzURNnXfEZRo9poAxyvA=="], + + "@oxc-minify/binding-openharmony-arm64": ["@oxc-minify/binding-openharmony-arm64@0.136.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nIGxkTOnN9GwslmooxbZ3Ti2RgbX9QLHy0X9uO0pslOT+4dsNVUawBU/6r4M5cWM+ZdRD2tOF4V+n0l8tS59nQ=="], + + "@oxc-minify/binding-wasm32-wasi": ["@oxc-minify/binding-wasm32-wasi@0.136.0", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.5" }, "cpu": "none" }, "sha512-YB3G8AHT6PpDS8/DfjlJrQjywj+/6gCkNBcAJdYzcYbQRIZE6SaTNowiGuZPR1m1jOl1WjcjefniKSmVxzhvRA=="], + + "@oxc-minify/binding-win32-arm64-msvc": ["@oxc-minify/binding-win32-arm64-msvc@0.136.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-UVRH3BlNc2rmla+sbSd5xTF/u+esCqZS6VSWPolZybcBh3TlVe1Sf5MOuHQWUaFoyv5uFOvee4hOfS/14nPvZw=="], + + "@oxc-minify/binding-win32-ia32-msvc": ["@oxc-minify/binding-win32-ia32-msvc@0.136.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zFGkUd82jci0E+isc2O36lVmmXVUDXyEMN2RYIyez+MBNqhdjPLIGx62FCsrJtLKeuiPf/8XkFv977aUtndUng=="], + + "@oxc-minify/binding-win32-x64-msvc": ["@oxc-minify/binding-win32-x64-msvc@0.136.0", "", { "os": "win32", "cpu": "x64" }, "sha512-VHgPwd5xibC9UYxBeq7nURTjY+wxhdumllij6JaCIUVfD9IRcrA66SVlJ5fPNoUXcRp8ZXhJfoIZwVz9wAEGJw=="], + + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.76.0", "", { "os": "android", "cpu": "arm64" }, "sha512-1XJW/16CDmF5bHE7LAyPPmEEVnxSadDgdJz+xiLqBrmC4lfAeuAfRw3HlOygcPGr+AJsbD4Z5sFJMkwjbSZlQg=="], + + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.76.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yoQwSom8xsB+JdGsPUU0xxmxLKiF2kdlrK7I56WtGKZilixuBf/TmOwNYJYLRWkBoW5l2/pDZOhBm2luwmLiLw=="], + + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.76.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-uRIopPLvr3pf2Xj7f5LKyCuqzIU6zOS+zEIR8UDYhcgJyZHnvBkfrYnfcztyIcrGdQehrFUi3uplmI09E7RdiQ=="], + + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.76.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-a0EOFvnOd2FqmDSvH6uWLROSlU6KV/JDKbsYDA/zRLyKcG6HCsmFnPsp8iV7/xr9WMbNgyJi6R5IMpePQlUq7Q=="], + + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.76.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ikRYDHL3fOdZwfJKmcdqjlLgkeNZ3Ez0qM8wAev5zlHZ+lY/Ig7qG5SCqPlvuTu+nNQ6zrFFaKvvt69EBKXU/g=="], + + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.76.0", "", { "os": "linux", "cpu": "arm" }, "sha512-dtRv5J5MRCLR7x39K8ufIIW4svIc7gYFUaI0YFXmmeOBhK/K2t/CkguPnDroKtsmXIPHDRtmJ1JJYzNcgJl6Wg=="], + + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.76.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IE4iiiggFH2snagQxHrY5bv6dDpRMMat+vdlMN/ibonA65eOmRLp8VLTXnDiNrcla/itJ1L9qGABHNKU+SnE8g=="], + + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.76.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-wi9zQPMDHrBuRuT7Iurfidc9qlZh7cKa5vfYzOWNBCaqJdgxmNOFzvYen02wVUxSWGKhpiPHxrPX0jdRyJ8Npg=="], + + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.76.0", "", { "os": "linux", "cpu": "none" }, "sha512-0tqqu1pqPee2lLGY8vtYlX1L415fFn89e0a3yp4q5N9f03j1rRs0R31qesTm3bt/UK8HYjECZ+56FCVPs2MEMQ=="], + + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.76.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-y36Hh1a5TA+oIGtlc8lT7N9vdHXBlhBetQJW0p457KbiVQ7jF7AZkaPWhESkjHWAsTVKD2OjCa9ZqfaqhSI0FQ=="], + + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.76.0", "", { "os": "linux", "cpu": "x64" }, "sha512-7/acaG9htovp3gp/J0kHgbItQTuHctl+rbqPPqZ9DRBYTz8iV8kv3QN8t8Or8i/hOmOjfZp9McDoSU1duoR4/A=="], + + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.76.0", "", { "os": "linux", "cpu": "x64" }, "sha512-AxFt0reY6Q2rfudABmMTFGR8tFFr58NlH2rRBQgcj+F+iEwgJ+jMwAPhXd2y1I2zaI8GspuahedUYQinqxWqjA=="], + + "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.76.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-wHdkHdhf6AWBoO8vs5cpoR6zEFY1rB+fXWtq6j/xb9j/lu1evlujRVMkh8IM/M/pOUIrNkna3nzST/mRImiveQ=="], + + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.76.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-G7ZlEWcb2hNwCK3qalzqJoyB6HaTigQ/GEa7CU8sAJ/WwMdG/NnPqiC9IqpEAEy1ARSo4XMALfKbKNuqbSs5mg=="], + + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.76.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0jLzzmnu8/mqNhKBnNS2lFUbPEzRdj5ReiZwHGHpjma0+ullmmwP2AqSEqx3ssHDK9CpcEMdKOK2LsbCfhHKIA=="], + + "@oxc-project/types": ["@oxc-project/types@0.76.0", "", {}, "sha512-CH3THIrSViKal8yV/Wh3FK0pFhp40nzW1MUDCik9fNuid2D/7JJXKJnfFOAvMxInGXDlvmgT6ACAzrl47TqzkQ=="], + + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.11", "", { "os": "android", "cpu": "arm" }, "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.11", "", { "os": "android", "cpu": "arm64" }, "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.11", "", { "os": "linux", "cpu": "arm" }, "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.11", "", { "os": "none", "cpu": "arm64" }, "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.11", "", { "os": "win32", "cpu": "x64" }, "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@spinframework/build-tools": ["@spinframework/build-tools@2.1.1", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bytecodealliance/componentize-js": "^0.23.0", "@bytecodealliance/jco": "^1.17.9", "acorn-walk": "^8.3.4", "acron": "^1.0.5", "magic-string": "^0.30.17", "regexpu-core": "^6.2.0", "yargs": "^18.0.0" }, "bin": { "j2w": "dist/index.js" } }, "sha512-VF4NRtkqW0vUv3MzgwbyZKzG4qC1D2DrmFbr+P3uJPLq0stuLqcuASqq3LzrxTbTnpAeeswn8JhEe+wPbH1Lng=="], + + "@spinframework/wasi-http-proxy": ["@spinframework/wasi-http-proxy@2.0.0", "", { "peerDependencies": { "@spinframework/build-tools": "^2.0.0" } }, "sha512-nMW4QcueYajZ4fqcC3gTbfWuJv3hm8oc6sJfc5FI4y1hEOH1K7ciV6g+9R3EEmQl8GjeZhI5rdQ9Anz3N4NVqQ=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], + + "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + + "@types/node": ["@types/node@26.6.3", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg=="], + + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + + "acorn": ["acorn@8.17.0", "", { "bin": "bin/acorn" }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], + + "acorn-walk": ["acorn-walk@8.3.5", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw=="], + + "acron": ["acron@1.0.5", "", { "dependencies": { "lodash": "^4.17.11" }, "bin": "cli.js" }, "sha512-Jm3uey9PSUl5k9RsjBSfDJG35nG0vd9B4FUCV1J/NeVHDQ5HJPVpAhmGrPYtPKh0gLhEUIEy4DKqBy/p60B+4Q=="], + + "ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "binaryen": ["binaryen@130.0.0", "", { "bin": { "wasm-as": "bin/wasm-as", "wasm-ctor-eval": "bin/wasm-ctor-eval", "wasm-dis": "bin/wasm-dis", "wasm-merge": "bin/wasm-merge", "wasm-metadce": "bin/wasm-metadce", "wasm-opt": "bin/wasm-opt", "wasm-reduce": "bin/wasm-reduce", "wasm-shell": "bin/wasm-shell", "wasm2js": "bin/wasm2js" } }, "sha512-XDrb+zql0RbFPKgj7MuH9zOc78R3Fa/P/VSGnnpdwYsvNZPWjcMYMdAkKCOQEL2A7yqgjSMTDRFp6gfSDW+/QQ=="], + + "brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "componentize-qjs": ["componentize-qjs@0.4.5", "", { "optionalDependencies": { "@andreiltd/componentize-qjs-binding-darwin-arm64": "0.4.5", "@andreiltd/componentize-qjs-binding-darwin-x64": "0.4.5", "@andreiltd/componentize-qjs-binding-linux-arm64-gnu": "0.4.5", "@andreiltd/componentize-qjs-binding-linux-arm64-musl": "0.4.5", "@andreiltd/componentize-qjs-binding-linux-x64-gnu": "0.4.5", "@andreiltd/componentize-qjs-binding-linux-x64-musl": "0.4.5", "@andreiltd/componentize-qjs-binding-win32-arm64-msvc": "0.4.5", "@andreiltd/componentize-qjs-binding-win32-x64-msvc": "0.4.5" }, "bin": "bin/componentize-qjs" }, "sha512-/Brt+/Ps7DQ1mYuUsDISTQTFSaZfCSoRHkHnh9Dm5WHCO6arc/vzVdD8TXssfyNex2Iv3UR8NHdahsqABPDh0w=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.7.0", "", {}, "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "mkdirp": ["mkdirp@3.0.1", "", { "bin": "dist/cjs/src/bin.js" }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + + "oxc-minify": ["oxc-minify@0.136.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm-eabi": "0.136.0", "@oxc-minify/binding-android-arm64": "0.136.0", "@oxc-minify/binding-darwin-arm64": "0.136.0", "@oxc-minify/binding-darwin-x64": "0.136.0", "@oxc-minify/binding-freebsd-x64": "0.136.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.136.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.136.0", "@oxc-minify/binding-linux-arm64-gnu": "0.136.0", "@oxc-minify/binding-linux-arm64-musl": "0.136.0", "@oxc-minify/binding-linux-ppc64-gnu": "0.136.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.136.0", "@oxc-minify/binding-linux-riscv64-musl": "0.136.0", "@oxc-minify/binding-linux-s390x-gnu": "0.136.0", "@oxc-minify/binding-linux-x64-gnu": "0.136.0", "@oxc-minify/binding-linux-x64-musl": "0.136.0", "@oxc-minify/binding-openharmony-arm64": "0.136.0", "@oxc-minify/binding-wasm32-wasi": "0.136.0", "@oxc-minify/binding-win32-arm64-msvc": "0.136.0", "@oxc-minify/binding-win32-ia32-msvc": "0.136.0", "@oxc-minify/binding-win32-x64-msvc": "0.136.0" } }, "sha512-u9UvPFTYVwQ/i1kpp/c20S/SbvLaLV0Ngzyhgi0YV4B1SlxM7RgnnePl4vY9N3Z9sTRcozL1MRStI3PlUN9RFA=="], + + "oxc-parser": ["oxc-parser@0.76.0", "", { "dependencies": { "@oxc-project/types": "^0.76.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm64": "0.76.0", "@oxc-parser/binding-darwin-arm64": "0.76.0", "@oxc-parser/binding-darwin-x64": "0.76.0", "@oxc-parser/binding-freebsd-x64": "0.76.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.76.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.76.0", "@oxc-parser/binding-linux-arm64-gnu": "0.76.0", "@oxc-parser/binding-linux-arm64-musl": "0.76.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.76.0", "@oxc-parser/binding-linux-s390x-gnu": "0.76.0", "@oxc-parser/binding-linux-x64-gnu": "0.76.0", "@oxc-parser/binding-linux-x64-musl": "0.76.0", "@oxc-parser/binding-wasm32-wasi": "0.76.0", "@oxc-parser/binding-win32-arm64-msvc": "0.76.0", "@oxc-parser/binding-win32-x64-msvc": "0.76.0" } }, "sha512-l98B2e9evuhES7zN99rb1QGhbzx25829TJFaKi2j0ib3/K/G5z1FdGYz6HZkrU3U8jdH7v2FC8mX1j2l9JrOUg=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "preact": ["preact@10.29.8", "", { "peerDependencies": { "preact-render-to-string": ">=5" }, "optionalPeers": ["preact-render-to-string"] }, "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q=="], + + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + + "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], + + "regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + + "regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], + + "regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="], + + "regjsparser": ["regjsparser@0.13.3", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": "bin/parser" }, "sha512-ycwFAS14Jw4mppvmK4GR/J6u3WpWpjkEApehuHtLc/8VpPNpDMbQ4WjqwplXifGeyKOzHSFLmSPqzksDQE2Sfg=="], + + "rolldown": ["rolldown@1.2.11", "", { "dependencies": { "@oxc-project/types": "=0.151.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.11", "@rolldown/binding-android-arm64": "1.2.11", "@rolldown/binding-darwin-arm64": "1.2.11", "@rolldown/binding-darwin-x64": "1.2.11", "@rolldown/binding-freebsd-x64": "1.2.11", "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", "@rolldown/binding-linux-arm64-gnu": "1.2.11", "@rolldown/binding-linux-arm64-musl": "1.2.11", "@rolldown/binding-linux-ppc64-gnu": "1.2.11", "@rolldown/binding-linux-s390x-gnu": "1.2.11", "@rolldown/binding-linux-x64-gnu": "1.2.11", "@rolldown/binding-linux-x64-musl": "1.2.11", "@rolldown/binding-openharmony-arm64": "1.2.11", "@rolldown/binding-win32-arm64-msvc": "1.2.11", "@rolldown/binding-win32-x64-msvc": "1.2.11" }, "bin": "bin/cli.mjs" }, "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], + + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + + "typescript-compiler-api": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], + + "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + + "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], + + "unicode-match-property-ecmascript": ["unicode-match-property-ecmascript@2.0.0", "", { "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" } }, "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q=="], + + "unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + + "unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.2.0", "", {}, "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ=="], + + "urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yargs": ["yargs@18.2.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w=="], + + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + + "@napi-rs/lzma-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="], + + "@oxc-minify/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], + + "@oxc-minify/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + + "@oxc-minify/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="], + + "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "rolldown/@oxc-project/types": ["@oxc-project/types@0.151.0", "", {}, "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA=="], + + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + } +} diff --git a/examples/git/viewer/package-lock.json b/examples/git/viewer/package-lock.json deleted file mode 100644 index b5dfa62..0000000 --- a/examples/git/viewer/package-lock.json +++ /dev/null @@ -1,3662 +0,0 @@ -{ - "name": "object-log-repo-viewer", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "object-log-repo-viewer", - "version": "0.1.0", - "license": "Apache-2.0", - "dependencies": { - "@spinframework/wasi-http-proxy": "2.0.0" - }, - "devDependencies": { - "@bytecodealliance/preview2-shim": "0.26.0", - "@spinframework/build-tools": "2.1.1", - "esbuild": "0.28.2", - "prettier": "3.8.1", - "typescript": "7.0.2" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@andreiltd/componentize-qjs-binding-darwin-arm64": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-darwin-arm64/-/componentize-qjs-binding-darwin-arm64-0.4.5.tgz", - "integrity": "sha512-CW+ukVP778x2NAHAC1FvXb5KVSXmUlUQxhQTFtF0mAqqOVnYrQgy4DDNUa48EGthwtLdgLT1+eE71qmQiyMEqg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@andreiltd/componentize-qjs-binding-darwin-x64": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-darwin-x64/-/componentize-qjs-binding-darwin-x64-0.4.5.tgz", - "integrity": "sha512-37uz97pCEuXNfoVuZMBbI6xUirSndxJyMzPa4UCQb9nJdZhzRujmrU79yiFl+5nDYQxcCjthhBVEKU3OxJNAdg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@andreiltd/componentize-qjs-binding-linux-arm64-gnu": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-linux-arm64-gnu/-/componentize-qjs-binding-linux-arm64-gnu-0.4.5.tgz", - "integrity": "sha512-L5S0C6qIx3f+DnwN42HnDuMhV8NgfqaY+J1PaJAw468Ip/JN+h67tVAp4wUvs8xgTqj6UnGSBbSZVGYifK2XtA==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@andreiltd/componentize-qjs-binding-linux-arm64-musl": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-linux-arm64-musl/-/componentize-qjs-binding-linux-arm64-musl-0.4.5.tgz", - "integrity": "sha512-bBaq8/tvQ6HweqBLKWkE5tsPEMf7g3l5EGtka3Z7AFDUOtFzkAkG67nW1OYiYT6/0pjwDYw7Mvya45f79PG28g==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@andreiltd/componentize-qjs-binding-linux-x64-gnu": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-linux-x64-gnu/-/componentize-qjs-binding-linux-x64-gnu-0.4.5.tgz", - "integrity": "sha512-Xec4O4sy/xHlhPPhf7GAV41D+AT7f9vxxjBpE7lUIElZNhX/kc+Ty6AQG50xbHsT5Mi4XQvSmtJJqhnB6xi8Vw==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@andreiltd/componentize-qjs-binding-linux-x64-musl": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-linux-x64-musl/-/componentize-qjs-binding-linux-x64-musl-0.4.5.tgz", - "integrity": "sha512-62+PgJ+tJOjgNvXzRuz3s76tPBaExCodeVhDIIXkGI/PNbulqTHiyYXRw4O3UX6/CO71Nux8onqnXxQkcXBlvg==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@andreiltd/componentize-qjs-binding-win32-arm64-msvc": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-win32-arm64-msvc/-/componentize-qjs-binding-win32-arm64-msvc-0.4.5.tgz", - "integrity": "sha512-OZO/omsMQFpw3wJJs3XFN+iEu2ZQCWMK8BxkhmXVm1sddEHtgSTY2sT/zq69VZeXjC0zgN1HzlxHbNBq0OkcOQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@andreiltd/componentize-qjs-binding-win32-x64-msvc": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@andreiltd/componentize-qjs-binding-win32-x64-msvc/-/componentize-qjs-binding-win32-x64-msvc-0.4.5.tgz", - "integrity": "sha512-IlhlcNOEFky46ypfiD6l8MS45zosfhKWUclr10evQFw5Cpky+pji2RAGJRnBXdLQvH4Q3bZr6KYlIhxmtGRhkw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@bytecodealliance/componentize-js": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@bytecodealliance/componentize-js/-/componentize-js-0.23.0.tgz", - "integrity": "sha512-Qc2+lrwJHU7lq4CBMyVVYE8ASyUFeegMYdRnMVG0wNpNiATzGDF/mWpjDNTeVZeT8L9chhK3vuLaQE9FVANvfw==", - "workspaces": [ - "." - ], - "dependencies": { - "@bytecodealliance/jco": "^1.15.1", - "@bytecodealliance/weval": "^0.5.0", - "@bytecodealliance/wizer": "^10.0.0", - "es-module-lexer": "^1.6.0", - "oxc-parser": "^0.76.0" - }, - "bin": { - "componentize-js": "src/cli.js" - } - }, - "node_modules/@bytecodealliance/componentize-js-0-19-3": { - "name": "@bytecodealliance/componentize-js", - "version": "0.19.3", - "resolved": "https://registry.npmjs.org/@bytecodealliance/componentize-js/-/componentize-js-0.19.3.tgz", - "integrity": "sha512-ju7Y4WeF0B9uMkSPHJgmT6ouEfSwbe9M1uR/YOnYZjBpxJjH9qzxIkJg/kf8NycVDyFJ2/lscmJ1E1uPiDQVRQ==", - "workspaces": [ - "." - ], - "dependencies": { - "@bytecodealliance/jco": "^1.15.1", - "@bytecodealliance/wizer": "^10.0.0", - "es-module-lexer": "^1.6.0", - "oxc-parser": "^0.76.0" - }, - "bin": { - "componentize-js": "src/cli.js" - } - }, - "node_modules/@bytecodealliance/jco": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/@bytecodealliance/jco/-/jco-1.35.0.tgz", - "integrity": "sha512-O53cWIMm/M1SpwLxKRicvAMDnzWf4zWs6I6qFZIGbn3HUovDMCCe9blkGxxYuyocHjVf/cXgNmlMu/x7Tyi77A==", - "license": "(Apache-2.0 WITH LLVM-exception)", - "dependencies": { - "@bytecodealliance/componentize-js": "^0.22.0", - "@bytecodealliance/componentize-js-0-19-3": "npm:@bytecodealliance/componentize-js@^0.19.3", - "@bytecodealliance/jco-std": "^0.5.0", - "@bytecodealliance/jco-transpile": "^0.15.0", - "@bytecodealliance/preview2-shim": "^0.26.0", - "binaryen": "^130.0.0", - "commander": "^14", - "componentize-qjs": "^0.4.5", - "mkdirp": "^3", - "regexpu-core": "6.4.0", - "rolldown": "^1.2.4", - "typescript": "7.0.2", - "typescript-compiler-api": "npm:typescript@6.0.3", - "unenv": "2.0.0-rc.24" - }, - "bin": { - "jco": "dist/jco.js" - } - }, - "node_modules/@bytecodealliance/jco-node-fs": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs/-/jco-node-fs-0.3.2.tgz", - "integrity": "sha512-yxvqQreNcOQdMsEKZomkSqThtkxrAz3f1Cmy8BsV0UHi9saa2/D1KDB14IAXPYwDq/maS6lCzS+MuMQx6QkUgA==", - "license": "(Apache-2.0 WITH LLVM-exception)", - "engines": { - "node": ">=20" - }, - "optionalDependencies": { - "@bytecodealliance/jco-node-fs-darwin-arm64": "0.3.2", - "@bytecodealliance/jco-node-fs-darwin-x64": "0.3.2", - "@bytecodealliance/jco-node-fs-linux-arm64-gnu": "0.3.2", - "@bytecodealliance/jco-node-fs-linux-arm64-musl": "0.3.2", - "@bytecodealliance/jco-node-fs-linux-x64-gnu": "0.3.2", - "@bytecodealliance/jco-node-fs-linux-x64-musl": "0.3.2", - "@bytecodealliance/jco-node-fs-win32-arm64-msvc": "0.3.2", - "@bytecodealliance/jco-node-fs-win32-x64-msvc": "0.3.2" - } - }, - "node_modules/@bytecodealliance/jco-node-fs-darwin-arm64": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-darwin-arm64/-/jco-node-fs-darwin-arm64-0.3.2.tgz", - "integrity": "sha512-WXQq1tRVUWhbK+ImH0G8Kni2AR0sdokLf/KyXRw5P6n4A9ZbgMHOuPfN/PEsYgw4eosuTAonutK0xILk/hA81A==", - "cpu": [ - "arm64" - ], - "license": "(Apache-2.0 WITH LLVM-exception)", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20" - } - }, - "node_modules/@bytecodealliance/jco-node-fs-darwin-x64": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-darwin-x64/-/jco-node-fs-darwin-x64-0.3.2.tgz", - "integrity": "sha512-Fy/OjHnto7hNE5ksewVkmAMXG/uXyj+RAKFZWQFTzFkp73UyT5vuPjKq3nvx9KolUGWBG37BudEMb4ZAoD9TbQ==", - "cpu": [ - "x64" - ], - "license": "(Apache-2.0 WITH LLVM-exception)", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20" - } - }, - "node_modules/@bytecodealliance/jco-node-fs-linux-arm64-gnu": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-linux-arm64-gnu/-/jco-node-fs-linux-arm64-gnu-0.3.2.tgz", - "integrity": "sha512-OafRtRu3tfOFQARfkia/LDEqSo2R3yAL1xPwzf4RcRW6JqkOY7qKWsUS5+LPVo/aFaSkMX7F7bdFGpFPmcoZJg==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "(Apache-2.0 WITH LLVM-exception)", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20" - } - }, - "node_modules/@bytecodealliance/jco-node-fs-linux-arm64-musl": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-linux-arm64-musl/-/jco-node-fs-linux-arm64-musl-0.3.2.tgz", - "integrity": "sha512-JblDiwD7+Y+w0d/tjaNDX4QdGvh5lyuDc+S/jDeT7sYA1caA9yiwIjLa8PC/lm2xudqSxD+Cuy4AosXvl+qI0g==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "(Apache-2.0 WITH LLVM-exception)", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20" - } - }, - "node_modules/@bytecodealliance/jco-node-fs-linux-x64-gnu": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-linux-x64-gnu/-/jco-node-fs-linux-x64-gnu-0.3.2.tgz", - "integrity": "sha512-6FzmHoLOsa6b4X9AuFLsvlpNF66Dt4sRRftZHaT1I1pYzCWrJkkpgNJhRXxf67ZPmOwBZJT4dan/g752Rx4xRw==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "(Apache-2.0 WITH LLVM-exception)", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20" - } - }, - "node_modules/@bytecodealliance/jco-node-fs-linux-x64-musl": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-linux-x64-musl/-/jco-node-fs-linux-x64-musl-0.3.2.tgz", - "integrity": "sha512-SbAJG2PDV8YAHBESHU9sVSckOitJyvlezdF4CGP5/mOp01JnijVkLVXzGK62yVSfa5TpAybbSd1rk+v36NAW/Q==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "(Apache-2.0 WITH LLVM-exception)", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20" - } - }, - "node_modules/@bytecodealliance/jco-node-fs-win32-arm64-msvc": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-win32-arm64-msvc/-/jco-node-fs-win32-arm64-msvc-0.3.2.tgz", - "integrity": "sha512-nUWYdFysRQJGsIOCk0KAYN2smkNYiHG7rZ7m+0ZXqP+5wqWxKEjkDsORwXiBjdaWFLVww0anZ3SBxrju6P7hXQ==", - "cpu": [ - "arm64" - ], - "license": "(Apache-2.0 WITH LLVM-exception)", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20" - } - }, - "node_modules/@bytecodealliance/jco-node-fs-win32-x64-msvc": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-win32-x64-msvc/-/jco-node-fs-win32-x64-msvc-0.3.2.tgz", - "integrity": "sha512-RsPj2t8TvltsMWK7/Z7JcVC/IWPMi07/AJkNLiNTCuGTdT/sseqTThJJwhuz3sjFw7dJdxA+bSGT+23y/KT0fw==", - "cpu": [ - "x64" - ], - "license": "(Apache-2.0 WITH LLVM-exception)", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20" - } - }, - "node_modules/@bytecodealliance/jco-std": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-std/-/jco-std-0.5.0.tgz", - "integrity": "sha512-TLklLNHUbCtXH9nRDDhZuk8rPYW0kd5R0ClvwpIXCBOyESHZGIGU1y7V47Uggz3i0h5WG3+srOg3aKfyZT4IIg==", - "license": "(Apache-2.0 WITH LLVM-exception)", - "dependencies": { - "acorn": "8.17.0", - "acorn-walk": "8.3.5", - "minimatch": "10.2.6", - "punycode": "2.3.1", - "readable-stream": "4.7.0", - "urlpattern-polyfill": "10.1.0", - "whatwg-url": "14.2.0" - }, - "peerDependencies": { - "@bytecodealliance/preview2-shim": "^0.25.0" - }, - "peerDependenciesMeta": { - "@bytecodealliance/preview2-shim": { - "optional": true - } - } - }, - "node_modules/@bytecodealliance/jco-transpile": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-transpile/-/jco-transpile-0.15.0.tgz", - "integrity": "sha512-MFftJ81B9TtHbvgCRZQod3jfgl62YvJszxsNdfx9IVNewxlX9q7U4Wd+w6gJzw+o9IjEGWoyg/BFpb/RnaJvlA==", - "license": "(Apache-2.0 WITH LLVM-exception)", - "dependencies": { - "@bytecodealliance/preview2-shim": "^0.26.0", - "@bytecodealliance/preview3-shim": "^0.7.0", - "binaryen": "^130.0.0", - "oxc-minify": "^0.136.0" - } - }, - "node_modules/@bytecodealliance/preview2-shim": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.26.0.tgz", - "integrity": "sha512-oYekIJYroM1yJygziKpuMGmL2XDKZ4XspkdxfKCQqF/E0tVkCxyRRT+gFP0dFzhrSL8oh6obRCv84bBf1sYmAA==", - "license": "(Apache-2.0 WITH LLVM-exception)", - "dependencies": { - "@bytecodealliance/jco-node-fs": "0.3.2" - } - }, - "node_modules/@bytecodealliance/preview3-shim": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@bytecodealliance/preview3-shim/-/preview3-shim-0.7.0.tgz", - "integrity": "sha512-5bBAE1nKSIT6ZyWoyLQwV1VjJciAdgLk7hbMeO3y5mLJhrkBL9egE48x5IfvU1ubMsQsKWFrP0jle7ow0b5I6w==", - "license": "(Apache-2.0 WITH LLVM-exception)", - "dependencies": { - "@bytecodealliance/jco-node-fs": "0.3.2", - "@bytecodealliance/preview2-shim": "^0.26.0" - } - }, - "node_modules/@bytecodealliance/weval": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@bytecodealliance/weval/-/weval-0.5.0.tgz", - "integrity": "sha512-lIR6+enn52gcdMQM8C3YaNoUlzm90TRkJrIgiVky/hED81ezDjhnq+6Z4w0eSFf9C6b2nmdKUFwbp6q1PhiuIQ==", - "license": "Apache-2.0", - "dependencies": { - "@napi-rs/lzma": "^1.1.2", - "fflate": "^0.8.3", - "tar": "^7.5.19" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@bytecodealliance/wizer": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer/-/wizer-10.0.0.tgz", - "integrity": "sha512-ziWmovyu1jQl9TsKlfC2bwuUZwxVPFHlX4fOqTzxhgS76jITIo45nzODEwPgU+jjmOr8F3YX2V2wAChC5NKujg==", - "license": "Apache-2.0", - "bin": { - "wizer": "wizer.js" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@bytecodealliance/wizer-darwin-arm64": "10.0.0", - "@bytecodealliance/wizer-darwin-x64": "10.0.0", - "@bytecodealliance/wizer-linux-arm64": "10.0.0", - "@bytecodealliance/wizer-linux-s390x": "10.0.0", - "@bytecodealliance/wizer-linux-x64": "10.0.0", - "@bytecodealliance/wizer-win32-x64": "10.0.0" - } - }, - "node_modules/@bytecodealliance/wizer-darwin-arm64": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-darwin-arm64/-/wizer-darwin-arm64-10.0.0.tgz", - "integrity": "sha512-dhZTWel+xccGTKSJtI9A7oM4yyP20FWflsT+AoqkOqkCY7kCNrj4tmMtZ6GXZFRDkrPY5+EnOh62sfShEibAMA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "bin": { - "wizer-darwin-arm64": "wizer" - } - }, - "node_modules/@bytecodealliance/wizer-darwin-x64": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-darwin-x64/-/wizer-darwin-x64-10.0.0.tgz", - "integrity": "sha512-r/LUIZw6Q3Hf4htd46mD+EBxfwjBkxVIrTM1r+B2pTCddoBYQnKVdVsI4UFyy7NoBxzEg8F8BwmTNoSLmFRjpw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "bin": { - "wizer-darwin-x64": "wizer" - } - }, - "node_modules/@bytecodealliance/wizer-linux-arm64": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-linux-arm64/-/wizer-linux-arm64-10.0.0.tgz", - "integrity": "sha512-pGSfFWXzeTqHm6z1PtVaEn+7Fm3QGC8YnHrzBV4sQDVS3N1NwmuHZAc8kslmlFPNdu61ycEvdOsSgCny8JPQvg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "wizer-linux-arm64": "wizer" - } - }, - "node_modules/@bytecodealliance/wizer-linux-s390x": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-linux-s390x/-/wizer-linux-s390x-10.0.0.tgz", - "integrity": "sha512-O8vHxRTAdb1lUnVXMIMTcp/9q4pq1D4iIKigJCipg2JN15taV9uFAWh0fO88wylXwuSlO7dOE1AwQl54fMKXQg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "wizer-linux-s390x": "wizer" - } - }, - "node_modules/@bytecodealliance/wizer-linux-x64": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-linux-x64/-/wizer-linux-x64-10.0.0.tgz", - "integrity": "sha512-fJtM1sy43FBMnp+xpapFX6U1YdTBKA/1T4CYfG/qeE8jn0SXk2EuiYoY/EnC2uyNy9hjTrvfdYO5n4MXW0EIdQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "wizer-linux-x64": "wizer" - } - }, - "node_modules/@bytecodealliance/wizer-win32-x64": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@bytecodealliance/wizer-win32-x64/-/wizer-win32-x64-10.0.0.tgz", - "integrity": "sha512-55BPLfGT7iT7gH5M69NpTM16QknJZ7OxJ0z73VOEoeGA9CT8QPKMRzFKsPIvLs+W8G28fdudFA94nElrdkp3Kg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "bin": { - "wizer-win32-x64": "wizer" - } - }, - "node_modules/@emnapi/core": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", - "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", - "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/lzma": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma/-/lzma-1.5.1.tgz", - "integrity": "sha512-sgOZ89+y8cDbY+3WbzR8CtIhCuFRWotZ9/2PjPVDJHz6np5KFTAev0DrwiyTJTgFsCRDhfGlbmhMgyhHbWdZ6g==", - "license": "MIT", - "engines": { - "node": "^22.20 || ^24.12 || >=25" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "optionalDependencies": { - "@napi-rs/lzma-android-arm-eabi": "1.5.1", - "@napi-rs/lzma-android-arm64": "1.5.1", - "@napi-rs/lzma-darwin-arm64": "1.5.1", - "@napi-rs/lzma-darwin-x64": "1.5.1", - "@napi-rs/lzma-freebsd-x64": "1.5.1", - "@napi-rs/lzma-linux-arm-gnueabihf": "1.5.1", - "@napi-rs/lzma-linux-arm64-gnu": "1.5.1", - "@napi-rs/lzma-linux-arm64-musl": "1.5.1", - "@napi-rs/lzma-linux-ppc64-gnu": "1.5.1", - "@napi-rs/lzma-linux-riscv64-gnu": "1.5.1", - "@napi-rs/lzma-linux-s390x-gnu": "1.5.1", - "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@napi-rs/lzma-linux-x64-musl": "1.5.1", - "@napi-rs/lzma-wasm32-wasi": "1.5.1", - "@napi-rs/lzma-win32-arm64-msvc": "1.5.1", - "@napi-rs/lzma-win32-ia32-msvc": "1.5.1", - "@napi-rs/lzma-win32-x64-msvc": "1.5.1" - } - }, - "node_modules/@napi-rs/lzma-android-arm-eabi": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-android-arm-eabi/-/lzma-android-arm-eabi-1.5.1.tgz", - "integrity": "sha512-sahBe4ko2Z69NPTddaX6ZgbQZu9SDoITxw1S3dWl1gAGynZG34qHHCT8UaUMFxf3h3zMhCJjEzz4basaBxiTuQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-android-arm64": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-android-arm64/-/lzma-android-arm64-1.5.1.tgz", - "integrity": "sha512-7tkQAJJuBHxAxiEBNFgSTpvrtGpbwZYYJUSOmGEK3OfbdbNeoT2rdBxpM/gY1s+itEVbtOSlpaRPPG19MnwOzA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-darwin-arm64": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-darwin-arm64/-/lzma-darwin-arm64-1.5.1.tgz", - "integrity": "sha512-XWX8gtF+GHGk3nH3Wm3QUZNcxw9QHsFVZz3MzVLhWWHhceede1J4/vD+3dj3E1iKB9G6mualaZxOoD08R3E+7g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-darwin-x64": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-darwin-x64/-/lzma-darwin-x64-1.5.1.tgz", - "integrity": "sha512-CfsqUpMTI1z8enrA/b+GcHM6YDI8D0kqCiqPYEnst4rbOABQ9KZ92ybTTNnlnZ7A017WoMZKUEWc36KXDwi0xg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-freebsd-x64": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-freebsd-x64/-/lzma-freebsd-x64-1.5.1.tgz", - "integrity": "sha512-bTyNfg90FXIgE61U7l14aMmVOqRQ6AyP5JMT3jmCStaZI18apLNPdzZ8i7yqxZfKvRMVfPjE2brXIw27c+RRgA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-linux-arm-gnueabihf": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm-gnueabihf/-/lzma-linux-arm-gnueabihf-1.5.1.tgz", - "integrity": "sha512-vNE+D8nrw+eOkBsdKCsmDhowDV3pIMKXEhedvXfbgrWbrO7GlZJH+RXL+X+RYLxGwi8Ym61ZMt15sIOnNmh9Sw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-linux-arm64-gnu": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm64-gnu/-/lzma-linux-arm64-gnu-1.5.1.tgz", - "integrity": "sha512-csUem4WgoKGTprv/pOPm9UIWbb+hrfUwYXefpTHPAEGVFLl5behEFabisJ7FtihCa3yG2Efcl+yw25rlhhrIYw==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-linux-arm64-musl": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm64-musl/-/lzma-linux-arm64-musl-1.5.1.tgz", - "integrity": "sha512-kB/xhlVN1eLvVmDJSKZEjp5Gg2xDYexNrB5jwpSMbOkeGS6N9AasByPBg5VqCpMYC+zZi7DM458DRhtWYhqXTQ==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-linux-ppc64-gnu": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-ppc64-gnu/-/lzma-linux-ppc64-gnu-1.5.1.tgz", - "integrity": "sha512-s28RW0W1yBWQc1nbPdF7tp14koqslY3ZWLVI8uaanX292Dc6ezd4NPVwxEoCNBVON/oD7BmUbWGtyFvmm7dQ5A==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-linux-riscv64-gnu": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-riscv64-gnu/-/lzma-linux-riscv64-gnu-1.5.1.tgz", - "integrity": "sha512-+lGNwYlIN14YPMTNvYtIJJqHFevDTd6Juw/1NmXbWx/iRd/LLrjhlM/yluMX6pxs6NkOGsuuEXJJrbbEUS59OQ==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-linux-s390x-gnu": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-s390x-gnu/-/lzma-linux-s390x-gnu-1.5.1.tgz", - "integrity": "sha512-PB44FFWWFrLeQowhcep1hPD1YcLqKlnnY60RMU74qrxTlr4YGEyzeMItJqh2uivBfv9kQScOF/B0J9+Vab/oyw==", - "cpu": [ - "s390x" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-linux-x64-gnu": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", - "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-linux-x64-musl": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-musl/-/lzma-linux-x64-musl-1.5.1.tgz", - "integrity": "sha512-I3nsYrWtrW9JpeCr+mkJIVDt0HY3m6qVUBs5vTtoIvJQxwqf1PBXSy5IS7T53ksQFH2kd2UX8rLxJ7B4WISpZg==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-wasm32-wasi": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-wasm32-wasi/-/lzma-wasm32-wasi-1.5.1.tgz", - "integrity": "sha512-gy3wwPBa6+XEyA4fUzq6CClrXA1ajXjuVf5zbnHytJRgoHznj+mvpU3+co2fxXwqTCmIpn6KrzqH5bRDztBPhA==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.2", - "@emnapi/runtime": "1.11.2", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-win32-arm64-msvc": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-arm64-msvc/-/lzma-win32-arm64-msvc-1.5.1.tgz", - "integrity": "sha512-dK+huOsHiyH6oJjij+cnjqFCakk2HgWmpI12Xm4pLUyPphe4ebYoJBgehaNAxprmjFqBQ7nL95YPVz9BHyqmPg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-win32-ia32-msvc": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-ia32-msvc/-/lzma-win32-ia32-msvc-1.5.1.tgz", - "integrity": "sha512-dGE8L+0EQ+GyU9ap9InqB/t/PmPG/bLj918q7OsJ29FuTdn8fK4OX3U4IQZhylHIA+/dQ/SXJk5n4yfah2XVvA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/lzma-win32-x64-msvc": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-x64-msvc/-/lzma-win32-x64-msvc-1.5.1.tgz", - "integrity": "sha512-EKW4t/iqdCT/xnd5t9oXLvVER/PMNAWXKqUAl3fgvUcOILeZIIht77/dVnfFcc9htA/DCBXC/6YQWdW+LusjFA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz", - "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" - } - }, - "node_modules/@oxc-minify/binding-android-arm-eabi": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm-eabi/-/binding-android-arm-eabi-0.136.0.tgz", - "integrity": "sha512-gk1jzky1vLeOcK7w9Ib2RT/UooZ7O21s8YSeXCsy4tzsmguKb3Cbsv/Ra2fKhMp4mKeKCzvC1Xa41OfSBWfWdQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-android-arm64": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm64/-/binding-android-arm64-0.136.0.tgz", - "integrity": "sha512-5GIYanqud/ohXEcg2dsdn0+eSKXqclw5qlRUkvSM1sKEa4ZQ7hqOk8XPJBNPh3LXXw+Qw85B6jkmOFLKsCHp5w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-darwin-arm64": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-darwin-arm64/-/binding-darwin-arm64-0.136.0.tgz", - "integrity": "sha512-QAMpHKXWOoMCTenSZjQjVT5YmMooujlCKAf1T3PHFl2LuaA6ukQFXba0bgSLgAxRrAeX/itkwi4lzVQXtYmnRg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-darwin-x64": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-darwin-x64/-/binding-darwin-x64-0.136.0.tgz", - "integrity": "sha512-rDnoPj8hztiU8C1VNKuzujh7SgTJlgiUvUCiWQjTnVSLsABsLW9ZWd1l1Ji0VN3UW39wGpptgvahIWTrHnGH/A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-freebsd-x64": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-freebsd-x64/-/binding-freebsd-x64-0.136.0.tgz", - "integrity": "sha512-9fGSgP7uyPLCTjMx9z3AUV4UjnfOVoGTH5JzamLegPwGLJ97Gr1jht1437CRk8ZNabGmxQR6qNn3e+Tox2lswg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-linux-arm-gnueabihf": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.136.0.tgz", - "integrity": "sha512-k0QvZS+x6ROI4O5Ikqn9dDU2dmkEJ46Yh6baZJI/eUk/a1k1GOs9/vyXItLZrKlwYrSgdEx2BFXbKgyd1NyRPw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-linux-arm-musleabihf": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.136.0.tgz", - "integrity": "sha512-iRFxUc8ubotVfuyq3e4LPGbHrjvWd1/B3499a55GueMLc1EDJ0fHzNXJiCx2eMQGfzHtHodjmvrJ2BSvSXBNhQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-linux-arm64-gnu": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.136.0.tgz", - "integrity": "sha512-hbKxNCXJsULrb6EyG5SumzJKuPfP8Y0UQ/TElRgXVUhyyPf6cBPlF8HMq+MAylF98ACBzV8jBp2Mm62lJyTo+w==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-linux-arm64-musl": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.136.0.tgz", - "integrity": "sha512-T+u/CGKW0UXemJeVux4ix6rK+ug3bQCUEbu5hFlX9K61KQNd44rOcrM15mDK99L7USaXwmiwHeIkEDvmmyDMSA==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-linux-ppc64-gnu": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.136.0.tgz", - "integrity": "sha512-WNPTN2F448tb49BoqjV9IMRzZWa0WB8GHzrB+bnxKQPOyzz07T6rmDKrr4Hjl81ADGjDYSsr1fgXcjyLQFdMdw==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-linux-riscv64-gnu": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.136.0.tgz", - "integrity": "sha512-MvcCfu8OeabrmNN9wqhh8Gqizg68RQ3iOvXBJmlLMy5p1PC6az9bAwqOk98lsCmKI4I5LpULekOlfxRYh3o5Kw==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-linux-riscv64-musl": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.136.0.tgz", - "integrity": "sha512-rwJXFUFFA0egAULtOOgfW8YhhLhtWrr2qFxftvZ7/U6Xtxiev8i+eCwSNW1RpHiX74QMsJVpRE3ykoefqb1MtQ==", - "cpu": [ - "riscv64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-linux-s390x-gnu": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.136.0.tgz", - "integrity": "sha512-qizAxHmj7VHm5/D5qiIlvzxAS2tlmadDOJ5XFI7zx/wfnk/+fdk5iVaHOv+C4tDE/4wLQyMoBYVIs6R1LeN91w==", - "cpu": [ - "s390x" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-linux-x64-gnu": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.136.0.tgz", - "integrity": "sha512-gcyLAad0UblkEhGjVfx5Mwq63OSx8r04IbcfwzpZYpHisO/vioMsrs4GkDsQZ2Ejc8cdUk6YMIrf9FwNBzh+Dw==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-linux-x64-musl": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-x64-musl/-/binding-linux-x64-musl-0.136.0.tgz", - "integrity": "sha512-QAK6kwBjljX6luYzeppVdRP3gTaFNFfp9iS/V0cCvU43wUZay5xFXBgmzBHYrzZV2MhzURNnXfEZRo9poAxyvA==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-openharmony-arm64": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-openharmony-arm64/-/binding-openharmony-arm64-0.136.0.tgz", - "integrity": "sha512-nIGxkTOnN9GwslmooxbZ3Ti2RgbX9QLHy0X9uO0pslOT+4dsNVUawBU/6r4M5cWM+ZdRD2tOF4V+n0l8tS59nQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-wasm32-wasi": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-wasm32-wasi/-/binding-wasm32-wasi-0.136.0.tgz", - "integrity": "sha512-YB3G8AHT6PpDS8/DfjlJrQjywj+/6gCkNBcAJdYzcYbQRIZE6SaTNowiGuZPR1m1jOl1WjcjefniKSmVxzhvRA==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.5" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@oxc-minify/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@oxc-minify/binding-win32-arm64-msvc": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.136.0.tgz", - "integrity": "sha512-UVRH3BlNc2rmla+sbSd5xTF/u+esCqZS6VSWPolZybcBh3TlVe1Sf5MOuHQWUaFoyv5uFOvee4hOfS/14nPvZw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-win32-ia32-msvc": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.136.0.tgz", - "integrity": "sha512-zFGkUd82jci0E+isc2O36lVmmXVUDXyEMN2RYIyez+MBNqhdjPLIGx62FCsrJtLKeuiPf/8XkFv977aUtndUng==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-minify/binding-win32-x64-msvc": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.136.0.tgz", - "integrity": "sha512-VHgPwd5xibC9UYxBeq7nURTjY+wxhdumllij6JaCIUVfD9IRcrA66SVlJ5fPNoUXcRp8ZXhJfoIZwVz9wAEGJw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-android-arm64": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.76.0.tgz", - "integrity": "sha512-1XJW/16CDmF5bHE7LAyPPmEEVnxSadDgdJz+xiLqBrmC4lfAeuAfRw3HlOygcPGr+AJsbD4Z5sFJMkwjbSZlQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-darwin-arm64": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.76.0.tgz", - "integrity": "sha512-yoQwSom8xsB+JdGsPUU0xxmxLKiF2kdlrK7I56WtGKZilixuBf/TmOwNYJYLRWkBoW5l2/pDZOhBm2luwmLiLw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-darwin-x64": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.76.0.tgz", - "integrity": "sha512-uRIopPLvr3pf2Xj7f5LKyCuqzIU6zOS+zEIR8UDYhcgJyZHnvBkfrYnfcztyIcrGdQehrFUi3uplmI09E7RdiQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-freebsd-x64": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.76.0.tgz", - "integrity": "sha512-a0EOFvnOd2FqmDSvH6uWLROSlU6KV/JDKbsYDA/zRLyKcG6HCsmFnPsp8iV7/xr9WMbNgyJi6R5IMpePQlUq7Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.76.0.tgz", - "integrity": "sha512-ikRYDHL3fOdZwfJKmcdqjlLgkeNZ3Ez0qM8wAev5zlHZ+lY/Ig7qG5SCqPlvuTu+nNQ6zrFFaKvvt69EBKXU/g==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.76.0.tgz", - "integrity": "sha512-dtRv5J5MRCLR7x39K8ufIIW4svIc7gYFUaI0YFXmmeOBhK/K2t/CkguPnDroKtsmXIPHDRtmJ1JJYzNcgJl6Wg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm64-gnu": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.76.0.tgz", - "integrity": "sha512-IE4iiiggFH2snagQxHrY5bv6dDpRMMat+vdlMN/ibonA65eOmRLp8VLTXnDiNrcla/itJ1L9qGABHNKU+SnE8g==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm64-musl": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.76.0.tgz", - "integrity": "sha512-wi9zQPMDHrBuRuT7Iurfidc9qlZh7cKa5vfYzOWNBCaqJdgxmNOFzvYen02wVUxSWGKhpiPHxrPX0jdRyJ8Npg==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.76.0.tgz", - "integrity": "sha512-0tqqu1pqPee2lLGY8vtYlX1L415fFn89e0a3yp4q5N9f03j1rRs0R31qesTm3bt/UK8HYjECZ+56FCVPs2MEMQ==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-s390x-gnu": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.76.0.tgz", - "integrity": "sha512-y36Hh1a5TA+oIGtlc8lT7N9vdHXBlhBetQJW0p457KbiVQ7jF7AZkaPWhESkjHWAsTVKD2OjCa9ZqfaqhSI0FQ==", - "cpu": [ - "s390x" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-x64-gnu": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.76.0.tgz", - "integrity": "sha512-7/acaG9htovp3gp/J0kHgbItQTuHctl+rbqPPqZ9DRBYTz8iV8kv3QN8t8Or8i/hOmOjfZp9McDoSU1duoR4/A==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-x64-musl": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.76.0.tgz", - "integrity": "sha512-AxFt0reY6Q2rfudABmMTFGR8tFFr58NlH2rRBQgcj+F+iEwgJ+jMwAPhXd2y1I2zaI8GspuahedUYQinqxWqjA==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-wasm32-wasi": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.76.0.tgz", - "integrity": "sha512-wHdkHdhf6AWBoO8vs5cpoR6zEFY1rB+fXWtq6j/xb9j/lu1evlujRVMkh8IM/M/pOUIrNkna3nzST/mRImiveQ==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, - "node_modules/@oxc-parser/binding-win32-arm64-msvc": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.76.0.tgz", - "integrity": "sha512-G7ZlEWcb2hNwCK3qalzqJoyB6HaTigQ/GEa7CU8sAJ/WwMdG/NnPqiC9IqpEAEy1ARSo4XMALfKbKNuqbSs5mg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-win32-x64-msvc": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.76.0.tgz", - "integrity": "sha512-0jLzzmnu8/mqNhKBnNS2lFUbPEzRdj5ReiZwHGHpjma0+ullmmwP2AqSEqx3ssHDK9CpcEMdKOK2LsbCfhHKIA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.76.0.tgz", - "integrity": "sha512-CH3THIrSViKal8yV/Wh3FK0pFhp40nzW1MUDCik9fNuid2D/7JJXKJnfFOAvMxInGXDlvmgT6ACAzrl47TqzkQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", - "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", - "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", - "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", - "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", - "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", - "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", - "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", - "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", - "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", - "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", - "cpu": [ - "s390x" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", - "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", - "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", - "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", - "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", - "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "license": "MIT" - }, - "node_modules/@spinframework/build-tools": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@spinframework/build-tools/-/build-tools-2.1.1.tgz", - "integrity": "sha512-VF4NRtkqW0vUv3MzgwbyZKzG4qC1D2DrmFbr+P3uJPLq0stuLqcuASqq3LzrxTbTnpAeeswn8JhEe+wPbH1Lng==", - "license": "Apache-2.0 WITH LLVM-exception", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bytecodealliance/componentize-js": "^0.23.0", - "@bytecodealliance/jco": "^1.17.9", - "acorn-walk": "^8.3.4", - "acron": "^1.0.5", - "magic-string": "^0.30.17", - "regexpu-core": "^6.2.0", - "yargs": "^18.0.0" - }, - "bin": { - "j2w": "dist/index.js" - } - }, - "node_modules/@spinframework/wasi-http-proxy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@spinframework/wasi-http-proxy/-/wasi-http-proxy-2.0.0.tgz", - "integrity": "sha512-nMW4QcueYajZ4fqcC3gTbfWuJv3hm8oc6sJfc5FI4y1hEOH1K7ciV6g+9R3EEmQl8GjeZhI5rdQ9Anz3N4NVqQ==", - "license": " Apache-2.0 WITH LLVM-exception", - "peerDependencies": { - "@spinframework/build-tools": "^2.0.0" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", - "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@typescript/typescript-aix-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", - "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-darwin-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", - "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-darwin-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", - "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", - "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", - "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-arm": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", - "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", - "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-loong64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", - "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", - "cpu": [ - "loong64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-mips64el": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", - "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", - "cpu": [ - "mips64el" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", - "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-riscv64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", - "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-s390x": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", - "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", - "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", - "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", - "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-openbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", - "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-openbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", - "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-sunos-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", - "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-win32-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", - "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-win32-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", - "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acron": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/acron/-/acron-1.0.5.tgz", - "integrity": "sha512-Jm3uey9PSUl5k9RsjBSfDJG35nG0vd9B4FUCV1J/NeVHDQ5HJPVpAhmGrPYtPKh0gLhEUIEy4DKqBy/p60B+4Q==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.11" - }, - "bin": { - "acron": "cli.js" - } - }, - "node_modules/ansi-regex": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", - "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/binaryen": { - "version": "130.0.0", - "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-130.0.0.tgz", - "integrity": "sha512-XDrb+zql0RbFPKgj7MuH9zOc78R3Fa/P/VSGnnpdwYsvNZPWjcMYMdAkKCOQEL2A7yqgjSMTDRFp6gfSDW+/QQ==", - "license": "Apache-2.0", - "bin": { - "wasm-as": "bin/wasm-as", - "wasm-ctor-eval": "bin/wasm-ctor-eval", - "wasm-dis": "bin/wasm-dis", - "wasm-merge": "bin/wasm-merge", - "wasm-metadce": "bin/wasm-metadce", - "wasm-opt": "bin/wasm-opt", - "wasm-reduce": "bin/wasm-reduce", - "wasm-shell": "bin/wasm-shell", - "wasm2js": "bin/wasm2js" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", - "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "license": "ISC", - "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/componentize-qjs": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/componentize-qjs/-/componentize-qjs-0.4.5.tgz", - "integrity": "sha512-/Brt+/Ps7DQ1mYuUsDISTQTFSaZfCSoRHkHnh9Dm5WHCO6arc/vzVdD8TXssfyNex2Iv3UR8NHdahsqABPDh0w==", - "license": "Apache-2.0", - "bin": { - "componentize-qjs": "bin/componentize-qjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "@andreiltd/componentize-qjs-binding-darwin-arm64": "0.4.5", - "@andreiltd/componentize-qjs-binding-darwin-x64": "0.4.5", - "@andreiltd/componentize-qjs-binding-linux-arm64-gnu": "0.4.5", - "@andreiltd/componentize-qjs-binding-linux-arm64-musl": "0.4.5", - "@andreiltd/componentize-qjs-binding-linux-x64-gnu": "0.4.5", - "@andreiltd/componentize-qjs-binding-linux-x64-musl": "0.4.5", - "@andreiltd/componentize-qjs-binding-win32-arm64-msvc": "0.4.5", - "@andreiltd/componentize-qjs-binding-win32-x64-msvc": "0.4.5" - } - }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT" - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "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" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fflate": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", - "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "license": "MIT" - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", - "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/oxc-minify": { - "version": "0.136.0", - "resolved": "https://registry.npmjs.org/oxc-minify/-/oxc-minify-0.136.0.tgz", - "integrity": "sha512-u9UvPFTYVwQ/i1kpp/c20S/SbvLaLV0Ngzyhgi0YV4B1SlxM7RgnnePl4vY9N3Z9sTRcozL1MRStI3PlUN9RFA==", - "license": "MIT", - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxc-minify/binding-android-arm-eabi": "0.136.0", - "@oxc-minify/binding-android-arm64": "0.136.0", - "@oxc-minify/binding-darwin-arm64": "0.136.0", - "@oxc-minify/binding-darwin-x64": "0.136.0", - "@oxc-minify/binding-freebsd-x64": "0.136.0", - "@oxc-minify/binding-linux-arm-gnueabihf": "0.136.0", - "@oxc-minify/binding-linux-arm-musleabihf": "0.136.0", - "@oxc-minify/binding-linux-arm64-gnu": "0.136.0", - "@oxc-minify/binding-linux-arm64-musl": "0.136.0", - "@oxc-minify/binding-linux-ppc64-gnu": "0.136.0", - "@oxc-minify/binding-linux-riscv64-gnu": "0.136.0", - "@oxc-minify/binding-linux-riscv64-musl": "0.136.0", - "@oxc-minify/binding-linux-s390x-gnu": "0.136.0", - "@oxc-minify/binding-linux-x64-gnu": "0.136.0", - "@oxc-minify/binding-linux-x64-musl": "0.136.0", - "@oxc-minify/binding-openharmony-arm64": "0.136.0", - "@oxc-minify/binding-wasm32-wasi": "0.136.0", - "@oxc-minify/binding-win32-arm64-msvc": "0.136.0", - "@oxc-minify/binding-win32-ia32-msvc": "0.136.0", - "@oxc-minify/binding-win32-x64-msvc": "0.136.0" - } - }, - "node_modules/oxc-parser": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.76.0.tgz", - "integrity": "sha512-l98B2e9evuhES7zN99rb1QGhbzx25829TJFaKi2j0ib3/K/G5z1FdGYz6HZkrU3U8jdH7v2FC8mX1j2l9JrOUg==", - "license": "MIT", - "dependencies": { - "@oxc-project/types": "^0.76.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxc-parser/binding-android-arm64": "0.76.0", - "@oxc-parser/binding-darwin-arm64": "0.76.0", - "@oxc-parser/binding-darwin-x64": "0.76.0", - "@oxc-parser/binding-freebsd-x64": "0.76.0", - "@oxc-parser/binding-linux-arm-gnueabihf": "0.76.0", - "@oxc-parser/binding-linux-arm-musleabihf": "0.76.0", - "@oxc-parser/binding-linux-arm64-gnu": "0.76.0", - "@oxc-parser/binding-linux-arm64-musl": "0.76.0", - "@oxc-parser/binding-linux-riscv64-gnu": "0.76.0", - "@oxc-parser/binding-linux-s390x-gnu": "0.76.0", - "@oxc-parser/binding-linux-x64-gnu": "0.76.0", - "@oxc-parser/binding-linux-x64-musl": "0.76.0", - "@oxc-parser/binding-wasm32-wasi": "0.76.0", - "@oxc-parser/binding-win32-arm64-msvc": "0.76.0", - "@oxc-parser/binding-win32-x64-msvc": "0.76.0" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" - }, - "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", - "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regexpu-core": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", - "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.2", - "regjsgen": "^0.8.0", - "regjsparser": "^0.13.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.2.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.3.tgz", - "integrity": "sha512-ycwFAS14Jw4mppvmK4GR/J6u3WpWpjkEApehuHtLc/8VpPNpDMbQ4WjqwplXifGeyKOzHSFLmSPqzksDQE2Sfg==", - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~3.1.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/rolldown": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", - "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.151.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.11", - "@rolldown/binding-android-arm64": "1.2.11", - "@rolldown/binding-darwin-arm64": "1.2.11", - "@rolldown/binding-darwin-x64": "1.2.11", - "@rolldown/binding-freebsd-x64": "1.2.11", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", - "@rolldown/binding-linux-arm64-gnu": "1.2.11", - "@rolldown/binding-linux-arm64-musl": "1.2.11", - "@rolldown/binding-linux-ppc64-gnu": "1.2.11", - "@rolldown/binding-linux-s390x-gnu": "1.2.11", - "@rolldown/binding-linux-x64-gnu": "1.2.11", - "@rolldown/binding-linux-x64-musl": "1.2.11", - "@rolldown/binding-openharmony-arm64": "1.2.11", - "@rolldown/binding-win32-arm64-msvc": "1.2.11", - "@rolldown/binding-win32-x64-msvc": "1.2.11" - } - }, - "node_modules/rolldown/node_modules/@oxc-project/types": { - "version": "0.151.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", - "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/oxc-project" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", - "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/tar": { - "version": "7.5.22", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", - "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, - "node_modules/typescript": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", - "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc" - }, - "engines": { - "node": ">=16.20.0" - }, - "optionalDependencies": { - "@typescript/typescript-aix-ppc64": "7.0.2", - "@typescript/typescript-darwin-arm64": "7.0.2", - "@typescript/typescript-darwin-x64": "7.0.2", - "@typescript/typescript-freebsd-arm64": "7.0.2", - "@typescript/typescript-freebsd-x64": "7.0.2", - "@typescript/typescript-linux-arm": "7.0.2", - "@typescript/typescript-linux-arm64": "7.0.2", - "@typescript/typescript-linux-loong64": "7.0.2", - "@typescript/typescript-linux-mips64el": "7.0.2", - "@typescript/typescript-linux-ppc64": "7.0.2", - "@typescript/typescript-linux-riscv64": "7.0.2", - "@typescript/typescript-linux-s390x": "7.0.2", - "@typescript/typescript-linux-x64": "7.0.2", - "@typescript/typescript-netbsd-arm64": "7.0.2", - "@typescript/typescript-netbsd-x64": "7.0.2", - "@typescript/typescript-openbsd-arm64": "7.0.2", - "@typescript/typescript-openbsd-x64": "7.0.2", - "@typescript/typescript-sunos-x64": "7.0.2", - "@typescript/typescript-win32-arm64": "7.0.2", - "@typescript/typescript-win32-x64": "7.0.2" - } - }, - "node_modules/typescript-compiler-api": { - "name": "typescript", - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/unenv": { - "version": "2.0.0-rc.24", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", - "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", - "license": "MIT", - "dependencies": { - "pathe": "^2.0.3" - } - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", - "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", - "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/urlpattern-polyfill": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", - "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", - "license": "MIT" - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/yargs": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", - "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", - "license": "MIT", - "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^8.2.1", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "license": "ISC", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - } - } -} diff --git a/examples/git/viewer/package.json b/examples/git/viewer/package.json index f8ec508..747355e 100644 --- a/examples/git/viewer/package.json +++ b/examples/git/viewer/package.json @@ -4,23 +4,24 @@ "version": "0.1.0", "type": "module", "license": "Apache-2.0", + "packageManager": "bun@1.3.13", "scripts": { - "build": "node build.mjs", - "check": "tsc --noEmit", - "format": "prettier --write src build.mjs", - "format:check": "prettier --check src build.mjs" + "build": "bun build.ts && bun x --bun j2w -i build/component.js -o dist/viewer.wasm", + "check": "biome check . && tsc --noEmit", + "format": "biome check --write ." }, "devDependencies": { + "@biomejs/biome": "2.5.14", "@bytecodealliance/preview2-shim": "0.26.0", "@spinframework/build-tools": "2.1.1", - "esbuild": "0.28.2", - "prettier": "3.8.1", + "@types/bun": "1.3.13", "typescript": "7.0.2" }, "overrides": { "@bytecodealliance/componentize-js": "0.23.0" }, "dependencies": { - "@spinframework/wasi-http-proxy": "2.0.0" + "@spinframework/wasi-http-proxy": "2.0.0", + "preact": "10.29.8" } } diff --git a/examples/git/viewer/src/client.ts b/examples/git/viewer/src/client.ts deleted file mode 100644 index a574e37..0000000 --- a/examples/git/viewer/src/client.ts +++ /dev/null @@ -1,301 +0,0 @@ -export {}; - -interface Commit { - id: string; - title: string; - author: string; - date: string; -} -interface Entry { - name: string; - kind: string; - id: string; -} -interface Snapshot { - branch: string; - branches: string[]; - path: string; - history: Commit[]; - entries: Entry[]; - more: boolean; - file?: { id: string; size: number; state: string; text?: string }; -} - -function get(id: string): T { - return document.getElementById(id) as T; -} -function node( - tag: K, - text = "", - className = "", -): HTMLElementTagNameMap[K] { - const element = document.createElement(tag); - element.textContent = text; - element.className = className; - return element; -} -function icon(kind: string): SVGSVGElement { - const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.setAttribute("viewBox", "0 0 16 16"); - svg.setAttribute("aria-hidden", "true"); - const path = document.createElementNS(svg.namespaceURI, "path"); - path.setAttribute( - "d", - kind === "directory" - ? "M1 2h5l2 2h7v9H1Z" - : "M3 1h6l4 4v10H3Zm6 1v4h3M5 9h6M5 12h6", - ); - if (kind === "directory") svg.classList.add("directory-icon"); - svg.append(path); - return svg; -} -function validRepository(name: string): boolean { - return ( - name.endsWith(".git") && - !/[\s%?#\\]/.test(name) && - name - .split("/") - .every((part) => part !== "" && part !== "." && part !== "..") && - new URL("/" + name, location.origin).pathname === "/" + name - ); -} - -const repository = new URLSearchParams(location.search).get("repo") ?? ""; -const statusMessage = get("status"); -const auth = get("auth"); -const content = get("content"); -const branch = get("branch"); -let authorization = ""; -let pending: AbortController | undefined; - -function link(label: string, path: string): HTMLAnchorElement { - const anchor = node("a", label); - const query = new URLSearchParams(location.search); - query.set("path", path); - anchor.href = "?" + query.toString(); - anchor.addEventListener("click", (event) => { - if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) - return; - event.preventDefault(); - history.pushState(null, "", anchor.href); - void load(); - }); - return anchor; -} -function date(value: string): string { - return new Date(value).toLocaleDateString(undefined, { - month: "short", - day: "numeric", - year: "numeric", - }); -} -function render(view: Snapshot): void { - branch.replaceChildren(); - for (const ref of view.branches) { - const option = node("option", ref.replace(/^refs\/heads\//, "")); - option.value = ref; - branch.append(option); - } - branch.value = view.branch; - branch.disabled = view.branches.length === 0; - get("branch-count").textContent = - `${view.branches.length} ${view.branches.length === 1 ? "branch" : "branches"}`; - get("commit-count").textContent = String(view.history.length); - const breadcrumbs = get("breadcrumbs"); - breadcrumbs.replaceChildren(link(repository.replace(/\.git$/, ""), "")); - const parts = view.path ? view.path.split("/") : []; - parts.forEach((part, i) => - breadcrumbs.append( - node("span", "/", "muted"), - link(part, parts.slice(0, i + 1).join("/")), - ), - ); - const tip = get("tip"); - tip.replaceChildren(); - const latest = view.history[0]; - tip.hidden = !latest; - if (latest) - tip.append( - node("span", latest.author.slice(0, 1).toUpperCase() || "G", "avatar"), - node("span", latest.title || "Untitled commit", "title"), - node("span", latest.id.slice(0, 7), "hash"), - ); - const files = get("files"); - files.replaceChildren(); - if (view.file) { - const header = node("div", "", "file-header"); - header.append( - node("span", `${view.file.size.toLocaleString()} bytes`, "muted"), - node("span", view.file.id.slice(0, 12), "hash"), - ); - files.append(header); - const messages: Record = { - binary: "Binary file · preview unavailable", - large: "This file exceeds the 256 KiB preview limit.", - submodule: "Submodule · " + view.file.id, - }; - files.append( - view.file.state === "text" - ? node("pre", view.file.text ?? "") - : node( - "div", - messages[view.file.state] ?? "Preview unavailable", - "empty", - ), - ); - } else if (!view.history.length) { - files.append( - node( - "div", - "This repository is empty. Push a branch to start exploring.", - "empty", - ), - ); - } else { - const entries = [...view.entries].sort( - (a, b) => - Number(b.kind === "directory") - Number(a.kind === "directory") || - a.name.localeCompare(b.name), - ); - for (const entry of entries) { - const row = node("div", "", "file-row"); - row.append( - icon(entry.kind), - link(entry.name, [...parts, entry.name].join("/")), - node("span", entry.id.slice(0, 7), "hash"), - ); - files.append(row); - } - if (!entries.length) - files.append(node("div", "This directory is empty.", "empty")); - if (view.more) - files.append(node("div", "Showing the first 500 entries.", "empty")); - } - const commits = get("history"); - commits.replaceChildren(); - for (const commit of view.history) { - const row = node("div", "", "history-row"); - const info = node("div", "", "history-info"); - info.append( - node("div", commit.title || "Untitled commit", "history-title"), - node( - "div", - `${commit.author || "Unknown author"} committed on ${date(commit.date)}`, - "muted", - ), - ); - row.append( - node("span", commit.author.slice(0, 1).toUpperCase() || "G", "avatar"), - info, - node("span", commit.id.slice(0, 7), "hash"), - ); - commits.append(row); - } - if (!view.history.length) - commits.append(node("div", "No commits yet.", "empty")); -} - -async function load(): Promise { - pending?.abort(); - const request = new AbortController(); - pending = request; - statusMessage.hidden = false; - statusMessage.textContent = "Loading repository…"; - auth.hidden = true; - content.hidden = true; - try { - const query = new URLSearchParams(location.search); - query.delete("repo"); - const response = await fetch(`/${repository}/_browse?${query}`, { - headers: authorization ? { Authorization: authorization } : {}, - credentials: "omit", - cache: "no-store", - signal: request.signal, - }); - if (response.status === 401) { - statusMessage.textContent = "Authentication required."; - auth.hidden = false; - get("credential").focus(); - return; - } - if (!response.ok) { - const messages: Record = { - 403: "This credential does not have read access.", - 404: "Repository or path not found. Check the path and push a branch before browsing.", - 400: "This branch or path query is invalid.", - }; - throw new Error( - messages[response.status] ?? - "Repository unavailable. Try refreshing this page.", - ); - } - const view = (await response.json()) as Snapshot; - if (pending !== request) return; - render(view); - statusMessage.hidden = true; - content.hidden = false; - } catch (error) { - if (request.signal.aborted) return; - statusMessage.textContent = - error instanceof Error ? error.message : "Repository unavailable."; - auth.hidden = false; - } -} - -auth.addEventListener("submit", (event) => { - event.preventDefault(); - const field = get("credential"); - const credential = field.value; - field.value = ""; - const bytes = new TextEncoder().encode("git:" + credential); - authorization = - get("auth-mode").value === "bearer" - ? "Bearer " + credential - : "Basic " + - btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join("")); - void load(); -}); -branch.addEventListener("change", () => { - const query = new URLSearchParams({ repo: repository, ref: branch.value }); - history.pushState(null, "", "?" + query); - void load(); -}); -for (const name of ["code", "commits"]) { - get(name + "-tab").addEventListener("click", () => { - for (const panel of ["code", "commits"]) { - const active = panel === name; - get(panel + "-view").hidden = !active; - get(panel + "-tab").classList.toggle("active", active); - get(panel + "-tab").setAttribute("aria-pressed", String(active)); - } - }); -} -get("open-repo").addEventListener("submit", (event) => { - event.preventDefault(); - const name = get("repo-name").value.trim(); - if (validRepository(name)) - location.href = "/browse?" + new URLSearchParams({ repo: name }); - else - statusMessage.textContent = - "Enter a repository path such as team/project.git."; -}); -get("copy-url").addEventListener("click", async () => { - try { - await navigator.clipboard.writeText( - get("clone-url").value, - ); - get("copy-url").textContent = "Copied"; - } catch { - get("clone-url").select(); - } -}); -addEventListener("popstate", () => void load()); -if (validRepository(repository)) { - get("repository").textContent = repository.replace(/\.git$/, ""); - document.title = repository + " · object-log"; - get("clone-url").value = location.origin + "/" + repository; - void load(); -} else { - get("open-repo").hidden = false; - statusMessage.textContent = "Enter the path of a configured Git repository."; -} diff --git a/examples/git/viewer/src/client.tsx b/examples/git/viewer/src/client.tsx new file mode 100644 index 0000000..802e0cf --- /dev/null +++ b/examples/git/viewer/src/client.tsx @@ -0,0 +1,347 @@ +import { render, type VNode } from "preact"; +import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; + +interface Commit { + id: string; + title: string; + author: string; + date: string; +} +interface Snapshot { + branch: string; + branches: string[]; + path: string; + history: Commit[]; + entries: { name: string; kind: string; id: string }[]; + more: boolean; + file?: { id: string; size: number; state: string; text?: string }; +} + +function validRepository(name: string): boolean { + return ( + name.endsWith(".git") && + !/[\s%?#\\]/.test(name) && + name.split("/").every((part) => part !== "" && part !== "." && part !== "..") && + new URL(`/${name}`, location.origin).pathname === `/${name}` + ); +} + +function History({ commits }: { commits: Commit[] }) { + return ( +
+

Recent commits on this branch · first parent

+
+ {commits.length ? ( + commits.map((commit) => ( +
+
+ {commit.title || "Untitled commit"} +

+ {commit.author || "Unknown author"} committed on{" "} + +

+
+ {commit.id.slice(0, 7)} +
+ )) + ) : ( +

No commits yet.

+ )} +
+
+ ); +} + +function Code({ + view, + repository, + link, +}: { + view: Snapshot; + repository: string; + link: (label: string, path: string) => VNode; +}) { + return ( +
+ +
+ {view.history[0] && ( +
+ {view.history[0].title || "Untitled commit"} + {view.history[0].id.slice(0, 7)} +
+ )} + {view.file ? ( + <> +
+ {view.file.size.toLocaleString()} bytes + {view.file.id.slice(0, 12)} +
+ {view.file.state === "text" ? ( +
{view.file.text}
+ ) : ( +

+ {view.file.state === "binary" + ? "Binary file · preview unavailable" + : view.file.state === "large" + ? "This file exceeds the 256 KiB preview limit." + : `Submodule · ${view.file.id}`} +

+ )} + + ) : !view.history.length ? ( +

This repository is empty. Push a branch to start exploring.

+ ) : ( + <> + {[...view.entries] + .sort( + (a, b) => + Number(b.kind === "directory") - Number(a.kind === "directory") || + a.name.localeCompare(b.name), + ) + .map((entry) => ( +
+ + {link(entry.name, [view.path, entry.name].filter(Boolean).join("/"))} + {entry.id.slice(0, 7)} +
+ ))} + {!view.entries.length &&

This directory is empty.

} + {view.more &&

Showing the first 500 entries.

} + + )} +
+
+ ); +} + +function App() { + const [search, setSearch] = useState(location.search); + const [authorization, setAuthorization] = useState({ header: "" }); + const [result, setResult] = useState<{ view?: Snapshot; error?: string }>({}); + const [tab, setTab] = useState("code"); + const [copied, setCopied] = useState(false); + const cloneInput = useRef(null); + const query = new URLSearchParams(search); + const repository = query.get("repo") ?? ""; + const valid = validRepository(repository); + const view = result.view; + const cloneURL = `${location.origin}/${repository}`; + + useEffect(() => { + const update = () => setSearch(location.search); + addEventListener("popstate", update); + return () => removeEventListener("popstate", update); + }, []); + + useLayoutEffect(() => { + document.title = repository ? `${repository} · object-log` : "Repository · object-log"; + if (!valid) return; + const request = new AbortController(); + setResult({}); + void (async () => { + try { + const params = new URLSearchParams(search); + params.delete("repo"); + const response = await fetch(`/${repository}/_browse?${params}`, { + headers: authorization.header ? { Authorization: authorization.header } : {}, + credentials: "omit", + cache: "no-store", + signal: request.signal, + }); + if (!response.ok) { + const messages: Record = { + 400: "Invalid branch or path.", + 401: "Authentication required.", + 403: "This credential does not have read access.", + 404: "Repository or path not found. Push a branch before browsing.", + }; + throw new Error(messages[response.status] ?? "Repository unavailable. Try again."); + } + const snapshot: Snapshot = await response.json(); + if (!request.signal.aborted) setResult({ view: snapshot }); + } catch (error) { + if (!request.signal.aborted) + setResult({ error: error instanceof Error ? error.message : "Repository unavailable." }); + } + })(); + return () => request.abort(); + }, [search, repository, valid, authorization]); + + function navigate(params: URLSearchParams) { + history.pushState(null, "", `?${params}`); + setSearch(location.search); + } + function pathLink(label: string, path: string) { + const params = new URLSearchParams(search); + params.set("path", path); + return ( + { + if (event.button || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) + return; + event.preventDefault(); + navigate(params); + }} + > + {label} + + ); + } + + return ( + <> +
+ ◈ object-log + Repository explorer +
+
+

+ {repository.replace(/\.git$/, "") || "Repository"} + Read only +

+ {!valid ? ( +
{ + event.preventDefault(); + const name = String(new FormData(event.currentTarget).get("repo")).trim(); + if (validRepository(name)) navigate(new URLSearchParams({ repo: name })); + else setResult({ error: "Enter a repository path such as team/project.git." }); + }} + > + + +

{result.error}

+
+ ) : !view ? ( + <> +

{result.error ?? "Loading repository…"}

+ {result.error && ( +
{ + event.preventDefault(); + const data = new FormData(event.currentTarget); + const credential = String(data.get("credential")); + event.currentTarget.reset(); + const encoded = btoa( + Array.from(new TextEncoder().encode(`git:${credential}`), (byte) => + String.fromCharCode(byte), + ).join(""), + ); + setAuthorization({ + header: + data.get("mode") === "bearer" ? `Bearer ${credential}` : `Basic ${encoded}`, + }); + }} + > +

Sign in to read this repository

+ + + +

Used for this page only.

+
+ )} + + ) : ( + <> + +
+ + + {view.branches.length} {view.branches.length === 1 ? "branch" : "branches"} + +
+ Clone +
+ + +
+
+
+ {tab === "commits" ? ( + + ) : ( + + )} + + )} +
+ + ); +} + +const root = document.getElementById("app"); +if (!root) throw new Error("Missing app root"); +render(, root); diff --git a/examples/git/viewer/src/index.ts b/examples/git/viewer/src/index.ts index 971ed4a..256123e 100644 --- a/examples/git/viewer/src/index.ts +++ b/examples/git/viewer/src/index.ts @@ -20,10 +20,7 @@ function handle(request: Request): Response { return new Response("Not found", { status: 404 }); } const assets: Record = { - "/browse/assets/client.js": [ - CLIENT_SCRIPT, - "text/javascript; charset=utf-8", - ], + "/browse/assets/client.js": [CLIENT_SCRIPT, "text/javascript; charset=utf-8"], "/browse/assets/style.css": [STYLES, "text/css; charset=utf-8"], }; const [body, type] = assets[path] ?? [PAGE, "text/html; charset=utf-8"]; diff --git a/examples/git/viewer/src/page.html b/examples/git/viewer/src/page.html index 7606580..de16829 100644 --- a/examples/git/viewer/src/page.html +++ b/examples/git/viewer/src/page.html @@ -7,86 +7,5 @@ - -
- object-logRepository explorer -
-
-
-

Repository

- Read only -
- -

Loading repository…

- - -
-
A small window into Git on object storage.
- +
diff --git a/examples/git/viewer/src/style.css b/examples/git/viewer/src/style.css index 10c169e..949279c 100644 --- a/examples/git/viewer/src/style.css +++ b/examples/git/viewer/src/style.css @@ -1,12 +1,9 @@ :root { - color-scheme: light; font: - 14px/1.5 -apple-system, - BlinkMacSystemFont, - "Segoe UI", + 14px / 1.5 system-ui, sans-serif; color: #1f2328; - background: #fff; + background: white; } * { box-sizing: border-box; @@ -26,275 +23,175 @@ input, select, summary { font: inherit; + padding: 6px 12px; + border: 1px solid #d0d7de; + border-radius: 6px; + background: #f6f8fa; + color: inherit; } button, select, summary { cursor: pointer; } -button, -input, -select, -summary { - border: 1px solid #d0d7de; - border-radius: 6px; - padding: 6px 12px; - background: #f6f8fa; - color: inherit; -} input { - background: #fff; + background: white; min-width: 0; } -button:hover, -summary:hover { - background: #eef1f4; -} :focus-visible { outline: 2px solid #0969da; outline-offset: 2px; } -[hidden] { - display: none !important; -} -.topbar { - height: 64px; - padding: 0 32px; +header { display: flex; - align-items: center; gap: 24px; + padding: 20px 32px; background: #f6f8fa; - border-bottom: 1px solid #d8dee4; + border-bottom: 1px solid #d0d7de; } -.brand { - color: #1f2328; - font-size: 16px; +header a { + color: inherit; font-weight: 650; - display: flex; - align-items: center; - gap: 10px; -} -.mark { - font-size: 30px; - line-height: 1; } -.topbar-note { +header span, +.muted, +code, +small { color: #656d76; - font-size: 13px; } main { max-width: 1120px; - margin: 0 auto; - padding: 32px 32px 64px; + margin: auto; + padding: 32px; } -.heading { +h1 { display: flex; align-items: center; - gap: 12px; - margin-bottom: 24px; -} -h1 { + gap: 16px; + margin: 0 0 24px; font-size: 22px; - font-weight: 550; - margin: 0; color: #0969da; overflow-wrap: anywhere; } -h2 { - font-size: 16px; - margin: 0 0 16px; -} -.badge { +small { + font-size: 12px; + font-weight: 400; border: 1px solid #d0d7de; border-radius: 20px; - font-size: 12px; - color: #656d76; - padding: 1px 8px; + padding: 2px 8px; white-space: nowrap; } -.muted { - color: #656d76; - font-size: 13px; +h2 { + margin: 0; + font-size: 16px; } .panel { border: 1px solid #d0d7de; border-radius: 6px; overflow: hidden; } +form.panel { + display: grid; + gap: 12px; + max-width: 520px; + padding: 24px; +} +label { + display: grid; + gap: 6px; +} +form button { + justify-self: start; +} +summary, +form button { + background: #1f883d; + color: white; + border-color: #1f883d; +} +p { + margin: 0; +} .tabs { display: flex; gap: 8px; - border-bottom: 1px solid #d8dee4; + border-bottom: 1px solid #d0d7de; margin-bottom: 20px; } -.tab { - border: none; - background: transparent; +.tabs button { + border: 0; border-radius: 0; padding: 10px 16px; + background: transparent; border-bottom: 2px solid transparent; } -.tab.active { - font-weight: 600; +.tabs [aria-pressed="true"] { border-bottom-color: #fd8c73; -} -.count { - margin-left: 6px; - padding: 0 6px; - border-radius: 12px; - background: #eaeef2; - font-size: 12px; + font-weight: 600; } .toolbar { display: flex; + flex-wrap: wrap; align-items: center; gap: 16px; margin-bottom: 20px; } -.toolbar select { - max-width: 55%; - font-weight: 550; -} -.primary { - color: white; - background: #1f883d; - border-color: #1f883d; - font-weight: 550; +.toolbar label { + display: flex; + align-items: center; } -.primary:hover { - background: #1a7f37; +.toolbar select { + max-width: 220px; } -.clone { +details { position: relative; margin-left: auto; } -.clone summary { - list-style: none; -} .clone-box { position: absolute; - top: 42px; right: 0; + z-index: 1; + width: min(340px, calc(100vw - 32px)); + padding: 16px; background: white; border: 1px solid #d0d7de; - padding: 16px; border-radius: 6px; - width: 340px; - box-shadow: 0 8px 24px #8c959f33; - z-index: 2; } -.clone-box input { - width: 100%; - margin: 8px 0; - font: - 12px ui-monospace, - monospace; +.clone-box button { + margin-top: 8px; } .breadcrumbs { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin: 0 0 12px; - font-weight: 550; -} -.commit-tip { - display: flex; - align-items: center; - gap: 12px; - background: #f6f8fa; - border: 1px solid #d0d7de; - border-radius: 6px 6px 0 0; - padding: 14px 16px; -} -.commit-tip + .panel { - border-top: 0; - border-radius: 0 0 6px 6px; -} -.avatar { - width: 26px; - height: 26px; - flex: 0 0 26px; - border-radius: 50%; - background: #ddf4ff; - color: #0969da; - text-align: center; - line-height: 26px; - font-weight: 600; -} -.title { - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.hash { - color: #656d76; - font: - 12px ui-monospace, - SFMono-Regular, - monospace; + margin-bottom: 12px; + overflow-wrap: anywhere; } -.file-row, -.history-row { +article { display: flex; align-items: center; gap: 12px; - padding: 11px 16px; - border-bottom: 1px solid #d8dee4; -} -.file-row:last-child, -.history-row:last-child { - border-bottom: none; + padding: 12px 16px; + border-bottom: 1px solid #d0d7de; } -.file-row:hover { - background: #f6f8fa; +article:last-child { + border-bottom: 0; } -.file-row a { +article > a, +article > div, +article > strong { flex: 1; min-width: 0; overflow-wrap: anywhere; - color: #1f2328; -} -.file-row a:hover { - color: #0969da; -} -svg { - width: 16px; - height: 16px; - flex-shrink: 0; - fill: #656d76; -} -.directory-icon { - fill: #54aeff; -} -.history-row { - align-items: flex-start; - padding: 16px; -} -.history-info { - flex: 1; - min-width: 0; } -.history-title { - font-weight: 600; - margin-bottom: 4px; - overflow-wrap: anywhere; -} -.file-header { - padding: 12px 16px; +.tip { background: #f6f8fa; - border-bottom: 1px solid #d8dee4; - display: flex; - gap: 12px; - align-items: center; + justify-content: space-between; } +code, pre { font: - 12px/1.65 ui-monospace, - SFMono-Regular, - Consolas, + 12px / 1.65 ui-monospace, monospace; +} +pre { margin: 0; padding: 20px; overflow: auto; @@ -302,80 +199,26 @@ pre { } .empty { padding: 40px 24px; - text-align: center; color: #656d76; -} -.open-form, -.auth-form { - padding: 24px; - max-width: 520px; - margin: 24px 0; -} -.auth-form { - display: flex; - flex-direction: column; - gap: 8px; -} -.auth-form button { - margin-top: 8px; - align-self: flex-start; -} -.auth-form p { - margin-bottom: 0; -} -.form-row { - display: flex; - gap: 8px; - margin-top: 8px; -} -.form-row input { - flex: 1; -} -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} -footer { text-align: center; - color: #8c959f; - font-size: 12px; - padding: 24px; - border-top: 1px solid #eaeef2; } @media (max-width: 600px) { - .topbar { - padding: 0 20px; + header { + padding: 16px; } - .topbar-note { + header span { display: none; } main { - padding: 24px 16px 40px; - } - .heading { - align-items: flex-start; + padding: 24px 16px; } h1 { font-size: 19px; } .toolbar { - gap: 10px; + gap: 8px; } - #branch-count, - .commit-tip .hash { + article code { display: none; } - .clone-box { - width: min(340px, calc(100vw - 32px)); - } - .form-row { - flex-direction: column; - } } diff --git a/examples/git/viewer/tsconfig.json b/examples/git/viewer/tsconfig.json index cf18cce..6b4ce86 100644 --- a/examples/git/viewer/tsconfig.json +++ b/examples/git/viewer/tsconfig.json @@ -3,10 +3,13 @@ "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", + "jsx": "react-jsx", + "jsxImportSource": "preact", "strict": true, "noEmit": true, "lib": ["ES2022", "DOM", "WebWorker"], - "skipLibCheck": true + "skipLibCheck": true, + "types": ["bun"] }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "src/**/*.tsx", "build.ts"] } From 288df993daeb435c545ca967bcdee049411555c0 Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Sat, 26 Sep 2026 16:06:46 -0700 Subject: [PATCH 05/16] Separate repository viewer into a TypeScript WAL reader Read the existing authenticated catalog from the independent Spin API. Restore the Git service unchanged and pin the focused upstream opaque-resource compiler fix. Co-authored-by: Codex --- .github/workflows/ci.yml | 11 +- GIT_PLAN.md | 4 +- HANDOFF.md | 5 + Makefile | 2 +- THIRD_PARTY.md | 19 ++ examples/git/browse.go | 216 ------------------ examples/git/browse_test.go | 207 ----------------- examples/git/main.go | 5 +- examples/git/repositories.go | 9 +- examples/git/viewer/README.md | 61 ++--- examples/git/viewer/biome.json | 15 +- examples/git/viewer/build.ts | 12 +- examples/git/viewer/bun.lock | 11 +- examples/git/viewer/package.json | 22 +- examples/git/viewer/prepare.ts | 11 + examples/git/viewer/src/api.ts | 86 +++++++ examples/git/viewer/src/client.tsx | 27 +-- examples/git/viewer/src/index.ts | 12 +- examples/git/viewer/src/repository.ts | 143 ++++++++++++ examples/git/viewer/src/wal.test.ts | 313 ++++++++++++++++++++++++++ examples/git/viewer/src/wal.ts | 206 +++++++++++++++++ examples/git/viewer/tsconfig.json | 7 +- 22 files changed, 905 insertions(+), 499 deletions(-) delete mode 100644 examples/git/browse.go delete mode 100644 examples/git/browse_test.go create mode 100644 examples/git/viewer/prepare.ts create mode 100644 examples/git/viewer/src/api.ts create mode 100644 examples/git/viewer/src/repository.ts create mode 100644 examples/git/viewer/src/wal.test.ts create mode 100644 examples/git/viewer/src/wal.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f80a77e..657e926 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: repo-viewer: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 20 defaults: run: working-directory: examples/git/viewer @@ -56,8 +56,15 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: "1.3.13" + - name: Install wac + run: | + mkdir -p "$RUNNER_TEMP/wasi-tools" + curl -fsSL https://github.com/bytecodealliance/wac/releases/download/v0.11.0/wac-cli-x86_64-unknown-linux-musl -o "$RUNNER_TEMP/wasi-tools/wac" + echo "83259349b630f79a60490322b068e7ad2dba784720b0d467decccc0aecc743b5 $RUNNER_TEMP/wasi-tools/wac" | sha256sum -c - + chmod +x "$RUNNER_TEMP/wasi-tools/wac" + echo "$RUNNER_TEMP/wasi-tools" >> "$GITHUB_PATH" - name: Lint, typecheck, and build viewer - run: bun install --frozen-lockfile && bun run check && bun run build + run: bun install --frozen-lockfile && bun run check && bun test && bun run build spin-key-value: runs-on: ubuntu-latest diff --git a/GIT_PLAN.md b/GIT_PLAN.md index add85a1..be1488c 100644 --- a/GIT_PLAN.md +++ b/GIT_PLAN.md @@ -84,7 +84,9 @@ The service uses unmodified upstream go-git and componentize-go, including its bundled adapter. It pins the unchanged go-pkg PR #13 revision pending upstream review. Exact revisions, licenses, and upstream references live in `THIRD_PARTY.md`. New fork-only behavior requires owner review and focused tests. The service uses -ordinary Spin and unmodified object storage. +ordinary Spin and unmodified object storage. The optional TypeScript viewer +uses the owner-approved opaque-resource compiler fix in ComponentizeJS PR #357; +remove its package override when a release includes the fix. ## Service readiness diff --git a/HANDOFF.md b/HANDOFF.md index 87079e2..94c12dc 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -87,6 +87,11 @@ go-pkg PR #13 while it remains under review. Dependency provenance and reference authority. Use ordinary Spin and unmodified S3-compatible storage. +The optional repository viewer has its own TypeScript Spin API and reads the +existing WAL catalog; it adds no routes or code to the Go Git service. Its +compiler temporarily uses the focused opaque-resource fix in ComponentizeJS +PR #357, with exact provenance in `THIRD_PARTY.md`. + Spin 4.1's affected OpenTelemetry SDK remains pinned pending its coordinated upstream upgrade. The optional Caddy host rejects oversized or overlong W3C `baggage` before Spin; other HTTP embeddings must enforce the same boundary. diff --git a/Makefile b/Makefile index 777ec04..5e8a4a2 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ gc-acceptance: GIT_EXAMPLE_DIR = examples/git WAL_COMPONENT = examples/wal-component/Cargo.toml -GIT_TEST_FILES = browse.go browse_test.go access.go access_test.go auth.go auth_test.go repositories.go repositories_test.go imports.go imports_test.go receive_transport.go receive_transport_test.go request_config.go request_config_test.go import_pack.go import_pack_test.go import_pack_lifecycle_test.go limits.go limits_test.go index.go index_test.go codec.go codec_test.go delta.go delta_test.go read_retry.go read_retry_test.go retention.go retention_test.go retry.go retry_test.go fetch_policy.go fetch_policy_test.go validate_objects.go validate_objects_test.go validate.go validate_refname.go validate_refname_test.go validate_native_test.go +GIT_TEST_FILES = access.go access_test.go auth.go auth_test.go repositories.go repositories_test.go imports.go imports_test.go receive_transport.go receive_transport_test.go request_config.go request_config_test.go import_pack.go import_pack_test.go import_pack_lifecycle_test.go limits.go limits_test.go index.go index_test.go codec.go codec_test.go delta.go delta_test.go read_retry.go read_retry_test.go retention.go retention_test.go retry.go retry_test.go fetch_policy.go fetch_policy_test.go validate_objects.go validate_objects_test.go validate.go validate_refname.go validate_refname_test.go validate_native_test.go git-check: git-qualification-tools-test test -z "$$(gofmt -l $(GIT_EXAMPLE_DIR)/*.go $(GIT_EXAMPLE_DIR)/tests/*.go)" diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md index df15fb9..f52bcd6 100644 --- a/THIRD_PARTY.md +++ b/THIRD_PARTY.md @@ -46,6 +46,25 @@ pins through go-pkg's public `Unpin` function after the bindings return Go values. Build inputs are recorded in `examples/git/Makefile` and `go.mod`; their upstream licenses apply. +## Optional TypeScript repository viewer + +The viewer uses Bun, TypeScript, Biome, Preact, `fflate`, `@noble/hashes` and the +Spin JavaScript SDK. Its `examples/git/viewer/bun.lock` records exact versions +and package integrity. Their upstream licenses apply. + +The compiler uses a temporary package based on official ComponentizeJS 0.23.0, +with the binding splicer rebuilt from +[revision `a47770b`](https://github.com/carsonfarmer/ComponentizeJS/commit/a47770bd1b63a7497d785c5b32e841812de16700). +[Upstream PR #357](https://github.com/bytecodealliance/ComponentizeJS/pull/357) +fixes missing classes for imported resources without methods, reported in +[issue #221](https://github.com/bytecodealliance/ComponentizeJS/issues/221). +The packaged JavaScript bindings and all engine/cache files remain identical +to the official release; only the compiled binding splicer changes. Source and +build details accompany the +[temporary package](https://github.com/carsonfarmer/ComponentizeJS/releases/tag/opaque-resource-v0.23.0). +Remove the package override once an upstream release includes this fix. +The compiler change adds no viewer behavior or Git-specific code. + ## Native Spin provider The isolated [Spin key-value provider](integrations/spin-key-value/README.md) diff --git a/examples/git/browse.go b/examples/git/browse.go deleted file mode 100644 index 504781d..0000000 --- a/examples/git/browse.go +++ /dev/null @@ -1,216 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "errors" - "io" - "net/http" - "net/url" - "slices" - "strings" - "unicode/utf8" - - "github.com/go-git/go-git/v6/plumbing" - "github.com/go-git/go-git/v6/plumbing/filemode" - "github.com/go-git/go-git/v6/plumbing/object" - "github.com/go-git/go-git/v6/plumbing/storer" -) - -const browseFileBytes = 256 << 10 - -var errBrowseNotFound = errors.New("branch or path not found") - -type browseView struct { - Branch string `json:"branch"` - Branches []string `json:"branches"` - Path string `json:"path"` - History []browseCommit `json:"history"` - Entries []browseEntry `json:"entries"` - More bool `json:"more"` - File *browseFile `json:"file,omitempty"` -} - -type browseCommit struct { - ID string `json:"id"` - Title string `json:"title"` - Author string `json:"author"` - Date string `json:"date"` -} - -type browseEntry struct { - Name string `json:"name"` - Kind string `json:"kind"` - ID string `json:"id"` -} - -type browseFile struct { - ID string `json:"id"` - Size int64 `json:"size"` - State string `json:"state"` - Text string `json:"text,omitempty"` -} - -func serveBrowse(w http.ResponseWriter, r *http.Request, s *store) error { - query, err := url.ParseQuery(r.URL.RawQuery) - if err != nil || !validBrowseQuery(query) { - http.Error(w, "invalid browse query", http.StatusBadRequest) - return nil - } - view, err := browseSnapshot(s, s.meta, query) - if errors.Is(err, errBrowseNotFound) { - http.NotFound(w, r) - return nil - } - if err != nil { - return err - } - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "no-store") - w.Header().Set("X-Content-Type-Options", "nosniff") - return json.NewEncoder(w).Encode(view) -} - -func validBrowseQuery(query url.Values) bool { - for name, values := range query { - if (name != "ref" && name != "path") || len(values) != 1 || len(values[0]) > 4096 { - return false - } - } - path := query.Get("path") - if path == "" { - return true - } - parts := strings.Split(path, "/") - if len(parts) > 64 || !utf8.ValidString(path) { - return false - } - for _, part := range parts { - if part == "" || part == "." || part == ".." || strings.ContainsRune(part, 0) { - return false - } - } - return true -} - -// Only configured branch tips are selectable. Object IDs cannot expose objects -// left behind by deleted refs, and paths are traversed within that tip's tree. -func browseSnapshot(s storer.EncodedObjectStorer, meta rootMeta, query url.Values) (*browseView, error) { - view := &browseView{ - Branch: query.Get("ref"), Path: query.Get("path"), - Branches: []string{}, History: []browseCommit{}, Entries: []browseEntry{}, - } - for ref := range meta.Refs { - if strings.HasPrefix(ref, "refs/heads/") { - view.Branches = append(view.Branches, ref) - } - } - slices.Sort(view.Branches) - if view.Branch == "" { - view.Branch = meta.Head - if _, exists := meta.Refs[view.Branch]; !exists && len(view.Branches) > 0 { - view.Branch = view.Branches[0] - } - } - if len(view.Branches) == 0 && query.Get("ref") == "" && view.Path == "" { - return view, nil - } - id, exists := meta.Refs[view.Branch] - if !exists || !slices.Contains(view.Branches, view.Branch) { - return nil, errBrowseNotFound - } - commit, err := object.GetCommit(s, plumbing.NewHash(id)) - if err != nil { - return nil, err - } - current := commit - for i := range 8 { - title := strings.SplitN(current.Message, "\n", 2)[0] - view.History = append(view.History, browseCommit{ - ID: current.Hash.String(), Title: strings.ToValidUTF8(title[:min(len(title), 512)], "�"), - Author: current.Author.Name[:min(len(current.Author.Name), 512)], Date: current.Author.When.Format("2006-01-02T15:04:05Z07:00"), - }) - if len(current.ParentHashes) == 0 || i == 7 { - break - } - current, err = object.GetCommit(s, current.ParentHashes[0]) - if err != nil { - return nil, err - } - } - tree, err := object.GetTree(s, commit.TreeHash) - if err != nil { - return nil, err - } - if view.Path != "" { - parts := strings.Split(view.Path, "/") - for i, part := range parts { - index := slices.IndexFunc(tree.Entries, func(e object.TreeEntry) bool { return e.Name == part }) - if index < 0 { - return nil, errBrowseNotFound - } - entry := tree.Entries[index] - if entry.Mode != filemode.Dir { - if i != len(parts)-1 { - return nil, errBrowseNotFound - } - view.File, err = browseBlob(s, entry) - return view, err - } - tree, err = object.GetTree(s, entry.Hash) - if err != nil { - return nil, err - } - } - } - view.More = len(tree.Entries) > 500 - for _, entry := range tree.Entries[:min(len(tree.Entries), 500)] { - view.Entries = append(view.Entries, browseEntry{Name: entry.Name, Kind: browseKind(entry.Mode), ID: entry.Hash.String()}) - } - return view, nil -} - -func browseKind(mode filemode.FileMode) string { - switch mode { - case filemode.Dir: - return "directory" - case filemode.Submodule: - return "submodule" - default: - return "file" - } -} - -func browseBlob(s storer.EncodedObjectStorer, entry object.TreeEntry) (*browseFile, error) { - file := &browseFile{ID: entry.Hash.String(), State: "submodule"} - if entry.Mode == filemode.Submodule { - return file, nil - } - blob, err := object.GetBlob(s, entry.Hash) - if err != nil { - return nil, err - } - file.Size = blob.Size - file.State = "large" - if blob.Size > browseFileBytes { - return file, nil - } - r, err := blob.Reader() - if err != nil { - return nil, err - } - data, err := io.ReadAll(io.LimitReader(r, browseFileBytes+1)) - err = errors.Join(err, r.Close()) - if err != nil { - return nil, err - } - if len(data) > browseFileBytes { - return file, nil - } - file.State = "binary" - if utf8.Valid(data) && !bytes.ContainsRune(data, 0) { - file.State = "text" - file.Text = string(data) - } - return file, nil -} diff --git a/examples/git/browse_test.go b/examples/git/browse_test.go deleted file mode 100644 index 0de525e..0000000 --- a/examples/git/browse_test.go +++ /dev/null @@ -1,207 +0,0 @@ -//go:build git_native_test - -package main - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - - "github.com/go-git/go-git/v6/plumbing" - "github.com/go-git/go-git/v6/plumbing/filemode" - "github.com/go-git/go-git/v6/plumbing/format/config" - "github.com/go-git/go-git/v6/plumbing/object" - "github.com/go-git/go-git/v6/plumbing/storer" - "github.com/go-git/go-git/v6/storage/memory" -) - -func browseFixture(t *testing.T, format config.ObjectFormat) *store { - t.Helper() - s := &store{Storage: memory.NewStorage(memory.WithObjectFormat(format))} - blob := func(text string) plumbing.Hash { - o := s.NewEncodedObject() - o.SetType(plumbing.BlobObject) - w, _ := o.Writer() - _, _ = w.Write([]byte(text)) - _ = w.Close() - id, err := s.SetEncodedObject(o) - if err != nil { - t.Fatal(err) - } - return id - } - put := func(value interface { - Encode(plumbing.EncodedObject) error - }) plumbing.Hash { - o := s.NewEncodedObject() - if err := value.Encode(o); err != nil { - t.Fatal(err) - } - id, err := s.SetEncodedObject(o) - if err != nil { - t.Fatal(err) - } - return id - } - subtree := put(&object.Tree{Entries: []object.TreeEntry{ - {Name: ".txt", Mode: filemode.Regular, Hash: blob("\n")}, - }}) - tree := put(&object.Tree{Entries: []object.TreeEntry{ - {Name: "binary", Mode: filemode.Regular, Hash: blob("a\x00b")}, - {Name: "docs", Mode: filemode.Dir, Hash: subtree}, - {Name: "large", Mode: filemode.Regular, Hash: blob(strings.Repeat("x", browseFileBytes+1))}, - {Name: "link", Mode: filemode.Symlink, Hash: blob("docs/.txt")}, - }}) - var tip plumbing.Hash - for i := range 10 { - commit := &object.Commit{TreeHash: tree, Message: "Commit " + string(rune('A'+i)) + "\nDetails"} - if i != 0 { - commit.ParentHashes = []plumbing.Hash{tip} - } - tip = put(commit) - } - s.meta = rootMeta{Format: format, Head: "refs/heads/main", Refs: map[string]string{ - "refs/heads/main": tip.String(), "refs/tags/v1": tip.String(), - }} - return s -} - -func TestBrowseSnapshotUsesBranchTreeAndBoundsHistory(t *testing.T) { - for _, format := range []config.ObjectFormat{config.SHA1, config.SHA256} { - t.Run(format.String(), func(t *testing.T) { - s := browseFixture(t, format) - view, err := browseSnapshot(s, s.meta, url.Values{}) - if err != nil { - t.Fatal(err) - } - if len(view.Branches) != 1 || len(view.History) != 8 || view.History[0].Title != "Commit J" { - t.Fatalf("unexpected branch/history: %+v", view) - } - if len(view.Entries) != 4 || len(view.History[0].ID) != format.HexSize() { - t.Fatalf("wrong tree or hash width: %+v", view) - } - for _, ref := range []string{"refs/tags/v1", view.History[0].ID, "refs/heads/missing"} { - _, err := browseSnapshot(s, s.meta, url.Values{"ref": {ref}}) - if !errors.Is(err, errBrowseNotFound) { - t.Fatalf("unselectable ref %q: %v", ref, err) - } - } - for _, path := range []string{"missing", "binary/child"} { - _, err := browseSnapshot(s, s.meta, url.Values{"path": {path}}) - if !errors.Is(err, errBrowseNotFound) { - t.Fatalf("missing path %q: %v", path, err) - } - } - }) - } -} - -func TestBrowseTextBinaryLargeAndSymlink(t *testing.T) { - s := browseFixture(t, config.SHA256) - for _, test := range []struct{ path, state, text string }{ - {path: "docs/.txt", state: "text", text: "\n"}, - {path: "binary", state: "binary"}, - {path: "large", state: "large"}, - {path: "link", state: "text", text: "docs/.txt"}, - } { - view, err := browseSnapshot(s, s.meta, url.Values{"path": {test.path}}) - if err != nil || view.File == nil || view.File.State != test.state || view.File.Text != test.text { - t.Fatalf("path %q: view=%+v err=%v", test.path, view, err) - } - } -} - -func TestBrowseRejectsAmbiguousQueriesAndUsesReadPermissions(t *testing.T) { - s := browseFixture(t, config.SHA1) - for _, query := range []string{"ref=a&ref=b", "path=../secret", "path=a//b", "path=/etc", "path=a/.", "path=%ZZ", "other=x"} { - w := httptest.NewRecorder() - err := serveBrowse(w, httptest.NewRequest(http.MethodGet, "/team/alpha.git/_browse?"+query, nil), s) - if err != nil || w.Code != http.StatusBadRequest { - t.Fatalf("query %q: status=%d err=%v", query, w.Code, err) - } - } - route, err := resolveRepository(repositoriesForTest(t), httptest.NewRequest(http.MethodGet, "/team/alpha.git/_browse?path=docs", nil)) - if err != nil || route.Action != gitRead || route.Method != http.MethodGet { - t.Fatalf("wrong authorization route: %+v %v", route, err) - } - _, err = resolveRepository(repositoriesForTest(t), httptest.NewRequest(http.MethodPost, "/team/alpha.git/_browse", nil)) - if !errors.Is(err, errRepositoryMethod) { - t.Fatalf("browse accepted a mutation method: %v", err) - } - w := httptest.NewRecorder() - if err := serveBrowse(w, httptest.NewRequest(http.MethodGet, "/team/alpha.git/_browse", nil), s); err != nil { - t.Fatal(err) - } - var view browseView - if err := json.Unmarshal(w.Body.Bytes(), &view); err != nil || w.Header().Get("Cache-Control") != "no-store" { - t.Fatalf("invalid uncached JSON: %v", err) - } -} - -func TestBrowseEscapedNamesAndDirectoryLimit(t *testing.T) { - s := browseFixture(t, config.SHA256) - commit, err := object.GetCommit(s, plumbing.NewHash(s.meta.Refs[s.meta.Head])) - if err != nil { - t.Fatal(err) - } - tree, err := object.GetTree(s, commit.TreeHash) - if err != nil { - t.Fatal(err) - } - blob := tree.Entries[0].Hash - name := "space #?.café\\file" - entries := []object.TreeEntry{} - for i := range 501 { - entries = append(entries, object.TreeEntry{Name: fmt.Sprintf("file%04d", i), Mode: filemode.Regular, Hash: blob}) - } - entries = append(entries, object.TreeEntry{Name: name, Mode: filemode.Regular, Hash: blob}) - o := s.NewEncodedObject() - if err := (&object.Tree{Entries: entries}).Encode(o); err != nil { - t.Fatal(err) - } - treeID, err := s.SetEncodedObject(o) - if err != nil { - t.Fatal(err) - } - o = s.NewEncodedObject() - if err := (&object.Commit{TreeHash: treeID}).Encode(o); err != nil { - t.Fatal(err) - } - tip, err := s.SetEncodedObject(o) - if err != nil { - t.Fatal(err) - } - ref := "refs/heads/feature/100%" - s.meta.Refs[ref] = tip.String() - view, err := browseSnapshot(s, s.meta, url.Values{"ref": {ref}}) - if err != nil || !view.More || len(view.Entries) != 500 { - t.Fatalf("directory bound: view=%+v err=%v", view, err) - } - query := url.Values{"ref": {ref}, "path": {name}} - w := httptest.NewRecorder() - if err := serveBrowse(w, httptest.NewRequest(http.MethodGet, "/showcase.git/_browse?"+query.Encode(), nil), s); err != nil { - t.Fatal(err) - } - if w.Code != http.StatusOK { - t.Fatalf("escaped path did not round trip: status=%d body=%s", w.Code, w.Body) - } -} - -type failingBrowseObjects struct{ storer.EncodedObjectStorer } - -func (failingBrowseObjects) EncodedObject(plumbing.ObjectType, plumbing.Hash) (plumbing.EncodedObject, error) { - return nil, errExpired -} - -func TestBrowseStorageFailuresRemainRetryable(t *testing.T) { - s := browseFixture(t, config.SHA1) - _, err := browseSnapshot(failingBrowseObjects{EncodedObjectStorer: s}, s.meta, url.Values{}) - if !errors.Is(err, errExpired) { - t.Fatalf("storage failure lost: %v", err) - } -} diff --git a/examples/git/main.go b/examples/git/main.go index 26f9060..2be44cc 100644 --- a/examples/git/main.go +++ b/examples/git/main.go @@ -202,7 +202,7 @@ func serve(response http.ResponseWriter, r *http.Request) { } return err } - if service == transport.UploadPackService || service == "_browse" { + if service == transport.UploadPackService { e = retryRead(w, r, refresh, func(attempt *readResponse, request *http.Request) error { retain, release := sessionRetention(session) return retained(r.Context(), retain, release, func() error { @@ -215,9 +215,6 @@ func serve(response http.ResponseWriter, r *http.Request) { return errLogMissing } attempt.failure = &s.failure - if service == "_browse" { - return serveBrowse(attempt, request, s) - } if request.Method == http.MethodPost { var body io.Reader = request.Body if request.Header.Get("Content-Encoding") == "gzip" { diff --git a/examples/git/repositories.go b/examples/git/repositories.go index 084dc30..aa4a317 100644 --- a/examples/git/repositories.go +++ b/examples/git/repositories.go @@ -183,7 +183,6 @@ func resolveRepository(repositories map[string]repositoryConfig, r *http.Request } for _, service := range []string{ "info/refs", transport.UploadPackService, transport.ReceivePackService, - "_browse", "maintenance", "collect", "prune-invalid-refs", "recover-retentions-after-drain", } { suffix := "/" + service @@ -199,9 +198,7 @@ func resolveRepository(repositories map[string]repositoryConfig, r *http.Request } route.Repository = repository route.Method = http.MethodPost - if route.Service == "_browse" { - route.Method = http.MethodGet - } else if route.Service == "info/refs" { + if route.Service == "info/refs" { query, err := url.ParseQuery(r.URL.RawQuery) services := query["service"] if err != nil || len(query) != 1 || len(services) != 1 { @@ -212,11 +209,11 @@ func resolveRepository(repositories map[string]repositoryConfig, r *http.Request if route.Service != transport.UploadPackService && route.Service != transport.ReceivePackService { return repositoryRoute{}, errRepositoryNotFound } - } else if route.Service != "_browse" && (r.URL.RawQuery != "" || r.URL.ForceQuery) { + } else if r.URL.RawQuery != "" || r.URL.ForceQuery { return repositoryRoute{}, errRepositoryNotFound } switch route.Service { - case transport.UploadPackService, "_browse": + case transport.UploadPackService: route.Action = gitRead case transport.ReceivePackService: route.Action = gitWrite diff --git a/examples/git/viewer/README.md b/examples/git/viewer/README.md index 868d7c5..9371b1e 100644 --- a/examples/git/viewer/README.md +++ b/examples/git/viewer/README.md @@ -1,51 +1,60 @@ # Repository explorer -A small read-only UI for the Git example, built with TypeScript and Preact. It -browses branches, directories, text files and recent first-parent commits. A -separate Spin component serves the page; the existing Git service supplies the -data through its read authorization, retention and retry path. The WAL is -unchanged. +A small repository browser with its own TypeScript Spin API and Preact UI. +It shows branches, directories, file previews and recent commits for configured +repositories. The Go Git service and the WAL are unchanged. + +The API checks access through ordinary Git discovery, then reads the repository's +existing WAL catalog directly. Reads follow only the selected history and paths; +opening a large file summary does not download the file. SHA-1 and SHA-256 both +work. Storage settings come from the same Spin variables as the Git example. ## Run locally -Install the [Git prerequisites](../README.md) and [Bun](https://bun.com/). +Install the [Git prerequisites](../README.md), [Bun](https://bun.com/), and Node.js. From the repository root: ```sh cd examples/git/viewer bun install --frozen-lockfile bun run check +bun test bun run build -cd ../../.. -OBJECT_LOG_GIT_LOCAL_MANIFEST=spin.viewer.toml make git-local ``` -Push to a printed Git URL, then open -`http://127.0.0.1:19100/browse?repo=sha256.git` and enter the printed password. -`/browse` also accepts a configured path such as `team/project.git`. Cognito -access tokens work with the same repository permissions. Credentials stay in -page memory and clear on reload. Branch and path selections stay in the URL; -Back/Forward and ordinary modified link clicks work. +Follow the [Git example's local setup](../README.md#run-locally), adding +`-f spin.viewer.toml` to its `spin up` command. Push a repository, then open +`http://127.0.0.1:19100/browse?repo=sha256.git` and enter the configured password. +Other configured paths, such as `team/project.git`, work too. Cognito access +tokens use the Git service's existing read permissions. Credentials stay in page +memory and clear on reload. Branch and path selections stay in the URL. -For an existing backend, use `spin up -f spin.viewer.toml` from `examples/git` -with your protected variables file. Hosted proxies must forward the three exact -viewer routes and `//_browse` to the service. +Hosted proxies must forward `/browse`, `/browse/api`, `/browse/assets/client.js` +and `/browse/assets/style.css` to Spin. The API is `GET /browse/api` with a +configured `repo` and optional `ref` and `path` query parameters. ## Build and checks -Bun bundles the browser and static handler; Spin's standard `j2w` compiler makes -the WASI component. `bun run check` runs Biome linting/formatting and strict -TypeScript checks. `bun run format` applies formatting and safe lint fixes. CI -checks and builds the viewer with the frozen Bun lockfile. +Bun bundles the browser and API; `j2w` builds the WASI component, then `wac` links +it with the reusable WAL component. The build generates `viewer/dist/viewer.wasm` +and `spin.viewer.toml` from the existing manifest. Rebuild after changing +`spin.toml`. Normal Git builds do not require Bun. + +`bun run check` runs Biome and strict TypeScript checks; `bun test` checks catalog +reads, object validation and authorization. `bun run format` applies formatting +and safe lint fixes. CI checks, tests and builds with the frozen Bun lockfile. -The build generates `viewer/dist/viewer.wasm` and `spin.viewer.toml` from the -existing manifest. Rebuild after changing `spin.toml`. Normal Git builds do not -require Bun. The component has no storage credentials or outbound hosts. +The compiler currently needs the small fix for +[ComponentizeJS #221](https://github.com/bytecodealliance/ComponentizeJS/issues/221), +which otherwise omits the classes for opaque WAL handles. Exact build provenance +is listed in [THIRD_PARTY.md](../../../THIRD_PARTY.md). ## Scope Only branch tips are selectable. The UI shows eight first-parent commits, 500 entries per directory and UTF-8 text previews up to 256 KiB. Binary and larger files show a summary. Symlinks show their stored target; submodules show -their commit ID. Names and file content render as text. The adapter's tests are -included in `make git-check`. +their commit ID. Non-UTF-8 filenames appear as byte escapes without a browse link; valid sibling +files remain browsable. Names and file content render as text. Git metadata reads are +limited to 16 MiB per object and 64 MiB of catalog data per request. An expired +view retries once, preserving cumulative read accounting. diff --git a/examples/git/viewer/biome.json b/examples/git/viewer/biome.json index b053781..cc7f22c 100644 --- a/examples/git/viewer/biome.json +++ b/examples/git/viewer/biome.json @@ -1,6 +1,15 @@ { "$schema": "https://biomejs.dev/schemas/2.5.14/schema.json", - "files": { "includes": ["src/**", "build.ts", "*.json"] }, - "formatter": { "indentStyle": "space", "lineWidth": 100 }, - "linter": { "rules": { "preset": "recommended" } } + "files": { + "includes": ["src/**", "build.ts", "*.json", "prepare.ts"] + }, + "formatter": { + "indentStyle": "space", + "lineWidth": 100 + }, + "linter": { + "rules": { + "preset": "recommended" + } + } } diff --git a/examples/git/viewer/build.ts b/examples/git/viewer/build.ts index ea958c7..d370cec 100644 --- a/examples/git/viewer/build.ts +++ b/examples/git/viewer/build.ts @@ -7,6 +7,7 @@ if (!client.success) throw new AggregateError(client.logs, "Browser build failed const component = await Bun.build({ entrypoints: ["src/index.ts"], target: "browser", + external: ["object-log:*", "fermyon:*"], outdir: "build", naming: "component.js", define: { @@ -16,14 +17,19 @@ const component = await Bun.build({ }, }); if (!component.success) throw new AggregateError(component.logs, "Spin bundle failed"); -const routes = ["/browse", "/browse/assets/client.js", "/browse/assets/style.css"]; +const manifest = await Bun.file("../spin.toml").text(); +const variables = [...manifest.matchAll(/^(wal_\w+|git_repositories) = "\{\{ (\w+) \}\}"$/gm)] + .map((match) => `${match[1]} = "{{ ${match[2]} }}"\n`) + .join(""); +const routes = ["/browse", "/browse/assets/client.js", "/browse/assets/style.css", "/browse/api"]; await Bun.write( "../spin.viewer.toml", - (await Bun.file("../spin.toml").text()) + + manifest + routes .map((route) => `\n[[trigger.http]]\nroute = "${route}"\ncomponent = "viewer"\n`) .join("") + - '\n[component.viewer]\nsource = "viewer/dist/viewer.wasm"\nallowed_outbound_hosts = []\n', + '\n[component.viewer]\nsource = "viewer/dist/viewer.wasm"\nallowed_outbound_hosts = ["http://git.spin.internal", "{{ wal_endpoint }}", "http://169.254.169.254"]\n[component.viewer.variables]\n' + + variables, ); export {}; diff --git a/examples/git/viewer/bun.lock b/examples/git/viewer/bun.lock index 6b7be7d..861596f 100644 --- a/examples/git/viewer/bun.lock +++ b/examples/git/viewer/bun.lock @@ -5,7 +5,10 @@ "": { "name": "object-log-repo-viewer", "dependencies": { + "@noble/hashes": "2.4.0", + "@spinframework/spin-variables": "1.0.1", "@spinframework/wasi-http-proxy": "2.0.0", + "fflate": "0.8.3", "preact": "10.29.8", }, "devDependencies": { @@ -18,7 +21,7 @@ }, }, "overrides": { - "@bytecodealliance/componentize-js": "0.23.0", + "@bytecodealliance/componentize-js": "https://github.com/carsonfarmer/ComponentizeJS/releases/download/opaque-resource-v0.23.0/bytecodealliance-componentize-js-0.23.0-opaque-resource.1.tgz", }, "packages": { "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], @@ -57,7 +60,7 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.14", "", { "os": "win32", "cpu": "x64" }, "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ=="], - "@bytecodealliance/componentize-js": ["@bytecodealliance/componentize-js@0.23.0", "", { "dependencies": { "@bytecodealliance/jco": "^1.15.1", "@bytecodealliance/weval": "^0.5.0", "@bytecodealliance/wizer": "^10.0.0", "es-module-lexer": "^1.6.0", "oxc-parser": "^0.76.0" }, "bin": { "componentize-js": "src/cli.js" } }, "sha512-Qc2+lrwJHU7lq4CBMyVVYE8ASyUFeegMYdRnMVG0wNpNiATzGDF/mWpjDNTeVZeT8L9chhK3vuLaQE9FVANvfw=="], + "@bytecodealliance/componentize-js": ["@bytecodealliance/componentize-js@https://github.com/carsonfarmer/ComponentizeJS/releases/download/opaque-resource-v0.23.0/bytecodealliance-componentize-js-0.23.0-opaque-resource.1.tgz", { "dependencies": { "@bytecodealliance/jco": "^1.15.1", "@bytecodealliance/weval": "^0.5.0", "@bytecodealliance/wizer": "^10.0.0", "es-module-lexer": "^1.6.0", "oxc-parser": "^0.76.0" }, "bin": { "componentize-js": "src/cli.js" } }, "sha512-yBBXoRshOde6eKRn4fbrUWNENUIsZvLpzAmd/ba4HPR/uxCwbDbrp5oZiWMe4TMvftIST/4hyz6Mj17d6FEffg=="], "@bytecodealliance/componentize-js-0-19-3": ["@bytecodealliance/componentize-js@0.19.3", "", { "dependencies": { "@bytecodealliance/jco": "^1.15.1", "@bytecodealliance/wizer": "^10.0.0", "es-module-lexer": "^1.6.0", "oxc-parser": "^0.76.0" }, "bin": { "componentize-js": "src/cli.js" } }, "sha512-ju7Y4WeF0B9uMkSPHJgmT6ouEfSwbe9M1uR/YOnYZjBpxJjH9qzxIkJg/kf8NycVDyFJ2/lscmJ1E1uPiDQVRQ=="], @@ -159,6 +162,8 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + "@noble/hashes": ["@noble/hashes@2.4.0", "", {}, "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA=="], + "@oxc-minify/binding-android-arm-eabi": ["@oxc-minify/binding-android-arm-eabi@0.136.0", "", { "os": "android", "cpu": "arm" }, "sha512-gk1jzky1vLeOcK7w9Ib2RT/UooZ7O21s8YSeXCsy4tzsmguKb3Cbsv/Ra2fKhMp4mKeKCzvC1Xa41OfSBWfWdQ=="], "@oxc-minify/binding-android-arm64": ["@oxc-minify/binding-android-arm64@0.136.0", "", { "os": "android", "cpu": "arm64" }, "sha512-5GIYanqud/ohXEcg2dsdn0+eSKXqclw5qlRUkvSM1sKEa4ZQ7hqOk8XPJBNPh3LXXw+Qw85B6jkmOFLKsCHp5w=="], @@ -265,6 +270,8 @@ "@spinframework/build-tools": ["@spinframework/build-tools@2.1.1", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bytecodealliance/componentize-js": "^0.23.0", "@bytecodealliance/jco": "^1.17.9", "acorn-walk": "^8.3.4", "acron": "^1.0.5", "magic-string": "^0.30.17", "regexpu-core": "^6.2.0", "yargs": "^18.0.0" }, "bin": { "j2w": "dist/index.js" } }, "sha512-VF4NRtkqW0vUv3MzgwbyZKzG4qC1D2DrmFbr+P3uJPLq0stuLqcuASqq3LzrxTbTnpAeeswn8JhEe+wPbH1Lng=="], + "@spinframework/spin-variables": ["@spinframework/spin-variables@1.0.1", "", { "peerDependencies": { "@spinframework/build-tools": ">=1.0.0 <3.0.0" } }, "sha512-PRapjCzTQSVSKpDcX8nNg/EJ/RWx6c+7PhtDGT0JCrqAM4G07+dvJLMjlKOssSsj0TMTxKfzEDzyXijFDXArAA=="], + "@spinframework/wasi-http-proxy": ["@spinframework/wasi-http-proxy@2.0.0", "", { "peerDependencies": { "@spinframework/build-tools": "^2.0.0" } }, "sha512-nMW4QcueYajZ4fqcC3gTbfWuJv3hm8oc6sJfc5FI4y1hEOH1K7ciV6g+9R3EEmQl8GjeZhI5rdQ9Anz3N4NVqQ=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], diff --git a/examples/git/viewer/package.json b/examples/git/viewer/package.json index 747355e..39610e2 100644 --- a/examples/git/viewer/package.json +++ b/examples/git/viewer/package.json @@ -6,9 +6,11 @@ "license": "Apache-2.0", "packageManager": "bun@1.3.13", "scripts": { - "build": "bun build.ts && bun x --bun j2w -i build/component.js -o dist/viewer.wasm", - "check": "biome check . && tsc --noEmit", - "format": "biome check --write ." + "build": "cargo build --locked --release --manifest-path ../../wal-component/Cargo.toml --target wasm32-wasip2 && bun run prepare && bun build.ts && j2w -i build/component.js -o build/viewer.wasm && wac plug --plug ../../wal-component/target/wasm32-wasip2/release/object_log_component.wasm build/viewer.wasm -o dist/viewer.wasm", + "check": "bun run prepare && biome check . && tsc --noEmit", + "format": "biome check --write .", + "prepare": "bun prepare.ts", + "test": "bun test" }, "devDependencies": { "@biomejs/biome": "2.5.14", @@ -18,10 +20,22 @@ "typescript": "7.0.2" }, "overrides": { - "@bytecodealliance/componentize-js": "0.23.0" + "@bytecodealliance/componentize-js": "https://github.com/carsonfarmer/ComponentizeJS/releases/download/opaque-resource-v0.23.0/bytecodealliance-componentize-js-0.23.0-opaque-resource.1.tgz" }, "dependencies": { + "@noble/hashes": "2.4.0", + "@spinframework/spin-variables": "1.0.1", "@spinframework/wasi-http-proxy": "2.0.0", + "fflate": "0.8.3", "preact": "10.29.8" + }, + "config": { + "witDependencies": [ + { + "witPath": "./build/wit", + "package": "object-log:storage@0.1.0", + "world": "reader" + } + ] } } diff --git a/examples/git/viewer/prepare.ts b/examples/git/viewer/prepare.ts new file mode 100644 index 0000000..e9c0d50 --- /dev/null +++ b/examples/git/viewer/prepare.ts @@ -0,0 +1,11 @@ +await Bun.write( + "build/wit/wal.wit", + `${await Bun.file("../../wal-component/wit/wal.wit").text()}\nworld reader { import wal; }\n`, +); +const types = Bun.spawn( + ["bun", "x", "--bun", "jco", "types", "build/wit", "-n", "reader", "-o", "build/types"], + { stdout: "inherit", stderr: "inherit" }, +); +if (await types.exited) throw new Error("WAL type generation failed"); + +export {}; diff --git a/examples/git/viewer/src/api.ts b/examples/git/viewer/src/api.ts new file mode 100644 index 0000000..5b827d9 --- /dev/null +++ b/examples/git/viewer/src/api.ts @@ -0,0 +1,86 @@ +import { NotFound, snapshot } from "./repository"; +import { Catalog, drop, openExisting, type Repository, settings, variable } from "./wal"; + +export async function browse(request: Request): Promise { + const query = new URL(request.url).searchParams; + const path = query.get("path") ?? ""; + if ( + [...query.keys()].some( + (key) => + !["repo", "ref", "path"].includes(key) || + query.getAll(key).length !== 1 || + (query.get(key)?.length ?? 0) > 4096, + ) || + (path && + (path.split("/").length > 64 || + path + .split("/") + .some((part) => !part || part === "." || part === ".." || part.includes("\0")))) + ) + return new Response("Invalid browse query", { status: 400 }); + const name = query.get("repo") ?? ""; + const repositories: Record = JSON.parse(variable("git_repositories")); + if (!Object.hasOwn(repositories, name)) + return new Response("Repository not found", { status: 404 }); + // Standard Git discovery enforces the unchanged service's repository permissions. + const authorized = await fetch( + `http://git.spin.internal/${name}/info/refs?service=git-upload-pack`, + { + headers: { Authorization: request.headers.get("Authorization") ?? "" }, + redirect: "manual", + }, + ); + const body = authorized.body?.getReader(); + try { + let bytes = 0; + for (let part = await body?.read(); part && !part.done; part = await body?.read()) { + bytes += part.value.length; + if (bytes > 8 << 20) throw new Error("Git discovery exceeds 8 MiB"); + } + } finally { + await body?.cancel(); + body?.releaseLock(); + } + if (authorized.status !== 200) + return new Response("Repository access denied", { + status: [401, 403, 404, 503].includes(authorized.status) ? authorized.status : 502, + }); + if ( + !authorized.headers + .get("Content-Type") + ?.startsWith("application/x-git-upload-pack-advertisement") + ) + return new Response("Invalid Git discovery response", { status: 502 }); + // Successful discovery has validated this same declaratively configured backend. + let session = openExisting(settings(repositories[name])); + const budget = { bytes: 0 }; + try { + for (let attempt = 0; ; attempt++) { + let catalog: Catalog | undefined; + try { + catalog = new Catalog(session, repositories[name].format, budget); + const view = snapshot(catalog, query), + usage = session.usage(); + return Response.json(view, { + headers: { + "Cache-Control": "no-store", + "X-Wal-Calls": String(usage.calls), + "X-Wal-Bytes": String(usage.bytes), + }, + }); + } catch (error) { + if (error instanceof NotFound) + return new Response("Branch or path not found", { status: 404 }); + if (attempt === 0 && (error as { payload?: { tag: string } }).payload?.tag === "expired") { + const fresh = session.refresh(); + drop(session); + session = fresh; + } else throw error; + } finally { + catalog?.close(); + } + } + } finally { + drop(session); + } +} diff --git a/examples/git/viewer/src/client.tsx b/examples/git/viewer/src/client.tsx index 802e0cf..a1c0ea8 100644 --- a/examples/git/viewer/src/client.tsx +++ b/examples/git/viewer/src/client.tsx @@ -1,21 +1,7 @@ import { render, type VNode } from "preact"; import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; -interface Commit { - id: string; - title: string; - author: string; - date: string; -} -interface Snapshot { - branch: string; - branches: string[]; - path: string; - history: Commit[]; - entries: { name: string; kind: string; id: string }[]; - more: boolean; - file?: { id: string; size: number; state: string; text?: string }; -} +import type { Commit, Snapshot } from "./repository"; function validRepository(name: string): boolean { return ( @@ -108,9 +94,13 @@ function Code({ a.name.localeCompare(b.name), ) .map((entry) => ( -
+
- {link(entry.name, [view.path, entry.name].filter(Boolean).join("/"))} + {entry.unavailable ? ( + {entry.name} + ) : ( + link(entry.name, [view.path, entry.name].filter(Boolean).join("/")) + )} {entry.id.slice(0, 7)}
))} @@ -150,8 +140,7 @@ function App() { void (async () => { try { const params = new URLSearchParams(search); - params.delete("repo"); - const response = await fetch(`/${repository}/_browse?${params}`, { + const response = await fetch(`/browse/api?${params}`, { headers: authorization.header ? { Authorization: authorization.header } : {}, credentials: "omit", cache: "no-store", diff --git a/examples/git/viewer/src/index.ts b/examples/git/viewer/src/index.ts index 256123e..32a091f 100644 --- a/examples/git/viewer/src/index.ts +++ b/examples/git/viewer/src/index.ts @@ -1,10 +1,10 @@ -export {}; +import { browse } from "./api"; declare const CLIENT_SCRIPT: string; declare const STYLES: string; declare const PAGE: string; -function handle(request: Request): Response { +async function handle(request: Request): Promise { if (request.method !== "GET" && request.method !== "HEAD") { return new Response("Method not allowed", { status: 405, @@ -12,6 +12,7 @@ function handle(request: Request): Response { }); } const path = new URL(request.url).pathname; + if (path === "/browse/api" && request.method === "GET") return browse(request); if ( path !== "/browse" && path !== "/browse/assets/client.js" && @@ -38,5 +39,10 @@ function handle(request: Request): Response { addEventListener("fetch", (event: Event) => { const request = event as FetchEvent; - request.respondWith(handle(request.request)); + request.respondWith( + handle(request.request).catch((error) => { + console.error(error); + return new Response("Repository unavailable", { status: 503 }); + }), + ); }); diff --git a/examples/git/viewer/src/repository.ts b/examples/git/viewer/src/repository.ts new file mode 100644 index 0000000..4296ee7 --- /dev/null +++ b/examples/git/viewer/src/repository.ts @@ -0,0 +1,143 @@ +import { type Catalog, hex } from "./wal"; + +export interface Commit { + id: string; + title: string; + author: string; + date: string; +} +interface TreeEntry { + name: string; + kind: string; + id: string; + unavailable?: true; +} +export interface Snapshot { + branch: string; + branches: string[]; + path: string; + history: Commit[]; + entries: TreeEntry[]; + more: boolean; + file?: { id: string; size: number; state: string; text?: string }; +} +const text = new TextDecoder(); +const strictText = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); +const previewBytes = 256 << 10; +const metadataBytes = 16 << 20; +export class NotFound extends Error {} + +// Decode only loose commit/tree objects; packs and deltas remain the Git service's concern. +function commit(catalog: Catalog, id: string) { + const raw = text.decode(catalog.object(id, 1, metadataBytes).bytes); + const split = raw.indexOf("\n\n"), + header = raw.slice(0, split).split("\n"); + const fields = (name: string) => + header.find((line) => line.startsWith(`${name} `))?.slice(name.length + 1); + const author = /^(.*?) <.*>[ \t]+(-?\d+)[ \t]+[+-]\d{4}$/.exec(fields("author") ?? ""); + const tree = fields("tree"); + if (split < 0 || !tree || !author) throw new Error("Invalid Git commit"); + return { + tree, + parent: fields("parent") ?? "", + summary: { + id, + title: raw + .slice(split + 2) + .split("\n", 1)[0] + .slice(0, 512), + author: author[1].slice(0, 512), + date: new Date(Number(author[2]) * 1000).toISOString(), + }, + }; +} + +function tree(catalog: Catalog, id: string): TreeEntry[] { + const bytes = catalog.object(id, 2, metadataBytes).bytes; + if (!bytes) throw new Error("Tree exceeds metadata limit"); + const width = catalog.root.Format === "sha256" ? 32 : 20, + result: TreeEntry[] = []; + for (let offset = 0; offset < bytes.length; ) { + const space = bytes.indexOf(32, offset), + nul = bytes.indexOf(0, space + 1); + if (space < offset || nul <= space || nul + 1 + width > bytes.length) + throw new Error("Invalid Git tree"); + const mode = text.decode(bytes.subarray(offset, space)), + rawName = bytes.subarray(space + 1, nul); + let name: string, + unavailable = false; + try { + name = strictText.decode(rawName); + } catch { + name = hex(rawName).replace(/../g, "\\x$&"); + unavailable = true; + } + if (!name || name === "." || name === ".." || name.includes("/")) + throw new Error("Invalid Git name"); + result.push({ + name, + kind: mode === "40000" ? "directory" : mode === "160000" ? "submodule" : "file", + id: hex(bytes.subarray(nul + 1, nul + 1 + width)), + ...(unavailable ? { unavailable: true as const } : {}), + }); + offset = nul + 1 + width; + } + return result; +} + +export function snapshot(catalog: Catalog, query: URLSearchParams): Snapshot { + const branches = Object.keys(catalog.root.Refs) + .filter((ref) => ref.startsWith("refs/heads/")) + .sort(); + const branch = + query.get("ref") || + (branches.includes(catalog.root.Head) ? catalog.root.Head : branches[0]) || + ""; + const view: Snapshot = { + branch, + branches, + path: query.get("path") ?? "", + history: [], + entries: [], + more: false, + }; + if (!branches.length && !query.get("ref") && !view.path) return view; + if (!branches.includes(branch)) throw new NotFound(); + let id = catalog.root.Refs[branch], + tip = ""; + for (let i = 0; id && i < 8; i++) { + const current = commit(catalog, id); + if (i === 0) tip = current.tree; + view.history.push(current.summary); + id = current.parent; + } + let entries = tree(catalog, tip); + const parts = view.path ? view.path.split("/") : []; + for (const [index, part] of parts.entries()) { + const entry = entries.find((value) => !value.unavailable && value.name === part); + if (!entry) throw new NotFound(); + if (entry.kind === "directory") { + entries = tree(catalog, entry.id); + continue; + } + if (index !== parts.length - 1) throw new NotFound(); + view.file = { id: entry.id, size: 0, state: "submodule" }; + if (entry.kind !== "submodule") { + const blob = catalog.object(entry.id, 3, previewBytes); + view.file.size = blob.size; + view.file.state = blob.bytes ? "binary" : "large"; + if (blob.bytes && !blob.bytes.includes(0)) { + try { + view.file.text = strictText.decode(blob.bytes); + view.file.state = "text"; + } catch { + /* Binary preview. */ + } + } + } + return view; + } + view.more = entries.length > 500; + view.entries = entries.slice(0, 500); + return view; +} diff --git a/examples/git/viewer/src/wal.test.ts b/examples/git/viewer/src/wal.test.ts new file mode 100644 index 0000000..9b11933 --- /dev/null +++ b/examples/git/viewer/src/wal.test.ts @@ -0,0 +1,313 @@ +import { afterEach, expect, mock, test } from "bun:test"; +import type { Session } from "object-log:storage/wal@0.1.0"; +import { zlibSync } from "fflate"; + +let opened = 0; +let openSession: () => Session = () => { + throw new Error("Unexpected storage access"); +}; +mock.module("object-log:storage/wal@0.1.0", () => ({ + openExisting: () => { + opened++; + return openSession(); + }, +})); +mock.module("@spinframework/spin-variables", () => ({ + get: (name: string) => + name === "git_repositories" ? '{"team/demo.git":{"log_id":"demo","format":"sha256"}}' : "", +})); +const { Catalog } = await import("./wal"); +const { browse } = await import("./api"); +const { snapshot } = await import("./repository"); +const originalFetch = globalThis.fetch; +afterEach(() => { + globalThis.fetch = originalFetch; + openSession = () => { + throw new Error("Unexpected storage access"); + }; +}); + +function fixture( + format: "sha1" | "sha256", + raw: Uint8Array, + encoded: Uint8Array, + storage: "inline" | "full" | "delta" = "inline", +) { + const id = new Bun.CryptoHasher(format).update(raw).digest("hex"); + let drops = 0, + reads = 0; + const handle = () => ({ + [Symbol.dispose]() { + drops++; + }, + }); + const root = handle(), + leaf = handle(), + wrapper = handle(), + full = handle(), + delta = handle(); + const item = { + ID: id, + Kind: 3, + Size: 3, + StoredSize: encoded.length, + Encoding: "zlib", + ...(storage === "inline" + ? { Inline: btoa(String.fromCharCode(...encoded)) } + : storage === "delta" + ? { Delta: { StoredSize: 12 } } + : {}), + }; + const entry = (value: unknown, children: unknown[]) => ({ + data: new TextEncoder().encode(JSON.stringify(value)), + objects: children, + }); + let history = true; + const recovery = { + next() { + if (!history) return undefined; + history = false; + return { tag: "checkpoint", val: { objects: [root] } }; + }, + readNode(value: unknown) { + if (value === root) + return entry( + { + Validated: true, + Format: format, + Head: "refs/heads/main", + Refs: {}, + Buckets: [id.slice(0, 2)], + }, + [leaf], + ); + if (value === leaf) + return entry( + { Items: [item] }, + storage === "inline" ? [] : storage === "full" ? [full] : [wrapper], + ); + return { data: new Uint8Array(), objects: [full, delta] }; + }, + openBytes(value: unknown) { + expect(value).toBe(full); + return { + length: () => BigInt(encoded.length), + readAt(offset: bigint, length: number) { + reads++; + return encoded.slice(Number(offset), Number(offset) + Math.min(3, length)); + }, + [Symbol.dispose]() { + drops++; + }, + }; + }, + [Symbol.dispose]() { + drops++; + }, + }; + const catalog = new Catalog({ recover: () => recovery } as unknown as Session, format, { + bytes: 0, + }); + return { catalog, id, counts: () => ({ drops, reads }) }; +} + +for (const format of ["sha1", "sha256"] as const) { + test(`${format}: inline and chunked full objects with a retained delta`, () => { + const raw = new TextEncoder().encode("blob 3\0abc"), + encoded = zlibSync(raw); + for (const storage of ["inline", "full", "delta"] as const) { + const sample = fixture(format, raw, encoded, storage); + try { + expect(new TextDecoder().decode(sample.catalog.object(sample.id, 3, 256).bytes)).toBe( + "abc", + ); + } finally { + sample.catalog.close(); + } + expect(sample.counts().drops).toBe(storage === "inline" ? 3 : storage === "full" ? 5 : 7); + expect(sample.counts().reads).toBe(storage === "inline" ? 0 : Math.ceil(encoded.length / 3)); + } + }); +} + +test("inflation rejects excess decoded bytes and truncated input", () => { + const raw = new TextEncoder().encode("blob 3\0abc"); + for (const encoded of [ + zlibSync(new TextEncoder().encode("blob 3\0abcEXTRA")), + zlibSync(raw).slice(0, 5), + ]) { + const sample = fixture("sha256", raw, encoded); + try { + expect(() => sample.catalog.object(sample.id, 3, 256)).toThrow(); + } finally { + sample.catalog.close(); + } + expect(sample.counts().drops).toBe(3); + } +}); + +test("large preview reads only metadata", () => { + const raw = new TextEncoder().encode("blob 3\0abc"), + sample = fixture("sha256", raw, zlibSync(raw), "delta"); + try { + expect(sample.catalog.object(sample.id, 3, 2)).toEqual({ size: 3 }); + } finally { + sample.catalog.close(); + } + expect(sample.counts().reads).toBe(0); +}); + +test("Git authorization completes before viewer storage is opened", async () => { + const baseline = opened; + globalThis.fetch = mock(async (input: RequestInfo | URL, options?: RequestInit) => { + expect(String(input)).toBe( + "http://git.spin.internal/team/demo.git/info/refs?service=git-upload-pack", + ); + expect(options?.headers).toEqual({ Authorization: "Bearer test-token" }); + expect(options?.redirect).toBe("manual"); + return new Response("Denied", { status: 403 }); + }) as unknown as typeof fetch; + const response = await browse( + new Request("https://viewer.test/browse/api?repo=team/demo.git", { + headers: { Authorization: "Bearer test-token" }, + }), + ); + expect(response.status).toBe(403); + expect(opened).toBe(baseline); + globalThis.fetch = mock(async () => { + throw new Error("Unexpected HTTP call"); + }) as unknown as typeof fetch; + expect( + (await browse(new Request("https://viewer.test/browse/api?repo=missing.git"))).status, + ).toBe(404); + expect( + (await browse(new Request("https://viewer.test/browse/api?repo=team/demo.git&path=.."))).status, + ).toBe(400); + expect(opened).toBe(baseline); +}); + +test("unusual filenames remain listed without hiding valid siblings", () => { + const encode = (value: string) => new TextEncoder().encode(value); + const commitId = "11".repeat(20), + treeId = "22".repeat(20), + blobId = "33".repeat(20); + const names = [ + Uint8Array.of(255), + encode("�"), + encode("\\xff"), + encode("\uFEFFREADME"), + encode("README"), + ]; + const tree = new Uint8Array(names.reduce((sum, name) => sum + 8 + name.length + 20, 0)); + let offset = 0; + for (const [index, name] of names.entries()) { + tree.set(encode("100644 "), offset); + offset += 7; + tree.set(name, offset); + offset += name.length; + tree[offset++] = 0; + tree.fill(0x33 + index, offset, offset + 20); + offset += 20; + } + for (const spacing of [" ", " ", " \t"]) { + const catalog = { + root: { Format: "sha1", Head: "refs/heads/main", Refs: { "refs/heads/main": commitId } }, + object(id: string) { + const bytes = + id === commitId + ? encode(`tree ${treeId}\nauthor A ${spacing}1700000000 +0000\n\nTitle\n`) + : id === treeId + ? tree + : encode("safe"); + return { size: bytes.length, bytes }; + }, + } as unknown as InstanceType; + const view = snapshot(catalog, new URLSearchParams()); + expect(view.history[0].title).toBe("Title"); + expect(view.entries).toEqual([ + { name: "\\xff", kind: "file", id: blobId, unavailable: true }, + { name: "�", kind: "file", id: "34".repeat(20) }, + { name: "\\xff", kind: "file", id: "35".repeat(20) }, + { name: "\uFEFFREADME", kind: "file", id: "36".repeat(20) }, + { name: "README", kind: "file", id: "37".repeat(20) }, + ]); + expect(snapshot(catalog, new URLSearchParams({ path: "�" })).file?.text).toBe("safe"); + expect(snapshot(catalog, new URLSearchParams({ path: "\\xff" })).file?.id).toBe( + "35".repeat(20), + ); + } +}); + +test("expired reads refresh once, close both views, and retain cumulative usage", async () => { + let calls = 0, + bytes = 0, + refreshes = 0; + const closed: string[] = []; + const makeSession = (fresh: boolean): Session => + ({ + recover() { + let next = true; + const root = { + [Symbol.dispose]() { + closed.push(`root:${fresh}`); + }, + }; + return { + next() { + if (!next) return undefined; + next = false; + return { tag: "checkpoint", val: { objects: [root] } }; + }, + readNode() { + calls++; + bytes += 100; + if (!fresh) throw { payload: { tag: "expired" } }; + return { + objects: [], + data: new TextEncoder().encode( + JSON.stringify({ + Validated: true, + Format: "sha256", + Head: "refs/heads/main", + Refs: {}, + Buckets: [], + }), + ), + }; + }, + [Symbol.dispose]() { + closed.push(`recovery:${fresh}`); + }, + }; + }, + refresh() { + refreshes++; + return makeSession(true); + }, + usage: () => ({ calls: BigInt(calls), bytes: BigInt(bytes) }), + [Symbol.dispose]() { + closed.push(`session:${fresh}`); + }, + }) as unknown as Session; + openSession = () => makeSession(false); + globalThis.fetch = mock( + async () => + new Response("refs", { + headers: { "Content-Type": "application/x-git-upload-pack-advertisement" }, + }), + ) as unknown as typeof fetch; + const response = await browse(new Request("https://viewer.test/browse/api?repo=team/demo.git")); + expect(response.status).toBe(200); + expect((await response.json()).history).toEqual([]); + expect(refreshes).toBe(1); + expect(response.headers.get("X-Wal-Calls")).toBe("2"); + expect(response.headers.get("X-Wal-Bytes")).toBe("200"); + expect(closed.sort()).toEqual([ + "recovery:false", + "recovery:true", + "root:false", + "root:true", + "session:false", + "session:true", + ]); +}); diff --git a/examples/git/viewer/src/wal.ts b/examples/git/viewer/src/wal.ts new file mode 100644 index 0000000..eb8274d --- /dev/null +++ b/examples/git/viewer/src/wal.ts @@ -0,0 +1,206 @@ +import type { + Config, + Entry, + Recovery, + Session, + Object as WalObject, +} from "object-log:storage/wal@0.1.0"; +import * as wal from "object-log:storage/wal@0.1.0"; +import { sha1 } from "@noble/hashes/legacy.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { get } from "@spinframework/spin-variables"; +import { unzlibSync } from "fflate"; + +export const variable = (name: string) => get(name) ?? ""; +export const drop = (resource: unknown) => (resource as Disposable)[Symbol.dispose](); +const decode = new TextDecoder("utf-8", { fatal: true }); +export const json = (data: Uint8Array): T => JSON.parse(decode.decode(data)); +export const hex = (bytes: Uint8Array) => + Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); + +export interface Repository { + log_id: string; + format: "sha1" | "sha256"; +} +interface Item { + ID: string; + Kind: number; + Size: number; + StoredSize: number; + Encoding: string; + Inline?: string; + Delta?: { StoredSize: number }; +} +interface Bucket { + Items?: Item[]; + Prefixes?: string[]; +} +export interface Root { + Validated: boolean; + Format: string; + Head: string; + Refs: Record; + Buckets: string[]; +} + +export function settings(repository: Repository): Config { + return { + endpoint: variable("wal_endpoint"), + bucket: variable("wal_bucket"), + region: variable("wal_region"), + credentialMode: + variable("wal_credential_mode") === "instance-role" ? "instance-role" : "static-credentials", + accessKey: variable("wal_access_key"), + secretKey: variable("wal_secret_key"), + sessionToken: variable("wal_session_token") || undefined, + prefix: variable("wal_prefix"), + logId: repository.log_id, + logLimits: { + maxTailEntries: 1024n, + resolutionWindow: 1024n, + maxInlineOperationBytes: 65536n, + maxInlineResultBytes: 4096n, + maxObjectRefs: 1024n, + maxObjectBytes: 2097152n, + maxCommitBytes: 1048576n, + maxHeadBytes: 262144n, + maxCheckpointBytes: 16777216n, + maxRetentionIds: 1024n, + maxCollectionObjects: BigInt(variable("wal_max_collection_objects")), + maxCollectionPlanBytes: 16777216n, + }, + transportLimits: { maxCalls: 25984n, maxBytes: 26180845568n }, + }; +} + +// Own one exact recovered view; no cache or local repository becomes authority. +export class Catalog { + readonly recovery: Recovery; + readonly root: Root; + private owned: WalObject[] = []; + private buckets = new Map(); + private nodes = new Map(); + + constructor( + session: Session, + format: string, + private budget: { bytes: number }, + ) { + this.recovery = session.recover(); + try { + let roots: WalObject[] = []; + for (let record = this.recovery.next(); record; record = this.recovery.next()) { + for (const root of roots) drop(root); + roots = record.val.objects; + this.owned = roots; + } + if (roots.length !== 1) throw new Error("Repository has no published root"); + const node = this.node(roots[0]); + this.root = json(node.data); + if ( + !this.root.Validated || + this.root.Format !== format || + this.root.Buckets.length !== node.objects.length + ) + throw new Error("Invalid repository catalog"); + this.root.Buckets.forEach((prefix, i) => { + this.buckets.set(prefix, node.objects[i]); + }); + } catch (error) { + this.close(); + throw error; + } + } + + private node(root: WalObject): Entry { + const cached = this.nodes.get(root); + if (cached) return cached; + const entry = this.recovery.readNode(root); + this.owned.push(...entry.objects); + this.budget.bytes += entry.data.length; + if (this.budget.bytes > 64 << 20) throw new Error("Catalog read exceeds 64 MiB"); + this.nodes.set(root, entry); + return entry; + } + + object(id: string, kind: number, maxBytes: number): { size: number; bytes?: Uint8Array } { + const hashBytes = this.root.Format === "sha256" ? 32 : 20; + if (!new RegExp(`^[0-9a-f]{${hashBytes * 2}}$`).test(id)) + throw new Error("Invalid Git object ID"); + let prefix = id.slice(0, 2), + root = this.buckets.get(prefix); + while (root) { + const node = this.node(root), + bucket = json(node.data); + if (bucket.Prefixes?.length) { + prefix = id.slice(0, prefix.length + 1); + if (prefix.length >= id.length || bucket.Prefixes.length !== node.objects.length) break; + root = node.objects[bucket.Prefixes.indexOf(prefix)]; + continue; + } + const items = bucket.Items ?? []; + let child = 0; + for (const item of items) { + const value = item.Inline ? undefined : node.objects[child++]; + if (item.ID !== id) continue; + if ( + item.Kind !== kind || + item.Encoding !== "zlib" || + !Number.isSafeInteger(item.Size) || + item.Size < 0 + ) + throw new Error("Invalid Git object metadata"); + if (item.Size > maxBytes) { + if (kind !== 3) throw new Error("Git object exceeds metadata limit"); + return { size: item.Size }; + } + let compressed: Uint8Array; + if (item.Inline) + compressed = Uint8Array.from(atob(item.Inline), (char) => char.charCodeAt(0)); + else { + if (!value) throw new Error("Missing object reference"); + const full = item.Delta?.StoredSize ? this.node(value).objects[0] : value; + const reader = this.recovery.openBytes(full); + try { + if (reader.length() !== BigInt(item.StoredSize) || item.StoredSize > maxBytes + 65536) + throw new Error("Invalid stored object length"); + compressed = new Uint8Array(item.StoredSize); + for (let offset = 0; offset < compressed.length; ) { + const chunk = reader.readAt(BigInt(offset), compressed.length - offset); + if (!chunk.length || chunk.length > compressed.length - offset) + throw new Error("Invalid object read length"); + compressed.set(chunk, offset); + offset += chunk.length; + } + } finally { + drop(reader); + } + } + if (compressed.length !== item.StoredSize || (item.Inline && compressed.length > 512)) + throw new Error("Invalid stored object length"); + const names: Record = { 1: "commit", 2: "tree", 3: "blob" }; + const header = new TextEncoder().encode(`${names[kind]} ${item.Size}\0`); + const expected = header.length + item.Size; + const output = new Uint8Array(expected + 1); + const content = unzlibSync(compressed, { out: output }); + const hash = this.root.Format === "sha256" ? sha256(content) : sha1(content); + if ( + content.length !== expected || + hex(hash) !== id || + !header.every((byte, i) => content[i] === byte) + ) + throw new Error("Git object differs from its catalog"); + return { size: item.Size, bytes: content.subarray(header.length) }; + } + break; + } + throw new Error("Git object not found"); + } + + close() { + for (const resource of this.owned.reverse()) drop(resource); + drop(this.recovery); + } +} + +export const openExisting = wal.openExisting; diff --git a/examples/git/viewer/tsconfig.json b/examples/git/viewer/tsconfig.json index 6b4ce86..50f6cb0 100644 --- a/examples/git/viewer/tsconfig.json +++ b/examples/git/viewer/tsconfig.json @@ -9,7 +9,10 @@ "noEmit": true, "lib": ["ES2022", "DOM", "WebWorker"], "skipLibCheck": true, - "types": ["bun"] + "types": ["bun"], + "paths": { + "object-log:storage/wal@0.1.0": ["./build/types/interfaces/object-log-storage-wal.d.ts"] + } }, - "include": ["src/**/*.ts", "src/**/*.tsx", "build.ts"] + "include": ["src/**/*.ts", "src/**/*.tsx", "build.ts", "prepare.ts"] } From 2255261ac7152f526203adfbb2d8f6ed00edee9a Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Sat, 26 Sep 2026 16:09:55 -0700 Subject: [PATCH 06/16] Create viewer output directory before composition Co-authored-by: Codex --- examples/git/viewer/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/git/viewer/package.json b/examples/git/viewer/package.json index 39610e2..874a95b 100644 --- a/examples/git/viewer/package.json +++ b/examples/git/viewer/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "packageManager": "bun@1.3.13", "scripts": { - "build": "cargo build --locked --release --manifest-path ../../wal-component/Cargo.toml --target wasm32-wasip2 && bun run prepare && bun build.ts && j2w -i build/component.js -o build/viewer.wasm && wac plug --plug ../../wal-component/target/wasm32-wasip2/release/object_log_component.wasm build/viewer.wasm -o dist/viewer.wasm", + "build": "cargo build --locked --release --manifest-path ../../wal-component/Cargo.toml --target wasm32-wasip2 && bun run prepare && bun build.ts && j2w -i build/component.js -o build/viewer.wasm && mkdir -p dist && wac plug --plug ../../wal-component/target/wasm32-wasip2/release/object_log_component.wasm build/viewer.wasm -o dist/viewer.wasm", "check": "bun run prepare && biome check . && tsc --noEmit", "format": "biome check --write .", "prepare": "bun prepare.ts", From 312017ed5037b7f43f8fc622f14106a643a9a4c5 Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Sat, 26 Sep 2026 22:14:51 -0700 Subject: [PATCH 07/16] Create repositories through an authenticated API Resolve new names under a default access policy, publish their format and default branch through the existing WAL, and recover stored formats without per-repository configuration. Co-authored-by: Codex --- GIT_PLAN.md | 15 ++- HANDOFF.md | 9 +- examples/git/README.md | 42 +++++-- examples/git/codec.go | 8 +- examples/git/codec_test.go | 25 +++++ examples/git/main.go | 53 ++++++++- examples/git/qualification/aws/hosting.tf | 14 +-- examples/git/repositories.go | 98 ++++++++++++++-- examples/git/repositories_test.go | 87 +++++++++++++- examples/git/spin.toml | 2 +- examples/git/store.go | 23 ++-- examples/git/tests/creation_test.go | 131 ++++++++++++++++++++++ 12 files changed, 449 insertions(+), 58 deletions(-) create mode 100644 examples/git/tests/creation_test.go diff --git a/GIT_PLAN.md b/GIT_PLAN.md index add85a1..c8a3558 100644 --- a/GIT_PLAN.md +++ b/GIT_PLAN.md @@ -11,9 +11,9 @@ rules remain outside the Rust core. - Classic receive-pack push, branches, annotated tags, stale-write rejection, fast-forward policy, connectivity validation, and malformed-input rejection. - Atomic publication of refs and the sparse object catalog. -- Arbitrary configured repository paths with isolated WAL identities and an - explicit SHA-1 or SHA-256 format. Adding a repository requires configuration, - not source changes. +- Arbitrary repository paths with isolated WAL identities and an explicit + SHA-1 or SHA-256 format. Authorized API creation requires no per-repository + configuration or restart; named access policies remain optional overrides. - Per-repository reader, writer, and administrator permissions; read-only mode; a persisted default branch; and recovery without a local repository cache. - Automatic maintenance that prunes unreachable Git objects and invokes bounded @@ -100,9 +100,12 @@ in #39. The declarative repository map supplies canonical nested paths, stable WAL identities, immutable object formats, default branches and independent read/write/admin groups. Duplicate identities, aliases and malformed paths are -rejected. Unknown repositories fail closed. Reads and administration open -existing WALs; an authorized first-push discovery can materialize a configured -repository before accepting a pack. +rejected. A wildcard policy admits authenticated API creation of new names; +missing repositories remain absent on reads, administration and wildcard push. +Explicitly provisioned names also support authorized first-push discovery. +The format and default branch are published through the existing repository WAL. +Scheduled maintenance currently requires named entries; dynamic discovery is +separate work. Remote tests added a nested SHA-256 repository through configuration while preserving the component artifact. Read discovery returned 404 with no durable diff --git a/HANDOFF.md b/HANDOFF.md index 87079e2..3ed5f06 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -58,9 +58,12 @@ The Git service supports unchanged SHA-1 and SHA-256 clients, protocol-v2 clone and have-aware fetch, shallow history, classic push, branches, tags, access control, cold recovery, automatic tail checkpoints, and explicit maintenance. Refs and its sparse object catalog publish atomically through one WAL commit. -Local MinIO tests pass with unmodified go-git. Repository paths, formats, default -branches and per-action groups now come from configuration. Only an authorized -writer can materialize a configured repository; reads open existing state. +Local MinIO tests pass with unmodified go-git. Repository creation is available through an authenticated API without per-name +configuration. A wildcard access policy supplies defaults; exact entries replace +it. Canonical names derive isolated WAL identities, and the creation publication +persists the format and default branch. Reads and wildcard pushes only open +existing Git roots; explicitly configured names also retain first-push provisioning. +The host maintenance worker still visits only named configuration entries. Cognito access-token validation and a separate administration-only machine client are wired. Password mode is the explicit local default and requires a password. Public EC2 HTTPS qualification has passed real Cognito browser login, diff --git a/examples/git/README.md b/examples/git/README.md index f3aac85..f80d8da 100644 --- a/examples/git/README.md +++ b/examples/git/README.md @@ -141,7 +141,7 @@ All values are Spin variables. The defaults target the local MinIO setup above. | `wal_collection_candidates` | `1000` | Maximum entries in a new deletion plan, capped by the durable graph limit | | `wal_max_collection_objects` | `100000` | Complete publication graph and distinct collection live-object bound; shared paths count separately for publication | | `wal_recover_retentions_after_drain` | `false` | Exclusive lost-retention recovery mode | -| `git_repositories` | two demo entries | JSON map of paths, WAL identities, formats, default branches and permissions | +| `git_repositories` | default policy and two demo entries | JSON access policies; optional fixed identities, formats and default branches | | `git_auth_mode` | `password` | `password`, `cognito`, or explicit local `anonymous` mode | | `git_password` | empty | Required password for local password mode | | `git_cognito_issuer` | empty | HTTPS Cognito user-pool issuer | @@ -186,17 +186,43 @@ configured limit high enough to read them; lowering it blocks those objects. ## Repositories and permissions -Repository names come from configuration, including nested names. For example: +Create a repository through the API, then use ordinary Git. Names can be nested, +and the `.git` suffix is optional; both URLs select the same repository. + +```sh +curl --fail-with-body -u git:local-git-password \ + -H 'Content-Type: application/json' \ + -d '{"format":"sha1","default_branch":"main"}' \ + http://127.0.0.1:19100/team/project/create +git push http://127.0.0.1:19100/team/project HEAD:main +``` + +Creation requires write permission and publishes the format and default branch +through the repository's WAL. Defaults are SHA-1 and `main`; choose SHA-256 for a +SHA-256 client repository. A successful creation returns 201; an existing +repository returns 409. A pending publication returns 503: check the repository +before retrying. Reads and ordinary pushes to a missing name do not create it. +The two explicitly configured demo repositories also support first-push setup. + +One `"*"` policy grants access to new names without a configuration edit or +restart. For Cognito, configure the groups once: ```toml -git_repositories = '{"team/project.git":{"log_id":"team-project","format":"sha1","default_branch":"main","read_groups":["developers"],"write_groups":["developers"],"admin_groups":["operators"]}}' +git_repositories = '{"*":{"read_groups":["developers"],"write_groups":["developers"],"admin_groups":["operators"]},"team/private":{"read_groups":["private-team"],"write_groups":["private-team"]}}' ``` -Each name has its own WAL identity. Duplicate identities, ambiguous names and -invalid formats are rejected. Add another entry and reload Spin to provision a -repository without rebuilding. Keep each identity stable. The first authorized -push discovery persists its format and default branch; subsequent configuration -cannot reinterpret that stored format or change its default branch. +An exact entry replaces the whole default policy; omitted groups deny access. +An omitted `log_id` derives an isolated identity from the canonical name, so +adding a permission override preserves the existing repository. Explicit IDs +must be unique and cannot use the reserved `auto-` prefix. An optional `format` +pins that policy to SHA-1 or SHA-256; otherwise readers recover the stored format. +Creation settings cannot change permissions or reinterpret an existing repository. +The local shared-password mode grants the password holder access to all names; +anonymous mode allows public creation and writes. + +The optional host maintenance worker currently visits named configuration entries. +Repositories created under `"*"` still checkpoint their log during pushes and expose +the same maintenance endpoints, but need an operator to schedule collection by name. Cognito mode checks signed access tokens, issuer, client, expiry and scope before opening storage. Git supplies the access token as its Basic password through an diff --git a/examples/git/codec.go b/examples/git/codec.go index d5e0b32..b5c8011 100644 --- a/examples/git/codec.go +++ b/examples/git/codec.go @@ -25,16 +25,20 @@ func decodeRoot(data []byte, format config.ObjectFormat, children int) (rootMeta if err := decodeMetadata(data, &meta); err != nil { return meta, err } - if !meta.Validated || meta.Format != format || len(meta.Buckets) != children { + validFormat := meta.Format == config.SHA1 || meta.Format == config.SHA256 + if !meta.Validated || !validFormat || len(meta.Buckets) != children { return meta, fmt.Errorf("invalid repository root") } + if format != "" && meta.Format != format { + return meta, fmt.Errorf("%w: repository format differs from stored state", config.ErrInvalidObjectFormat) + } if meta.Head != "" { head := plumbing.ReferenceName(meta.Head) if !head.IsBranch() || head.Validate() != nil { return meta, fmt.Errorf("invalid repository head") } } - if err := validateRefs(format, meta.Refs); err != nil { + if err := validateRefs(meta.Format, meta.Refs); err != nil { return meta, err } for i, prefix := range meta.Buckets { diff --git a/examples/git/codec_test.go b/examples/git/codec_test.go index f811f69..bced052 100644 --- a/examples/git/codec_test.go +++ b/examples/git/codec_test.go @@ -21,6 +21,31 @@ type closeProbe struct { closeErr error } +func TestRootInfersOnlyAuthenticatedSupportedFormat(t *testing.T) { + for _, format := range []config.ObjectFormat{config.SHA1, config.SHA256} { + data, err := json.Marshal(rootMeta{Validated: true, Format: format, Head: "refs/heads/main", Refs: map[string]string{ + "refs/heads/main": strings.Repeat("1", format.HexSize()), + }}) + if err != nil { + t.Fatal(err) + } + root, err := decodeRoot(data, "", 0) + if err != nil || root.Format != format { + t.Fatalf("stored format not recovered: %v", err) + } + other := config.SHA1 + if format == other { + other = config.SHA256 + } + if _, err := decodeRoot(data, other, 0); !errors.Is(err, config.ErrInvalidObjectFormat) { + t.Fatal("configured format mismatch accepted") + } + } + if _, err := decodeRoot([]byte(`{"Validated":true,"Format":"sha512","Refs":{}}`), "", 0); err == nil { + t.Fatal("unsupported stored format accepted") + } +} + func (r *closeProbe) Close() error { r.closed = true; return r.closeErr } func TestLooseRoundTripAndValidation(t *testing.T) { diff --git a/examples/git/main.go b/examples/git/main.go index 2be44cc..a4b8740 100644 --- a/examples/git/main.go +++ b/examples/git/main.go @@ -133,6 +133,7 @@ func serve(response http.ResponseWriter, r *http.Request) { return } service, method := route.Service, route.Method + creating := service == "create" maintenance := service == "maintenance" collect := service == "collect" pruneInvalidRefs := service == "prune-invalid-refs" @@ -150,7 +151,7 @@ func serve(response http.ResponseWriter, r *http.Request) { http.Error(response, message, status) return } - if limits.readOnly && (service == transport.ReceivePackService || maintenance || collect || pruneInvalidRefs) { + if limits.readOnly && (route.Action == gitWrite || maintenance || collect || pruneInvalidRefs) { http.Error(response, "repository is read-only", http.StatusForbidden) return } @@ -164,15 +165,26 @@ func serve(response http.ResponseWriter, r *http.Request) { http.Error(response, err.Error(), operationStatus(err)) return } + if creating { + route.Repository, e = repositoryCreation(r.Body, route.Repository) + if e != nil { + status := operationStatus(e) + if status == http.StatusInternalServerError { + status = http.StatusBadRequest + } + http.Error(response, e.Error(), status) + return + } + } + settings, e := walSettings(getConfig, route.Repository.LogID, limits) if e != nil { http.Error(response, e.Error(), http.StatusInternalServerError) return } session, e := unwrap(func() wt.Result[*wal.Session, wal.Failure] { - // First-push discovery requires an empty repository advertisement. Only - // a configured repository and an authorized writer may create its head. - if route.Action == gitWrite { + // Only creation and explicitly provisioned writer requests may open a log. + if creating || route.Action == gitWrite && route.Configured { return wal.Open(settings) } return wal.OpenExisting(settings) @@ -274,7 +286,11 @@ func serve(response http.ResponseWriter, r *http.Request) { s, e := retryOpenStore(open, refresh) if e != nil { log.Printf("git request setup failed stage=open-store: %v", e) - http.Error(w, "Git storage unavailable", operationStatus(e)) + status := operationStatus(e) + if creating && errors.Is(e, config.ErrInvalidObjectFormat) { + status = http.StatusConflict + } + http.Error(w, "Git storage unavailable", status) return } defer func() { @@ -282,8 +298,33 @@ func serve(response http.ResponseWriter, r *http.Request) { s.Close() } }() + if creating { + if s.stateRoot != nil { + http.Error(w, "repository already exists", http.StatusConflict) + return + } + if err := s.publish(s.meta.Refs); err != nil { + var pending *pendingError + if errors.As(err, &pending) { + http.Error(w, "repository creation pending; check the repository before retrying", http.StatusServiceUnavailable) + return + } + status := operationStatus(err) + if errors.Is(err, errPublicationConflict) { + status = http.StatusConflict + } + http.Error(w, "repository creation was not confirmed", status) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(struct { + Repository string `json:"repository"` + }{route.Name}) + return + } if s.stateRoot == nil { - if route.Action != gitWrite { + if route.Action != gitWrite || !route.Configured { http.NotFound(w, r) return } diff --git a/examples/git/qualification/aws/hosting.tf b/examples/git/qualification/aws/hosting.tf index f94f928..bbde410 100644 --- a/examples/git/qualification/aws/hosting.tf +++ b/examples/git/qualification/aws/hosting.tf @@ -49,10 +49,10 @@ variable "host_git_max_object_bytes" { } variable "host_repositories" { - description = "Explicit repository provisioning and independent group permissions." + description = "Repository policies; '*' supplies defaults for API-created repositories." type = map(object({ - log_id = string - format = string + log_id = optional(string, "") + format = optional(string, "") default_branch = optional(string, "main") read_groups = optional(list(string), []) write_groups = optional(list(string), []) @@ -60,8 +60,8 @@ variable "host_repositories" { })) default = {} validation { - condition = alltrue([for repo in var.host_repositories : contains(["sha1", "sha256"], repo.format)]) - error_message = "Repository format must be sha1 or sha256." + condition = alltrue([for repo in var.host_repositories : contains(["", "sha1", "sha256"], repo.format)]) + error_message = "Repository format must be empty, sha1 or sha256." } } @@ -183,7 +183,7 @@ locals { token_url = "${local.cognito_login_origin}/oauth2/token" service_url = "http://127.0.0.1:8081" validation_url = "http://127.0.0.1:3000/_validate_backend" - repositories = sort(keys(var.host_repositories)) + repositories = sort([for name in keys(var.host_repositories) : name if name != "*"]) budget_seconds = var.host_maintenance_budget_seconds pause_seconds = var.host_maintenance_pause_seconds })) @@ -192,7 +192,7 @@ locals { retry_after = max(1, length(var.host_repositories) * var.host_maintenance_pause_seconds) admin_paths = jsonencode(flatten([for name in sort(keys(var.host_repositories)) : [ for operation in ["maintenance", "collect", "prune-invalid-refs", "recover-retentions-after-drain"] : "/${name}/${operation}" - ]])) + ] if name != "*"])) }) : "" } diff --git a/examples/git/repositories.go b/examples/git/repositories.go index aa4a317..80c5042 100644 --- a/examples/git/repositories.go +++ b/examples/git/repositories.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "crypto/sha256" "encoding/json" "errors" "fmt" @@ -35,8 +36,7 @@ type repositoryConfig struct { repositoryAccess } -// GIT_REPOSITORIES is an object keyed by canonical names such as team/project.git. -// Missing configuration provides no repositories. There are no implicit aliases. +// Exact policies override "*". Bare names and their .git URLs identify one log. func loadRepositories(getenv func(string) string) (map[string]repositoryConfig, error) { text := getenv("GIT_REPOSITORIES") if len(text) > repositoriesConfigBytes || !utf8.ValidString(text) { @@ -52,17 +52,32 @@ func loadRepositories(getenv func(string) string) (map[string]repositoryConfig, } identities := map[string]string{} for name, encoded := range entries { - if !validRepositoryName(name) { + if name != "*" && !validRepositoryName(name) { return nil, fmt.Errorf("GIT_REPOSITORIES: noncanonical repository name %q", name) } + if name != "*" { + name = canonicalRepositoryName(name) + } + if _, exists := repositories[name]; exists { + return nil, fmt.Errorf("duplicate repository name %q", name) + } repository, err := parseRepository(encoded) if err != nil { return nil, fmt.Errorf("GIT_REPOSITORIES: repository %q: %w", name, err) } - if other, exists := identities[repository.LogID]; exists { - return nil, fmt.Errorf("repositories %q and %q share log_id %q", other, name, repository.LogID) + if name == "*" { + if repository.LogID != "" { + return nil, errors.New("wildcard policy must not specify log_id") + } + } else { + if repository.LogID == "" { + repository.LogID = automaticRepositoryID(name) + } + if other, exists := identities[repository.LogID]; exists { + return nil, fmt.Errorf("repositories %q and %q share log_id %q", other, name, repository.LogID) + } + identities[repository.LogID] = name } - identities[repository.LogID] = name repositories[name] = repository } return repositories, nil @@ -88,10 +103,11 @@ func parseRepository(encoded []byte) (repositoryConfig, error) { return repository, err } validID := repositoryLogID.MatchString(repository.LogID) - if !validID || repository.LogID == "." || repository.LogID == ".." { + if repository.LogID != "" && (!validID || repository.LogID == "." || repository.LogID == ".." || + strings.HasPrefix(repository.LogID, "auto-")) { return repository, errors.New("log_id must satisfy the WAL log identifier contract") } - if repository.Format != config.SHA1 && repository.Format != config.SHA256 { + if repository.Format != "" && repository.Format != config.SHA1 && repository.Format != config.SHA256 { return repository, config.ErrInvalidObjectFormat } if repository.DefaultBranch != "" { @@ -109,6 +125,43 @@ func parseRepository(encoded []byte) (repositoryConfig, error) { return repository, nil } +func repositoryCreation(body io.Reader, policy repositoryConfig) (repositoryConfig, error) { + data, err := io.ReadAll(io.LimitReader(body, 4097)) + if err != nil { + return policy, err + } + if len(data) > 4096 { + return policy, errObjectLimit + } + fields, err := uniqueRepositoryObject(data) + if err != nil { + return policy, err + } + for name := range fields { + if name != "format" && name != "default_branch" { + return policy, fmt.Errorf("unknown creation field %q", name) + } + } + creation, err := parseRepository(data) + if err != nil { + return policy, err + } + if creation.Format == "" { + creation.Format = policy.Format + } + if creation.Format == "" { + creation.Format = config.SHA1 + } + if policy.Format != "" && policy.Format != creation.Format { + return policy, config.ErrInvalidObjectFormat + } + policy.Format = creation.Format + if creation.DefaultBranch != "" { + policy.DefaultBranch = creation.DefaultBranch + } + return policy, nil +} + // Decode only an object, rejecting duplicate keys before normal JSON decoding // could silently overwrite them. Both repository names and fields use this path. func uniqueRepositoryObject(encoded []byte) (map[string]json.RawMessage, error) { @@ -146,7 +199,7 @@ func uniqueRepositoryObject(encoded []byte) (map[string]json.RawMessage, error) } func validRepositoryName(name string) bool { - if !strings.HasSuffix(name, ".git") || strings.Contains(name, `\`) { + if len(name) == 0 || len(name) > 4096 || name == "*" || strings.Contains(name, `\`) { return false } path := &url.URL{Path: name} @@ -162,9 +215,21 @@ func validRepositoryName(name string) bool { return segments[len(segments)-1] != ".git" } +func canonicalRepositoryName(name string) string { + if !strings.HasSuffix(name, ".git") { + return name + ".git" + } + return name +} + +func automaticRepositoryID(name string) string { + return fmt.Sprintf("auto-%x", sha256.Sum256([]byte(name))) +} + type repositoryRoute struct { Name string Repository repositoryConfig + Configured bool Service string Method string Action gitAction @@ -183,7 +248,7 @@ func resolveRepository(repositories map[string]repositoryConfig, r *http.Request } for _, service := range []string{ "info/refs", transport.UploadPackService, transport.ReceivePackService, - "maintenance", "collect", "prune-invalid-refs", "recover-retentions-after-drain", + "create", "maintenance", "collect", "prune-invalid-refs", "recover-retentions-after-drain", } { suffix := "/" + service if strings.HasSuffix(r.URL.Path, suffix) { @@ -192,8 +257,17 @@ func resolveRepository(repositories map[string]repositoryConfig, r *http.Request break } } + if !validRepositoryName(route.Name) { + return repositoryRoute{}, errRepositoryNotFound + } + route.Name = canonicalRepositoryName(route.Name) repository, ok := repositories[route.Name] - if !ok || !validRepositoryName(route.Name) { + route.Configured = ok + if !ok { + repository, ok = repositories["*"] + repository.LogID = automaticRepositoryID(route.Name) + } + if !ok { return repositoryRoute{}, errRepositoryNotFound } route.Repository = repository @@ -215,7 +289,7 @@ func resolveRepository(repositories map[string]repositoryConfig, r *http.Request switch route.Service { case transport.UploadPackService: route.Action = gitRead - case transport.ReceivePackService: + case transport.ReceivePackService, "create": route.Action = gitWrite default: route.Action = gitAdmin diff --git a/examples/git/repositories_test.go b/examples/git/repositories_test.go index ca1279c..eb61e53 100644 --- a/examples/git/repositories_test.go +++ b/examples/git/repositories_test.go @@ -78,8 +78,6 @@ func TestLoadRepositoriesRejectsInvalidConfiguration(t *testing.T) { "a.git":{"log_id":"same","format":"sha1"}, "b.git":{"log_id":"same","format":"sha256"} }`}, - {name: "missing identity", text: `{"r.git":{"format":"sha1"}}`}, - {name: "missing format", text: `{"r.git":{"log_id":"one"}}`}, {name: "invalid format", text: `{"r.git":{"log_id":"one","format":"SHA1"}}`}, {name: "null format", text: `{"r.git":{"log_id":"one","format":null}}`}, {name: "path identity", text: `{"r.git":{"log_id":"a/b","format":"sha1"}}`}, @@ -109,7 +107,7 @@ func TestLoadRepositoriesRejectsInvalidConfiguration(t *testing.T) { func TestLoadRepositoriesRejectsNoncanonicalNames(t *testing.T) { t.Parallel() for _, name := range []string{ - "", "r", ".git", "team/.git", "/r.git", "r.git/", "team//r.git", "./r.git", + "", ".git", "team/.git", "/r.git", "r.git/", "team//r.git", "./r.git", "team/./r.git", "team/../r.git", "../r.git", "r%2egit", `team\r.git`, "r?.git", "r#.git", "r space.git", "résumé.git", } { @@ -182,6 +180,7 @@ func TestResolveRepositorySelectsServiceAndAction(t *testing.T) { service: transport.ReceivePackService, action: gitWrite}, {name: "read RPC", method: http.MethodPost, path: "git-upload-pack", service: transport.UploadPackService, action: gitRead}, + {name: "creation", method: http.MethodPost, path: "create", service: "create", action: gitWrite}, {name: "write RPC", method: http.MethodPost, path: "git-receive-pack", service: transport.ReceivePackService, action: gitWrite}, {name: "logical maintenance", method: http.MethodPost, path: "maintenance", service: "maintenance", action: gitAdmin}, @@ -285,3 +284,85 @@ func TestRepositoryRoutePolicyIsIndependentPerRepositoryAndAction(t *testing.T) }) } } + +func TestAutomaticRepositoryPolicyAndIdentity(t *testing.T) { + repositories, err := repositoriesFromText(`{"*":{"write_groups":["writers"]},"team/private":{}}`) + if err != nil { + t.Fatal(err) + } + var first repositoryRoute + for _, name := range []string{"team/project", "team/project.git"} { + route, err := resolveRepository(repositories, httptest.NewRequest(http.MethodPost, "/"+name+"/create", nil)) + if err != nil { + t.Fatal(err) + } + if route.Configured || route.Name != "team/project.git" || route.Action != gitWrite || route.Repository.Format != "" { + t.Fatalf("unexpected automatic route: %+v", route) + } + if first.Name != "" && first.Repository.LogID != route.Repository.LogID { + t.Fatal("URL aliases select different logs") + } + first = route + } + principal := gitPrincipal{subject: "writer", groups: []string{"writers"}} + if !principal.Allows(first.Repository.repositoryAccess, gitWrite) || principal.Allows(first.Repository.repositoryAccess, gitRead) { + t.Fatal("automatic policy lost independent permissions") + } + private, err := resolveRepository(repositories, httptest.NewRequest(http.MethodPost, "/team/private.git/create", nil)) + if err != nil { + t.Fatal(err) + } + if principal.Allows(private.Repository.repositoryAccess, gitWrite) || private.Repository.LogID == first.Repository.LogID { + t.Fatal("exact denial or repository isolation lost") + } + for _, name := range []string{"*", "team/../project", "team//project", "%70roject"} { + if _, err := resolveRepository(repositories, httptest.NewRequest(http.MethodPost, "/"+name+"/create", nil)); err == nil { + t.Fatalf("wildcard accepted invalid name %q", name) + } + } + // Adding a permission override must preserve the automatically created log. + override, err := repositoriesFromText(`{"team/project":{"read_groups":["readers"]}}`) + if err != nil || override["team/project.git"].LogID != first.Repository.LogID { + t.Fatal("permission override moved storage") + } + for _, text := range []string{ + `{"*":{"log_id":"shared"}}`, + `{"project":{},"project.git":{}}`, + `{"project":{"log_id":"auto-forbidden"}}`, + } { + if _, err := repositoriesFromText(text); err == nil { + t.Fatalf("accepted ambiguous policy: %s", text) + } + } +} + +func TestRepositoryCreationSettings(t *testing.T) { + policy := repositoryConfig{LogID: "isolated", DefaultBranch: "release", repositoryAccess: repositoryAccess{WriteGroups: []string{"writers"}}} + for _, format := range []config.ObjectFormat{config.SHA1, config.SHA256} { + created, err := repositoryCreation(strings.NewReader(`{"format":"`+string(format)+`","default_branch":"main"}`), policy) + if err != nil { + t.Fatal(err) + } + if created.Format != format || created.LogID != policy.LogID || created.DefaultBranch != "main" || + len(created.WriteGroups) != 1 || created.WriteGroups[0] != "writers" { + t.Fatalf("creation changed policy: %+v", created) + } + } + created, err := repositoryCreation(strings.NewReader(`{}`), policy) + if err != nil || created.Format != config.SHA1 || created.DefaultBranch != "release" { + t.Fatal("creation defaults lost") + } + for _, text := range []string{ + `{"format":"sha512"}`, `{"format":null}`, `{"format":"sha1","format":"sha256"}`, + `{"default_branch":"../main"}`, `{"write_groups":["everyone"]}`, `{"log_id":"other"}`, + `{} {}`, strings.Repeat(" ", 4097), + } { + if _, err := repositoryCreation(strings.NewReader(text), policy); err == nil { + t.Fatalf("invalid creation accepted: %s", text) + } + } + policy.Format = config.SHA256 + if _, err := repositoryCreation(strings.NewReader(`{"format":"sha1"}`), policy); err == nil { + t.Fatal("creation bypassed configured format") + } +} diff --git a/examples/git/spin.toml b/examples/git/spin.toml index 49a978c..581d4fa 100644 --- a/examples/git/spin.toml +++ b/examples/git/spin.toml @@ -15,7 +15,7 @@ wal_secret_key = { default = "", secret = true } wal_session_token = { default = "", secret = true } wal_max_collection_objects = { default = "100000" } wal_recover_retentions_after_drain = { default = "false" } -git_repositories = { default = '{"sha1.git":{"log_id":"repo-sha1","format":"sha1"},"sha256.git":{"log_id":"repo-sha256","format":"sha256"}}' } +git_repositories = { default = '{"*":{},"sha1.git":{"log_id":"repo-sha1","format":"sha1"},"sha256.git":{"log_id":"repo-sha256","format":"sha256"}}' } git_auth_mode = { default = "password" } git_cognito_issuer = { default = "" } git_cognito_client_id = { default = "" } diff --git a/examples/git/store.go b/examples/git/store.go index cfedc56..e1b33ed 100644 --- a/examples/git/store.go +++ b/examples/git/store.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/plumbing/format/config" "github.com/go-git/go-git/v6/plumbing/format/objfile" "github.com/go-git/go-git/v6/plumbing/storer" "github.com/go-git/go-git/v6/storage" @@ -46,12 +47,7 @@ func openStore(ctx context.Context, session *wal.Session, repository repositoryC return nil, err } format := repository.Format - mem := memory.NewStorage(memory.WithObjectFormat(format)) - // Memory storage returns its owned configuration; no save is needed. - cfg, _ := mem.Config() - // Reuse stored deltas without comparing object contents to generate new ones. - cfg.Pack.Window = 1 - s := &store{ctx: ctx, limits: limits, Storer: mem, session: session, buckets: map[string]*wal.Object{}, loaded: map[*wal.Object]radixNode[indexed, *wal.Object]{}, pending: map[string]indexed{}} + s := &store{ctx: ctx, limits: limits, session: session, buckets: map[string]*wal.Object{}, loaded: map[*wal.Object]radixNode[indexed, *wal.Object]{}, pending: map[string]indexed{}} defer func() { if result == nil { s.Close() @@ -78,10 +74,17 @@ func openStore(ctx context.Context, session *wal.Session, repository repositoryC for i, key := range s.meta.Buckets { s.buckets[key] = root.Objects[i] } - for name, id := range s.meta.Refs { - if e := s.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(name), plumbing.NewHash(id))); e != nil { - return nil, e - } + } + if s.meta.Format == "" { + s.meta.Format = config.SHA1 + } + s.Storer = memory.NewStorage(memory.WithObjectFormat(s.meta.Format)) + cfg, _ := s.Storer.Config() + // Reuse stored deltas without comparing object contents to generate new ones. + cfg.Pack.Window = 1 + for name, id := range s.meta.Refs { + if e := s.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(name), plumbing.NewHash(id))); e != nil { + return nil, e } } // An empty repository has no unchecked objects. Existing roots must certify validation. diff --git a/examples/git/tests/creation_test.go b/examples/git/tests/creation_test.go new file mode 100644 index 0000000..60a8b78 --- /dev/null +++ b/examples/git/tests/creation_test.go @@ -0,0 +1,131 @@ +package tests + +import ( + "bytes" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestRepositoryCreation(t *testing.T) { + endpoint := os.Getenv("GIT_PROBE_URL") + if endpoint == "" || os.Getenv("GIT_PROBE_CREATE") != "1" { + t.Skip("set GIT_PROBE_URL and GIT_PROBE_CREATE=1 for a local service with wildcard policy") + } + request := func(method, url, password, body string, want int) { + t.Helper() + r, err := http.NewRequest(method, url, strings.NewReader(body)) + if err != nil { + t.Fatal(err) + } + r.SetBasicAuth("git", password) + r.Header.Set("Content-Type", "application/json") + response, err := http.DefaultClient.Do(r) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + data, err := io.ReadAll(response.Body) + if err != nil || response.StatusCode != want { + t.Fatalf("%s: HTTP %d, want %d: %s (%v)", url, response.StatusCode, want, data, err) + } + } + for _, format := range []string{"sha1", "sha256"} { + t.Run(format, func(t *testing.T) { + name := fmt.Sprintf("created/%s-%d", format, time.Now().UnixNano()) + url := strings.TrimRight(endpoint, "/") + "/" + name + password := os.Getenv("GIT_PROBE_PASSWORD") + settings := `{"format":"` + format + `","default_branch":"release"}` + request(http.MethodPost, url+"/create", "wrong-password", settings, 401) + request(http.MethodGet, url+"/info/refs?service=git-upload-pack", password, "", 404) + request(http.MethodGet, url+"/info/refs?service=git-receive-pack", password, "", 404) + request(http.MethodPost, url+"/maintenance", password, "{}", 404) + request(http.MethodPost, url+"/create", password, strings.Repeat(" ", 4097), 413) + request(http.MethodPost, url+"/create", password, `{"log_id":"override"}`, 400) + request(http.MethodPost, url+"/create", password, settings, 201) + request(http.MethodPost, url+".git/create", password, settings, 409) + other := "sha1" + if format == other { + other = "sha256" + } + request(http.MethodPost, url+"/create", password, `{"format":"`+other+`"}`, 409) + root := t.TempDir() + git(t, nil, "init", "--object-format="+format, "-b", "release", root) + write(t, filepath.Join(root, "README.md"), []byte(name)) + git(t, nil, "-C", root, "add", ".") + git(t, nil, "-C", root, "commit", "-m", "created through API") + git(t, nil, "-C", root, "push", url, "HEAD:refs/heads/release") + clone := filepath.Join(t.TempDir(), "clone") + git(t, nil, "-c", "protocol.version=2", "clone", url+".git", clone) + git(t, nil, "-C", clone, "fsck", "--full") + if got := git(t, nil, "-C", clone, "symbolic-ref", "--short", "HEAD"); strings.TrimSpace(string(got)) != "release" { + t.Fatal("created default branch was lost") + } + if got := git(t, nil, "-C", clone, "show", "HEAD:README.md"); !bytes.Equal(got, []byte(name)) { + t.Fatal("created repository data differs") + } + }) + } + t.Run("concurrent creators", func(t *testing.T) { + url := fmt.Sprintf("%s/created/race-%d", strings.TrimRight(endpoint, "/"), time.Now().UnixNano()) + type result struct { + format string + status int + err error + } + results := make(chan result, 2) + for _, format := range []string{"sha1", "sha256"} { + go func() { + r, err := http.NewRequest(http.MethodPost, url+"/create", strings.NewReader(`{"format":"`+format+`"}`)) + if err != nil { + results <- result{format: format, err: err} + return + } + r.SetBasicAuth("git", os.Getenv("GIT_PROBE_PASSWORD")) + response, err := http.DefaultClient.Do(r) + status := 0 + if err == nil { + status = response.StatusCode + _, _ = io.Copy(io.Discard, response.Body) + err = response.Body.Close() + } + results <- result{format: format, status: status, err: err} + }() + } + winner := "" + for range 2 { + r := <-results + if r.err != nil || (r.status != 201 && r.status != 409) { + t.Fatalf("concurrent creation: %+v", r) + } + if r.status == 201 { + if winner != "" { + t.Fatal("two creations committed") + } + winner = r.format + } + } + if winner == "" { + t.Fatal("no creation committed") + } + r, err := http.NewRequest(http.MethodGet, url+"/info/refs?service=git-receive-pack", nil) + if err != nil { + t.Fatal(err) + } + r.SetBasicAuth("git", os.Getenv("GIT_PROBE_PASSWORD")) + response, err := http.DefaultClient.Do(r) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + data, err := io.ReadAll(response.Body) + if err != nil || response.StatusCode != 200 || !bytes.Contains(data, []byte("object-format="+winner)) { + t.Fatal("stored format does not match winning creator") + } + }) +} From d1cf1a4a5533a236fa2c374f8c18c26d6b8f35a9 Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Sat, 26 Sep 2026 22:06:43 -0700 Subject: [PATCH 08/16] Support explicit repository creation in the TypeScript viewer Resolve canonical repository names and wildcard policies against deterministic WAL identities. Forward creation to the Git service, expose the action in the viewer, and recover the stored Git format when no policy pins it. Co-authored-by: Codex --- examples/git/viewer/README.md | 34 +++-- examples/git/viewer/src/api.ts | 92 ++++++++++++- examples/git/viewer/src/client.tsx | 92 +++++++++++-- examples/git/viewer/src/index.ts | 8 +- examples/git/viewer/src/wal.test.ts | 196 +++++++++++++++++++++++++++- examples/git/viewer/src/wal.ts | 10 +- 6 files changed, 392 insertions(+), 40 deletions(-) diff --git a/examples/git/viewer/README.md b/examples/git/viewer/README.md index 5c245d8..e5a1a9c 100644 --- a/examples/git/viewer/README.md +++ b/examples/git/viewer/README.md @@ -1,8 +1,8 @@ # Repository explorer -A small repository browser with its own TypeScript Spin API and Preact UI. -It shows branches, directories, file previews and recent commits for configured -repositories. The Go Git service and the WAL are unchanged. +A small repository browser with creation through its TypeScript Spin API and Preact UI. +It shows branches, directories, file previews and recent commits for authorized +repositories. The API and UI live in a separate component; the WAL is unchanged. The API checks access through ordinary Git discovery, then reads the repository's existing WAL catalog directly. Reads follow only the selected history and paths; @@ -21,19 +21,23 @@ bun start ``` This builds both components, starts disposable local MinIO and Spin, and prints -the Git URLs. Push a repository, then open -`http://127.0.0.1:19100/browse?repo=sha1.git` with password `local-git-password`. -For example, from an existing SHA-1 repository in another terminal: +the Git URL. Open `http://127.0.0.1:19100/browse?repo=team/project.git`, sign in +with password `local-git-password`, choose SHA-1 or SHA-256 and click +**Create repository**. Then push an existing repository of that format: ```sh -git push http://127.0.0.1:19100/sha1.git HEAD:main +git push http://127.0.0.1:19100/team/project.git HEAD:main ``` When Git prompts, use username `git` and password `local-git-password`. Press Ctrl-C in the startup terminal to stop both services and remove their disposable data. Each restart begins with empty storage. -Other configured paths, such as `team/project.git`, work too. Cognito access +The default `"*"` policy allows authorized creation of any repository through +the API or UI, without per-repository TOML or a restart. Creation records its +Git format; reads never create repositories. Bare names such as `team/project` identify the same +repository as `team/project.git`. Exact configuration entries override the +whole wildcard policy and may pin a WAL identity or Git format. Cognito access tokens use the Git service's existing read permissions. Credentials stay in page memory and clear on reload. Branch and path selections stay in the URL. @@ -42,8 +46,18 @@ For an existing backend, use `bun run build`, then follow the `-f spin.viewer.toml` to its `spin up` command. Hosted proxies must forward `/browse`, `/browse/api`, `/browse/assets/client.js` -and `/browse/assets/style.css` to Spin. The API is `GET /browse/api` with a -configured `repo` and optional `ref` and `path` query parameters. +and `/browse/assets/style.css` to Spin. `GET /browse/api` takes `repo` and +optional `ref` and `path` query parameters. `POST /browse/api?repo=` +forwards creation to the Git service, using its write permission: + +```sh +curl --fail-with-body -u git:local-git-password \ + -H 'Content-Type: application/json' -d '{"format":"sha1"}' \ + 'http://127.0.0.1:19100/browse/api?repo=team/project.git' +``` + +Creation defaults to SHA-1 and `main`; `default_branch` is optional. A successful +creation returns 201; creating an existing repository returns 409. ## Build and checks diff --git a/examples/git/viewer/src/api.ts b/examples/git/viewer/src/api.ts index 5b827d9..8c7b490 100644 --- a/examples/git/viewer/src/api.ts +++ b/examples/git/viewer/src/api.ts @@ -1,5 +1,32 @@ import { NotFound, snapshot } from "./repository"; -import { Catalog, drop, openExisting, type Repository, settings, variable } from "./wal"; +import { + automaticLogId, + Catalog, + canonicalName, + drop, + openExisting, + type Repository, + settings, + variable, +} from "./wal"; + +async function boundedBody(message: Request | Response, limit: number): Promise { + const reader = message.body?.getReader(), + decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + let bytes = 0, + text = ""; + try { + for (let part = await reader?.read(); part && !part.done; part = await reader?.read()) { + bytes += part.value.length; + if (bytes > limit) throw new Error("HTTP body exceeds limit"); + text += decoder.decode(part.value, { stream: true }); + } + return text + decoder.decode(); + } finally { + await reader?.cancel(); + reader?.releaseLock(); + } +} export async function browse(request: Request): Promise { const query = new URL(request.url).searchParams; @@ -18,10 +45,63 @@ export async function browse(request: Request): Promise { .some((part) => !part || part === "." || part === ".." || part.includes("\0")))) ) return new Response("Invalid browse query", { status: 400 }); - const name = query.get("repo") ?? ""; - const repositories: Record = JSON.parse(variable("git_repositories")); - if (!Object.hasOwn(repositories, name)) + const input = query.get("repo") ?? "", + name = canonicalName(input); + if ( + !input || + input === "*" || + name === ".git" || + name.length > 4096 || + /[\s%?#\\]/.test(input) || + input.split("/").some((part) => !part || part === "." || part === "..") || + new URL(`/${name}`, request.url).pathname !== `/${name}` + ) return new Response("Repository not found", { status: 404 }); + const repositories: Record> = JSON.parse( + variable("git_repositories"), + ); + const policy = + Object.entries(repositories).find(([key]) => key !== "*" && canonicalName(key) === name)?.[1] ?? + repositories["*"]; + if (!policy) return new Response("Repository not found", { status: 404 }); + const repository = { ...policy, log_id: policy.log_id || automaticLogId(name) }; + if (request.method === "POST") { + if (query.has("ref") || query.has("path")) + return new Response("Invalid create query", { status: 400 }); + let body: string; + try { + body = await boundedBody(request, 4096); + } catch (error) { + return new Response("Invalid creation request", { + status: error instanceof Error && error.message === "HTTP body exceeds limit" ? 413 : 400, + }); + } + const created = await fetch(`http://git.spin.internal/${name}/create`, { + method: "POST", + headers: { + Authorization: request.headers.get("Authorization") ?? "", + "Content-Type": "application/json", + }, + redirect: "manual", + body, + }); + body = await boundedBody(created, 65536); + if ( + created.status === 201 && + !created.headers.get("Content-Type")?.startsWith("application/json") + ) + return new Response("Invalid creation response", { status: 502 }); + const status = [201, 400, 401, 403, 404, 408, 409, 413, 503].includes(created.status) + ? created.status + : 502; + return new Response(status === 502 ? "Repository creation unavailable" : body, { + status, + headers: { + "Content-Type": status === 201 ? "application/json" : "text/plain; charset=utf-8", + "Cache-Control": "no-store", + }, + }); + } // Standard Git discovery enforces the unchanged service's repository permissions. const authorized = await fetch( `http://git.spin.internal/${name}/info/refs?service=git-upload-pack`, @@ -52,13 +132,13 @@ export async function browse(request: Request): Promise { ) return new Response("Invalid Git discovery response", { status: 502 }); // Successful discovery has validated this same declaratively configured backend. - let session = openExisting(settings(repositories[name])); + let session = openExisting(settings(repository)); const budget = { bytes: 0 }; try { for (let attempt = 0; ; attempt++) { let catalog: Catalog | undefined; try { - catalog = new Catalog(session, repositories[name].format, budget); + catalog = new Catalog(session, repository.format, budget); const view = snapshot(catalog, query), usage = session.usage(); return Response.json(view, { diff --git a/examples/git/viewer/src/client.tsx b/examples/git/viewer/src/client.tsx index a1c0ea8..e1261f9 100644 --- a/examples/git/viewer/src/client.tsx +++ b/examples/git/viewer/src/client.tsx @@ -3,9 +3,19 @@ import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; import type { Commit, Snapshot } from "./repository"; +const messages: Record = { + 400: "Invalid repository, branch or path.", + 401: "Authentication required.", + 403: "This credential cannot read this repository.", + 404: "Repository or path not found.", + 409: "This repository already exists. Open it to browse.", +}; + function validRepository(name: string): boolean { return ( - name.endsWith(".git") && + name !== "" && + name !== "*" && + name !== ".git" && !/[\s%?#\\]/.test(name) && name.split("/").every((part) => part !== "" && part !== "." && part !== "..") && new URL(`/${name}`, location.origin).pathname === `/${name}` @@ -116,7 +126,8 @@ function Code({ function App() { const [search, setSearch] = useState(location.search); const [authorization, setAuthorization] = useState({ header: "" }); - const [result, setResult] = useState<{ view?: Snapshot; error?: string }>({}); + const [result, setResult] = useState<{ view?: Snapshot; error?: string; status?: number }>({}); + const [created, setCreated] = useState(""); const [tab, setTab] = useState("code"); const [copied, setCopied] = useState(false); const cloneInput = useRef(null); @@ -147,13 +158,12 @@ function App() { signal: request.signal, }); if (!response.ok) { - const messages: Record = { - 400: "Invalid branch or path.", - 401: "Authentication required.", - 403: "This credential does not have read access.", - 404: "Repository or path not found. Push a branch before browsing.", - }; - throw new Error(messages[response.status] ?? "Repository unavailable. Try again."); + if (!request.signal.aborted) + setResult({ + error: messages[response.status] ?? "Repository unavailable. Try again.", + status: response.status, + }); + return; } const snapshot: Snapshot = await response.json(); if (!request.signal.aborted) setResult({ view: snapshot }); @@ -194,10 +204,12 @@ function App() { Repository explorer
-

- {repository.replace(/\.git$/, "") || "Repository"} - Read only -

+

{repository.replace(/\.git$/, "") || "Repository"}

+ {created && created === repository && ( +

+ Repository created. Push to {cloneURL}. +

+ )} {!valid ? (

{result.error ?? "Loading repository…"}

+ {created !== repository && + (result.status === 403 || result.status === 404) && + !query.get("ref") && + !query.get("path") && ( + { + event.preventDefault(); + const format = String(new FormData(event.currentTarget).get("format")); + setResult({}); + try { + const response = await fetch( + `/browse/api?${new URLSearchParams({ repo: repository })}`, + { + method: "POST", + headers: { + Authorization: authorization.header, + "Content-Type": "application/json", + }, + body: JSON.stringify({ format }), + credentials: "omit", + cache: "no-store", + }, + ); + if (response.ok || response.status === 409) { + if (response.status === 201) setCreated(repository); + navigate(new URLSearchParams({ repo: repository })); + setAuthorization({ header: authorization.header }); + } else + setResult({ + error: + (await response.text()) || + messages[response.status] || + "Repository unavailable. Try again.", + status: response.status, + }); + } catch { + setResult({ error: "Repository unavailable. Try again." }); + } + }} + > + + +

+ Creates an empty repository at {repository}; default branch main. +

+
+ )} {result.error && (
{ + const path = new URL(request.url).pathname; + if (path === "/browse/api") { + if (request.method !== "GET" && request.method !== "POST") + return new Response("Method not allowed", { status: 405, headers: { Allow: "GET, POST" } }); + return browse(request); + } if (request.method !== "GET" && request.method !== "HEAD") { return new Response("Method not allowed", { status: 405, headers: { Allow: "GET, HEAD" }, }); } - const path = new URL(request.url).pathname; - if (path === "/browse/api" && request.method === "GET") return browse(request); if ( path !== "/browse" && path !== "/browse/assets/client.js" && diff --git a/examples/git/viewer/src/wal.test.ts b/examples/git/viewer/src/wal.test.ts index 9b11933..23e943e 100644 --- a/examples/git/viewer/src/wal.test.ts +++ b/examples/git/viewer/src/wal.test.ts @@ -1,20 +1,21 @@ import { afterEach, expect, mock, test } from "bun:test"; -import type { Session } from "object-log:storage/wal@0.1.0"; +import type { Config, Session } from "object-log:storage/wal@0.1.0"; import { zlibSync } from "fflate"; let opened = 0; -let openSession: () => Session = () => { +const defaultPolicies = '{"team/demo.git":{"log_id":"demo","format":"sha256"}}'; +let policies = defaultPolicies; +let openSession: (config: Config) => Session = () => { throw new Error("Unexpected storage access"); }; mock.module("object-log:storage/wal@0.1.0", () => ({ - openExisting: () => { + openExisting: (config: Config) => { opened++; - return openSession(); + return openSession(config); }, })); mock.module("@spinframework/spin-variables", () => ({ - get: (name: string) => - name === "git_repositories" ? '{"team/demo.git":{"log_id":"demo","format":"sha256"}}' : "", + get: (name: string) => (name === "git_repositories" ? policies : ""), })); const { Catalog } = await import("./wal"); const { browse } = await import("./api"); @@ -22,11 +23,194 @@ const { snapshot } = await import("./repository"); const originalFetch = globalThis.fetch; afterEach(() => { globalThis.fetch = originalFetch; + policies = defaultPolicies; openSession = () => { throw new Error("Unexpected storage access"); }; }); +function emptySession(format: string): Session { + const dispose = () => undefined; + let next = true; + return { + recover: () => ({ + next: () => { + if (!next) return undefined; + next = false; + return { tag: "checkpoint", val: { objects: [{ [Symbol.dispose]: dispose }] } }; + }, + readNode: () => ({ + data: new TextEncoder().encode( + JSON.stringify({ + Validated: true, + Format: format, + Head: "refs/heads/main", + Refs: {}, + Buckets: [], + }), + ), + objects: [], + }), + [Symbol.dispose]: dispose, + }), + usage: () => ({ calls: 0n, bytes: 0n }), + [Symbol.dispose]: dispose, + } as unknown as Session; +} + +const discovery = () => + new Response("refs", { + headers: { "Content-Type": "application/x-git-upload-pack-advertisement" }, + }); + +test("wildcard repositories recover their stored format and canonical WAL identity", async () => { + policies = '{"*":{}}'; + for (const format of ["sha1", "sha256"]) { + policies = format === "sha1" ? '{"*":{}}' : '{"*":{},"team/project":{"log_id":"","format":""}}'; + for (const name of ["team/project", "team/project.git", "team/project.GIT"]) { + const canonical = name === "team/project.GIT" ? "team/project.GIT.git" : "team/project.git"; + let authorized = false; + globalThis.fetch = mock(async (url: RequestInfo | URL) => { + expect(String(url)).toBe( + `http://git.spin.internal/${canonical}/info/refs?service=git-upload-pack`, + ); + authorized = true; + return discovery(); + }) as unknown as typeof fetch; + openSession = (config) => { + expect(authorized).toBe(true); + expect(config.logId).toBe( + `auto-${new Bun.CryptoHasher("sha256").update(canonical).digest("hex")}`, + ); + return emptySession(format); + }; + expect( + (await browse(new Request(`https://viewer.test/browse/api?repo=${name}`))).status, + ).toBe(200); + } + } +}); + +test("exact aliases override the whole wildcard policy and enforce pinned formats", async () => { + globalThis.fetch = mock(async () => discovery()) as unknown as typeof fetch; + for (const key of ["team/project", "team/project.git"]) { + policies = JSON.stringify({ + "*": { format: "sha256" }, + [key]: { log_id: "custom", format: "" }, + }); + openSession = (config) => { + expect(config.logId).toBe("custom"); + return emptySession("sha1"); + }; + for (const name of ["team/project", "team/project.git"]) + expect( + (await browse(new Request(`https://viewer.test/browse/api?repo=${name}`))).status, + ).toBe(200); + } + policies = '{"*":{"format":"sha256"}}'; + openSession = () => emptySession("sha1"); + await expect( + browse(new Request("https://viewer.test/browse/api?repo=team/project")), + ).rejects.toThrow("Invalid repository catalog"); + policies = '{"*":{}}'; + openSession = () => emptySession("md5"); + await expect( + browse(new Request("https://viewer.test/browse/api?repo=team/project")), + ).rejects.toThrow("Invalid repository catalog"); +}); + +test("creation forwards raw JSON and credentials and preserves outcomes without WAL access", async () => { + policies = '{"*":{}}'; + const baseline = opened; + for (const status of [201, 400, 401, 403, 408, 409, 413, 503]) { + const body = + status === 400 + ? '{"format":"sha1","format":"sha256"}' + : ' { "format": "sha256", "default_branch": "main" }\n'; + const responseBody = + status === 503 ? "Publication pending; recover before retrying" : "Creation denied"; + globalThis.fetch = mock(async (url: RequestInfo | URL, options?: RequestInit) => { + expect(String(url)).toBe("http://git.spin.internal/team/project.git/create"); + expect(options?.method).toBe("POST"); + expect(options?.headers).toEqual({ + Authorization: "Bearer write-token", + "Content-Type": "application/json", + }); + expect(options?.redirect).toBe("manual"); + expect(options?.body).toBe(body); + return status === 201 + ? Response.json({ created: true }, { status }) + : new Response(responseBody, { status }); + }) as unknown as typeof fetch; + const response = await browse( + new Request("https://viewer.test/browse/api?repo=team/project", { + method: "POST", + headers: { Authorization: "Bearer write-token" }, + body, + }), + ); + expect(response.status).toBe(status); + if (status === 201) expect(await response.json()).toEqual({ created: true }); + else expect(await response.text()).toBe(responseBody); + expect(opened).toBe(baseline); + } +}); + +test("creation bounds HTTP bodies and rejects invalid names before forwarding", async () => { + policies = '{"*":{}}'; + const forwarded = mock(async () => { + throw new Error("Unexpected HTTP call"); + }); + globalThis.fetch = forwarded as unknown as typeof fetch; + for (const [body, status] of [ + ["x".repeat(4097), 413], + [Uint8Array.of(255), 400], + ] as const) + expect( + ( + await browse( + new Request("https://viewer.test/browse/api?repo=team/project", { method: "POST", body }), + ) + ).status, + ).toBe(status); + for (const name of ["*", "", "../project", "team//project", "project%3Fother"]) + expect( + ( + await browse( + new Request(`https://viewer.test/browse/api?repo=${name}`, { + method: "POST", + body: "{}", + }), + ) + ).status, + ).toBe(404); + expect(forwarded).not.toHaveBeenCalled(); + globalThis.fetch = mock( + async () => new Response("unexpected response", { status: 201 }), + ) as unknown as typeof fetch; + expect( + ( + await browse( + new Request("https://viewer.test/browse/api?repo=team/project", { + method: "POST", + body: "{}", + }), + ) + ).status, + ).toBe(502); + globalThis.fetch = mock(async () => + Response.json({ data: "x".repeat(65536) }, { status: 201 }), + ) as unknown as typeof fetch; + await expect( + browse( + new Request("https://viewer.test/browse/api?repo=team/project", { + method: "POST", + body: "{}", + }), + ), + ).rejects.toThrow("HTTP body exceeds limit"); +}); + function fixture( format: "sha1" | "sha256", raw: Uint8Array, diff --git a/examples/git/viewer/src/wal.ts b/examples/git/viewer/src/wal.ts index eb8274d..2cfaa65 100644 --- a/examples/git/viewer/src/wal.ts +++ b/examples/git/viewer/src/wal.ts @@ -20,8 +20,11 @@ export const hex = (bytes: Uint8Array) => export interface Repository { log_id: string; - format: "sha1" | "sha256"; + format?: "sha1" | "sha256" | ""; } +export const canonicalName = (name: string) => (name.endsWith(".git") ? name : `${name}.git`); +export const automaticLogId = (name: string) => + `auto-${hex(sha256(new TextEncoder().encode(name)))}`; interface Item { ID: string; Kind: number; @@ -83,7 +86,7 @@ export class Catalog { constructor( session: Session, - format: string, + format: string | undefined, private budget: { bytes: number }, ) { this.recovery = session.recover(); @@ -99,7 +102,8 @@ export class Catalog { this.root = json(node.data); if ( !this.root.Validated || - this.root.Format !== format || + !["sha1", "sha256"].includes(this.root.Format) || + (format && this.root.Format !== format) || this.root.Buckets.length !== node.objects.length ) throw new Error("Invalid repository catalog"); From 696661f850f82de104beadb9851a7e5ff47faff0 Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Sat, 26 Sep 2026 22:21:30 -0700 Subject: [PATCH 09/16] Count the canonical repository suffix in the name limit Co-authored-by: Codex --- examples/git/repositories.go | 2 +- examples/git/repositories_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/git/repositories.go b/examples/git/repositories.go index 80c5042..631d376 100644 --- a/examples/git/repositories.go +++ b/examples/git/repositories.go @@ -199,7 +199,7 @@ func uniqueRepositoryObject(encoded []byte) (map[string]json.RawMessage, error) } func validRepositoryName(name string) bool { - if len(name) == 0 || len(name) > 4096 || name == "*" || strings.Contains(name, `\`) { + if len(name) == 0 || len(canonicalRepositoryName(name)) > 4096 || name == "*" || strings.Contains(name, `\`) { return false } path := &url.URL{Path: name} diff --git a/examples/git/repositories_test.go b/examples/git/repositories_test.go index eb61e53..5748d78 100644 --- a/examples/git/repositories_test.go +++ b/examples/git/repositories_test.go @@ -286,6 +286,9 @@ func TestRepositoryRoutePolicyIsIndependentPerRepositoryAndAction(t *testing.T) } func TestAutomaticRepositoryPolicyAndIdentity(t *testing.T) { + if !validRepositoryName(strings.Repeat("a", 4092)) || validRepositoryName(strings.Repeat("a", 4093)) { + t.Fatal("name limit must include the optional .git suffix") + } repositories, err := repositoriesFromText(`{"*":{"write_groups":["writers"]},"team/private":{}}`) if err != nil { t.Fatal(err) From 65329adf0e36c91825072aca4b3a1645955bc496 Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Sun, 27 Sep 2026 15:42:52 -0700 Subject: [PATCH 10/16] Use explicit API creation for Git repositories Remove configured first-push initialization and preserve the one-command local demo by creating its repositories through the API. Update provider coverage and documentation. Co-authored-by: Codex --- GIT_PLAN.md | 2 +- HANDOFF.md | 2 +- docs/blog/02-running-git-on-the-wal.md | 7 ++--- examples/git/README.md | 5 ++-- examples/git/main.go | 26 +++---------------- examples/git/qualification/aws/HOSTING.md | 14 +++++----- examples/git/qualification/aws/maintenance.py | 2 +- examples/git/repositories.go | 2 -- examples/git/repositories_test.go | 2 +- .../git/tests/repositories_provider_test.go | 15 ++++++++++- scripts/run-git-local.sh | 9 ++++++- 11 files changed, 43 insertions(+), 43 deletions(-) diff --git a/GIT_PLAN.md b/GIT_PLAN.md index dd30cd2..098c17f 100644 --- a/GIT_PLAN.md +++ b/GIT_PLAN.md @@ -104,7 +104,7 @@ identities, immutable object formats, default branches and independent read/write/admin groups. Duplicate identities, aliases and malformed paths are rejected. A wildcard policy admits authenticated API creation of new names; missing repositories remain absent on reads, administration and wildcard push. -Explicitly provisioned names also support authorized first-push discovery. +All names require authorized API creation before the first Git push. The format and default branch are published through the existing repository WAL. Scheduled maintenance currently requires named entries; dynamic discovery is separate work. diff --git a/HANDOFF.md b/HANDOFF.md index a441d80..e8e32cf 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -62,7 +62,7 @@ Local MinIO tests pass with unmodified go-git. Repository creation is available configuration. A wildcard access policy supplies defaults; exact entries replace it. Canonical names derive isolated WAL identities, and the creation publication persists the format and default branch. Reads and wildcard pushes only open -existing Git roots; explicitly configured names also retain first-push provisioning. +existing Git roots; creation is the only path that initializes a repository. The host maintenance worker still visits only named configuration entries. Cognito access-token validation and a separate administration-only machine client are wired. Password mode is the explicit local default and requires a diff --git a/docs/blog/02-running-git-on-the-wal.md b/docs/blog/02-running-git-on-the-wal.md index e7ea037..879a34e 100644 --- a/docs/blog/02-running-git-on-the-wal.md +++ b/docs/blog/02-running-git-on-the-wal.md @@ -39,9 +39,10 @@ of both Spin and Git, and can also run natively. Configuration maps repository paths to stable WAL identities, hash formats, default branches, and permissions. Each repository has its own storage -namespace. An authorized writer's first push discovery creates its durable -state; reads only open existing repositories. Adding a repository changes -configuration, not the component binary. +namespace. An authorized writer creates durable state through the repository +API before the first push; reads only open existing repositories. A wildcard +access policy can admit new repository names without changing the component +binary or configuration. ## From a push to durable refs diff --git a/examples/git/README.md b/examples/git/README.md index 404d64f..cda8505 100644 --- a/examples/git/README.md +++ b/examples/git/README.md @@ -56,8 +56,8 @@ http://127.0.0.1:19100/sha1.git http://127.0.0.1:19100/sha256.git ``` -Initialize a repository with its first push. Enter `git` and `local-git-password` -when Git prompts. Reads never create repositories: +The local launcher creates the two demo repositories through the API. Push an +initial commit, entering `git` and `local-git-password` when Git prompts: ```sh git init --object-format=sha256 -b main demo @@ -162,7 +162,6 @@ through the repository's WAL. Defaults are SHA-1 and `main`; choose SHA-256 for SHA-256 client repository. A successful creation returns 201; an existing repository returns 409. A pending publication returns 503: check the repository before retrying. Reads and ordinary pushes to a missing name do not create it. -The two explicitly configured demo repositories also support first-push setup. One `"*"` policy grants access to new names without a configuration edit or restart. For Cognito, configure the groups once: diff --git a/examples/git/main.go b/examples/git/main.go index a4b8740..391a8ec 100644 --- a/examples/git/main.go +++ b/examples/git/main.go @@ -183,8 +183,7 @@ func serve(response http.ResponseWriter, r *http.Request) { return } session, e := unwrap(func() wt.Result[*wal.Session, wal.Failure] { - // Only creation and explicitly provisioned writer requests may open a log. - if creating || route.Action == gitWrite && route.Configured { + if creating { return wal.Open(settings) } return wal.OpenExisting(settings) @@ -324,27 +323,8 @@ func serve(response http.ResponseWriter, r *http.Request) { return } if s.stateRoot == nil { - if route.Action != gitWrite || !route.Configured { - http.NotFound(w, r) - return - } - // Persist the format and default branch before the first advertisement. - // A competing creator must reload the winning root before proceeding. - if err := s.publish(s.meta.Refs); err != nil && !errors.Is(err, errPublicationConflict) { - http.Error(w, "repository initialization failed", operationStatus(err)) - return - } - s.Close() - if e = refresh(); e == nil { - s, e = retryOpenStore(open, refresh) - } - if e == nil && s.stateRoot == nil { - e = errPublicationConflict - } - if e != nil { - http.Error(w, "repository initialization failed", operationStatus(e)) - return - } + http.NotFound(w, r) + return } if service == transport.ReceivePackService && method == http.MethodPost { e = retryBeforePush(func() (bool, error) { return s.beforePush() }, func() error { diff --git a/examples/git/qualification/aws/HOSTING.md b/examples/git/qualification/aws/HOSTING.md index 2bcffd1..d1bcd93 100644 --- a/examples/git/qualification/aws/HOSTING.md +++ b/examples/git/qualification/aws/HOSTING.md @@ -30,9 +30,11 @@ use that larger limit only on a host with measured headroom. `host_repositories` provisions canonical names, unique stable WAL IDs, SHA-1 or SHA-256 formats, default branches, and independent read/write/admin groups. Declare all referenced groups in `host_groups`. Users and membership are managed -in Cognito outside Terraform; self-registration is disabled. Adding a repository -requires configuration, not rebuilding. An authorized first-push discovery -materializes it. Do not change a stored repository's format or identity to migrate it. +in Cognito outside Terraform; self-registration is disabled. Named policies +require a configuration change to admit new paths; a wildcard policy admits +new names without one. An authorized writer creates a repository with +`POST //create`. Do not change a stored repository's format or +identity to migrate it. The artifact is staged under this run's `artifacts/` prefix and SHA-256 checked at boot. Data uses the separate `git/` prefix. The EC2 role can read that artifact @@ -132,9 +134,9 @@ git config --global --add credential.https://git.example.com.helper oauth git clone https://git.example.com/team/project.git ``` -A fresh configured repository returns 404 to reads until a writer's first push -materializes it. Push from a local repository with the configured object format -and initial branch before trying to clone it. +A fresh configured repository returns 404 until an authorized writer calls +`POST //create` with its format and initial branch. Then push from a +local repository with that object format before trying to clone it. The helper uses authorization-code S256 PKCE and sends the access token as an ordinary Basic password. Its fixed `http://localhost:53119` callback matches the diff --git a/examples/git/qualification/aws/maintenance.py b/examples/git/qualification/aws/maintenance.py index 598f266..c8a1324 100644 --- a/examples/git/qualification/aws/maintenance.py +++ b/examples/git/qualification/aws/maintenance.py @@ -82,7 +82,7 @@ def post(self, repository, operation, deadline): url, data=b"", headers={"Authorization": f"Bearer {token}"}, ), deadline)["state"] except urllib.error.HTTPError as error: - # Configured repositories are materialized by their first writer. + # A configured repository may not yet have been created. if error.code == 404: error.close() return "not materialized" diff --git a/examples/git/repositories.go b/examples/git/repositories.go index 631d376..a052bd2 100644 --- a/examples/git/repositories.go +++ b/examples/git/repositories.go @@ -229,7 +229,6 @@ func automaticRepositoryID(name string) string { type repositoryRoute struct { Name string Repository repositoryConfig - Configured bool Service string Method string Action gitAction @@ -262,7 +261,6 @@ func resolveRepository(repositories map[string]repositoryConfig, r *http.Request } route.Name = canonicalRepositoryName(route.Name) repository, ok := repositories[route.Name] - route.Configured = ok if !ok { repository, ok = repositories["*"] repository.LogID = automaticRepositoryID(route.Name) diff --git a/examples/git/repositories_test.go b/examples/git/repositories_test.go index 5748d78..9a4778e 100644 --- a/examples/git/repositories_test.go +++ b/examples/git/repositories_test.go @@ -299,7 +299,7 @@ func TestAutomaticRepositoryPolicyAndIdentity(t *testing.T) { if err != nil { t.Fatal(err) } - if route.Configured || route.Name != "team/project.git" || route.Action != gitWrite || route.Repository.Format != "" { + if route.Name != "team/project.git" || route.Action != gitWrite || route.Repository.Format != "" { t.Fatalf("unexpected automatic route: %+v", route) } if first.Name != "" && first.Repository.LogID != route.Repository.LogID { diff --git a/examples/git/tests/repositories_provider_test.go b/examples/git/tests/repositories_provider_test.go index d5a16bc..6ec3f3d 100644 --- a/examples/git/tests/repositories_provider_test.go +++ b/examples/git/tests/repositories_provider_test.go @@ -37,7 +37,20 @@ func TestRepositoryIsolation(t *testing.T) { repositoryStatus(t, read, os.Getenv("GIT_PROBE_PASSWORD"), http.StatusNotFound) repositoryStatus(t, write, "invalid-token", http.StatusUnauthorized) repositoryStatus(t, read, os.Getenv("GIT_PROBE_PASSWORD"), http.StatusNotFound) - repositoryStatus(t, write, os.Getenv("GIT_PROBE_PASSWORD"), http.StatusOK) + repositoryStatus(t, write, os.Getenv("GIT_PROBE_PASSWORD"), http.StatusNotFound) + request, err := http.NewRequest(http.MethodPost, repo.url+"/create", strings.NewReader(`{"format":"`+repo.format+`"}`)) + if err != nil { + t.Fatal(err) + } + request.SetBasicAuth("git", os.Getenv("GIT_PROBE_PASSWORD")) + response, err := http.DefaultClient.Do(request) + if err != nil { + t.Fatal(err) + } + response.Body.Close() + if response.StatusCode != http.StatusCreated { + t.Fatalf("%s: creation returned HTTP %d", repo.name, response.StatusCode) + } repositoryStatus(t, read, os.Getenv("GIT_PROBE_PASSWORD"), http.StatusOK) } for _, path := range []string{ diff --git a/scripts/run-git-local.sh b/scripts/run-git-local.sh index 0639b39..67bc845 100755 --- a/scripts/run-git-local.sh +++ b/scripts/run-git-local.sh @@ -135,13 +135,20 @@ if [[ "$validated" != 1 ]]; then exit 1 fi +for format in sha1 sha256; do + curl --fail-with-body --silent --show-error --max-time 30 \ + --user git:local-git-password -H 'Content-Type: application/json' \ + --data "{\"format\":\"$format\"}" \ + "http://127.0.0.1:$git_port/$format.git/create" >/dev/null +done + cat < README.md From db5b289ff9f60c1f80e3d50518532e7769da71c7 Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Sun, 27 Sep 2026 15:43:02 -0700 Subject: [PATCH 11/16] Remove unused Git object storage implementations Keep explicit unsupported interface methods so go-git cannot fall back to its embedded memory store outside the WAL. Smart HTTP pack import uses RawObjectWriter. Co-authored-by: Codex --- examples/git/store.go | 36 ++++-------------------------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/examples/git/store.go b/examples/git/store.go index e1b33ed..1be5736 100644 --- a/examples/git/store.go +++ b/examples/git/store.go @@ -169,21 +169,8 @@ func (s *store) EncodedObjectSize(id plumbing.Hash) (int64, error) { v, e := s.lookup(id) return v.Size, e } -func (s *store) IterEncodedObjects(kind plumbing.ObjectType) (storer.EncodedObjectIter, error) { - items := map[string]indexed{} - for prefix, root := range s.buckets { - if e := walkRadix(prefix, root, s.loadBucket, func(id string, item indexed) { items[id] = item }); e != nil { - return nil, e - } - } - maps.Copy(items, s.pending) - result := make([]plumbing.EncodedObject, 0, len(items)) - for _, item := range items { - if kind == plumbing.AnyObject || item.Kind == kind { - result = append(result, &storedObject{s, item}) - } - } - return storer.NewEncodedObjectSliceIter(result), nil +func (*store) IterEncodedObjects(plumbing.ObjectType) (storer.EncodedObjectIter, error) { + return nil, errors.ErrUnsupported } func (s *store) RawObjectWriter(kind plumbing.ObjectType, size int64) (io.WriteCloser, error) { if err := s.ctx.Err(); err != nil { @@ -201,23 +188,8 @@ func (s *store) RawObjectWriter(kind plumbing.ObjectType, size int64) (io.WriteC } return &objectWriter{s: s, kind: kind, size: size, codec: codec, sink: sink}, nil } -func (s *store) SetEncodedObject(o plumbing.EncodedObject) (plumbing.Hash, error) { - r, e := o.Reader() - if e != nil { - return plumbing.ZeroHash, e - } - defer r.Close() - w, e := s.RawObjectWriter(o.Type(), o.Size()) - if e != nil { - return plumbing.ZeroHash, e - } - if _, e = io.Copy(w, r); e != nil { - return plumbing.ZeroHash, e - } - if e = w.Close(); e != nil { - return plumbing.ZeroHash, e - } - return w.(*objectWriter).codec.Hash(), nil +func (*store) SetEncodedObject(plumbing.EncodedObject) (plumbing.Hash, error) { + return plumbing.ZeroHash, errors.ErrUnsupported } type objectWriter struct { From 8e072d54a929307c0763b36add530777d5041283 Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Sun, 27 Sep 2026 16:10:51 -0700 Subject: [PATCH 12/16] Remove compatibility paths for discarded Git state Delete old-ref repair and missing-HEAD substitution. Apply the current ref-name rules to stored catalogs and repository creation, and require persisted branch HEAD in the viewer. Keep recovery for interrupted current-format operations. Co-authored-by: Codex --- AGENTS.md | 2 + examples/git/README.md | 14 ----- examples/git/codec.go | 16 +++--- examples/git/codec_test.go | 22 +++++--- examples/git/main.go | 37 +----------- examples/git/qualification/aws/hosting.tf | 2 +- .../git/qualification/aws/test_admission.py | 6 +- examples/git/repositories.go | 10 +++- examples/git/repositories_test.go | 4 +- examples/git/store.go | 21 ++----- examples/git/tests/access_test.go | 2 - examples/git/validate.go | 2 +- examples/git/validate_refname.go | 29 ---------- examples/git/validate_refname_test.go | 56 ------------------- examples/git/viewer/src/wal.test.ts | 15 ++++- examples/git/viewer/src/wal.ts | 2 + 16 files changed, 61 insertions(+), 179 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c7fca30..eb82189 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,8 @@ - Do not add garbage collection until checkpoint safety and uncertain-result resolution have tests. - Do not use OpenSpec. +- Do not add compatibility readers, migrations, or repair paths for discarded + revisions. Reject unsupported durable state and use a fresh storage namespace. ## Design diff --git a/examples/git/README.md b/examples/git/README.md index cda8505..e680f60 100644 --- a/examples/git/README.md +++ b/examples/git/README.md @@ -290,20 +290,6 @@ without reading opaque leaf payloads. Ordinary reads and publication still verify those bytes. Continuous readers can delay cleanup; scheduling alone does not remove that constraint. -Repositories published before the current ref-name rule may contain refs that -Git can advertise but no longer accepts for updates or deletion. An operator -can explicitly remove only those invalid names with `POST -//prune-invalid-refs` using administrator credentials. The response -lists removed names after the new ref map is committed through the WAL head. -An empty list with no `default_head` means no change. If the stored default -HEAD is invalid, ensure another valid branch exists, then supply -its full name in `X-Git-Replacement-Head` on this call; the new HEAD and any -removed refs publish together. Without that header, the call refuses to -change an invalid default HEAD. -On a publication conflict, retry; on an uncertain result, inspect the refs -and repeat the request. This action does not run on the maintenance timer. Run -ordinary maintenance afterward to reclaim objects no longer reachable from a ref. - Every fetch acquires WAL retention before opening catalog data and releases it after the last response byte. If a stopped instance loses a retention ID, stop new traffic and drain all readers. Start one authenticated instance with diff --git a/examples/git/codec.go b/examples/git/codec.go index b5c8011..5b3009b 100644 --- a/examples/git/codec.go +++ b/examples/git/codec.go @@ -5,9 +5,11 @@ import ( "encoding/json" "errors" "fmt" + "github.com/go-git/go-billy/v6/memfs" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/format/config" "github.com/go-git/go-git/v6/plumbing/format/objfile" + "github.com/go-git/go-git/v6/storage/filesystem/dotgit" "io" "strings" ) @@ -32,13 +34,11 @@ func decodeRoot(data []byte, format config.ObjectFormat, children int) (rootMeta if format != "" && meta.Format != format { return meta, fmt.Errorf("%w: repository format differs from stored state", config.ErrInvalidObjectFormat) } - if meta.Head != "" { - head := plumbing.ReferenceName(meta.Head) - if !head.IsBranch() || head.Validate() != nil { - return meta, fmt.Errorf("invalid repository head") - } + refNames := dotgit.New(memfs.New()) + if head := plumbing.ReferenceName(meta.Head); !head.IsBranch() || validateRefName(refNames, head) != nil { + return meta, fmt.Errorf("invalid repository head") } - if err := validateRefs(meta.Format, meta.Refs); err != nil { + if err := validateRefs(refNames, meta.Format, meta.Refs); err != nil { return meta, err } for i, prefix := range meta.Buckets { @@ -61,10 +61,10 @@ func decodeMetadata(data []byte, value any) error { return nil } -func validateRefs(format config.ObjectFormat, refs map[string]string) error { +func validateRefs(refNames *dotgit.DotGit, format config.ObjectFormat, refs map[string]string) error { for name, id := range refs { ref := plumbing.ReferenceName(name) - if !strings.HasPrefix(name, "refs/") || ref.Validate() != nil || !validID(format, id) { + if validateRefName(refNames, ref) != nil || !validID(format, id) { return fmt.Errorf("invalid repository reference") } for parent := name; ; { diff --git a/examples/git/codec_test.go b/examples/git/codec_test.go index bced052..c6d16a0 100644 --- a/examples/git/codec_test.go +++ b/examples/git/codec_test.go @@ -237,7 +237,7 @@ func TestInlineMetadataBound(t *testing.T) { func TestRepositoryRootAdmission(t *testing.T) { for _, format := range []config.ObjectFormat{config.SHA1, config.SHA256} { t.Run(format.String(), func(t *testing.T) { - empty := rootMeta{Validated: true, Format: format, Refs: map[string]string{}, Buckets: []string{}} + empty := rootMeta{Validated: true, Format: format, Head: "refs/heads/main", Refs: map[string]string{}, Buckets: []string{}} data, err := json.Marshal(empty) if err != nil { t.Fatal(err) @@ -248,13 +248,16 @@ func TestRepositoryRootAdmission(t *testing.T) { id := strings.Repeat("a", format.HexSize()) valid := rootMeta{Validated: true, Format: format, Head: "refs/heads/main", Refs: map[string]string{"refs/heads/main": id}, Buckets: []string{"0a", "ab"}} invalid := map[string]rootMeta{ - "head": {Validated: true, Format: format, Head: "refs/tags/main", Refs: valid.Refs, Buckets: valid.Buckets}, - "reference": {Validated: true, Format: format, Head: valid.Head, Refs: map[string]string{"HEAD": id}, Buckets: valid.Buckets}, - "hash": {Validated: true, Format: format, Head: valid.Head, Refs: map[string]string{"refs/heads/main": strings.ToUpper(id)}, Buckets: valid.Buckets}, - "zero hash": {Validated: true, Format: format, Head: valid.Head, Refs: map[string]string{"refs/heads/main": strings.Repeat("0", format.HexSize())}, Buckets: valid.Buckets}, - "ref collision": {Validated: true, Format: format, Head: valid.Head, Refs: map[string]string{"refs/heads/main": id, "refs/heads/main/nested": id}, Buckets: valid.Buckets}, - "bucket order": {Validated: true, Format: format, Head: valid.Head, Refs: valid.Refs, Buckets: []string{"ab", "0a"}}, - "bucket encoding": {Validated: true, Format: format, Head: valid.Head, Refs: valid.Refs, Buckets: []string{"AZ"}}, + "empty head": {Validated: true, Format: format, Refs: valid.Refs, Buckets: valid.Buckets}, + "head": {Validated: true, Format: format, Head: "refs/tags/main", Refs: valid.Refs, Buckets: valid.Buckets}, + "unsafe head": {Validated: true, Format: format, Head: "refs/heads/\u200c./review-probe", Refs: valid.Refs, Buckets: valid.Buckets}, + "reference": {Validated: true, Format: format, Head: valid.Head, Refs: map[string]string{"HEAD": id}, Buckets: valid.Buckets}, + "unsafe reference": {Validated: true, Format: format, Head: valid.Head, Refs: map[string]string{"refs/heads/\u200c./review-probe": id}, Buckets: valid.Buckets}, + "hash": {Validated: true, Format: format, Head: valid.Head, Refs: map[string]string{"refs/heads/main": strings.ToUpper(id)}, Buckets: valid.Buckets}, + "zero hash": {Validated: true, Format: format, Head: valid.Head, Refs: map[string]string{"refs/heads/main": strings.Repeat("0", format.HexSize())}, Buckets: valid.Buckets}, + "ref collision": {Validated: true, Format: format, Head: valid.Head, Refs: map[string]string{"refs/heads/main": id, "refs/heads/main/nested": id}, Buckets: valid.Buckets}, + "bucket order": {Validated: true, Format: format, Head: valid.Head, Refs: valid.Refs, Buckets: []string{"ab", "0a"}}, + "bucket encoding": {Validated: true, Format: format, Head: valid.Head, Refs: valid.Refs, Buckets: []string{"AZ"}}, } data, err = json.Marshal(valid) if err != nil { @@ -264,6 +267,7 @@ func TestRepositoryRootAdmission(t *testing.T) { t.Fatalf("rejected valid root: %v", err) } for name, malformed := range map[string][]byte{ + "missing head": []byte(strings.Replace(string(data), `"Head":"refs/heads/main",`, "", 1)), "unknown field": []byte(strings.Replace(string(data), `"Buckets":`, `"Unknown":true,"Buckets":`, 1)), "trailing value": append(append([]byte(nil), data...), []byte(`{}`)...), } { @@ -294,7 +298,7 @@ func TestRepositoryRootAdmission(t *testing.T) { {"validated", true, true}, } { t.Run(test.name, func(t *testing.T) { - root := map[string]any{"Format": format, "Buckets": []string{"ab"}} + root := map[string]any{"Format": format, "Head": "refs/heads/main", "Buckets": []string{"ab"}} if test.marker != nil { root["Validated"] = test.marker } diff --git a/examples/git/main.go b/examples/git/main.go index 391a8ec..fd7af6b 100644 --- a/examples/git/main.go +++ b/examples/git/main.go @@ -136,7 +136,6 @@ func serve(response http.ResponseWriter, r *http.Request) { creating := service == "create" maintenance := service == "maintenance" collect := service == "collect" - pruneInvalidRefs := service == "prune-invalid-refs" recoverRetentions := service == "recover-retentions-after-drain" limits, e := loadLimits(getConfig) if e != nil { @@ -151,7 +150,7 @@ func serve(response http.ResponseWriter, r *http.Request) { http.Error(response, message, status) return } - if limits.readOnly && (route.Action == gitWrite || maintenance || collect || pruneInvalidRefs) { + if limits.readOnly && (route.Action == gitWrite || maintenance || collect) { http.Error(response, "repository is read-only", http.StatusForbidden) return } @@ -349,40 +348,6 @@ func serve(response http.ResponseWriter, r *http.Request) { report, err := s.maintain() writeMaintenance(w, report, err) return - } else if pruneInvalidRefs { - if len(r.Header.Values("X-Git-Replacement-Head")) > 1 { - http.Error(w, "multiple replacement HEAD values", http.StatusBadRequest) - return - } - clean, removed, err := withoutInvalidRefs(s.meta, r.Header.Get("X-Git-Replacement-Head")) - if err != nil { - http.Error(w, err.Error(), http.StatusConflict) - return - } - newHead := "" - if clean.Head != s.meta.Head { - newHead = clean.Head - } - if len(removed) != 0 || newHead != "" { - s.meta.Head = clean.Head - if err := s.publish(clean.Refs); err != nil { - status := operationStatus(err) - var pending *pendingError - if errors.Is(err, errPublicationConflict) { - status = http.StatusConflict - } else if errors.As(err, &pending) { - status = http.StatusServiceUnavailable - } - http.Error(w, "ref cleanup was not confirmed: "+err.Error(), status) - return - } - } - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(struct { - Removed []string `json:"removed_refs"` - Head string `json:"default_head,omitempty"` - }{removed, newHead}) - return } else if service == transport.ReceivePackService && method == http.MethodGet { w.Header().Set("Content-Type", "application/x-git-receive-pack-advertisement") e = advertise(w, s) diff --git a/examples/git/qualification/aws/hosting.tf b/examples/git/qualification/aws/hosting.tf index bbde410..196f0f9 100644 --- a/examples/git/qualification/aws/hosting.tf +++ b/examples/git/qualification/aws/hosting.tf @@ -191,7 +191,7 @@ locals { run_timeout = length(var.host_repositories) * var.host_maintenance_budget_seconds + 65 retry_after = max(1, length(var.host_repositories) * var.host_maintenance_pause_seconds) admin_paths = jsonencode(flatten([for name in sort(keys(var.host_repositories)) : [ - for operation in ["maintenance", "collect", "prune-invalid-refs", "recover-retentions-after-drain"] : "/${name}/${operation}" + for operation in ["maintenance", "collect", "recover-retentions-after-drain"] : "/${name}/${operation}" ] if name != "*"])) }) : "" } diff --git a/examples/git/qualification/aws/test_admission.py b/examples/git/qualification/aws/test_admission.py index 4f17ab9..d429f0d 100644 --- a/examples/git/qualification/aws/test_admission.py +++ b/examples/git/qualification/aws/test_admission.py @@ -59,7 +59,7 @@ def log_message(self, *_): def handle_request(self): self.rfile.read(int(self.headers.get("Content-Length", 0))) Backend.calls.append((self.command, self.path)) - admin = self.path.endswith(("/maintenance", "/collect", "/prune-invalid-refs")) + admin = self.path.endswith(("/maintenance", "/collect")) status = 401 if admin and self.headers.get("Authorization") != "Bearer fixture" else 200 data = b"git" if self.path == "/_validate_backend": @@ -110,7 +110,7 @@ def setUpClass(cls): # This local HTTP fixture omits the public-IP certificate configuration. snippet = re.sub(r"%\{ if public_ip_https ~\}.*?%\{ endif ~\}\n", "", snippet, flags=re.S) paths = [f"/{name}/{operation}" for name in ("alpha/project.git", "star*project.git") - for operation in ("maintenance", "collect", "prune-invalid-refs", "recover-retentions-after-drain")] + for operation in ("maintenance", "collect", "recover-retentions-after-drain")] snippet = snippet.replace("${hostname}", "http://:8080") snippet = snippet.replace("http://127.0.0.1:8081", "http://:8081") snippet = snippet.replace("${admin_paths}", json.dumps(paths)).replace("${retry_after}", "2") @@ -201,7 +201,7 @@ def test_existing_request_survives_and_admin_paths_are_exact(self): ("POST", "/starXproject.git/maintenance")]: self.assertEqual(self.request(method, path)[0], 503, path) for path in ("/alpha/project.git/maintenance", "/alpha/project.git/collect", - "/alpha/project.git/prune-invalid-refs", "/star*project.git/maintenance"): + "/star*project.git/maintenance"): self.assertEqual(self.request("POST", path)[0], 401, path) self.assertEqual(self.request("POST", path, {"Authorization": "Bearer fixture"})[0], 200, path) Backend.release.set() diff --git a/examples/git/repositories.go b/examples/git/repositories.go index a052bd2..8798f6a 100644 --- a/examples/git/repositories.go +++ b/examples/git/repositories.go @@ -13,9 +13,11 @@ import ( "strings" "unicode/utf8" + "github.com/go-git/go-billy/v6/memfs" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/format/config" "github.com/go-git/go-git/v6/plumbing/transport" + "github.com/go-git/go-git/v6/storage/filesystem/dotgit" ) const repositoriesConfigBytes = 64 << 10 @@ -111,7 +113,11 @@ func parseRepository(encoded []byte) (repositoryConfig, error) { return repository, config.ErrInvalidObjectFormat } if repository.DefaultBranch != "" { - if err := plumbing.ValidateBranchName(repository.DefaultBranch); err != nil { + err := plumbing.ValidateBranchName(repository.DefaultBranch) + if err == nil { + err = validateRefName(dotgit.New(memfs.New()), plumbing.NewBranchReferenceName(repository.DefaultBranch)) + } + if err != nil { return repository, fmt.Errorf("default_branch: %w", err) } } @@ -247,7 +253,7 @@ func resolveRepository(repositories map[string]repositoryConfig, r *http.Request } for _, service := range []string{ "info/refs", transport.UploadPackService, transport.ReceivePackService, - "create", "maintenance", "collect", "prune-invalid-refs", "recover-retentions-after-drain", + "create", "maintenance", "collect", "recover-retentions-after-drain", } { suffix := "/" + service if strings.HasSuffix(r.URL.Path, suffix) { diff --git a/examples/git/repositories_test.go b/examples/git/repositories_test.go index 9a4778e..d66a7eb 100644 --- a/examples/git/repositories_test.go +++ b/examples/git/repositories_test.go @@ -86,6 +86,7 @@ func TestLoadRepositoriesRejectsInvalidConfiguration(t *testing.T) { {name: "invalid branch", text: `{"r.git":{"log_id":"one","format":"sha1","default_branch":"a..b"}}`}, {name: "HEAD branch", text: `{"r.git":{"log_id":"one","format":"sha1","default_branch":"HEAD"}}`}, {name: "hyphen branch", text: `{"r.git":{"log_id":"one","format":"sha1","default_branch":"-bad"}}`}, + {name: "unsafe branch", text: `{"r.git":{"log_id":"one","format":"sha1","default_branch":"\u200c./review-probe"}}`}, {name: "null branch", text: `{"r.git":{"log_id":"one","format":"sha1","default_branch":null}}`}, {name: "string groups", text: `{"r.git":{"log_id":"one","format":"sha1","read_groups":"everyone"}}`}, {name: "null groups", text: `{"r.git":{"log_id":"one","format":"sha1","read_groups":null}}`}, @@ -184,7 +185,6 @@ func TestResolveRepositorySelectsServiceAndAction(t *testing.T) { {name: "write RPC", method: http.MethodPost, path: "git-receive-pack", service: transport.ReceivePackService, action: gitWrite}, {name: "logical maintenance", method: http.MethodPost, path: "maintenance", service: "maintenance", action: gitAdmin}, - {name: "legacy ref cleanup", method: http.MethodPost, path: "prune-invalid-refs", service: "prune-invalid-refs", action: gitAdmin}, {name: "physical collection", method: http.MethodPost, path: "collect", service: "collect", action: gitAdmin}, {name: "retention recovery", method: http.MethodPost, path: "recover-retentions-after-drain", service: "recover-retentions-after-drain", action: gitAdmin}, @@ -259,7 +259,6 @@ func TestRepositoryRoutePolicyIsIndependentPerRepositoryAndAction(t *testing.T) {name: "writer read denied", group: "writers", path: "team/alpha.git/git-upload-pack"}, {name: "writer admin denied", group: "writers", path: "team/alpha.git/maintenance"}, {name: "admin maintenance", group: "operators", path: "team/alpha.git/maintenance", want: true}, - {name: "admin ref cleanup", group: "operators", path: "team/alpha.git/prune-invalid-refs", want: true}, {name: "admin collection", group: "operators", path: "team/alpha.git/collect", want: true}, {name: "admin recovery", group: "operators", path: "team/alpha.git/recover-retentions-after-drain", want: true}, {name: "admin read denied", group: "operators", path: "team/alpha.git/git-upload-pack"}, @@ -358,6 +357,7 @@ func TestRepositoryCreationSettings(t *testing.T) { for _, text := range []string{ `{"format":"sha512"}`, `{"format":null}`, `{"format":"sha1","format":"sha256"}`, `{"default_branch":"../main"}`, `{"write_groups":["everyone"]}`, `{"log_id":"other"}`, + `{"default_branch":"\u200c./review-probe"}`, `{} {}`, strings.Repeat(" ", 4097), } { if _, err := repositoryCreation(strings.NewReader(text), policy); err == nil { diff --git a/examples/git/store.go b/examples/git/store.go index 1be5736..e21969f 100644 --- a/examples/git/store.go +++ b/examples/git/store.go @@ -9,7 +9,6 @@ import ( "io" "maps" "slices" - "strings" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/format/config" @@ -89,22 +88,14 @@ func openStore(ctx context.Context, session *wal.Session, repository repositoryC } // An empty repository has no unchecked objects. Existing roots must certify validation. s.meta.Validated = true - head := s.meta.Head - if head == "" { - branch := "main" - if configured := repository.DefaultBranch; s.stateRoot == nil && configured != "" { - branch = configured + if s.stateRoot == nil { + branch := repository.DefaultBranch + if branch == "" { + branch = "main" } - head = "refs/heads/" + branch + s.meta.Head = "refs/heads/" + branch } - if !strings.HasPrefix(head, "refs/heads/") { - return nil, fmt.Errorf("invalid default branch") - } - if e := plumbing.ReferenceName(head).Validate(); e != nil { - return nil, e - } - s.meta.Head = head - if e := s.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.ReferenceName(head))); e != nil { + if e := s.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.ReferenceName(s.meta.Head))); e != nil { return nil, e } return s, nil diff --git a/examples/git/tests/access_test.go b/examples/git/tests/access_test.go index 617f145..2fd8998 100644 --- a/examples/git/tests/access_test.go +++ b/examples/git/tests/access_test.go @@ -30,7 +30,6 @@ func TestAccess(t *testing.T) { {"POST", "/sha1.git/info/refs?service=git-upload-pack", password, 405}, {"GET", "/sha1.git/git-upload-pack", password, 405}, {"GET", "/sha1.git/maintenance", password, 405}, - {"GET", "/sha1.git/prune-invalid-refs", password, 405}, {"GET", "/sha1.git/recover-retentions-after-drain", password, 405}, {"POST", "/sha1.git/recover-retentions-after-drain", password, 403}, {"GET", "/sha1.git/info/refs?service=maintenance", password, 404}, @@ -40,7 +39,6 @@ func TestAccess(t *testing.T) { } if os.Getenv("GIT_PROBE_READ_ONLY") == "true" { cases = append(cases, request{"POST", "/sha1.git/maintenance", password, 403}) - cases = append(cases, request{"POST", "/sha1.git/prune-invalid-refs", password, 403}) } if password != "" { cases = append(cases, request{"GET", "/sha1.git/info/refs?service=git-upload-pack", "wrong", 401}) diff --git a/examples/git/validate.go b/examples/git/validate.go index 05f881c..e2914f5 100644 --- a/examples/git/validate.go +++ b/examples/git/validate.go @@ -57,7 +57,7 @@ func validate(st *store, cmds []*packp.Command) (map[string]string, error) { } refs[name] = cmd.New.String() } - if err := validateRefs(st.meta.Format, refs); err != nil { + if err := validateRefs(refNames, st.meta.Format, refs); err != nil { return nil, err } ids := make([]plumbing.Hash, 0, len(st.pending)) diff --git a/examples/git/validate_refname.go b/examples/git/validate_refname.go index ef0e15d..a1d7f39 100644 --- a/examples/git/validate_refname.go +++ b/examples/git/validate_refname.go @@ -3,10 +3,7 @@ package main import ( "errors" "fmt" - "maps" - "slices" - "github.com/go-git/go-billy/v6/memfs" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/storage/filesystem/dotgit" ) @@ -28,29 +25,3 @@ func validateRefName(refNames *dotgit.DotGit, name plumbing.ReferenceName) error } return nil } - -// Remove only refs admitted by the old catalog rules but refused by receive-pack. -// An invalid default HEAD requires an explicit, already-existing replacement. -func withoutInvalidRefs(meta rootMeta, replacement string) (rootMeta, []string, error) { - refNames := dotgit.New(memfs.New()) - clean := meta - clean.Refs = maps.Clone(meta.Refs) - removed := []string{} - headInvalid := validateRefName(refNames, plumbing.ReferenceName(meta.Head)) != nil - for name := range meta.Refs { - if validateRefName(refNames, plumbing.ReferenceName(name)) != nil { - delete(clean.Refs, name) - removed = append(removed, name) - } - } - slices.Sort(removed) - if headInvalid { - if !plumbing.ReferenceName(replacement).IsBranch() || clean.Refs[replacement] == "" { - return rootMeta{}, nil, fmt.Errorf("invalid default HEAD requires an existing valid replacement branch") - } - clean.Head = replacement - } else if replacement != "" { - return rootMeta{}, nil, fmt.Errorf("replacement HEAD is only allowed when the stored HEAD is invalid") - } - return clean, removed, nil -} diff --git a/examples/git/validate_refname_test.go b/examples/git/validate_refname_test.go index fb9d6ee..841d346 100644 --- a/examples/git/validate_refname_test.go +++ b/examples/git/validate_refname_test.go @@ -3,7 +3,6 @@ package main import ( "bytes" "context" - "encoding/json" "errors" "io" "os/exec" @@ -71,61 +70,6 @@ func TestReceiveRefNamesBeforePublication(t *testing.T) { } } -func TestPruneLegacyRefNames(t *testing.T) { - legacy := "refs/heads/\u200c./review-probe" - for _, format := range []config.ObjectFormat{config.SHA1, config.SHA256} { - t.Run(format.String(), func(t *testing.T) { - id := strings.Repeat("1", format.HexSize()) - original := rootMeta{Validated: true, Format: format, Head: "refs/heads/main", Refs: map[string]string{ - legacy: id, "refs/heads/main": id, "refs/tags/release": id, - }} - encoded, err := json.Marshal(original) - if err != nil { - t.Fatal(err) - } - loaded, err := decodeRoot(encoded, format, 0) - if err != nil { - t.Fatalf("old catalog must remain readable: %v", err) - } - clean, removed, err := withoutInvalidRefs(loaded, "") - if err != nil || len(removed) != 1 || removed[0] != legacy || len(clean.Refs) != 2 || len(loaded.Refs) != 3 { - t.Fatalf("cleanup removed wrong refs: clean=%v removed=%v", clean, removed) - } - if clean.Refs["refs/heads/main"] != id || clean.Refs["refs/tags/release"] != id { - t.Fatalf("valid refs changed: %v", clean) - } - clean, removed, err = withoutInvalidRefs(clean, "") - if err != nil || len(removed) != 0 || len(clean.Refs) != 2 { - t.Fatalf("repeated cleanup changed valid refs: %v %v", clean, removed) - } - loaded.Head = legacy - encoded, err = json.Marshal(loaded) - if err != nil { - t.Fatal(err) - } - loaded, err = decodeRoot(encoded, format, 0) - if err != nil { - t.Fatalf("old default HEAD must remain readable: %v", err) - } - if _, _, err := withoutInvalidRefs(loaded, ""); err == nil { - t.Fatal("invalid default HEAD was removed without replacement") - } - if _, _, err := withoutInvalidRefs(loaded, "refs/tags/release"); err == nil { - t.Fatal("replacement HEAD accepted a tag") - } - clean, removed, err = withoutInvalidRefs(loaded, "refs/heads/main") - if err != nil || clean.Head != "refs/heads/main" || len(removed) != 1 || len(loaded.Refs) != 3 { - t.Fatalf("explicit HEAD replacement failed: clean=%v removed=%v error=%v", clean, removed, err) - } - delete(loaded.Refs, legacy) - clean, removed, err = withoutInvalidRefs(loaded, "refs/heads/main") - if err != nil || clean.Head != "refs/heads/main" || len(removed) != 0 { - t.Fatalf("unborn invalid HEAD was not replaced: clean=%v removed=%v error=%v", clean, removed, err) - } - }) - } -} - func receiveRefForTest(t *testing.T, format config.ObjectFormat, command *packp.Command, pack []byte, hook func(*transport.PreReceiveInfo) error) (string, error) { t.Helper() var request, reply bytes.Buffer diff --git a/examples/git/viewer/src/wal.test.ts b/examples/git/viewer/src/wal.test.ts index 23e943e..cad5508 100644 --- a/examples/git/viewer/src/wal.test.ts +++ b/examples/git/viewer/src/wal.test.ts @@ -29,7 +29,7 @@ afterEach(() => { }; }); -function emptySession(format: string): Session { +function emptySession(format: string, overrides: Record = {}): Session { const dispose = () => undefined; let next = true; return { @@ -47,6 +47,7 @@ function emptySession(format: string): Session { Head: "refs/heads/main", Refs: {}, Buckets: [], + ...overrides, }), ), objects: [], @@ -63,6 +64,18 @@ const discovery = () => headers: { "Content-Type": "application/x-git-upload-pack-advertisement" }, }); +test("catalog requires a persisted branch HEAD and accepts an unborn branch", () => { + for (const format of ["sha1", "sha256"]) { + for (const Head of [undefined, "", "refs/tags/main", "refs/heads/"]) + expect(() => new Catalog(emptySession(format, { Head }), "", { bytes: 0 })).toThrow( + "Invalid repository catalog", + ); + const catalog = new Catalog(emptySession(format), "", { bytes: 0 }); + expect(snapshot(catalog, new URLSearchParams()).branches).toEqual([]); + catalog.close(); + } +}); + test("wildcard repositories recover their stored format and canonical WAL identity", async () => { policies = '{"*":{}}'; for (const format of ["sha1", "sha256"]) { diff --git a/examples/git/viewer/src/wal.ts b/examples/git/viewer/src/wal.ts index 2cfaa65..457d710 100644 --- a/examples/git/viewer/src/wal.ts +++ b/examples/git/viewer/src/wal.ts @@ -103,6 +103,8 @@ export class Catalog { if ( !this.root.Validated || !["sha1", "sha256"].includes(this.root.Format) || + typeof this.root.Head !== "string" || + !/^refs\/heads\/.+$/.test(this.root.Head) || (format && this.root.Format !== format) || this.root.Buckets.length !== node.objects.length ) From dee95f3c059eccc6ffe6e2b36578148a30a5a27c Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Sun, 27 Sep 2026 21:28:42 -0700 Subject: [PATCH 13/16] Create Git repositories on their first successful push Use go-git to negotiate the format before opening storage, and publish refs, catalog, and default branch together. Remove the separate creation API and viewer form; start the local demo empty with the viewer at the root. Co-authored-by: Codex --- GIT_PLAN.md | 10 +- HANDOFF.md | 10 +- docs/blog/02-running-git-on-the-wal.md | 13 +- examples/git/README.md | 61 +++++----- examples/git/main.go | 81 ++++++------ examples/git/qualification/aws/HOSTING.md | 11 +- examples/git/receive_transport.go | 48 ++++++++ examples/git/receive_transport_test.go | 59 +++++++++ examples/git/repositories.go | 47 +------ examples/git/repositories_test.go | 41 +------ examples/git/spin.toml | 2 +- examples/git/tests/access_test.go | 8 +- examples/git/tests/creation_test.go | 115 ++++++++++-------- examples/git/tests/provider_test.go | 3 + .../git/tests/repositories_provider_test.go | 21 +--- examples/git/validate.go | 3 + examples/git/validate_native_test.go | 19 +++ examples/git/viewer/README.md | 49 +++----- examples/git/viewer/build.ts | 2 +- examples/git/viewer/src/api.ts | 55 --------- examples/git/viewer/src/client.tsx | 79 ++---------- examples/git/viewer/src/index.ts | 16 +-- examples/git/viewer/src/page.html | 4 +- examples/git/viewer/src/wal.test.ts | 109 ++--------------- scripts/run-git-local.sh | 16 +-- 25 files changed, 368 insertions(+), 514 deletions(-) diff --git a/GIT_PLAN.md b/GIT_PLAN.md index 098c17f..68c5a6c 100644 --- a/GIT_PLAN.md +++ b/GIT_PLAN.md @@ -12,7 +12,7 @@ rules remain outside the Rust core. fast-forward policy, connectivity validation, and malformed-input rejection. - Atomic publication of refs and the sparse object catalog. - Arbitrary repository paths with isolated WAL identities and an explicit - SHA-1 or SHA-256 format. Authorized API creation requires no per-repository + SHA-1 or SHA-256 format. First-push creation requires no per-repository configuration or restart; named access policies remain optional overrides. - Per-repository reader, writer, and administrator permissions; read-only mode; a persisted default branch; and recovery without a local repository cache. @@ -102,10 +102,10 @@ in #39. The declarative repository map supplies canonical nested paths, stable WAL identities, immutable object formats, default branches and independent read/write/admin groups. Duplicate identities, aliases and malformed paths are -rejected. A wildcard policy admits authenticated API creation of new names; -missing repositories remain absent on reads, administration and wildcard push. -All names require authorized API creation before the first Git push. -The format and default branch are published through the existing repository WAL. +rejected. A wildcard policy admits authorized first pushes to new names. +Missing repositories remain absent on reads; discovery for a first push advertises +both formats without initializing storage. The first successful push publishes +format, default branch, refs and catalog through the existing repository WAL. Scheduled maintenance currently requires named entries; dynamic discovery is separate work. diff --git a/HANDOFF.md b/HANDOFF.md index e8e32cf..56e8673 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -58,11 +58,11 @@ The Git service supports unchanged SHA-1 and SHA-256 clients, protocol-v2 clone and have-aware fetch, shallow history, classic push, branches, tags, access control, cold recovery, automatic tail checkpoints, and explicit maintenance. Refs and its sparse object catalog publish atomically through one WAL commit. -Local MinIO tests pass with unmodified go-git. Repository creation is available through an authenticated API without per-name -configuration. A wildcard access policy supplies defaults; exact entries replace -it. Canonical names derive isolated WAL identities, and the creation publication -persists the format and default branch. Reads and wildcard pushes only open -existing Git roots; creation is the only path that initializes a repository. +Local MinIO tests pass with unmodified go-git. The first successful authorized +push creates a repository without per-name configuration. A wildcard policy +supplies defaults; exact entries replace it. Canonical names derive isolated WAL +identities. The initial push publishes format, default branch, refs and catalog +through one WAL commit; browsing and rejected pushes publish no Git root. The host maintenance worker still visits only named configuration entries. Cognito access-token validation and a separate administration-only machine client are wired. Password mode is the explicit local default and requires a diff --git a/docs/blog/02-running-git-on-the-wal.md b/docs/blog/02-running-git-on-the-wal.md index 879a34e..d49a177 100644 --- a/docs/blog/02-running-git-on-the-wal.md +++ b/docs/blog/02-running-git-on-the-wal.md @@ -39,8 +39,8 @@ of both Spin and Git, and can also run natively. Configuration maps repository paths to stable WAL identities, hash formats, default branches, and permissions. Each repository has its own storage -namespace. An authorized writer creates durable state through the repository -API before the first push; reads only open existing repositories. A wildcard +namespace. The first successful authorized push publishes the format, refs and +catalog together; reads only open existing repositories. A wildcard access policy can admit new repository names without changing the component binary or configuration. @@ -134,9 +134,8 @@ curl --fail-with-body -u git:local-git-password \ -X POST http://127.0.0.1:19100/_validate_backend ``` -The default configuration exposes -`http://127.0.0.1:19100/sha1.git` and -`http://127.0.0.1:19100/sha256.git`. +Storage starts empty. The first successful push creates a repository at the +chosen path and records its Git format with the refs. In a third terminal, push a commit and clone it back. Use an empty working directory for these commands and keep your ordinary Git author configuration: @@ -147,11 +146,11 @@ cd demo echo 'Stored through object-log.' >README.md git add README.md git commit -m 'Try object-log' -git remote add origin http://127.0.0.1:19100/sha256.git +git remote add origin http://127.0.0.1:19100/team/project.git git push -u origin main cd .. git -c protocol.version=2 clone \ - http://127.0.0.1:19100/sha256.git demo-clone + http://127.0.0.1:19100/team/project.git demo-clone git -C demo-clone fsck --full ``` diff --git a/examples/git/README.md b/examples/git/README.md index e680f60..7fd930c 100644 --- a/examples/git/README.md +++ b/examples/git/README.md @@ -46,30 +46,26 @@ With the tools above installed, run from the repository root: make git-local ``` -This builds the service, starts isolated MinIO and Spin instances, creates the -bucket, and validates storage before printing the Git URLs. It uses a native -MinIO binary when available, otherwise the pinned Docker image. No AWS account -is involved. The default URLs are: - -```text -http://127.0.0.1:19100/sha1.git -http://127.0.0.1:19100/sha256.git -``` - -The local launcher creates the two demo repositories through the API. Push an -initial commit, entering `git` and `local-git-password` when Git prompts: +This builds the service, starts disposable MinIO and Spin instances, creates the +bucket, and validates storage. It uses a native MinIO binary when available, +otherwise the pinned Docker image. No AWS account is involved. Storage starts +empty. Push an initial commit to any permitted repository name to create it, +entering `git` and `local-git-password` when Git prompts: ```sh -git init --object-format=sha256 -b main demo +git init -b main demo cd demo echo 'Git on object-log' > README.md git add README.md git commit -m 'Initial commit' -git push http://127.0.0.1:19100/sha256.git main +git push http://127.0.0.1:19100/team/demo.git main cd .. -git clone http://127.0.0.1:19100/sha256.git cloned-demo +git clone http://127.0.0.1:19100/team/demo.git cloned-demo ``` +Git defaults to SHA-1. To use SHA-256, initialize the local repository with +`git init --object-format=sha256`; its first push selects the remote format. + Press Ctrl-C in the service terminal to stop Spin and MinIO and remove all demo data. Set `OBJECT_LOG_GIT_LOCAL_PORT` to choose another loopback port. To keep data or use an existing backend, build with `make git-build`, configure @@ -101,7 +97,7 @@ credentials for its disposable MinIO instance. | `wal_collection_candidates` | `1000` | Maximum entries in a new deletion plan, capped by the durable graph limit | | `wal_max_collection_objects` | `100000` | Complete publication graph and distinct collection live-object bound; shared paths count separately for publication | | `wal_recover_retentions_after_drain` | `false` | Exclusive lost-retention recovery mode | -| `git_repositories` | default policy and two demo entries | JSON access policies; optional fixed identities, formats and default branches | +| `git_repositories` | `{"*":{}}` | JSON access policies; optional fixed identities, formats and default branches | | `git_auth_mode` | `password` | `password`, `cognito`, or explicit local `anonymous` mode | | `git_password` | empty | Required password for local password mode | | `git_cognito_issuer` | empty | HTTPS Cognito user-pool issuer | @@ -146,22 +142,20 @@ configured limit high enough to read them; lowering it blocks those objects. ## Repositories and permissions -Create a repository through the API, then use ordinary Git. Names can be nested, -and the `.git` suffix is optional; both URLs select the same repository. +The first successful push creates a repository. Names can be nested, and the +`.git` suffix is optional; both URLs select the same repository: ```sh -curl --fail-with-body -u git:local-git-password \ - -H 'Content-Type: application/json' \ - -d '{"format":"sha1","default_branch":"main"}' \ - http://127.0.0.1:19100/team/project/create git push http://127.0.0.1:19100/team/project HEAD:main ``` -Creation requires write permission and publishes the format and default branch -through the repository's WAL. Defaults are SHA-1 and `main`; choose SHA-256 for a -SHA-256 client repository. A successful creation returns 201; an existing -repository returns 409. A pending publication returns 503: check the repository -before retrying. Reads and ordinary pushes to a missing name do not create it. +The push requires write permission and publishes its Git format, default branch, +refs and object catalog together through the existing WAL. The first pushed +branch becomes the default unless configuration specifies `default_branch`; +a tag-only first push leaves an unborn `main`. Later pushes preserve the stored +format and default branch. Read discovery never initializes storage. Failed +pushes can leave unreachable staged objects or an empty WAL head, but no +published Git repository. One `"*"` policy grants access to new names without a configuration edit or restart. For Cognito, configure the groups once: @@ -175,9 +169,9 @@ An omitted `log_id` derives an isolated identity from the canonical name, so adding a permission override preserves the existing repository. Explicit IDs must be unique and cannot use the reserved `auto-` prefix. An optional `format` pins that policy to SHA-1 or SHA-256; otherwise readers recover the stored format. -Creation settings cannot change permissions or reinterpret an existing repository. +Clients cannot change permissions or reinterpret an existing repository. The local shared-password mode grants the password holder access to all names; -anonymous mode allows public creation and writes. +anonymous mode allows public writes, including first-push creation. The optional host maintenance worker currently visits named configuration entries. Repositories created under `"*"` still checkpoint their log during pushes and expose @@ -227,17 +221,20 @@ make wasi-credential-test Against a service with a fresh WAL prefix, run the unchanged-client provider suite. The suite creates its own refs; do not reuse a repository containing -manual demo commits. Stop Spin, choose a new `wal_prefix` in the variables file, +manual demo commits. `GIT_PROBE_CREATE=1` enables first-push creation checks and +gives the ordinary Git workflow its own repository names; it requires a wildcard +policy. Stop Spin, choose a new `wal_prefix` in the variables file, and restart it before the first suite run or a rerun: ```sh GIT_PROBE_URL=http://127.0.0.1:19100 \ GIT_PROBE_PASSWORD=local-git-password \ -GIT_PROBE_LOG=/path/to/printed/spin.log make git-provider-test +GIT_PROBE_CREATE=1 GIT_MULTI_REPOSITORIES=1 \ +GIT_PROBE_LOG="$PWD/examples/git/.spin/logs/git_stderr.txt" make git-provider-test ``` Set `GIT_PROBE_PASSWORD` and `GIT_PROBE_BRANCH` when those values are configured. -Use the Spin log path printed by `make git-local` for `GIT_PROBE_LOG`; it supplies +Use the Git component log path printed by `make git-local` for `GIT_PROBE_LOG`; it supplies per-request storage counters when Spin omits HTTP trailers. The provider suite uses installed Git as an independent protocol and integrity oracle. Larger opt-in cases are controlled by these environment variables: diff --git a/examples/git/main.go b/examples/git/main.go index fd7af6b..6d2a80f 100644 --- a/examples/git/main.go +++ b/examples/git/main.go @@ -40,7 +40,7 @@ func sessionRetention(session *wal.Session) (retentionCall, retentionCall) { } } -func advertise(w io.Writer, s *store) error { +func advertise(w io.Writer, s *store, unknownFormat bool) error { if err := (&packp.SmartReply{Service: transport.ReceivePackService}).Encode(w); err != nil { return err } @@ -49,6 +49,9 @@ func advertise(w io.Writer, s *store) error { adv.Capabilities.Add(feature) } adv.Capabilities.Set(capability.ObjectFormat, s.meta.Format.String()) + if unknownFormat { + adv.Capabilities.Set(capability.ObjectFormat, "sha1", "sha256") + } for _, name := range slices.Sorted(maps.Keys(s.meta.Refs)) { id := s.meta.Refs[name] adv.References = append(adv.References, plumbing.NewHashReference(plumbing.ReferenceName(name), plumbing.NewHash(id))) @@ -133,7 +136,6 @@ func serve(response http.ResponseWriter, r *http.Request) { return } service, method := route.Service, route.Method - creating := service == "create" maintenance := service == "maintenance" collect := service == "collect" recoverRetentions := service == "recover-retentions-after-drain" @@ -164,16 +166,27 @@ func serve(response http.ResponseWriter, r *http.Request) { http.Error(response, err.Error(), operationStatus(err)) return } - if creating { - route.Repository, e = repositoryCreation(r.Body, route.Repository) - if e != nil { - status := operationStatus(e) + if service == transport.ReceivePackService && method == http.MethodPost { + format, body, err := receiveFormat(r.Body, limits.negotiationBytes) + if err == nil && format != "" && route.Repository.Format != "" && route.Repository.Format != format { + err = config.ErrInvalidObjectFormat + } + if err != nil { + status := operationStatus(err) if status == http.StatusInternalServerError { status = http.StatusBadRequest } - http.Error(response, e.Error(), status) + http.Error(response, "invalid push format", status) return } + if format == "" { + response.Header().Set("Content-Type", "application/x-git-receive-pack-result") + log.Printf("wal %s %s id=%s calls=0 bytes=0", r.Method, r.URL.Path, requestID) + response.WriteHeader(http.StatusOK) + return + } + route.Repository.Format = format + r.Body = gitio.NewReadCloser(body, r.Body) } settings, e := walSettings(getConfig, route.Repository.LogID, limits) @@ -182,11 +195,22 @@ func serve(response http.ResponseWriter, r *http.Request) { return } session, e := unwrap(func() wt.Result[*wal.Session, wal.Failure] { - if creating { + if service == transport.ReceivePackService && method == http.MethodPost { return wal.Open(settings) } return wal.OpenExisting(settings) }) + if errors.Is(e, errLogMissing) && service == transport.ReceivePackService && method == http.MethodGet { + response.Header().Set("Content-Type", "application/x-git-receive-pack-advertisement") + format := route.Repository.Format + if format == "" { + format = config.SHA1 + } + if err := advertise(response, &store{meta: rootMeta{Format: format}}, route.Repository.Format == ""); err != nil { + log.Printf("git discovery failed: %v", err) + } + return + } if e != nil { http.Error(response, e.Error(), operationStatus(e)) return @@ -285,8 +309,8 @@ func serve(response http.ResponseWriter, r *http.Request) { if e != nil { log.Printf("git request setup failed stage=open-store: %v", e) status := operationStatus(e) - if creating && errors.Is(e, config.ErrInvalidObjectFormat) { - status = http.StatusConflict + if errors.Is(e, config.ErrInvalidObjectFormat) { + status = http.StatusBadRequest } http.Error(w, "Git storage unavailable", status) return @@ -296,32 +320,7 @@ func serve(response http.ResponseWriter, r *http.Request) { s.Close() } }() - if creating { - if s.stateRoot != nil { - http.Error(w, "repository already exists", http.StatusConflict) - return - } - if err := s.publish(s.meta.Refs); err != nil { - var pending *pendingError - if errors.As(err, &pending) { - http.Error(w, "repository creation pending; check the repository before retrying", http.StatusServiceUnavailable) - return - } - status := operationStatus(err) - if errors.Is(err, errPublicationConflict) { - status = http.StatusConflict - } - http.Error(w, "repository creation was not confirmed", status) - return - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(struct { - Repository string `json:"repository"` - }{route.Name}) - return - } - if s.stateRoot == nil { + if s.stateRoot == nil && service != transport.ReceivePackService { http.NotFound(w, r) return } @@ -350,7 +349,7 @@ func serve(response http.ResponseWriter, r *http.Request) { return } else if service == transport.ReceivePackService && method == http.MethodGet { w.Header().Set("Content-Type", "application/x-git-receive-pack-advertisement") - e = advertise(w, s) + e = advertise(w, s, s.stateRoot == nil && route.Repository.Format == "") } else if service == transport.ReceivePackService { w.Header().Set("Content-Type", "application/x-git-receive-pack-result") commands := &commandReader{LimitedReader: io.LimitedReader{R: r.Body, N: limits.negotiationBytes}} @@ -363,6 +362,14 @@ func serve(response http.ResponseWriter, r *http.Request) { if e != nil { return e } + if s.stateRoot == nil && route.Repository.DefaultBranch == "" { + for _, command := range info.Commands { + if command.Action() != packp.Delete && command.Name.IsBranch() { + s.meta.Head = command.Name.String() + break + } + } + } e = s.publish(refs) return e }}}) diff --git a/examples/git/qualification/aws/HOSTING.md b/examples/git/qualification/aws/HOSTING.md index d1bcd93..fb72b41 100644 --- a/examples/git/qualification/aws/HOSTING.md +++ b/examples/git/qualification/aws/HOSTING.md @@ -32,8 +32,8 @@ SHA-256 formats, default branches, and independent read/write/admin groups. Declare all referenced groups in `host_groups`. Users and membership are managed in Cognito outside Terraform; self-registration is disabled. Named policies require a configuration change to admit new paths; a wildcard policy admits -new names without one. An authorized writer creates a repository with -`POST //create`. Do not change a stored repository's format or +new names without one. The first successful authorized push creates the +repository. Do not change a stored repository's format or identity to migrate it. The artifact is staged under this run's `artifacts/` prefix and SHA-256 checked @@ -134,9 +134,10 @@ git config --global --add credential.https://git.example.com.helper oauth git clone https://git.example.com/team/project.git ``` -A fresh configured repository returns 404 until an authorized writer calls -`POST //create` with its format and initial branch. Then push from a -local repository with that object format before trying to clone it. +A fresh repository returns 404 on reads until an authorized writer pushes its +first branch. That push records the Git format and refs together. An optional +policy format must match the local repository; use a matching repository before +trying to clone it. The helper uses authorization-code S256 PKCE and sends the access token as an ordinary Basic password. Its fixed `http://localhost:53119` callback matches the diff --git a/examples/git/receive_transport.go b/examples/git/receive_transport.go index e904a58..79ca375 100644 --- a/examples/git/receive_transport.go +++ b/examples/git/receive_transport.go @@ -1,14 +1,62 @@ package main import ( + "bytes" "fmt" "io" "math" + "strings" + "github.com/go-git/go-git/v6/plumbing/format/config" + "github.com/go-git/go-git/v6/plumbing/format/pktline" + "github.com/go-git/go-git/v6/plumbing/protocol/capability" + "github.com/go-git/go-git/v6/plumbing/protocol/packp" "github.com/go-git/go-git/v6/plumbing/storer" "github.com/go-git/go-git/v6/storage" ) +// Decode the first command with go-git, then replay its bounded prefix so the +// ordinary receive-pack path still validates every command and the entire pack. +func receiveFormat(body io.Reader, limit int64) (config.ObjectFormat, io.Reader, error) { + var prefix bytes.Buffer + scanner := pktline.NewScanner(&commandReader{io.LimitedReader{R: io.TeeReader(body, &prefix), N: limit}}) + for scanner.Scan() { + if scanner.Len() == pktline.Flush || bytes.IndexByte(scanner.Bytes(), 0) >= 0 { + break + } + } + if err := scanner.Err(); err != nil { + return "", nil, err + } + replay := io.MultiReader(bytes.NewReader(prefix.Bytes()), body) + if prefix.Len() == 4 && scanner.Len() == pktline.Flush { + return "", replay, nil + } + request := &packp.UpdateRequests{} + if err := request.Decode(io.MultiReader(bytes.NewReader(prefix.Bytes()), strings.NewReader("0000"))); err != nil { + return "", nil, err + } + if len(request.Commands) == 0 { + return "", replay, nil + } + formats := request.Capabilities.Get(capability.ObjectFormat) + format := config.SHA1 + if request.Capabilities.Supports(capability.ObjectFormat) && len(formats) != 1 { + return "", nil, config.ErrInvalidObjectFormat + } + if len(formats) == 1 { + format = config.ObjectFormat(formats[0]) + } + if format != config.SHA1 && format != config.SHA256 { + return "", nil, config.ErrInvalidObjectFormat + } + command := request.Commands[0] + if command.Old.HexSize() != format.HexSize() || command.New.HexSize() != format.HexSize() { + return "", nil, config.ErrInvalidObjectFormat + } + return format, replay, nil +} + // ReceivePack opens the pack writer after decoding commands and push options. // Release their smaller read limit; the HTTP body retains the total push limit. type receiveStore struct { diff --git a/examples/git/receive_transport_test.go b/examples/git/receive_transport_test.go index a7d39f7..7339d74 100644 --- a/examples/git/receive_transport_test.go +++ b/examples/git/receive_transport_test.go @@ -73,3 +73,62 @@ func TestReceiveCommandLimit(t *testing.T) { } } } + +func TestReceiveFormat(t *testing.T) { + for _, tc := range []struct { + name, caps string + width int + want config.ObjectFormat + }{ + {"legacy SHA-1", "report-status", 40, config.SHA1}, + {"SHA-1", "object-format=sha1", 40, config.SHA1}, + {"SHA-256", "object-format=sha256", 64, config.SHA256}, + {"missing SHA-256 capability", "report-status", 64, ""}, + {"wrong width", "object-format=sha256", 40, ""}, + {"unknown format", "object-format=other", 40, ""}, + {"missing format", "object-format", 40, ""}, + {"multiple formats", "object-format=sha1 object-format=sha256", 40, ""}, + } { + t.Run(tc.name, func(t *testing.T) { + var prefix bytes.Buffer + _, err := pktline.Writef(&prefix, "%s %s refs/heads/main\x00%s\n", strings.Repeat("0", tc.width), strings.Repeat("1", tc.width), tc.caps) + if err != nil { + t.Fatal(err) + } + packet := bytes.Clone(prefix.Bytes()) + input := append(bytes.Clone(packet), []byte("0000PACKpayload")...) + body := bytes.NewBuffer(input) + format, replay, err := receiveFormat(body, int64(len(packet))) + if tc.want == "" { + if err == nil { + t.Fatal("invalid format accepted") + } + return + } + if err != nil || format != tc.want || body.Len() != len("0000PACKpayload") { + t.Fatalf("format=%s error=%v unread=%d", format, err, body.Len()) + } + data, err := io.ReadAll(replay) + if err != nil || !bytes.Equal(data, input) { + t.Fatalf("push body was changed: %v", err) + } + if _, _, err := receiveFormat(bytes.NewReader(input), int64(len(packet)-1)); !errors.Is(err, errObjectLimit) { + t.Fatalf("negotiation limit: %v", err) + } + }) + } + for _, input := range []string{"0000", string(packetForTest("shallow "+strings.Repeat("1", 40)+"\n")) + "0000"} { + format, replay, err := receiveFormat(strings.NewReader(input), int64(len(input))) + if err != nil || format != "" { + t.Fatalf("no-op push: format=%s error=%v", format, err) + } + data, err := io.ReadAll(replay) + if err != nil || string(data) != input { + t.Fatalf("no-op body was changed: %v", err) + } + } +} + +func packetForTest(text string) []byte { + return []byte(fmt.Sprintf("%04x%s", len(text)+4, text)) +} diff --git a/examples/git/repositories.go b/examples/git/repositories.go index 8798f6a..58c58dd 100644 --- a/examples/git/repositories.go +++ b/examples/git/repositories.go @@ -29,8 +29,8 @@ var ( repositoryLogID = regexp.MustCompile(`^[A-Za-z0-9._-]{1,128}$`) ) -// A map entry provisions a name. Opening or creating storage remains the caller's -// responsibility, after checking the route's action against repositoryAccess. +// A map entry permits a name. Opening storage remains the caller's responsibility, +// after checking the route's action against repositoryAccess. type repositoryConfig struct { LogID string `json:"log_id"` Format config.ObjectFormat `json:"format"` @@ -131,43 +131,6 @@ func parseRepository(encoded []byte) (repositoryConfig, error) { return repository, nil } -func repositoryCreation(body io.Reader, policy repositoryConfig) (repositoryConfig, error) { - data, err := io.ReadAll(io.LimitReader(body, 4097)) - if err != nil { - return policy, err - } - if len(data) > 4096 { - return policy, errObjectLimit - } - fields, err := uniqueRepositoryObject(data) - if err != nil { - return policy, err - } - for name := range fields { - if name != "format" && name != "default_branch" { - return policy, fmt.Errorf("unknown creation field %q", name) - } - } - creation, err := parseRepository(data) - if err != nil { - return policy, err - } - if creation.Format == "" { - creation.Format = policy.Format - } - if creation.Format == "" { - creation.Format = config.SHA1 - } - if policy.Format != "" && policy.Format != creation.Format { - return policy, config.ErrInvalidObjectFormat - } - policy.Format = creation.Format - if creation.DefaultBranch != "" { - policy.DefaultBranch = creation.DefaultBranch - } - return policy, nil -} - // Decode only an object, rejecting duplicate keys before normal JSON decoding // could silently overwrite them. Both repository names and fields use this path. func uniqueRepositoryObject(encoded []byte) (map[string]json.RawMessage, error) { @@ -240,7 +203,7 @@ type repositoryRoute struct { Action gitAction } -// Resolve exact provisioned names and endpoint suffixes without cleaning or +// Resolve exact permitted names and endpoint suffixes without cleaning or // decoding path aliases. The caller must authorize Action before opening a WAL. // A method error preserves Method so the caller can send an Allow header. func resolveRepository(repositories map[string]repositoryConfig, r *http.Request) (repositoryRoute, error) { @@ -253,7 +216,7 @@ func resolveRepository(repositories map[string]repositoryConfig, r *http.Request } for _, service := range []string{ "info/refs", transport.UploadPackService, transport.ReceivePackService, - "create", "maintenance", "collect", "recover-retentions-after-drain", + "maintenance", "collect", "recover-retentions-after-drain", } { suffix := "/" + service if strings.HasSuffix(r.URL.Path, suffix) { @@ -293,7 +256,7 @@ func resolveRepository(repositories map[string]repositoryConfig, r *http.Request switch route.Service { case transport.UploadPackService: route.Action = gitRead - case transport.ReceivePackService, "create": + case transport.ReceivePackService: route.Action = gitWrite default: route.Action = gitAdmin diff --git a/examples/git/repositories_test.go b/examples/git/repositories_test.go index d66a7eb..6bc95b4 100644 --- a/examples/git/repositories_test.go +++ b/examples/git/repositories_test.go @@ -181,7 +181,6 @@ func TestResolveRepositorySelectsServiceAndAction(t *testing.T) { service: transport.ReceivePackService, action: gitWrite}, {name: "read RPC", method: http.MethodPost, path: "git-upload-pack", service: transport.UploadPackService, action: gitRead}, - {name: "creation", method: http.MethodPost, path: "create", service: "create", action: gitWrite}, {name: "write RPC", method: http.MethodPost, path: "git-receive-pack", service: transport.ReceivePackService, action: gitWrite}, {name: "logical maintenance", method: http.MethodPost, path: "maintenance", service: "maintenance", action: gitAdmin}, @@ -211,7 +210,7 @@ func TestResolveRepositoryRejectsAliasesAndUnknownRoutes(t *testing.T) { repositories := repositoriesForTest(t) for _, path := range []string{ "/missing.git/git-upload-pack", "/sha1.git/git-upload-pack", "/team/alpha.git", - "/team/alpha.git/unknown", "/team/alpha.git/git-upload-pack/", "/team/alpha.git//git-upload-pack", + "/team/alpha.git/unknown", "/team/alpha.git/create", "/team/alpha.git/git-upload-pack/", "/team/alpha.git//git-upload-pack", "//team/alpha.git/git-upload-pack", "/team/./alpha.git/git-upload-pack", "/team/other/../alpha.git/git-upload-pack", "/team%2falpha.git/git-upload-pack", "/team/%61lpha.git/git-upload-pack", "/team/alpha%2egit/git-upload-pack", @@ -294,7 +293,7 @@ func TestAutomaticRepositoryPolicyAndIdentity(t *testing.T) { } var first repositoryRoute for _, name := range []string{"team/project", "team/project.git"} { - route, err := resolveRepository(repositories, httptest.NewRequest(http.MethodPost, "/"+name+"/create", nil)) + route, err := resolveRepository(repositories, httptest.NewRequest(http.MethodPost, "/"+name+"/git-receive-pack", nil)) if err != nil { t.Fatal(err) } @@ -310,7 +309,7 @@ func TestAutomaticRepositoryPolicyAndIdentity(t *testing.T) { if !principal.Allows(first.Repository.repositoryAccess, gitWrite) || principal.Allows(first.Repository.repositoryAccess, gitRead) { t.Fatal("automatic policy lost independent permissions") } - private, err := resolveRepository(repositories, httptest.NewRequest(http.MethodPost, "/team/private.git/create", nil)) + private, err := resolveRepository(repositories, httptest.NewRequest(http.MethodPost, "/team/private.git/git-receive-pack", nil)) if err != nil { t.Fatal(err) } @@ -318,7 +317,7 @@ func TestAutomaticRepositoryPolicyAndIdentity(t *testing.T) { t.Fatal("exact denial or repository isolation lost") } for _, name := range []string{"*", "team/../project", "team//project", "%70roject"} { - if _, err := resolveRepository(repositories, httptest.NewRequest(http.MethodPost, "/"+name+"/create", nil)); err == nil { + if _, err := resolveRepository(repositories, httptest.NewRequest(http.MethodPost, "/"+name+"/git-receive-pack", nil)); err == nil { t.Fatalf("wildcard accepted invalid name %q", name) } } @@ -337,35 +336,3 @@ func TestAutomaticRepositoryPolicyAndIdentity(t *testing.T) { } } } - -func TestRepositoryCreationSettings(t *testing.T) { - policy := repositoryConfig{LogID: "isolated", DefaultBranch: "release", repositoryAccess: repositoryAccess{WriteGroups: []string{"writers"}}} - for _, format := range []config.ObjectFormat{config.SHA1, config.SHA256} { - created, err := repositoryCreation(strings.NewReader(`{"format":"`+string(format)+`","default_branch":"main"}`), policy) - if err != nil { - t.Fatal(err) - } - if created.Format != format || created.LogID != policy.LogID || created.DefaultBranch != "main" || - len(created.WriteGroups) != 1 || created.WriteGroups[0] != "writers" { - t.Fatalf("creation changed policy: %+v", created) - } - } - created, err := repositoryCreation(strings.NewReader(`{}`), policy) - if err != nil || created.Format != config.SHA1 || created.DefaultBranch != "release" { - t.Fatal("creation defaults lost") - } - for _, text := range []string{ - `{"format":"sha512"}`, `{"format":null}`, `{"format":"sha1","format":"sha256"}`, - `{"default_branch":"../main"}`, `{"write_groups":["everyone"]}`, `{"log_id":"other"}`, - `{"default_branch":"\u200c./review-probe"}`, - `{} {}`, strings.Repeat(" ", 4097), - } { - if _, err := repositoryCreation(strings.NewReader(text), policy); err == nil { - t.Fatalf("invalid creation accepted: %s", text) - } - } - policy.Format = config.SHA256 - if _, err := repositoryCreation(strings.NewReader(`{"format":"sha1"}`), policy); err == nil { - t.Fatal("creation bypassed configured format") - } -} diff --git a/examples/git/spin.toml b/examples/git/spin.toml index 581d4fa..ce453ec 100644 --- a/examples/git/spin.toml +++ b/examples/git/spin.toml @@ -15,7 +15,7 @@ wal_secret_key = { default = "", secret = true } wal_session_token = { default = "", secret = true } wal_max_collection_objects = { default = "100000" } wal_recover_retentions_after_drain = { default = "false" } -git_repositories = { default = '{"*":{},"sha1.git":{"log_id":"repo-sha1","format":"sha1"},"sha256.git":{"log_id":"repo-sha256","format":"sha256"}}' } +git_repositories = { default = '{"*":{}}' } git_auth_mode = { default = "password" } git_cognito_issuer = { default = "" } git_cognito_client_id = { default = "" } diff --git a/examples/git/tests/access_test.go b/examples/git/tests/access_test.go index 2fd8998..2c08dbc 100644 --- a/examples/git/tests/access_test.go +++ b/examples/git/tests/access_test.go @@ -58,7 +58,10 @@ func TestAccess(t *testing.T) { if err != nil { t.Fatal(err) } - if response.StatusCode != c.status { + // Fresh storage has no catalog until the first push. The Git workflow + // test verifies successful reads after publication. + absent := os.Getenv("GIT_PROBE_CREATE") == "1" && c.status == http.StatusOK && strings.HasSuffix(c.path, "service=git-upload-pack") && response.StatusCode == http.StatusNotFound + if response.StatusCode != c.status && !absent { t.Errorf("%s %s: got %d, want %d", c.method, c.path, response.StatusCode, c.status) } if bootID != "" && response.Header.Get("X-Git-Boot-ID") != bootID { @@ -76,6 +79,9 @@ func TestPersistedHead(t *testing.T) { if endpoint == "" || branch == "" || os.Getenv("GIT_PROBE_PERSISTED_HEAD") != "true" { t.Skip("set GIT_PROBE_URL and GIT_PROBE_BRANCH for an initialized repository") } + if os.Getenv("GIT_PROBE_CREATE") == "1" { + endpoint = strings.TrimRight(endpoint, "/") + "/workflow" + } for _, format := range []string{"sha1", "sha256"} { refs := string(git(t, nil, "ls-remote", "--symref", strings.TrimRight(endpoint, "/")+"/"+format+".git", "HEAD")) if !strings.Contains(refs, "ref: refs/heads/"+branch+"\tHEAD") { diff --git a/examples/git/tests/creation_test.go b/examples/git/tests/creation_test.go index 60a8b78..f3f8cb2 100644 --- a/examples/git/tests/creation_test.go +++ b/examples/git/tests/creation_test.go @@ -24,7 +24,7 @@ func TestRepositoryCreation(t *testing.T) { t.Fatal(err) } r.SetBasicAuth("git", password) - r.Header.Set("Content-Type", "application/json") + r.Header.Set("Content-Type", "application/x-git-receive-pack-request") response, err := http.DefaultClient.Do(r) if err != nil { t.Fatal(err) @@ -40,92 +40,107 @@ func TestRepositoryCreation(t *testing.T) { name := fmt.Sprintf("created/%s-%d", format, time.Now().UnixNano()) url := strings.TrimRight(endpoint, "/") + "/" + name password := os.Getenv("GIT_PROBE_PASSWORD") - settings := `{"format":"` + format + `","default_branch":"release"}` - request(http.MethodPost, url+"/create", "wrong-password", settings, 401) - request(http.MethodGet, url+"/info/refs?service=git-upload-pack", password, "", 404) - request(http.MethodGet, url+"/info/refs?service=git-receive-pack", password, "", 404) - request(http.MethodPost, url+"/maintenance", password, "{}", 404) - request(http.MethodPost, url+"/create", password, strings.Repeat(" ", 4097), 413) - request(http.MethodPost, url+"/create", password, `{"log_id":"override"}`, 400) - request(http.MethodPost, url+"/create", password, settings, 201) - request(http.MethodPost, url+".git/create", password, settings, 409) - other := "sha1" - if format == other { - other = "sha256" + read := url + "/info/refs?service=git-upload-pack" + request(http.MethodGet, url+"/info/refs?service=git-receive-pack", "wrong-password", "", 401) + request(http.MethodGet, read, password, "", 404) + request(http.MethodGet, url+"/info/refs?service=git-receive-pack", password, "", 200) + request(http.MethodPost, url+"/git-receive-pack", password, "0000", 200) + request(http.MethodGet, read, password, "", 404) + request(http.MethodPost, url+"/git-receive-pack", password, "invalid", 400) + width := 40 + if format == "sha256" { + width = 64 } - request(http.MethodPost, url+"/create", password, `{"format":"`+other+`"}`, 409) + command := packet(strings.Repeat("0", width) + " " + strings.Repeat("1", width) + " refs/heads/release\x00report-status object-format=" + format + "\n") + request(http.MethodPost, url+"/git-receive-pack", password, string(command)+"0000broken pack", 200) + request(http.MethodGet, read, password, "", 404) root := t.TempDir() git(t, nil, "init", "--object-format="+format, "-b", "release", root) write(t, filepath.Join(root, "README.md"), []byte(name)) git(t, nil, "-C", root, "add", ".") - git(t, nil, "-C", root, "commit", "-m", "created through API") + git(t, nil, "-C", root, "commit", "-m", "created by first push") git(t, nil, "-C", root, "push", url, "HEAD:refs/heads/release") clone := filepath.Join(t.TempDir(), "clone") git(t, nil, "-c", "protocol.version=2", "clone", url+".git", clone) git(t, nil, "-C", clone, "fsck", "--full") if got := git(t, nil, "-C", clone, "symbolic-ref", "--short", "HEAD"); strings.TrimSpace(string(got)) != "release" { - t.Fatal("created default branch was lost") + t.Fatal("first branch was not retained as default") } if got := git(t, nil, "-C", clone, "show", "HEAD:README.md"); !bytes.Equal(got, []byte(name)) { t.Fatal("created repository data differs") } }) } - t.Run("concurrent creators", func(t *testing.T) { + t.Run("concurrent first pushes", func(t *testing.T) { url := fmt.Sprintf("%s/created/race-%d", strings.TrimRight(endpoint, "/"), time.Now().UnixNano()) + type candidate struct { + format, tip string + body []byte + } + var pushes []candidate + for _, format := range []string{"sha1", "sha256"} { + root := t.TempDir() + git(t, nil, "init", "--object-format="+format, "-b", "main", root) + write(t, filepath.Join(root, "README.md"), []byte(format)) + git(t, nil, "-C", root, "add", ".") + git(t, nil, "-C", root, "commit", "-m", "first push") + tip := strings.TrimSpace(string(git(t, nil, "-C", root, "rev-parse", "HEAD"))) + body := append(packet(strings.Repeat("0", len(tip))+" "+tip+" refs/heads/main\x00report-status object-format="+format+"\n"), []byte("0000")...) + body = append(body, git(t, []byte(tip+"\n"), "-C", root, "pack-objects", "--revs", "--stdout")...) + pushes = append(pushes, candidate{format, tip, body}) + } type result struct { - format string + candidate status int + body []byte err error } results := make(chan result, 2) - for _, format := range []string{"sha1", "sha256"} { + start := make(chan struct{}) + for _, push := range pushes { go func() { - r, err := http.NewRequest(http.MethodPost, url+"/create", strings.NewReader(`{"format":"`+format+`"}`)) - if err != nil { - results <- result{format: format, err: err} - return - } - r.SetBasicAuth("git", os.Getenv("GIT_PROBE_PASSWORD")) - response, err := http.DefaultClient.Do(r) - status := 0 + r := result{candidate: push} + request, err := http.NewRequest(http.MethodPost, url+"/git-receive-pack", bytes.NewReader(push.body)) if err == nil { - status = response.StatusCode - _, _ = io.Copy(io.Discard, response.Body) - err = response.Body.Close() + request.SetBasicAuth("git", os.Getenv("GIT_PROBE_PASSWORD")) + request.Header.Set("Content-Type", "application/x-git-receive-pack-request") + <-start + var response *http.Response + response, err = http.DefaultClient.Do(request) + if err == nil { + r.status = response.StatusCode + r.body, err = io.ReadAll(response.Body) + response.Body.Close() + } } - results <- result{format: format, status: status, err: err} + r.err = err + results <- r }() } + close(start) winner := "" for range 2 { r := <-results - if r.err != nil || (r.status != 201 && r.status != 409) { - t.Fatalf("concurrent creation: %+v", r) + if r.err != nil { + t.Fatal(r.err) } - if r.status == 201 { + if r.status == 200 && bytes.Contains(r.body, []byte("ok refs/heads/main")) { if winner != "" { - t.Fatal("two creations committed") + t.Fatal("two incompatible first pushes committed") } - winner = r.format + winner = r.tip + } else if r.status != 400 && !bytes.Contains(r.body, []byte("ng refs/heads/main")) { + t.Fatalf("unexpected losing push: status=%d reply=%s", r.status, r.body) } } if winner == "" { - t.Fatal("no creation committed") - } - r, err := http.NewRequest(http.MethodGet, url+"/info/refs?service=git-receive-pack", nil) - if err != nil { - t.Fatal(err) - } - r.SetBasicAuth("git", os.Getenv("GIT_PROBE_PASSWORD")) - response, err := http.DefaultClient.Do(r) - if err != nil { - t.Fatal(err) + t.Fatal("no first push committed") } - defer response.Body.Close() - data, err := io.ReadAll(response.Body) - if err != nil || response.StatusCode != 200 || !bytes.Contains(data, []byte("object-format="+winner)) { - t.Fatal("stored format does not match winning creator") + clone := filepath.Join(t.TempDir(), "clone") + git(t, nil, "clone", url, clone) + git(t, nil, "-C", clone, "fsck", "--full") + if tip := strings.TrimSpace(string(git(t, nil, "-C", clone, "rev-parse", "HEAD"))); tip != winner { + t.Fatal("acknowledged first push was overwritten") } }) } diff --git a/examples/git/tests/provider_test.go b/examples/git/tests/provider_test.go index fbb1008..a11a164 100644 --- a/examples/git/tests/provider_test.go +++ b/examples/git/tests/provider_test.go @@ -27,6 +27,9 @@ func TestWALGit(t *testing.T) { if endpoint == "" { t.Skip("set GIT_PROBE_URL to an isolated local Spin/MinIO instance") } + if os.Getenv("GIT_PROBE_CREATE") == "1" { + endpoint = strings.TrimRight(endpoint, "/") + "/workflow" + } branch := os.Getenv("GIT_PROBE_BRANCH") if branch == "" { branch = "main" diff --git a/examples/git/tests/repositories_provider_test.go b/examples/git/tests/repositories_provider_test.go index 6ec3f3d..1dbc391 100644 --- a/examples/git/tests/repositories_provider_test.go +++ b/examples/git/tests/repositories_provider_test.go @@ -12,7 +12,7 @@ import ( ) // Configure these three names with distinct log IDs on a fresh storage prefix, -// password/token authentication, and default branch main. The test uses the +// password/token authentication. The first push selects default branch main. The test uses the // supplied server as-is; it does not rebuild or relaunch Spin. func TestRepositoryIsolation(t *testing.T) { if os.Getenv("GIT_MULTI_REPOSITORIES") != "1" { @@ -37,26 +37,13 @@ func TestRepositoryIsolation(t *testing.T) { repositoryStatus(t, read, os.Getenv("GIT_PROBE_PASSWORD"), http.StatusNotFound) repositoryStatus(t, write, "invalid-token", http.StatusUnauthorized) repositoryStatus(t, read, os.Getenv("GIT_PROBE_PASSWORD"), http.StatusNotFound) - repositoryStatus(t, write, os.Getenv("GIT_PROBE_PASSWORD"), http.StatusNotFound) - request, err := http.NewRequest(http.MethodPost, repo.url+"/create", strings.NewReader(`{"format":"`+repo.format+`"}`)) - if err != nil { - t.Fatal(err) - } - request.SetBasicAuth("git", os.Getenv("GIT_PROBE_PASSWORD")) - response, err := http.DefaultClient.Do(request) - if err != nil { - t.Fatal(err) - } - response.Body.Close() - if response.StatusCode != http.StatusCreated { - t.Fatalf("%s: creation returned HTTP %d", repo.name, response.StatusCode) - } - repositoryStatus(t, read, os.Getenv("GIT_PROBE_PASSWORD"), http.StatusOK) + repositoryStatus(t, write, os.Getenv("GIT_PROBE_PASSWORD"), http.StatusOK) + } for _, path := range []string{ "unknown/project.git", "alpha/project.git/extra", "alpha//project.git", } { - repositoryStatus(t, endpoint+"/"+path+"/info/refs?service=git-receive-pack", + repositoryStatus(t, endpoint+"/"+path+"/info/refs?service=git-upload-pack", os.Getenv("GIT_PROBE_PASSWORD"), http.StatusNotFound) } for i := range repositories { diff --git a/examples/git/validate.go b/examples/git/validate.go index e2914f5..19cd3c2 100644 --- a/examples/git/validate.go +++ b/examples/git/validate.go @@ -18,6 +18,9 @@ func validate(st *store, cmds []*packp.Command) (map[string]string, error) { refNames := dotgit.New(memfs.New()) for _, cmd := range cmds { name := string(cmd.Name) + if cmd.Old.HexSize() != st.meta.Format.HexSize() || cmd.New.HexSize() != st.meta.Format.HexSize() { + return nil, fmt.Errorf("invalid object format") + } if seen[name] || (cmd.Old.IsZero() && cmd.New.IsZero()) { return nil, fmt.Errorf("invalid ref update") } diff --git a/examples/git/validate_native_test.go b/examples/git/validate_native_test.go index b211b67..1b5693a 100644 --- a/examples/git/validate_native_test.go +++ b/examples/git/validate_native_test.go @@ -36,3 +36,22 @@ func TestValidateRejectsUnsafeRefBeforePublication(t *testing.T) { }) } } + +func TestValidateRejectsMixedCommandFormats(t *testing.T) { + for _, format := range []config.ObjectFormat{config.SHA1, config.SHA256} { + st := &store{Storage: memory.NewStorage(memory.WithObjectFormat(format)), meta: rootMeta{Format: format, Refs: map[string]string{}}} + other := config.SHA1 + if format == other { + other = config.SHA256 + } + for _, ids := range [][2]config.ObjectFormat{{other, format}, {format, other}} { + command := &packp.Command{Name: "refs/heads/main", + Old: plumbing.NewHash(strings.Repeat("1", ids[0].HexSize())), + New: plumbing.NewHash(strings.Repeat("0", ids[1].HexSize())), + } + if refs, err := validate(st, []*packp.Command{command}); err == nil || refs != nil || !strings.Contains(err.Error(), "object format") { + t.Fatalf("mixed-format deletion accepted: refs=%v error=%v", refs, err) + } + } + } +} diff --git a/examples/git/viewer/README.md b/examples/git/viewer/README.md index e5a1a9c..d2c723d 100644 --- a/examples/git/viewer/README.md +++ b/examples/git/viewer/README.md @@ -1,6 +1,6 @@ # Repository explorer -A small repository browser with creation through its TypeScript Spin API and Preact UI. +A small repository browser with a TypeScript Spin API and Preact UI. It shows branches, directories, file previews and recent commits for authorized repositories. The API and UI live in a separate component; the WAL is unchanged. @@ -20,44 +20,35 @@ bun install --frozen-lockfile bun start ``` -This builds both components, starts disposable local MinIO and Spin, and prints -the Git URL. Open `http://127.0.0.1:19100/browse?repo=team/project.git`, sign in -with password `local-git-password`, choose SHA-1 or SHA-256 and click -**Create repository**. Then push an existing repository of that format: +This builds both components and starts disposable local MinIO and Spin. Open +`http://127.0.0.1:19100/`. Storage starts empty; push an existing repository: ```sh -git push http://127.0.0.1:19100/team/project.git HEAD:main +git push http://127.0.0.1:19100/team/project.git HEAD ``` -When Git prompts, use username `git` and password `local-git-password`. -Press Ctrl-C in the startup terminal to stop both services and remove their -disposable data. Each restart begins with empty storage. +The first successful push creates the repository using the local Git format and +first branch. SHA-1 is Git's default; SHA-256 repositories work too. When Git +prompts, use username `git` and password `local-git-password`. -The default `"*"` policy allows authorized creation of any repository through -the API or UI, without per-repository TOML or a restart. Creation records its -Git format; reads never create repositories. Bare names such as `team/project` identify the same -repository as `team/project.git`. Exact configuration entries override the -whole wildcard policy and may pin a WAL identity or Git format. Cognito access -tokens use the Git service's existing read permissions. Credentials stay in page -memory and clear on reload. Branch and path selections stay in the URL. +Enter `team/project` in the viewer and sign in with the same password to browse +its files and commits. The default `"*"` policy admits new names without editing +TOML or restarting. Browsing a missing repository creates nothing. Bare names +and their `.git` URLs select the same repository. Exact policies may override +permissions or pin a WAL identity, format or default branch. + +Credentials stay in page memory and clear on reload. Branch and path selections +stay in the URL. Press Ctrl-C in the startup terminal to stop both services and +remove their disposable data. Each restart begins with empty storage. For an existing backend, use `bun run build`, then follow the [Git example's configuration](../README.md#configuration) and add `-f spin.viewer.toml` to its `spin up` command. -Hosted proxies must forward `/browse`, `/browse/api`, `/browse/assets/client.js` -and `/browse/assets/style.css` to Spin. `GET /browse/api` takes `repo` and -optional `ref` and `path` query parameters. `POST /browse/api?repo=` -forwards creation to the Git service, using its write permission: - -```sh -curl --fail-with-body -u git:local-git-password \ - -H 'Content-Type: application/json' -d '{"format":"sha1"}' \ - 'http://127.0.0.1:19100/browse/api?repo=team/project.git' -``` - -Creation defaults to SHA-1 and `main`; `default_branch` is optional. A successful -creation returns 201; creating an existing repository returns 409. +Hosted proxies must forward `/`, `/_viewer/api`, `/_viewer/client.js` and +`/_viewer/style.css` to Spin. `GET /_viewer/api` takes `repo` and optional +`ref` and `path` query parameters. Git traffic goes directly to the Git component; +the viewer has no repository-creation endpoint. ## Build and checks diff --git a/examples/git/viewer/build.ts b/examples/git/viewer/build.ts index d370cec..d80144d 100644 --- a/examples/git/viewer/build.ts +++ b/examples/git/viewer/build.ts @@ -21,7 +21,7 @@ const manifest = await Bun.file("../spin.toml").text(); const variables = [...manifest.matchAll(/^(wal_\w+|git_repositories) = "\{\{ (\w+) \}\}"$/gm)] .map((match) => `${match[1]} = "{{ ${match[2]} }}"\n`) .join(""); -const routes = ["/browse", "/browse/assets/client.js", "/browse/assets/style.css", "/browse/api"]; +const routes = ["/", "/_viewer/api", "/_viewer/client.js", "/_viewer/style.css"]; await Bun.write( "../spin.viewer.toml", manifest + diff --git a/examples/git/viewer/src/api.ts b/examples/git/viewer/src/api.ts index 8c7b490..e399bbb 100644 --- a/examples/git/viewer/src/api.ts +++ b/examples/git/viewer/src/api.ts @@ -10,24 +10,6 @@ import { variable, } from "./wal"; -async function boundedBody(message: Request | Response, limit: number): Promise { - const reader = message.body?.getReader(), - decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); - let bytes = 0, - text = ""; - try { - for (let part = await reader?.read(); part && !part.done; part = await reader?.read()) { - bytes += part.value.length; - if (bytes > limit) throw new Error("HTTP body exceeds limit"); - text += decoder.decode(part.value, { stream: true }); - } - return text + decoder.decode(); - } finally { - await reader?.cancel(); - reader?.releaseLock(); - } -} - export async function browse(request: Request): Promise { const query = new URL(request.url).searchParams; const path = query.get("path") ?? ""; @@ -65,43 +47,6 @@ export async function browse(request: Request): Promise { repositories["*"]; if (!policy) return new Response("Repository not found", { status: 404 }); const repository = { ...policy, log_id: policy.log_id || automaticLogId(name) }; - if (request.method === "POST") { - if (query.has("ref") || query.has("path")) - return new Response("Invalid create query", { status: 400 }); - let body: string; - try { - body = await boundedBody(request, 4096); - } catch (error) { - return new Response("Invalid creation request", { - status: error instanceof Error && error.message === "HTTP body exceeds limit" ? 413 : 400, - }); - } - const created = await fetch(`http://git.spin.internal/${name}/create`, { - method: "POST", - headers: { - Authorization: request.headers.get("Authorization") ?? "", - "Content-Type": "application/json", - }, - redirect: "manual", - body, - }); - body = await boundedBody(created, 65536); - if ( - created.status === 201 && - !created.headers.get("Content-Type")?.startsWith("application/json") - ) - return new Response("Invalid creation response", { status: 502 }); - const status = [201, 400, 401, 403, 404, 408, 409, 413, 503].includes(created.status) - ? created.status - : 502; - return new Response(status === 502 ? "Repository creation unavailable" : body, { - status, - headers: { - "Content-Type": status === 201 ? "application/json" : "text/plain; charset=utf-8", - "Cache-Control": "no-store", - }, - }); - } // Standard Git discovery enforces the unchanged service's repository permissions. const authorized = await fetch( `http://git.spin.internal/${name}/info/refs?service=git-upload-pack`, diff --git a/examples/git/viewer/src/client.tsx b/examples/git/viewer/src/client.tsx index e1261f9..5b2ba81 100644 --- a/examples/git/viewer/src/client.tsx +++ b/examples/git/viewer/src/client.tsx @@ -8,7 +8,6 @@ const messages: Record = { 401: "Authentication required.", 403: "This credential cannot read this repository.", 404: "Repository or path not found.", - 409: "This repository already exists. Open it to browse.", }; function validRepository(name: string): boolean { @@ -127,7 +126,6 @@ function App() { const [search, setSearch] = useState(location.search); const [authorization, setAuthorization] = useState({ header: "" }); const [result, setResult] = useState<{ view?: Snapshot; error?: string; status?: number }>({}); - const [created, setCreated] = useState(""); const [tab, setTab] = useState("code"); const [copied, setCopied] = useState(false); const cloneInput = useRef(null); @@ -151,7 +149,7 @@ function App() { void (async () => { try { const params = new URLSearchParams(search); - const response = await fetch(`/browse/api?${params}`, { + const response = await fetch(`/_viewer/api?${params}`, { headers: authorization.header ? { Authorization: authorization.header } : {}, credentials: "omit", cache: "no-store", @@ -200,16 +198,11 @@ function App() { return ( <>
- ◈ object-log + ◈ object-log Repository explorer
-

{repository.replace(/\.git$/, "") || "Repository"}

- {created && created === repository && ( -

- Repository created. Push to {cloneURL}. -

- )} +

{repository.replace(/\.git$/, "") || "Repositories"}

{!valid ? ( +

Open a repository. A first push creates it at the same Git URL.

{result.error}

) : !view ? ( <>

{result.error ?? "Loading repository…"}

- {created !== repository && - (result.status === 403 || result.status === 404) && - !query.get("ref") && - !query.get("path") && ( -
{ - event.preventDefault(); - const format = String(new FormData(event.currentTarget).get("format")); - setResult({}); - try { - const response = await fetch( - `/browse/api?${new URLSearchParams({ repo: repository })}`, - { - method: "POST", - headers: { - Authorization: authorization.header, - "Content-Type": "application/json", - }, - body: JSON.stringify({ format }), - credentials: "omit", - cache: "no-store", - }, - ); - if (response.ok || response.status === 409) { - if (response.status === 201) setCreated(repository); - navigate(new URLSearchParams({ repo: repository })); - setAuthorization({ header: authorization.header }); - } else - setResult({ - error: - (await response.text()) || - messages[response.status] || - "Repository unavailable. Try again.", - status: response.status, - }); - } catch { - setResult({ error: "Repository unavailable. Try again." }); - } - }} - > - - -

- Creates an empty repository at {repository}; default branch main. -

-
- )} + {result.status === 404 && !query.get("ref") && !query.get("path") && ( +

+ Push your first branch to create this repository: +
+ git push {cloneURL} HEAD +

+ )} {result.error && (
{ const path = new URL(request.url).pathname; - if (path === "/browse/api") { - if (request.method !== "GET" && request.method !== "POST") - return new Response("Method not allowed", { status: 405, headers: { Allow: "GET, POST" } }); + if (path === "/_viewer/api") { + if (request.method !== "GET" && request.method !== "HEAD") + return new Response("Method not allowed", { status: 405, headers: { Allow: "GET, HEAD" } }); return browse(request); } if (request.method !== "GET" && request.method !== "HEAD") { @@ -17,16 +17,12 @@ async function handle(request: Request): Promise { headers: { Allow: "GET, HEAD" }, }); } - if ( - path !== "/browse" && - path !== "/browse/assets/client.js" && - path !== "/browse/assets/style.css" - ) { + if (path !== "/" && path !== "/_viewer/client.js" && path !== "/_viewer/style.css") { return new Response("Not found", { status: 404 }); } const assets: Record = { - "/browse/assets/client.js": [CLIENT_SCRIPT, "text/javascript; charset=utf-8"], - "/browse/assets/style.css": [STYLES, "text/css; charset=utf-8"], + "/_viewer/client.js": [CLIENT_SCRIPT, "text/javascript; charset=utf-8"], + "/_viewer/style.css": [STYLES, "text/css; charset=utf-8"], }; const [body, type] = assets[path] ?? [PAGE, "text/html; charset=utf-8"]; return new Response(request.method === "HEAD" ? null : body, { diff --git a/examples/git/viewer/src/page.html b/examples/git/viewer/src/page.html index de16829..9cb5d23 100644 --- a/examples/git/viewer/src/page.html +++ b/examples/git/viewer/src/page.html @@ -4,8 +4,8 @@ Repository · object-log - - + +
diff --git a/examples/git/viewer/src/wal.test.ts b/examples/git/viewer/src/wal.test.ts index cad5508..e0899ee 100644 --- a/examples/git/viewer/src/wal.test.ts +++ b/examples/git/viewer/src/wal.test.ts @@ -98,7 +98,7 @@ test("wildcard repositories recover their stored format and canonical WAL identi return emptySession(format); }; expect( - (await browse(new Request(`https://viewer.test/browse/api?repo=${name}`))).status, + (await browse(new Request(`https://viewer.test/_viewer/api?repo=${name}`))).status, ).toBe(200); } } @@ -117,113 +117,21 @@ test("exact aliases override the whole wildcard policy and enforce pinned format }; for (const name of ["team/project", "team/project.git"]) expect( - (await browse(new Request(`https://viewer.test/browse/api?repo=${name}`))).status, + (await browse(new Request(`https://viewer.test/_viewer/api?repo=${name}`))).status, ).toBe(200); } policies = '{"*":{"format":"sha256"}}'; openSession = () => emptySession("sha1"); await expect( - browse(new Request("https://viewer.test/browse/api?repo=team/project")), + browse(new Request("https://viewer.test/_viewer/api?repo=team/project")), ).rejects.toThrow("Invalid repository catalog"); policies = '{"*":{}}'; openSession = () => emptySession("md5"); await expect( - browse(new Request("https://viewer.test/browse/api?repo=team/project")), + browse(new Request("https://viewer.test/_viewer/api?repo=team/project")), ).rejects.toThrow("Invalid repository catalog"); }); -test("creation forwards raw JSON and credentials and preserves outcomes without WAL access", async () => { - policies = '{"*":{}}'; - const baseline = opened; - for (const status of [201, 400, 401, 403, 408, 409, 413, 503]) { - const body = - status === 400 - ? '{"format":"sha1","format":"sha256"}' - : ' { "format": "sha256", "default_branch": "main" }\n'; - const responseBody = - status === 503 ? "Publication pending; recover before retrying" : "Creation denied"; - globalThis.fetch = mock(async (url: RequestInfo | URL, options?: RequestInit) => { - expect(String(url)).toBe("http://git.spin.internal/team/project.git/create"); - expect(options?.method).toBe("POST"); - expect(options?.headers).toEqual({ - Authorization: "Bearer write-token", - "Content-Type": "application/json", - }); - expect(options?.redirect).toBe("manual"); - expect(options?.body).toBe(body); - return status === 201 - ? Response.json({ created: true }, { status }) - : new Response(responseBody, { status }); - }) as unknown as typeof fetch; - const response = await browse( - new Request("https://viewer.test/browse/api?repo=team/project", { - method: "POST", - headers: { Authorization: "Bearer write-token" }, - body, - }), - ); - expect(response.status).toBe(status); - if (status === 201) expect(await response.json()).toEqual({ created: true }); - else expect(await response.text()).toBe(responseBody); - expect(opened).toBe(baseline); - } -}); - -test("creation bounds HTTP bodies and rejects invalid names before forwarding", async () => { - policies = '{"*":{}}'; - const forwarded = mock(async () => { - throw new Error("Unexpected HTTP call"); - }); - globalThis.fetch = forwarded as unknown as typeof fetch; - for (const [body, status] of [ - ["x".repeat(4097), 413], - [Uint8Array.of(255), 400], - ] as const) - expect( - ( - await browse( - new Request("https://viewer.test/browse/api?repo=team/project", { method: "POST", body }), - ) - ).status, - ).toBe(status); - for (const name of ["*", "", "../project", "team//project", "project%3Fother"]) - expect( - ( - await browse( - new Request(`https://viewer.test/browse/api?repo=${name}`, { - method: "POST", - body: "{}", - }), - ) - ).status, - ).toBe(404); - expect(forwarded).not.toHaveBeenCalled(); - globalThis.fetch = mock( - async () => new Response("unexpected response", { status: 201 }), - ) as unknown as typeof fetch; - expect( - ( - await browse( - new Request("https://viewer.test/browse/api?repo=team/project", { - method: "POST", - body: "{}", - }), - ) - ).status, - ).toBe(502); - globalThis.fetch = mock(async () => - Response.json({ data: "x".repeat(65536) }, { status: 201 }), - ) as unknown as typeof fetch; - await expect( - browse( - new Request("https://viewer.test/browse/api?repo=team/project", { - method: "POST", - body: "{}", - }), - ), - ).rejects.toThrow("HTTP body exceeds limit"); -}); - function fixture( format: "sha1" | "sha256", raw: Uint8Array, @@ -365,7 +273,7 @@ test("Git authorization completes before viewer storage is opened", async () => return new Response("Denied", { status: 403 }); }) as unknown as typeof fetch; const response = await browse( - new Request("https://viewer.test/browse/api?repo=team/demo.git", { + new Request("https://viewer.test/_viewer/api?repo=team/demo.git", { headers: { Authorization: "Bearer test-token" }, }), ); @@ -375,10 +283,11 @@ test("Git authorization completes before viewer storage is opened", async () => throw new Error("Unexpected HTTP call"); }) as unknown as typeof fetch; expect( - (await browse(new Request("https://viewer.test/browse/api?repo=missing.git"))).status, + (await browse(new Request("https://viewer.test/_viewer/api?repo=missing.git"))).status, ).toBe(404); expect( - (await browse(new Request("https://viewer.test/browse/api?repo=team/demo.git&path=.."))).status, + (await browse(new Request("https://viewer.test/_viewer/api?repo=team/demo.git&path=.."))) + .status, ).toBe(400); expect(opened).toBe(baseline); }); @@ -493,7 +402,7 @@ test("expired reads refresh once, close both views, and retain cumulative usage" headers: { "Content-Type": "application/x-git-upload-pack-advertisement" }, }), ) as unknown as typeof fetch; - const response = await browse(new Request("https://viewer.test/browse/api?repo=team/demo.git")); + const response = await browse(new Request("https://viewer.test/_viewer/api?repo=team/demo.git")); expect(response.status).toBe(200); expect((await response.json()).history).toEqual([]); expect(refreshes).toBe(1); diff --git a/scripts/run-git-local.sh b/scripts/run-git-local.sh index 67bc845..d2781d1 100755 --- a/scripts/run-git-local.sh +++ b/scripts/run-git-local.sh @@ -135,25 +135,17 @@ if [[ "$validated" != 1 ]]; then exit 1 fi -for format in sha1 sha256; do - curl --fail-with-body --silent --show-error --max-time 30 \ - --user git:local-git-password -H 'Content-Type: application/json' \ - --data "{\"format\":\"$format\"}" \ - "http://127.0.0.1:$git_port/$format.git/create" >/dev/null -done - cat < README.md git add README.md && git commit -m 'Initial commit' - git push http://127.0.0.1:$git_port/sha256.git main + git push http://127.0.0.1:$git_port/team/demo.git main When prompted, use username git and password local-git-password. Press Ctrl-C here to stop Spin and MinIO and remove their disposable data. From 5a87731716d0944c34d37327a04f36895e562e1f Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Mon, 28 Sep 2026 09:37:50 -0700 Subject: [PATCH 14/16] Show progress while importing Git pushes Report negotiated import counts and validation/publication phases, respecting quiet clients and stopping work after a progress write failure. Co-authored-by: Codex --- Makefile | 2 +- examples/git/README.md | 6 + examples/git/main.go | 19 +- examples/git/receive.go | 8 +- examples/git/receive_progress.go | 88 ++++++++ examples/git/receive_progress_test.go | 299 +++++++++++++++++++++++++ examples/git/receive_transport.go | 18 +- examples/git/receive_transport_test.go | 11 +- examples/git/store.go | 1 + 9 files changed, 435 insertions(+), 17 deletions(-) create mode 100644 examples/git/receive_progress.go create mode 100644 examples/git/receive_progress_test.go diff --git a/Makefile b/Makefile index b21e5b3..f3f9ff8 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ gc-acceptance: GIT_EXAMPLE_DIR = examples/git WAL_COMPONENT = examples/wal-component/Cargo.toml -GIT_TEST_FILES = access.go access_test.go auth.go auth_test.go repositories.go repositories_test.go imports.go imports_test.go receive_transport.go receive_transport_test.go request_config.go request_config_test.go import_pack.go import_pack_test.go import_pack_lifecycle_test.go limits.go limits_test.go index.go index_test.go codec.go codec_test.go delta.go delta_test.go read_retry.go read_retry_test.go retention.go retention_test.go retry.go retry_test.go fetch_policy.go fetch_policy_test.go validate_objects.go validate_objects_test.go validate.go validate_refname.go validate_refname_test.go validate_native_test.go +GIT_TEST_FILES = access.go access_test.go auth.go auth_test.go repositories.go repositories_test.go imports.go imports_test.go receive_transport.go receive_transport_test.go receive_progress.go receive_progress_test.go request_config.go request_config_test.go import_pack.go import_pack_test.go import_pack_lifecycle_test.go limits.go limits_test.go index.go index_test.go codec.go codec_test.go delta.go delta_test.go read_retry.go read_retry_test.go retention.go retention_test.go retry.go retry_test.go fetch_policy.go fetch_policy_test.go validate_objects.go validate_objects_test.go validate.go validate_refname.go validate_refname_test.go validate_native_test.go git-check: git-qualification-tools-test test -z "$$(gofmt -l $(GIT_EXAMPLE_DIR)/*.go $(GIT_EXAMPLE_DIR)/tests/*.go)" diff --git a/examples/git/README.md b/examples/git/README.md index 7fd930c..2c7640a 100644 --- a/examples/git/README.md +++ b/examples/git/README.md @@ -66,6 +66,12 @@ git clone http://127.0.0.1:19100/team/demo.git cloned-demo Git defaults to SHA-1. To use SHA-256, initialize the local repository with `git init --object-format=sha256`; its first push selects the remote format. +Push progress shows pack checks, imported-object counts, delta indexing, +validation, and publication. Import counts update about once a second as objects +finish; final push status follows durable publication. Use `git push --progress` +to request feedback when stderr is redirected. `-q` and `--no-progress` suppress +these messages. + Press Ctrl-C in the service terminal to stop Spin and MinIO and remove all demo data. Set `OBJECT_LOG_GIT_LOCAL_PORT` to choose another loopback port. To keep data or use an existing backend, build with `make git-build`, configure diff --git a/examples/git/main.go b/examples/git/main.go index 6d2a80f..2b2bf21 100644 --- a/examples/git/main.go +++ b/examples/git/main.go @@ -45,7 +45,10 @@ func advertise(w io.Writer, s *store, unknownFormat bool) error { return err } adv := &packp.AdvRefs{} - for _, feature := range []string{capability.ReportStatus, capability.DeleteRefs, capability.OFSDelta, capability.Atomic, capability.NoThin} { + for _, feature := range []string{ + capability.ReportStatus, capability.DeleteRefs, capability.OFSDelta, + capability.Atomic, capability.NoThin, capability.Sideband64k, capability.Quiet, + } { adv.Capabilities.Add(feature) } adv.Capabilities.Set(capability.ObjectFormat, s.meta.Format.String()) @@ -166,8 +169,9 @@ func serve(response http.ResponseWriter, r *http.Request) { http.Error(response, err.Error(), operationStatus(err)) return } + var pushCapabilities *capability.List if service == transport.ReceivePackService && method == http.MethodPost { - format, body, err := receiveFormat(r.Body, limits.negotiationBytes) + format, capabilities, body, err := receiveFormat(r.Body, limits.negotiationBytes) if err == nil && format != "" && route.Repository.Format != "" && route.Repository.Format != format { err = config.ErrInvalidObjectFormat } @@ -186,6 +190,7 @@ func serve(response http.ResponseWriter, r *http.Request) { return } route.Repository.Format = format + pushCapabilities = capabilities r.Body = gitio.NewReadCloser(body, r.Body) } @@ -352,12 +357,19 @@ func serve(response http.ResponseWriter, r *http.Request) { e = advertise(w, s, s.stateRoot == nil && route.Repository.Format == "") } else if service == transport.ReceivePackService { w.Header().Set("Content-Type", "application/x-git-receive-pack-result") + s.progress = newReceiveProgress(w, pushCapabilities, cancel) commands := &commandReader{LimitedReader: io.LimitedReader{R: r.Body, N: limits.negotiationBytes}} push := &receiveStore{Storer: s, commands: commands} e = transport.ReceivePack(r.Context(), push, gitio.NewReadCloser(commands, r.Body), gitio.WriteNopCloser(w), &transport.ReceivePackRequest{StatelessRPC: true, Hooks: transport.ReceivePackHooks{PreReceive: func(_ context.Context, info *transport.PreReceiveInfo) error { if len(info.Commands) == 0 { return nil } + if s.progress != nil { + s.progress.writer = info.Progress + } + if err := s.progress.message("Validating update...\n"); err != nil { + return err + } refs, e := validate(s, info.Commands) if e != nil { return e @@ -370,6 +382,9 @@ func serve(response http.ResponseWriter, r *http.Request) { } } } + if err := s.progress.message("Publishing update...\n"); err != nil { + return err + } e = s.publish(refs) return e }}}) diff --git a/examples/git/receive.go b/examples/git/receive.go index f246b66..7689871 100644 --- a/examples/git/receive.go +++ b/examples/git/receive.go @@ -46,6 +46,9 @@ func (p *incomingPack) Close() (err error) { if p.err != nil { return p.err } + if err := p.s.progress.message("Checking received pack...\n"); err != nil { + return err + } root, err := p.finish() if err != nil { return err @@ -58,7 +61,10 @@ func (p *incomingPack) Close() (err error) { } defer reader.Close() offsets := packOffsets{} - if err := importPack(p.s.ctx, reader, p.s, p.s.meta.Format, p.s.limits, offsets); err != nil { + if err := importPack(p.s.ctx, reader, p.s, p.s.meta.Format, p.s.limits, offsets, p.s.progress); err != nil { + return err + } + if err := p.s.progress.message("Indexing deltas...\n"); err != nil { return err } remaining := p.s.limits.catalogBytes diff --git a/examples/git/receive_progress.go b/examples/git/receive_progress.go new file mode 100644 index 0000000..bd9adba --- /dev/null +++ b/examples/git/receive_progress.go @@ -0,0 +1,88 @@ +package main + +import ( + "context" + "fmt" + "io" + "time" + + "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/plumbing/protocol/capability" + "github.com/go-git/go-git/v6/plumbing/protocol/packp/sideband" +) + +// ReceivePack creates its hook progress writer after pack import. Use the same +// upstream mux for import, then its hook writer for validation and publication. +type receiveProgress struct { + writer io.Writer + cancel context.CancelFunc + total, done uint32 + last time.Time + err error +} + +func newReceiveProgress(w io.Writer, caps *capability.List, cancel context.CancelFunc) *receiveProgress { + if caps == nil || !caps.Supports(capability.Sideband64k) { + return nil + } + if caps.Supports(capability.Quiet) || caps.Supports(capability.NoProgress) { + return nil + } + return &receiveProgress{ + writer: progressChannel{mux: sideband.NewMuxer(sideband.Sideband64k, w)}, + cancel: cancel, + } +} + +type progressChannel struct{ mux *sideband.Muxer } + +func (w progressChannel) Write(data []byte) (int, error) { + return w.mux.WriteChannel(sideband.ProgressMessage, data) +} + +func (p *receiveProgress) message(text string) error { + if p == nil { + return nil + } + if p.err == nil { + _, p.err = io.WriteString(p.writer, text) + if p.err != nil && p.cancel != nil { + p.cancel() + } + p.last = time.Now() + } + return p.err +} + +func (p *receiveProgress) OnHeader(total uint32) error { + if p == nil { + return nil + } + p.total = total + return p.message(fmt.Sprintf("Importing objects: 0/%d\n", total)) +} + +func (p *receiveProgress) OnInflatedObjectHeader(plumbing.ObjectType, int64, int64) error { + if p == nil { + return nil + } + return p.err +} + +func (p *receiveProgress) OnInflatedObjectContent(plumbing.Hash, int64, uint32, []byte) error { + if p == nil { + return nil + } + p.done++ + if time.Since(p.last) < time.Second { + return p.err + } + return p.message(fmt.Sprintf("Importing objects: %d/%d\r", p.done, p.total)) +} + +func (p *receiveProgress) OnFooter(plumbing.Hash) error { + if p == nil { + return nil + } + return p.message(fmt.Sprintf("Importing objects: %d/%d\n", p.done, p.total)) +} diff --git a/examples/git/receive_progress_test.go b/examples/git/receive_progress_test.go new file mode 100644 index 0000000..2cbd08e --- /dev/null +++ b/examples/git/receive_progress_test.go @@ -0,0 +1,299 @@ +package main + +import ( + "bytes" + "context" + "errors" + "io" + "strings" + "testing" + "testing/synctest" + "time" + + "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/plumbing/format/config" + "github.com/go-git/go-git/v6/plumbing/format/pktline" + "github.com/go-git/go-git/v6/plumbing/protocol/capability" + "github.com/go-git/go-git/v6/plumbing/protocol/packp" + "github.com/go-git/go-git/v6/plumbing/protocol/packp/sideband" + "github.com/go-git/go-git/v6/plumbing/transport" + "github.com/go-git/go-git/v6/storage/memory" + gitio "github.com/go-git/go-git/v6/utils/ioutil" +) + +func TestReceiveProgressNegotiation(t *testing.T) { + for _, tc := range []struct { + name, caps string + progress bool + }{ + {name: "unnegotiated", caps: "report-status"}, + {name: "quiet", caps: "side-band-64k quiet"}, + {name: "no progress", caps: "side-band-64k no-progress"}, + {name: "sideband 64k", caps: "side-band-64k", progress: true}, + } { + t.Run(tc.name, func(t *testing.T) { + var output bytes.Buffer + var caps capability.List + capability.DecodeList([]byte(tc.caps), &caps) + progress := newReceiveProgress(&output, &caps, nil) + if err := progress.message("Checking received pack...\n"); err != nil { + t.Fatal(err) + } + if !tc.progress { + if output.Len() != 0 { + t.Fatalf("unrequested progress: %q", output.String()) + } + return + } + scanner := pktline.NewScanner(&output) + if !scanner.Scan() || string(scanner.Bytes()) != "\x02Checking received pack...\n" { + t.Fatalf("invalid progress packet: %q, %v", scanner.Bytes(), scanner.Err()) + } + if scanner.Scan() || scanner.Err() != nil { + t.Fatalf("progress terminated the receive response: %v", scanner.Err()) + } + }) + } +} + +func TestReceiveProgressCountsImportedDeltaObjects(t *testing.T) { + for _, format := range []config.ObjectFormat{config.SHA1, config.SHA256} { + t.Run(format.String(), func(t *testing.T) { + var output bytes.Buffer + progress := &receiveProgress{writer: &output} + pack := fixturePack(t, format, []packFixtureEntry{ + {kind: plumbing.BlobObject, data: []byte("abc")}, + {kind: plumbing.OFSDeltaObject, data: []byte{3, 1, 0x90, 1}, ofs: 0}, + {kind: plumbing.REFDeltaObject, data: []byte{1, 1, 1, 'b'}, ref: blobID(format, []byte("a"))}, + }) + if err := importPack(t.Context(), bytes.NewReader(pack), newImportStorage(format), format, testPackLimits(1024), progress); err != nil { + t.Fatal(err) + } + if output.String() != "Importing objects: 0/3\nImporting objects: 3/3\n" { + t.Fatalf("inaccurate imported-object progress: %q", output.String()) + } + }) + } +} + +func TestReceiveProgressThrottlesObjectMessages(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + var output bytes.Buffer + progress := &receiveProgress{writer: &output} + if err := progress.OnHeader(3); err != nil { + t.Fatal(err) + } + if err := progress.OnInflatedObjectContent(plumbing.ZeroHash, 0, 0, nil); err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + if err := progress.OnInflatedObjectContent(plumbing.ZeroHash, 0, 0, nil); err != nil { + t.Fatal(err) + } + if output.String() != "Importing objects: 0/3\nImporting objects: 2/3\r" { + t.Fatalf("progress cadence: %q", output.String()) + } + }) +} + +type failedProgress struct { + err error + writes int +} + +func (w *failedProgress) Write([]byte) (int, error) { + w.writes++ + return 0, w.err +} + +type cancellableImportStorage struct { + *importStorage + ctx context.Context +} + +func (s *cancellableImportStorage) RawObjectWriter(kind plumbing.ObjectType, size int64) (io.WriteCloser, error) { + if err := s.ctx.Err(); err != nil { + return nil, err + } + return s.importStorage.RawObjectWriter(kind, size) +} + +func TestReceiveProgressWriteFailureStopsImport(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + failure := errors.New("response closed") + output := &failedProgress{err: failure} + progress := &receiveProgress{writer: output, cancel: cancel} + st := &cancellableImportStorage{importStorage: newImportStorage(config.SHA1), ctx: ctx} + pack := fixturePack(t, config.SHA1, []packFixtureEntry{ + {kind: plumbing.BlobObject, data: []byte("abc")}, + {kind: plumbing.BlobObject, data: []byte("def")}, + }) + err := importPack(ctx, bytes.NewReader(pack), st, config.SHA1, testPackLimits(1024), progress) + // Cancellation stops storage work even when upstream ignores callback errors. + if err == nil || ctx.Err() != context.Canceled || st.writes != 0 { + t.Fatalf("import continued: err=%v context=%v object writes=%d", err, ctx.Err(), st.writes) + } + if !errors.Is(progress.err, failure) || output.writes != 1 { + t.Fatalf("progress write error lost: err=%v writes=%d", progress.err, output.writes) + } + if err := progress.message("Publishing update...\n"); !errors.Is(err, failure) || output.writes != 1 { + t.Fatalf("progress retried a failed writer: err=%v writes=%d", err, output.writes) + } +} + +type progressPackStorage struct { + *memory.Storage + closePack func() error +} + +func (s *progressPackStorage) PackfileWriter() (io.WriteCloser, error) { + return gitio.NewWriteCloser(io.Discard, progressPackClose{s.closePack}), nil +} + +type progressPackClose struct{ close func() error } + +func (c progressPackClose) Close() error { return c.close() } + +func TestReceiveProgressPreservesReportStatus(t *testing.T) { + for _, tc := range []struct { + name, reason string + caps string + unpackError bool + plain bool + }{ + {name: "committed"}, + {name: "validation rejected", reason: "stale ref"}, + {name: "publication uncertain", reason: "publication pending"}, + {name: "pack rejected", reason: "malformed pack", unpackError: true}, + {name: "quiet committed", caps: "report-status side-band-64k quiet"}, + {name: "quiet uncertain", caps: "report-status side-band-64k quiet", reason: "publication pending"}, + {name: "plain committed", caps: "report-status", plain: true}, + {name: "plain uncertain", caps: "report-status", reason: "publication pending", plain: true}, + {name: "no progress", caps: "report-status side-band-64k no-progress", plain: true}, + } { + t.Run(tc.name, func(t *testing.T) { + var output bytes.Buffer + var caps capability.List + features := tc.caps + if features == "" { + features = "report-status side-band-64k" + } + capability.DecodeList([]byte(features), &caps) + progress := newReceiveProgress(&output, &caps, nil) + st := &progressPackStorage{Storage: memory.NewStorage()} + st.closePack = func() error { + if err := progress.message("Importing objects...\n"); err != nil { + return err + } + if tc.unpackError { + return errors.New(tc.reason) + } + return nil + } + object := plumbing.NewMemoryObject(plumbing.FromObjectFormat(config.SHA1)) + object.SetType(plumbing.BlobObject) + writer, _ := object.Writer() + _, _ = io.WriteString(writer, "progress test") + _ = writer.Close() + tip, err := st.SetEncodedObject(object) + if err != nil { + t.Fatal(err) + } + request := packetForTest(strings.Repeat("0", 40) + " " + tip.String() + " refs/tags/progress\x00" + caps.String() + "\n") + request = append(request, []byte("0000pack bytes")...) + hookCalled := false + err = transport.ReceivePack( + t.Context(), + st, + io.NopCloser(bytes.NewReader(request)), + gitio.WriteNopCloser(&output), + &transport.ReceivePackRequest{StatelessRPC: true, Hooks: transport.ReceivePackHooks{ + PreReceive: func(_ context.Context, info *transport.PreReceiveInfo) error { + hookCalled = true + if progress != nil { + progress.writer = info.Progress + } + if err := progress.message("Publishing update...\n"); err != nil { + return err + } + if tc.reason != "" { + return errors.New(tc.reason) + } + return nil + }, + }}, + ) + if (err == nil) != (tc.reason == "") || hookCalled == tc.unpackError { + t.Fatalf("receive outcome: err=%v hooked=%t", err, hookCalled) + } + var messages bytes.Buffer + reportReader := io.Reader(bytes.NewReader(output.Bytes())) + if !tc.plain { + if !bytes.HasSuffix(output.Bytes(), []byte("0009\x0100000000")) { + t.Fatalf("sideband termination changed: %q", output.String()) + } + demux := sideband.NewDemuxer(sideband.Sideband64k, reportReader) + demux.Progress = &messages + reportReader = demux + } + var report packp.ReportStatus + if err := report.Decode(reportReader); err != nil { + t.Fatal(err) + } + if strings.Contains(messages.String(), "Importing objects") != (progress != nil) { + t.Fatalf("unexpected import feedback: %q", messages.String()) + } + if tc.unpackError { + if report.UnpackStatus == "ok" || len(report.CommandStatuses) != 0 { + t.Fatalf("pack failure reported success: %+v", report) + } + return + } + status := report.CommandStatuses[0] + want := "ok" + if tc.reason != "" { + want = tc.reason + } + if status.Status != want || status.ReferenceName != "refs/tags/progress" { + t.Fatalf("publication status changed: %+v", status) + } + ref, refErr := st.Reference("refs/tags/progress") + if tc.reason == "" && (refErr != nil || ref.Hash() != tip) { + t.Fatalf("committed ref missing: %v", refErr) + } + if tc.reason != "" && !errors.Is(refErr, plumbing.ErrReferenceNotFound) { + t.Fatalf("rejected or uncertain update moved a ref: %v", ref) + } + }) + } +} + +func TestReceiveProgressWriteFailurePreventsPublication(t *testing.T) { + failure := errors.New("response closed") + var caps capability.List + capability.DecodeList([]byte("report-status side-band-64k"), &caps) + output := &failedProgress{err: failure} + progress := newReceiveProgress(output, &caps, nil) + st := &progressPackStorage{Storage: memory.NewStorage(), closePack: func() error { + return progress.message("Checking received pack...\n") + }} + request := packetForTest(strings.Repeat("0", 40) + " " + strings.Repeat("1", 40) + " refs/heads/main\x00" + caps.String() + "\n") + request = append(request, []byte("0000pack bytes")...) + hooked := false + err := transport.ReceivePack( + t.Context(), + st, + io.NopCloser(bytes.NewReader(request)), + gitio.WriteNopCloser(output), + &transport.ReceivePackRequest{StatelessRPC: true, Hooks: transport.ReceivePackHooks{ + PreReceive: func(context.Context, *transport.PreReceiveInfo) error { + hooked = true + return nil + }, + }}, + ) + if !errors.Is(err, failure) || hooked { + t.Fatalf("progress failure reached publication: error=%v hook=%t", err, hooked) + } +} diff --git a/examples/git/receive_transport.go b/examples/git/receive_transport.go index 79ca375..c1d748a 100644 --- a/examples/git/receive_transport.go +++ b/examples/git/receive_transport.go @@ -17,7 +17,7 @@ import ( // Decode the first command with go-git, then replay its bounded prefix so the // ordinary receive-pack path still validates every command and the entire pack. -func receiveFormat(body io.Reader, limit int64) (config.ObjectFormat, io.Reader, error) { +func receiveFormat(body io.Reader, limit int64) (config.ObjectFormat, *capability.List, io.Reader, error) { var prefix bytes.Buffer scanner := pktline.NewScanner(&commandReader{io.LimitedReader{R: io.TeeReader(body, &prefix), N: limit}}) for scanner.Scan() { @@ -26,35 +26,35 @@ func receiveFormat(body io.Reader, limit int64) (config.ObjectFormat, io.Reader, } } if err := scanner.Err(); err != nil { - return "", nil, err + return "", nil, nil, err } replay := io.MultiReader(bytes.NewReader(prefix.Bytes()), body) if prefix.Len() == 4 && scanner.Len() == pktline.Flush { - return "", replay, nil + return "", nil, replay, nil } request := &packp.UpdateRequests{} if err := request.Decode(io.MultiReader(bytes.NewReader(prefix.Bytes()), strings.NewReader("0000"))); err != nil { - return "", nil, err + return "", nil, nil, err } if len(request.Commands) == 0 { - return "", replay, nil + return "", nil, replay, nil } formats := request.Capabilities.Get(capability.ObjectFormat) format := config.SHA1 if request.Capabilities.Supports(capability.ObjectFormat) && len(formats) != 1 { - return "", nil, config.ErrInvalidObjectFormat + return "", nil, nil, config.ErrInvalidObjectFormat } if len(formats) == 1 { format = config.ObjectFormat(formats[0]) } if format != config.SHA1 && format != config.SHA256 { - return "", nil, config.ErrInvalidObjectFormat + return "", nil, nil, config.ErrInvalidObjectFormat } command := request.Commands[0] if command.Old.HexSize() != format.HexSize() || command.New.HexSize() != format.HexSize() { - return "", nil, config.ErrInvalidObjectFormat + return "", nil, nil, config.ErrInvalidObjectFormat } - return format, replay, nil + return format, &request.Capabilities, replay, nil } // ReceivePack opens the pack writer after decoding commands and push options. diff --git a/examples/git/receive_transport_test.go b/examples/git/receive_transport_test.go index 7339d74..7d92518 100644 --- a/examples/git/receive_transport_test.go +++ b/examples/git/receive_transport_test.go @@ -98,7 +98,7 @@ func TestReceiveFormat(t *testing.T) { packet := bytes.Clone(prefix.Bytes()) input := append(bytes.Clone(packet), []byte("0000PACKpayload")...) body := bytes.NewBuffer(input) - format, replay, err := receiveFormat(body, int64(len(packet))) + format, caps, replay, err := receiveFormat(body, int64(len(packet))) if tc.want == "" { if err == nil { t.Fatal("invalid format accepted") @@ -108,18 +108,21 @@ func TestReceiveFormat(t *testing.T) { if err != nil || format != tc.want || body.Len() != len("0000PACKpayload") { t.Fatalf("format=%s error=%v unread=%d", format, err, body.Len()) } + if caps.String() != tc.caps { + t.Fatalf("capabilities changed: %s", caps.String()) + } data, err := io.ReadAll(replay) if err != nil || !bytes.Equal(data, input) { t.Fatalf("push body was changed: %v", err) } - if _, _, err := receiveFormat(bytes.NewReader(input), int64(len(packet)-1)); !errors.Is(err, errObjectLimit) { + if _, _, _, err := receiveFormat(bytes.NewReader(input), int64(len(packet)-1)); !errors.Is(err, errObjectLimit) { t.Fatalf("negotiation limit: %v", err) } }) } for _, input := range []string{"0000", string(packetForTest("shallow "+strings.Repeat("1", 40)+"\n")) + "0000"} { - format, replay, err := receiveFormat(strings.NewReader(input), int64(len(input))) - if err != nil || format != "" { + format, caps, replay, err := receiveFormat(strings.NewReader(input), int64(len(input))) + if err != nil || format != "" || caps != nil { t.Fatalf("no-op push: format=%s error=%v", format, err) } data, err := io.ReadAll(replay) diff --git a/examples/git/store.go b/examples/git/store.go index e21969f..c68452a 100644 --- a/examples/git/store.go +++ b/examples/git/store.go @@ -31,6 +31,7 @@ type store struct { writers []*byteWriter storage.Storer failure error + progress *receiveProgress tailEntries uint64 session *wal.Session recovery *wal.Recovery From c0b20cb476d500dd290faa5abe0e019394bc7160 Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Mon, 28 Sep 2026 09:42:20 -0700 Subject: [PATCH 15/16] Polish repository controls and lazy commit navigation Use simple icons and native controls, add parent-directory links, and browse selected commits directly through the authorized catalog. Co-authored-by: Codex --- examples/git/viewer/README.md | 12 ++- examples/git/viewer/src/api.ts | 5 +- examples/git/viewer/src/client.tsx | 126 +++++++++++++++++--------- examples/git/viewer/src/repository.ts | 17 +++- examples/git/viewer/src/style.css | 49 +++++++++- examples/git/viewer/src/wal.test.ts | 112 ++++++++++++++++++++++- examples/git/viewer/src/wal.ts | 11 +-- 7 files changed, 269 insertions(+), 63 deletions(-) diff --git a/examples/git/viewer/README.md b/examples/git/viewer/README.md index d2c723d..5efd3e6 100644 --- a/examples/git/viewer/README.md +++ b/examples/git/viewer/README.md @@ -37,7 +37,7 @@ TOML or restarting. Browsing a missing repository creates nothing. Bare names and their `.git` URLs select the same repository. Exact policies may override permissions or pin a WAL identity, format or default branch. -Credentials stay in page memory and clear on reload. Branch and path selections +Credentials stay in page memory and clear on reload. Branch, commit and path selections stay in the URL. Press Ctrl-C in the startup terminal to stop both services and remove their disposable data. Each restart begins with empty storage. @@ -47,8 +47,8 @@ For an existing backend, use `bun run build`, then follow the Hosted proxies must forward `/`, `/_viewer/api`, `/_viewer/client.js` and `/_viewer/style.css` to Spin. `GET /_viewer/api` takes `repo` and optional -`ref` and `path` query parameters. Git traffic goes directly to the Git component; -the viewer has no repository-creation endpoint. +`ref`, `commit` (full SHA-1 or SHA-256 hash) and `path` query parameters. Git traffic +goes directly to the Git component; the viewer has no repository-creation endpoint. ## Build and checks @@ -68,8 +68,10 @@ is listed in [THIRD_PARTY.md](../../../THIRD_PARTY.md). ## Scope -Only branch tips are selectable. The UI shows eight first-parent commits, -500 entries per directory and UTF-8 text previews up to 256 KiB. Binary and +Branch tips and commits in the authorized repository catalog are selectable. +Commit links retain branch context and load that commit's tree and eight first-parent +summaries; selecting an older entry continues history without scanning from the tip. +The UI shows 500 entries per directory and UTF-8 text previews up to 256 KiB. Binary and larger files show a summary. Symlinks show their stored target; submodules show their commit ID. Non-UTF-8 filenames appear as byte escapes without a browse link; valid sibling files remain browsable. Names and file content render as text. Git metadata reads are diff --git a/examples/git/viewer/src/api.ts b/examples/git/viewer/src/api.ts index e399bbb..03826f7 100644 --- a/examples/git/viewer/src/api.ts +++ b/examples/git/viewer/src/api.ts @@ -16,10 +16,11 @@ export async function browse(request: Request): Promise { if ( [...query.keys()].some( (key) => - !["repo", "ref", "path"].includes(key) || + !["repo", "ref", "commit", "path"].includes(key) || query.getAll(key).length !== 1 || (query.get(key)?.length ?? 0) > 4096, ) || + (query.has("commit") && !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(query.get("commit") ?? "")) || (path && (path.split("/").length > 64 || path @@ -95,7 +96,7 @@ export async function browse(request: Request): Promise { }); } catch (error) { if (error instanceof NotFound) - return new Response("Branch or path not found", { status: 404 }); + return new Response("Branch, commit or path not found", { status: 404 }); if (attempt === 0 && (error as { payload?: { tag: string } }).payload?.tag === "expired") { const fresh = session.refresh(); drop(session); diff --git a/examples/git/viewer/src/client.tsx b/examples/git/viewer/src/client.tsx index 5b2ba81..7b6e699 100644 --- a/examples/git/viewer/src/client.tsx +++ b/examples/git/viewer/src/client.tsx @@ -4,12 +4,26 @@ import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; import type { Commit, Snapshot } from "./repository"; const messages: Record = { - 400: "Invalid repository, branch or path.", + 400: "Invalid repository, branch, commit or path.", 401: "Authentication required.", 403: "This credential cannot read this repository.", - 404: "Repository or path not found.", + 404: "Repository, commit or path not found.", }; +function Icon({ kind }: { kind: "folder" | "file" | "code" | "chevron" }) { + const paths = { + folder: "M2 4h5l2 2h5v7H2z", + file: "M4 2h5l3 3v9H4zM9 2v4h3", + code: "M6 4 2 8l4 4m4-8 4 4-4 4", + chevron: "m4 6 4 4 4-4", + }; + return ( + + ); +} + function validRepository(name: string): boolean { return ( name !== "" && @@ -21,22 +35,23 @@ function validRepository(name: string): boolean { ); } -function History({ commits }: { commits: Commit[] }) { +function History({ commits, link }: { commits: Commit[]; link: (commit: Commit) => VNode }) { return (
-

Recent commits on this branch · first parent

+

+ Recent commits · first parent · select a commit to explore earlier history +

{commits.length ? ( commits.map((commit) => (
- {commit.title || "Untitled commit"} + {link(commit)}

{commit.author || "Unknown author"} committed on{" "}

- {commit.id.slice(0, 7)}
)) ) : ( @@ -51,10 +66,12 @@ function Code({ view, repository, link, + commitLink, }: { view: Snapshot; repository: string; link: (label: string, path: string) => VNode; + commitLink: (commit: Commit) => VNode; }) { return (
@@ -68,12 +85,7 @@ function Code({ ))}
- {view.history[0] && ( -
- {view.history[0].title || "Untitled commit"} - {view.history[0].id.slice(0, 7)} -
- )} + {view.history[0] &&
{commitLink(view.history[0])}
} {view.file ? ( <>
@@ -96,6 +108,12 @@ function Code({

This repository is empty. Push a branch to start exploring.

) : ( <> + {view.path && ( +
+ + {link("..", view.path.split("/").slice(0, -1).join("/"))} +
+ )} {[...view.entries] .sort( (a, b) => @@ -104,7 +122,7 @@ function Code({ ) .map((entry) => (
- + {entry.unavailable ? ( {entry.name} ) : ( @@ -177,11 +195,10 @@ function App() { history.pushState(null, "", `?${params}`); setSearch(location.search); } - function pathLink(label: string, path: string) { - const params = new URLSearchParams(search); - params.set("path", path); + function link(params: URLSearchParams, label: string | VNode, className?: string) { return ( { if (event.button || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) @@ -194,6 +211,23 @@ function App() { ); } + function pathLink(label: string, path: string) { + const params = new URLSearchParams(search); + params.set("path", path); + return link(params, label); + } + function commitLink(commit: Commit) { + return link( + new URLSearchParams({ repo: repository, ref: view?.branch ?? "", commit: commit.id }), + <> + {commit.id.slice(0, 7)} + + {commit.title || "Untitled commit"} + + , + "commit-link", + ); + } return ( <> @@ -224,13 +258,16 @@ function App() { ) : !view ? ( <>

{result.error ?? "Loading repository…"}

- {result.status === 404 && !query.get("ref") && !query.get("path") && ( -

- Push your first branch to create this repository: -
- git push {cloneURL} HEAD -

- )} + {result.status === 404 && + !query.get("ref") && + !query.get("commit") && + !query.get("path") && ( +

+ Push your first branch to create this repository: +
+ git push {cloneURL} HEAD +

+ )} {result.error && ( {view.branches.length} {view.branches.length === 1 ? "branch" : "branches"} + {query.has("commit") && + link(new URLSearchParams({ repo: repository, ref: view.branch }), "Branch tip")}
- Clone + + Code +
{tab === "commits" ? ( - + ) : ( - + )} )} diff --git a/examples/git/viewer/src/repository.ts b/examples/git/viewer/src/repository.ts index 4296ee7..de943b5 100644 --- a/examples/git/viewer/src/repository.ts +++ b/examples/git/viewer/src/repository.ts @@ -1,4 +1,4 @@ -import { type Catalog, hex } from "./wal"; +import { type Catalog, hex, MissingObject } from "./wal"; export interface Commit { id: string; @@ -101,12 +101,21 @@ export function snapshot(catalog: Catalog, query: URLSearchParams): Snapshot { entries: [], more: false, }; - if (!branches.length && !query.get("ref") && !view.path) return view; + const selected = query.get("commit"); + if (!branches.length && !query.get("ref") && !selected && !view.path) return view; if (!branches.includes(branch)) throw new NotFound(); - let id = catalog.root.Refs[branch], + if (selected && selected.length !== (catalog.root.Format === "sha256" ? 64 : 40)) + throw new NotFound(); + let id = selected || catalog.root.Refs[branch], tip = ""; for (let i = 0; id && i < 8; i++) { - const current = commit(catalog, id); + let current: ReturnType; + try { + current = commit(catalog, id); + } catch (error) { + if (i === 0 && selected && error instanceof MissingObject) throw new NotFound(); + throw error; + } if (i === 0) tip = current.tree; view.history.push(current.summary); id = current.parent; diff --git a/examples/git/viewer/src/style.css b/examples/git/viewer/src/style.css index 949279c..0645049 100644 --- a/examples/git/viewer/src/style.css +++ b/examples/git/viewer/src/style.css @@ -1,4 +1,5 @@ :root { + color-scheme: light; font: 14px / 1.5 system-ui, sans-serif; @@ -8,6 +9,9 @@ * { box-sizing: border-box; } +svg { + flex-shrink: 0; +} body { margin: 0; } @@ -143,6 +147,34 @@ p { .toolbar select { max-width: 220px; } +.select-control { + position: relative; + display: inline-flex; +} +.select-control select { + appearance: none; + -webkit-appearance: none; + padding-right: 32px; +} +.select-control svg { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; +} +summary { + display: flex; + align-items: center; + gap: 8px; + list-style: none; +} +summary::-webkit-details-marker { + display: none; +} +summary svg:last-child { + margin-left: 8px; +} details { position: relative; margin-left: auto; @@ -185,12 +217,27 @@ article > strong { background: #f6f8fa; justify-content: space-between; } +.commit-link { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} +.commit-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 600; +} code, pre { font: 12px / 1.65 ui-monospace, monospace; } +.commit-link code { + flex-shrink: 0; +} pre { margin: 0; padding: 20px; @@ -218,7 +265,7 @@ pre { .toolbar { gap: 8px; } - article code { + article > code { display: none; } } diff --git a/examples/git/viewer/src/wal.test.ts b/examples/git/viewer/src/wal.test.ts index e0899ee..891920e 100644 --- a/examples/git/viewer/src/wal.test.ts +++ b/examples/git/viewer/src/wal.test.ts @@ -17,9 +17,9 @@ mock.module("object-log:storage/wal@0.1.0", () => ({ mock.module("@spinframework/spin-variables", () => ({ get: (name: string) => (name === "git_repositories" ? policies : ""), })); -const { Catalog } = await import("./wal"); +const { Catalog, MissingObject } = await import("./wal"); const { browse } = await import("./api"); -const { snapshot } = await import("./repository"); +const { NotFound, snapshot } = await import("./repository"); const originalFetch = globalThis.fetch; afterEach(() => { globalThis.fetch = originalFetch; @@ -262,6 +262,20 @@ test("large preview reads only metadata", () => { expect(sample.counts().reads).toBe(0); }); +test("a catalogued blob cannot be selected as a commit", () => { + const raw = new TextEncoder().encode("blob 3\0abc"), + sample = fixture("sha256", raw, zlibSync(raw)); + sample.catalog.root.Refs["refs/heads/main"] = "22".repeat(32); + try { + expect(() => snapshot(sample.catalog, new URLSearchParams({ commit: sample.id }))).toThrow( + NotFound, + ); + } finally { + sample.catalog.close(); + } + expect(sample.counts()).toEqual({ reads: 0, drops: 3 }); +}); + test("Git authorization completes before viewer storage is opened", async () => { const baseline = opened; globalThis.fetch = mock(async (input: RequestInfo | URL, options?: RequestInit) => { @@ -273,7 +287,7 @@ test("Git authorization completes before viewer storage is opened", async () => return new Response("Denied", { status: 403 }); }) as unknown as typeof fetch; const response = await browse( - new Request("https://viewer.test/_viewer/api?repo=team/demo.git", { + new Request(`https://viewer.test/_viewer/api?repo=team/demo.git&commit=${"11".repeat(32)}`, { headers: { Authorization: "Bearer test-token" }, }), ); @@ -292,6 +306,98 @@ test("Git authorization completes before viewer storage is opened", async () => expect(opened).toBe(baseline); }); +test("commit queries reject malformed selectors before authorization and return 404 for missing IDs", async () => { + const baseline = opened; + globalThis.fetch = mock(async () => { + throw new Error("Unexpected discovery"); + }) as unknown as typeof fetch; + for (const selector of [ + "", + "1234567", + "AA".repeat(20), + "11".repeat(21), + `${"11".repeat(20)}&commit=${"22".repeat(20)}`, + ]) + expect( + ( + await browse( + new Request(`https://viewer.test/_viewer/api?repo=team/demo.git&commit=${selector}`), + ) + ).status, + ).toBe(400); + expect(opened).toBe(baseline); + globalThis.fetch = mock(async () => discovery()) as unknown as typeof fetch; + openSession = () => emptySession("sha256", { Refs: { "refs/heads/main": "22".repeat(32) } }); + for (const selector of ["11".repeat(32), "11".repeat(20)]) + expect( + ( + await browse( + new Request(`https://viewer.test/_viewer/api?repo=team/demo.git&commit=${selector}`), + ) + ).status, + ).toBe(404); +}); + +for (const format of ["sha1", "sha256"] as const) { + test(`${format}: commit selection lazily reads its history and requested path without scanning the branch tip`, () => { + const width = format === "sha1" ? 20 : 32, + id = (n: number) => n.toString(16).padStart(width * 2, "0"), + encode = (value: string) => new TextEncoder().encode(value); + const objects = new Map(); + for (let i = 1; i <= 12; i++) + objects.set(id(i), { + kind: 1, + bytes: encode( + `tree ${id(20)}\n${i > 1 ? `parent ${id(i - 1)}\n` : ""}author A 1700000000 +0000\n\nCommit ${i}\n`, + ), + }); + const tree = (mode: string, name: string, target: number) => { + const header = encode(`${mode} ${name}\0`), + bytes = new Uint8Array(header.length + width); + bytes.set(header); + bytes[bytes.length - 1] = target; + return bytes; + }; + objects.set(id(20), { kind: 2, bytes: tree("40000", "src", 21) }); + objects.set(id(21), { kind: 2, bytes: tree("100644", "README", 22) }); + objects.set(id(22), { kind: 3, bytes: encode("selected content") }); + const reads: string[] = []; + const catalog = { + root: { Format: format, Head: "refs/heads/main", Refs: { "refs/heads/main": id(12) } }, + object(key: string, kind: number) { + reads.push(key); + const object = objects.get(key); + if (!object || object.kind !== kind) throw new MissingObject(); + return { size: object.bytes.length, bytes: object.bytes }; + }, + } as unknown as InstanceType; + const view = snapshot(catalog, new URLSearchParams({ commit: id(9), path: "src/README" })); + expect(view.branch).toBe("refs/heads/main"); + expect(view.history.map((commit) => commit.title)).toEqual( + Array.from({ length: 8 }, (_, i) => `Commit ${9 - i}`), + ); + expect(view.file?.text).toBe("selected content"); + expect(reads).toEqual([ + ...Array.from({ length: 8 }, (_, i) => id(9 - i)), + id(20), + id(21), + id(22), + ]); + reads.length = 0; + expect( + snapshot(catalog, new URLSearchParams({ commit: id(2), path: "src" })).history.map( + (commit) => commit.id, + ), + ).toEqual([id(2), id(1)]); + expect(reads).toEqual([id(2), id(1), id(20), id(21)]); + for (const commit of [id(99), id(22)]) + expect(() => snapshot(catalog, new URLSearchParams({ commit }))).toThrow(NotFound); + expect(() => + snapshot(catalog, new URLSearchParams({ ref: "refs/heads/missing", commit: id(9) })), + ).toThrow(NotFound); + }); +} + test("unusual filenames remain listed without hiding valid siblings", () => { const encode = (value: string) => new TextEncoder().encode(value); const commitId = "11".repeat(20), diff --git a/examples/git/viewer/src/wal.ts b/examples/git/viewer/src/wal.ts index 457d710..9d6797d 100644 --- a/examples/git/viewer/src/wal.ts +++ b/examples/git/viewer/src/wal.ts @@ -25,6 +25,7 @@ export interface Repository { export const canonicalName = (name: string) => (name.endsWith(".git") ? name : `${name}.git`); export const automaticLogId = (name: string) => `auto-${hex(sha256(new TextEncoder().encode(name)))}`; +export class MissingObject extends Error {} interface Item { ID: string; Kind: number; @@ -149,12 +150,8 @@ export class Catalog { for (const item of items) { const value = item.Inline ? undefined : node.objects[child++]; if (item.ID !== id) continue; - if ( - item.Kind !== kind || - item.Encoding !== "zlib" || - !Number.isSafeInteger(item.Size) || - item.Size < 0 - ) + if (item.Kind !== kind) throw new MissingObject("Git object has a different kind"); + if (item.Encoding !== "zlib" || !Number.isSafeInteger(item.Size) || item.Size < 0) throw new Error("Invalid Git object metadata"); if (item.Size > maxBytes) { if (kind !== 3) throw new Error("Git object exceeds metadata limit"); @@ -200,7 +197,7 @@ export class Catalog { } break; } - throw new Error("Git object not found"); + throw new MissingObject("Git object not found"); } close() { From d19ca2a10649ec3c07c05ddc0c3a22568f8eb3b2 Mon Sep 17 00:00:00 2001 From: carsonfarmer Date: Mon, 28 Sep 2026 10:11:02 -0700 Subject: [PATCH 16/16] Pad viewer instructions and separate status panels Co-authored-by: Codex --- examples/git/viewer/src/style.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/git/viewer/src/style.css b/examples/git/viewer/src/style.css index 0645049..4299a58 100644 --- a/examples/git/viewer/src/style.css +++ b/examples/git/viewer/src/style.css @@ -94,6 +94,14 @@ h2 { border-radius: 6px; overflow: hidden; } +p.panel { + padding: 16px 24px; + overflow-wrap: anywhere; +} +.panel + .panel, +p[role="status"] + .panel { + margin-top: 16px; +} form.panel { display: grid; gap: 12px;