diff --git a/.github/workflows/generated-pr.yml b/.github/workflows/generated-pr.yml new file mode 100644 index 00000000..b8c5cc63 --- /dev/null +++ b/.github/workflows/generated-pr.yml @@ -0,0 +1,14 @@ +name: Close Generated PRs + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + uses: ipdxco/unified-github-workflows/.github/workflows/reusable-generated-pr.yml@v1 diff --git a/.github/workflows/go-ci-config.json b/.github/workflows/go-ci-config.json new file mode 100644 index 00000000..209dca21 --- /dev/null +++ b/.github/workflows/go-ci-config.json @@ -0,0 +1,3 @@ +{ + "skip32bit": true +} diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml index 4a4f6a60..a13f2a84 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -1,9 +1,9 @@ name: go-peer ci -on: +on: pull_request: paths: - - 'go-peer/**' + - "go-peer/**" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -11,10 +11,10 @@ concurrency: jobs: go-check: - uses: libp2p/uci/.github/workflows/go-check.yml@v0.0 + uses: ipdxco/unified-github-workflows/.github/workflows/go-check.yml@v0.0 with: - go-version: '1.23.x' + go-version: "1.25.x" go-test: - uses: libp2p/uci/.github/workflows/go-test.yml@v0.0 + uses: ipdxco/unified-github-workflows/.github/workflows/go-test.yml@v0.0 with: - go-versions: '["1.23.x"]' + go-versions: '["1.25.x"]' diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 16d65d72..7c955c41 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,8 +1,9 @@ -name: Close and mark stale issue +name: Close Stale Issues on: schedule: - cron: '0 0 * * *' + workflow_dispatch: permissions: issues: write @@ -10,4 +11,4 @@ permissions: jobs: stale: - uses: pl-strflt/.github/.github/workflows/reusable-stale-issue.yml@v0.3 + uses: ipdxco/unified-github-workflows/.github/workflows/reusable-stale-issue.yml@v1 diff --git a/.gitignore b/.gitignore index 8e8f5b54..4a4ab393 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ yarn.lock .DS_Store go-peer/go-peer **/.idea +nim-peer/nim_peer +nim-peer/local.* diff --git a/README.md b/README.md index ea46370a..fcfb3584 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,14 @@ Some of the cool and cutting-edge [transport protocols](https://connectivity.lib ## Packages -| Package | Description | WebTransport | WebRTC | WebRTC-direct | QUIC | TCP | -| :-------------------------- | :------------------------------ | ------------ | ------ | ------------- | ---- | --- | -| [`js-peer`](./js-peer/) | Browser Chat Peer in TypeScript | ✅ | ✅ | ✅ | ❌ | ❌ | -| [`go-peer`](./go-peer/) | Chat peer implemented in Go | ✅ | ❌ | ✅ | ✅ | ✅ | -| [`rust-peer`](./rust-peer/) | Chat peer implemented in Rust | ❌ | ❌ | ✅ | ✅ | ❌ | -| [`py-peer`](./py-peer/) | Chat peer implemented in Python | ❌ | ❌ | ❌ | ✅ | ✅ | +| Package | Description | WebTransport | WebRTC | WebRTC-direct | QUIC | TCP | +| :-------------------------------- | :------------------------------ | ------------ | ------ | ------------- | ---- | --- | +| [`js-peer`](./js-peer/) | Browser Chat Peer in TypeScript | ✅ | ✅ | ✅ | ❌ | ❌ | +| [`node-js-peer`](./node-js-peer/) | Node.js Chat Peer in TypeScript | ✅ | ✅ | ✅ | ✅ | ✅ | +| [`go-peer`](./go-peer/) | Chat peer implemented in Go | ✅ | ❌ | ✅ | ✅ | ✅ | +| [`rust-peer`](./rust-peer/) | Chat peer implemented in Rust | ❌ | ❌ | ✅ | ✅ | ✅ | +| [`py-peer`](./py-peer/) | Chat peer implemented in Python | ❌ | ❌ | ❌ | ✅ | ✅ | +| [`nim-peer`](./nim-peer/) | Chat peer implemented in Nim | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ - Protocol supported ❌ - Protocol not supported @@ -42,8 +44,7 @@ There are two ways to connect to a peer: Load the UI, and enter the multiaddr into the UI. Ensure that it includes the peerID, e.g.`/ip4/192.168.178.21/udp/61838/quic-v1/webtransport/certhash/uEiCQCALYac4V3LJ2ourLdauXOswIXpIuJ_JNT-8Wavmxyw/certhash/uEiCdYghq5FlXGkVONQXT07CteA16BDyMPI23-0GjA9Ej_w/p2p/12D3KooWF7ovRNBKPxERf6GtUbFdiqJsQviKUb7Z8a2Uuuo6MrDX` - -## Getting started: JS +## Getting started: Browser JS ### 1. Install dependencies @@ -62,6 +63,21 @@ Start the dev server: npm run dev ``` +## Getting started: Node.js + +### 1. Install dependencies + +``` +cd node-js-peer +npm i +``` + +### 2. Start the app + +``` +npm start +``` + ## Getting started: Rust ``` @@ -69,7 +85,7 @@ cd rust-peer cargo run ``` -This will automatically connect you to the bootstrap node running on [fly.io](https://fly.io). +This will automatically connect you to the bootstrap nodes running on bootstrap.libp2p.io. To explore more advanced configurations if you e.g. want to set up our own network, try: @@ -89,7 +105,45 @@ go run . Make sure you have the [uv package manager](https://github.com/astral-sh/uv) installed first. Follow the instructions on their Github to install it. -``` +### 1. Create a virtual environment + +Create and activate a virtual environment for the Python peer: + +```bash cd py-peer -uv run hello.py +uv venv +source .venv/bin/activate +``` + +### 2. Install dependencies + +Install the Python peer dependencies: + +```bash +uv pip install -e . +``` + +### 3. Start the Python peer + +Start the peer: + +```bash +python main.py +``` + +If you want a specific mode, you can pass flags such as `--ui`, `--kivy`, or `--api`. For example: + +```bash +python main.py --ui + +## Getting started: Nim +``` +cd nim-peer +nimble build + +# Wait for connections in tcp/9093 +./nim_peer + +# Connect to another node (e.g. in localhost tcp/9092) +./nim_peer --connect /ip4/127.0.0.1/tcp/9092/p2p/12D3KooSomePeerId ``` diff --git a/go-peer/Dockerfile b/go-peer/Dockerfile index 404aa200..9e781277 100644 --- a/go-peer/Dockerfile +++ b/go-peer/Dockerfile @@ -1,5 +1,5 @@ # Use a specific version of golang alpine for better reproducibility -FROM golang:1.23-alpine AS builder +FROM golang:1.25-alpine AS builder WORKDIR /usr/src/app @@ -18,11 +18,11 @@ FROM alpine:latest # Add CA certificates for HTTPS and create non-root user RUN apk --no-cache add ca-certificates && \ - adduser -D appuser + adduser -D appuser # Create a directory for the application and set proper permissions RUN mkdir -p /app/data && \ - chown -R appuser:appuser /app + chown -R appuser:appuser /app # Copy the binary from builder COPY --from=builder /usr/local/bin/universal-chat-go /usr/local/bin/universal-chat-go diff --git a/go-peer/chatroom.go b/go-peer/chatroom.go index f5fcd6a6..77c53222 100644 --- a/go-peer/chatroom.go +++ b/go-peer/chatroom.go @@ -18,9 +18,6 @@ const ChatRoomBufSize = 128 // Topic used to broadcast browser WebRTC addresses const PubSubDiscoveryTopic string = "universal-connectivity-browser-peer-discovery" -const ChatTopic string = "universal-connectivity" -const ChatFileTopic string = "universal-connectivity-file" - // ChatRoom represents a subscription to a single PubSub topic. Messages // can be published to the topic with ChatRoom.Publish, and received // messages are pushed to the Messages channel. @@ -46,15 +43,15 @@ type ChatRoom struct { // ChatMessage gets converted to/from JSON and sent in the body of pubsub messages. type ChatMessage struct { Message string - SenderID string + SenderID peer.ID SenderNick string } // JoinChatRoom tries to subscribe to the PubSub topic for the room name, returning // a ChatRoom on success. -func JoinChatRoom(ctx context.Context, h host.Host, ps *pubsub.PubSub, nickname string) (*ChatRoom, error) { +func JoinChatRoom(ctx context.Context, h host.Host, ps *pubsub.PubSub, nickname string, roomName string) (*ChatRoom, error) { // join the pubsub chatTopic - chatTopic, err := ps.Join(ChatTopic) + chatTopic, err := ps.Join(roomName) if err != nil { return nil, err } @@ -66,7 +63,8 @@ func JoinChatRoom(ctx context.Context, h host.Host, ps *pubsub.PubSub, nickname } // join the pubsub fileTopic - fileTopic, err := ps.Join(ChatFileTopic) + fileTopicName := roomName + "-file" + fileTopic, err := ps.Join(fileTopicName) if err != nil { return nil, err } @@ -100,6 +98,7 @@ func JoinChatRoom(ctx context.Context, h host.Host, ps *pubsub.PubSub, nickname peerDiscoveryTopic: peerDiscoveryTopic, peerDiscoverySub: peerDiscoverySub, nick: nickname, + roomName: roomName, Messages: make(chan *ChatMessage, ChatRoomBufSize), SysMessages: make(chan *ChatMessage, ChatRoomBufSize), } @@ -115,7 +114,7 @@ func (cr *ChatRoom) Publish(message string) error { } func (cr *ChatRoom) ListPeers() []peer.ID { - return cr.ps.ListPeers(ChatTopic) + return cr.ps.ListPeers(cr.roomName) } // readLoop pulls messages from the pubsub chat/file topic and handles them. @@ -138,8 +137,9 @@ func (cr *ChatRoom) readChatLoop() { } cm := new(ChatMessage) cm.Message = string(msg.Data) - cm.SenderID = msg.ID - cm.SenderNick = string(msg.ID[len(msg.ID)-8]) + cm.SenderID = msg.GetFrom() + senderStr := cm.SenderID.String() + cm.SenderNick = senderStr[len(senderStr)-8:] // send valid messages onto the Messages channel cr.Messages <- cm } @@ -167,8 +167,9 @@ func (cr *ChatRoom) readFileLoop() { cm := new(ChatMessage) cm.Message = fmt.Sprintf("File: %s (%v bytes) from %s", string(fileID), len(fileBody), msg.GetFrom().String()) - cm.SenderID = msg.ID - cm.SenderNick = string(msg.ID[len(msg.ID)-8]) + cm.SenderID = msg.GetFrom() + senderStr := cm.SenderID.String() + cm.SenderNick = senderStr[len(senderStr)-8:] // send valid messages onto the Messages channel cr.Messages <- cm } @@ -183,13 +184,13 @@ func (cr *ChatRoom) requestFile(toPeer peer.ID, fileID []byte) ([]byte, error) { defer stream.Close() reqLen := binary.AppendUvarint([]byte{}, uint64(len(fileID))) - if _, err := stream.Write(reqLen); err != nil { + if _, err = stream.Write(reqLen); err != nil { return nil, fmt.Errorf("failed to write fileID to the stream: %w", err) } - if _, err := stream.Write(fileID); err != nil { + if _, err = stream.Write(fileID); err != nil { return nil, fmt.Errorf("failed to write fileID to the stream: %w", err) } - if err := stream.CloseWrite(); err != nil { + if err = stream.CloseWrite(); err != nil { return nil, fmt.Errorf("failed to close write stream: %w", err) } @@ -207,4 +208,4 @@ func (cr *ChatRoom) requestFile(toPeer peer.ID, fileID []byte) ([]byte, error) { } return fileBody, nil -} +} \ No newline at end of file diff --git a/go-peer/go.mod b/go-peer/go.mod index 056b724b..c749da40 100644 --- a/go-peer/go.mod +++ b/go-peer/go.mod @@ -1,19 +1,17 @@ module github.com/libp2p/universal-connectivity/go-peer -go 1.22.0 - -toolchain go1.22.8 +go 1.25.4 require ( - github.com/caddyserver/certmagic v0.21.7 - github.com/gdamore/tcell/v2 v2.7.4 - github.com/ipfs/go-log/v2 v2.5.1 - github.com/ipshipyard/p2p-forge v0.3.1 - github.com/libp2p/go-libp2p v0.39.1 - github.com/libp2p/go-libp2p-kad-dht v0.29.0 - github.com/libp2p/go-libp2p-pubsub v0.13.0 - github.com/multiformats/go-multiaddr v0.14.0 - github.com/rivo/tview v0.0.0-20241030223020-e34b54cd4c27 + github.com/caddyserver/certmagic v0.25.0 + github.com/gdamore/tcell/v2 v2.9.0 + github.com/ipfs/go-log/v2 v2.9.0 + github.com/ipshipyard/p2p-forge v0.6.1 + github.com/libp2p/go-libp2p v0.45.0 + github.com/libp2p/go-libp2p-kad-dht v0.35.1 + github.com/libp2p/go-libp2p-pubsub v0.15.0 + github.com/multiformats/go-multiaddr v0.16.1 + github.com/rivo/tview v0.42.0 ) require ( @@ -21,60 +19,52 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/containerd/cgroups v1.1.0 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/elastic/gosigar v0.14.3 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/filecoin-project/go-clock v0.1.0 // indirect github.com/flynn/noise v1.1.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect github.com/gdamore/encoding v1.0.1 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gopacket v1.1.19 // indirect - github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/huin/goupnp v1.3.0 // indirect - github.com/ipfs/boxo v0.27.4 // indirect - github.com/ipfs/go-cid v0.5.0 // indirect - github.com/ipfs/go-datastore v0.6.0 // indirect + github.com/ipfs/boxo v0.35.1 // indirect + github.com/ipfs/go-cid v0.6.0 // indirect + github.com/ipfs/go-datastore v0.9.0 // indirect github.com/ipld/go-ipld-prime v0.21.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect - github.com/jbenet/goprocess v0.1.4 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect - github.com/koron/go-ssdp v0.0.5 // indirect - github.com/libdns/libdns v0.2.2 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/koron/go-ssdp v0.1.0 // indirect + github.com/libdns/libdns v1.1.1 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/libp2p/go-cidranger v1.1.0 // indirect - github.com/libp2p/go-flow-metrics v0.2.0 // indirect + github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kbucket v0.6.5 // indirect + github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-record v0.3.1 // indirect - github.com/libp2p/go-libp2p-routing-helpers v0.7.4 // indirect + github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect - github.com/libp2p/go-nat v0.2.0 // indirect - github.com/libp2p/go-netroute v0.2.2 // indirect + github.com/libp2p/go-netroute v0.4.0 // indirect github.com/libp2p/go-reuseport v0.4.0 // indirect - github.com/libp2p/go-yamux/v4 v4.0.2 // indirect + github.com/libp2p/go-yamux/v5 v5.1.0 // indirect github.com/libp2p/zeroconf/v2 v2.2.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mholt/acmez/v3 v3.0.1 // indirect - github.com/miekg/dns v1.1.63 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mholt/acmez/v3 v3.1.4 // indirect + github.com/miekg/dns v1.1.68 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/minio/sha256-simd v1.0.1 // indirect @@ -84,75 +74,67 @@ require ( github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect - github.com/multiformats/go-multicodec v0.9.0 // indirect + github.com/multiformats/go-multicodec v0.10.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect - github.com/multiformats/go-multistream v0.6.0 // indirect - github.com/multiformats/go-varint v0.0.7 // indirect + github.com/multiformats/go-multistream v0.6.1 // indirect + github.com/multiformats/go-varint v0.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.22.2 // indirect - github.com/opencontainers/runtime-spec v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/dtls/v3 v3.0.4 // indirect - github.com/pion/ice/v2 v2.3.37 // indirect - github.com/pion/ice/v4 v4.0.6 // indirect - github.com/pion/interceptor v0.1.37 // indirect - github.com/pion/logging v0.2.3 // indirect - github.com/pion/mdns v0.0.12 // indirect + github.com/pion/dtls/v3 v3.0.7 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/interceptor v0.1.41 // indirect + github.com/pion/logging v0.2.4 // indirect github.com/pion/mdns/v2 v2.0.7 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/rtcp v1.2.15 // indirect - github.com/pion/rtp v1.8.11 // indirect - github.com/pion/sctp v1.8.35 // indirect - github.com/pion/sdp/v3 v3.0.10 // indirect - github.com/pion/srtp/v3 v3.0.4 // indirect + github.com/pion/rtcp v1.2.16 // indirect + github.com/pion/rtp v1.8.25 // indirect + github.com/pion/sctp v1.8.40 // indirect + github.com/pion/sdp/v3 v3.0.16 // indirect + github.com/pion/srtp/v3 v3.0.8 // indirect github.com/pion/stun v0.6.1 // indirect - github.com/pion/stun/v3 v3.0.0 // indirect + github.com/pion/stun/v3 v3.0.1 // indirect github.com/pion/transport/v2 v2.2.10 // indirect - github.com/pion/transport/v3 v3.0.7 // indirect - github.com/pion/turn/v2 v2.1.6 // indirect - github.com/pion/turn/v4 v4.0.0 // indirect - github.com/pion/webrtc/v4 v4.0.9 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/pion/transport/v3 v3.0.8 // indirect + github.com/pion/turn/v4 v4.1.2 // indirect + github.com/pion/webrtc/v4 v4.1.6 // indirect github.com/polydawn/refmt v0.89.0 // indirect - github.com/prometheus/client_golang v1.20.5 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.49.0 // indirect - github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect - github.com/raulk/go-watchdog v1.3.0 // indirect + github.com/quic-go/quic-go v0.55.0 // indirect + github.com/quic-go/webtransport-go v0.9.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/stretchr/testify v1.10.0 // indirect github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect github.com/wlynxg/anet v0.0.5 // indirect github.com/zeebo/blake3 v0.2.4 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.34.0 // indirect - go.uber.org/dig v1.18.0 // indirect - go.uber.org/fx v1.23.0 // indirect - go.uber.org/mock v0.5.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/fx v1.24.0 // indirect + go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect - golang.org/x/crypto v0.33.0 // indirect - golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect - golang.org/x/mod v0.23.0 // indirect - golang.org/x/net v0.35.0 // indirect - golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/term v0.29.0 // indirect - golang.org/x/text v0.22.0 // indirect - golang.org/x/tools v0.30.0 // indirect - gonum.org/v1/gonum v0.15.1 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - lukechampine.com/blake3 v1.3.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/telemetry v0.0.0-20251105150722-cbe4531f26c3 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.38.0 // indirect + gonum.org/v1/gonum v0.16.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + lukechampine.com/blake3 v1.4.1 // indirect ) diff --git a/go-peer/go.sum b/go-peer/go.sum index 87b41b7c..e2275086 100644 --- a/go-peer/go.sum +++ b/go-peer/go.sum @@ -9,8 +9,6 @@ dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -18,46 +16,32 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg= -github.com/caddyserver/certmagic v0.21.7/go.mod h1:LCPG3WLxcnjVKl/xpjzM0gqh0knrKKKiO5WVttX2eEI= +github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic= +github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= -github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= -github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= -github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= -github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= -github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= -github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= +github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= @@ -66,67 +50,38 @@ github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiD github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= -github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= -github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= +github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys= +github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc= -github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= @@ -138,58 +93,47 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= -github.com/ipfs/boxo v0.27.4 h1:6nC8lY5GnR6whAbW88hFz6L13wZUj2vr5BRe3iTvYBI= -github.com/ipfs/boxo v0.27.4/go.mod h1:qEIRrGNr0bitDedTCzyzBHxzNWqYmyuHgK8LG9Q83EM= -github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= -github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= -github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= -github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= -github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= -github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= +github.com/ipfs/boxo v0.35.1 h1:MGL3aaaxnu/h9KKq+X/6FxapI/qlDmnRNk33U7tz/fQ= +github.com/ipfs/boxo v0.35.1/go.mod h1:/p1XZVp+Yzv78RuKjb3BESBYEQglRgDrWvmN5mFrsus= +github.com/ipfs/go-block-format v0.2.3 h1:mpCuDaNXJ4wrBJLrtEaGFGXkferrw5eqVvzaHhtFKQk= +github.com/ipfs/go-block-format v0.2.3/go.mod h1:WJaQmPAKhD3LspLixqlqNFxiZ3BZ3xgqxxoSR/76pnA= +github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30= +github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ= +github.com/ipfs/go-datastore v0.9.0 h1:WocriPOayqalEsueHv6SdD4nPVl4rYMfYGLD4bqCZ+w= +github.com/ipfs/go-datastore v0.9.0/go.mod h1:uT77w/XEGrvJWwHgdrMr8bqCN6ZTW9gzmi+3uK+ouHg= github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= -github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= -github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= -github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= -github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= -github.com/ipfs/go-test v0.0.4 h1:DKT66T6GBB6PsDFLoO56QZPrOmzJkqU1FZH5C9ySkew= -github.com/ipfs/go-test v0.0.4/go.mod h1:qhIM1EluEfElKKM6fnWxGn822/z9knUGM1+I/OAQNKI= +github.com/ipfs/go-log/v2 v2.9.0 h1:l4b06AwVXwldIzbVPZy5z7sKp9lHFTX0KWfTBCtHaOk= +github.com/ipfs/go-log/v2 v2.9.0/go.mod h1:UhIYAwMV7Nb4ZmihUxfIRM2Istw/y9cAk3xaK+4Zs2c= +github.com/ipfs/go-test v0.2.3 h1:Z/jXNAReQFtCYyn7bsv/ZqUwS6E7iIcSpJ2CuzCvnrc= +github.com/ipfs/go-test v0.2.3/go.mod h1:QW8vSKkwYvWFwIZQLGQXdkt9Ud76eQXRQ9Ao2H+cA1o= github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= -github.com/ipshipyard/p2p-forge v0.3.1 h1:Vr0l6wzX4zL7l8+UHJlsRBNHmlHpP3c//NrCZeGj4KU= -github.com/ipshipyard/p2p-forge v0.3.1/go.mod h1:XQAvFJeXGo4oiyVPXkC3cph//5kF785L5Pjd3/kWFWo= +github.com/ipshipyard/p2p-forge v0.6.1 h1:987/hUC1YxI56CcMX6iTB+9BLjFV0d2SJnig9Z1pf8A= +github.com/ipshipyard/p2p-forge v0.6.1/go.mod h1:pj8Zcs+ex5OMq5a1bFLHqW0oL3qYO0v5eGLZmit0l7U= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= -github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= -github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= -github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= -github.com/koron/go-ssdp v0.0.5 h1:E1iSMxIs4WqxTbIBLtmNBeOOC+1sCIXQeqTWVnpmwhk= -github.com/koron/go-ssdp v0.0.5/go.mod h1:Qm59B7hpKpDqfyRNWRNr00jGwLdXjDyZh6y7rH6VS0w= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/koron/go-ssdp v0.1.0 h1:ckl5x5H6qSNFmi+wCuROvvGUu2FQnMbQrU95IHCcv3Y= +github.com/koron/go-ssdp v0.1.0/go.mod h1:GltaDBjtK1kemZOusWYLGotV0kBeEf59Bp0wtSB0uyU= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -199,61 +143,59 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= -github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= +github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= -github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= -github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= -github.com/libp2p/go-libp2p v0.39.1 h1:1Ur6rPCf3GR+g8jkrnaQaM0ha2IGespsnNlCqJLLALE= -github.com/libp2p/go-libp2p v0.39.1/go.mod h1:3zicI8Lp7Isun+Afo/JOACUbbJqqR2owK6RQWFsVAbI= +github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= +github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= +github.com/libp2p/go-libp2p v0.45.0 h1:Pdhr2HsFXaYjtfiNcBP4CcRUONvbMFdH3puM9vV4Tiw= +github.com/libp2p/go-libp2p v0.45.0/go.mod h1:NovCojezAt4dnDd4fH048K7PKEqH0UFYYqJRjIIu8zc= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= -github.com/libp2p/go-libp2p-kad-dht v0.29.0 h1:045eW21lGlMSD9aKSZZGH4fnBMIInPwQLxIQ35P962I= -github.com/libp2p/go-libp2p-kad-dht v0.29.0/go.mod h1:mIci3rHSwDsxQWcCjfmxD8vMTgh5xLuvwb1D5WP8ZNk= -github.com/libp2p/go-libp2p-kbucket v0.6.5 h1:Fsl1YvZcMwqrR4DYrTO02yo9PGYs2HBQIT3lGXFMTxg= -github.com/libp2p/go-libp2p-kbucket v0.6.5/go.mod h1:U6WOd0BvnSp03IQSrjgM54tg7zh1UUNsXLJqAQzClTA= -github.com/libp2p/go-libp2p-pubsub v0.13.0 h1:RmFQ2XAy3zQtbt2iNPy7Tt0/3fwTnHpCQSSnmGnt1Ps= -github.com/libp2p/go-libp2p-pubsub v0.13.0/go.mod h1:m0gpUOyrXKXdE7c8FNQ9/HLfWbxaEw7xku45w+PaqZo= +github.com/libp2p/go-libp2p-kad-dht v0.35.1 h1:RQglhc9OxqDwlFFdhQMwKxIPBIBfGsleROnK5hqVsoE= +github.com/libp2p/go-libp2p-kad-dht v0.35.1/go.mod h1:1oCXzkkBiYh3d5cMWLpInSOZ6am2AlpC4G+GDcZFcE0= +github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= +github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= +github.com/libp2p/go-libp2p-pubsub v0.15.0 h1:cG7Cng2BT82WttmPFMi50gDNV+58K626m/wR00vGL1o= +github.com/libp2p/go-libp2p-pubsub v0.15.0/go.mod h1:lr4oE8bFgQaifRcoc2uWhWWiK6tPdOEKpUuR408GFN4= github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg= github.com/libp2p/go-libp2p-record v0.3.1/go.mod h1:T8itUkLcWQLCYMqtX7Th6r7SexyUJpIyPgks757td/E= -github.com/libp2p/go-libp2p-routing-helpers v0.7.4 h1:6LqS1Bzn5CfDJ4tzvP9uwh42IB7TJLNFJA6dEeGBv84= -github.com/libp2p/go-libp2p-routing-helpers v0.7.4/go.mod h1:we5WDj9tbolBXOuF1hGOkR+r7Uh1408tQbAKaT5n1LE= +github.com/libp2p/go-libp2p-routing-helpers v0.7.5 h1:HdwZj9NKovMx0vqq6YNPTh6aaNzey5zHD7HeLJtq6fI= +github.com/libp2p/go-libp2p-routing-helpers v0.7.5/go.mod h1:3YaxrwP0OBPDD7my3D0KxfR89FlcX/IEbxDEDfAmj98= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= -github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk= -github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk= -github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8= -github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE= +github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= +github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= -github.com/libp2p/go-yamux/v4 v4.0.2 h1:nrLh89LN/LEiqcFiqdKDRHjGstN300C1269K/EX0CPU= -github.com/libp2p/go-yamux/v4 v4.0.2/go.mod h1:C808cCRgOs1iBwY4S71T5oxgMxgLmqUw56qh4AeBW2o= +github.com/libp2p/go-yamux/v5 v5.1.0 h1:8Qlxj4E9JGJAQVW6+uj2o7mqkqsIVlSUGmTWhlXzoHE= +github.com/libp2p/go-yamux/v5 v5.1.0/go.mod h1:tgIQ07ObtRR/I0IWsFOyQIL9/dR5UXgc2s8xKmNZv1o= github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q= github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marcopolo/simnet v0.0.1 h1:rSMslhPz6q9IvJeFWDoMGxMIrlsbXau3NkuIXHGJxfg= +github.com/marcopolo/simnet v0.0.1/go.mod h1:WDaQkgLAjqDUEBAOXz22+1j6wXKfGlC5sD5XWt3ddOs= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mholt/acmez/v3 v3.0.1 h1:4PcjKjaySlgXK857aTfDuRbmnM5gb3Ruz3tvoSJAUp8= -github.com/mholt/acmez/v3 v3.0.1/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= +github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ= +github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= -github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= +github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= @@ -274,34 +216,27 @@ github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYg github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= -github.com/multiformats/go-multiaddr v0.14.0 h1:bfrHrJhrRuh/NXH5mCnemjpbGjzRw/b+tJFOD41g2tU= -github.com/multiformats/go-multiaddr v0.14.0/go.mod h1:6EkVAxtznq2yC3QT5CM1UTAwG0GTP3EWAIcjHuzQ+r4= +github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= +github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= -github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= -github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= +github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= -github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA= -github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg= -github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= -github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= +github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= +github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= -github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= -github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= -github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= @@ -310,88 +245,73 @@ github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oL github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U= -github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg= -github.com/pion/ice/v2 v2.3.37 h1:ObIdaNDu1rCo7hObhs34YSBcO7fjslJMZV0ux+uZWh0= -github.com/pion/ice/v2 v2.3.37/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= -github.com/pion/ice/v4 v4.0.6 h1:jmM9HwI9lfetQV/39uD0nY4y++XZNPhvzIPCb8EwxUM= -github.com/pion/ice/v4 v4.0.6/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= -github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= -github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= +github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q= +github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.41 h1:NpvX3HgWIukTf2yTBVjVGFXtpSpWgXjqz7IIpu7NsOw= +github.com/pion/interceptor v0.1.41/go.mod h1:nEt4187unvRXJFyjiw00GKo+kIuXMWQI9K89fsosDLY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= -github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= -github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= -github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= -github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= -github.com/pion/rtp v1.8.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk= -github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= -github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA= -github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg= -github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA= -github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= -github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= -github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= +github.com/pion/rtp v1.8.25 h1:b8+y44GNbwOJTYWuVan7SglX/hMlicVCAtL50ztyZHw= +github.com/pion/rtp v1.8.25/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= +github.com/pion/sctp v1.8.40 h1:bqbgWYOrUhsYItEnRObUYZuzvOMsVplS3oNgzedBlG8= +github.com/pion/sctp v1.8.40/go.mod h1:SPBBUENXE6ThkEksN5ZavfAhFYll+h+66ZiG6IZQuzo= +github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo= +github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo= +github.com/pion/srtp/v3 v3.0.8 h1:RjRrjcIeQsilPzxvdaElN0CpuQZdMvcl9VZ5UY9suUM= +github.com/pion/srtp/v3 v3.0.8/go.mod h1:2Sq6YnDH7/UDCvkSoHSDNDeyBcFgWL0sAVycVbAsXFg= github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= -github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= -github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/stun/v3 v3.0.1 h1:jx1uUq6BdPihF0yF33Jj2mh+C9p0atY94IkdnW174kA= +github.com/pion/stun/v3 v3.0.1/go.mod h1:RHnvlKFg+qHgoKIqtQWMOJF52wsImCAf/Jh5GjX+4Tw= github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= -github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= -github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= -github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= -github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= -github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= -github.com/pion/webrtc/v4 v4.0.9 h1:PyOYMRKJgfy0dzPcYtFD/4oW9zaw3Ze3oZzzbj2LV9E= -github.com/pion/webrtc/v4 v4.0.9/go.mod h1:ViHLVaNpiuvaH8pdiuQxuA9awuE6KVzAXx3vVWilOck= +github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc= +github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= +github.com/pion/turn/v4 v4.1.2 h1:Em2svpl6aBFa88dLhxypMUzaLjC79kWZWx8FIov01cc= +github.com/pion/turn/v4 v4.1.2/go.mod h1:ISYWfZYy0Z3tXzRpyYZHTL+U23yFQIspfxogdQ8pn9Y= +github.com/pion/webrtc/v4 v4.1.6 h1:srHH2HwvCGwPba25EYJgUzgLqCQoXl1VCUnrGQMSzUw= +github.com/pion/webrtc/v4 v4.1.6/go.mod h1:wKecGRlkl3ox/As/MYghJL+b/cVXMEhoPMJWPuGQFhU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.49.0 h1:w5iJHXwHxs1QxyBv1EHKuC50GX5to8mJAxvtnttJp94= -github.com/quic-go/quic-go v0.49.0/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s= -github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg= -github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= -github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= -github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= -github.com/rivo/tview v0.0.0-20241030223020-e34b54cd4c27 h1:jXLPO4iCqeAJkP5nNu5q1Iax0RBcOz8slK9Rm31eY40= -github.com/rivo/tview v0.0.0-20241030223020-e34b54cd4c27/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= +github.com/quic-go/webtransport-go v0.9.0 h1:jgys+7/wm6JarGDrW+lD/r9BGqBAmqY/ssklE09bA70= +github.com/quic-go/webtransport-go v0.9.0/go.mod h1:4FUYIiUc75XSsF6HShcLeXXYZJ9AGwo/xh3L8M/P1ao= +github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c= +github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -418,7 +338,6 @@ github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go. github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= @@ -431,18 +350,14 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= @@ -455,7 +370,6 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= @@ -464,34 +378,30 @@ github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCR github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= -go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= -go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -506,25 +416,22 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -538,10 +445,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -549,8 +454,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -566,39 +471,33 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251105150722-cbe4531f26c3 h1:AaZNtU/Wqf10yau+xxHWRIpmo7fkP7wmq7spiOFpuCA= +golang.org/x/telemetry v0.0.0-20251105150722-cbe4531f26c3/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -606,9 +505,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -618,36 +516,32 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= -golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= -gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= @@ -660,48 +554,28 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= -lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/go-peer/identity.go b/go-peer/identity.go index 5fdedd53..535c50f8 100644 --- a/go-peer/identity.go +++ b/go-peer/identity.go @@ -47,4 +47,4 @@ func GenerateIdentity(path string) (crypto.PrivKey, error) { err = os.WriteFile(path, bytes, 0400) return privk, err -} +} \ No newline at end of file diff --git a/go-peer/main.go b/go-peer/main.go index 5ec66ab5..f427fce5 100644 --- a/go-peer/main.go +++ b/go-peer/main.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "math" + "math/rand" "os" "time" @@ -34,8 +35,9 @@ import ( // DiscoveryInterval is how often we re-publish our mDNS records. const DiscoveryInterval = time.Hour -// DiscoveryServiceTag is used in our mDNS advertisements to discover other chat peers. -const DiscoveryServiceTag = "universal-connectivity" +// DefaultRoom is used as the gossipsub topic to join and the DiscoveryServiceTag in mDNS advertisements. +// It can be overridden by the -room flag. The concept of different rooms is only supported by mDNS in Go. +const DefaultRoom = "universal-connectivity" var SysMsgChan chan *ChatMessage @@ -45,7 +47,6 @@ var logger = log.Logger("app") // NewDHT attempts to connect to a bunch of bootstrap peers and returns a new DHT. // If you don't have any bootstrapPeers, you can use dht.DefaultBootstrapPeers or an empty list. func NewDHT(ctx context.Context, host host.Host, bootstrapPeers []multiaddr.Multiaddr) (*dht.IpfsDHT, error) { - kdht, err := dht.New(ctx, host, dht.BootstrapPeers(dht.GetDefaultBootstrapPeerAddrInfos()...), dht.Mode(dht.ModeAuto), @@ -64,10 +65,10 @@ func NewDHT(ctx context.Context, host host.Host, bootstrapPeers []multiaddr.Mult // Borrowed from https://medium.com/rahasak/libp2p-pubsub-peer-discovery-with-kademlia-dht-c8b131550ac7 // Only used by Go peer to find each other. // TODO: since this isn't implemented on the Rust or the JS side, can probably be removed -func Discover(ctx context.Context, h host.Host, dht *dht.IpfsDHT) { +func Discover(ctx context.Context, h host.Host, dht *dht.IpfsDHT, tag string) { routingDiscovery := routing.NewRoutingDiscovery(dht) - discovery.Advertise(ctx, routingDiscovery, DiscoveryServiceTag) + discovery.Advertise(ctx, routingDiscovery, tag) ticker := time.NewTicker(time.Second * 10) defer ticker.Stop() @@ -78,7 +79,7 @@ func Discover(ctx context.Context, h host.Host, dht *dht.IpfsDHT) { return case <-ticker.C: - peers, err := discovery.FindPeers(ctx, routingDiscovery, DiscoveryServiceTag) + peers, err := discovery.FindPeers(ctx, routingDiscovery, tag) if err != nil { panic(err) } @@ -108,14 +109,34 @@ func main() { // parse some flags to set our nickname and the room to join nickFlag := flag.String("nick", "", "nickname to use in chat. will be generated if empty") idPath := flag.String("identity", "identity.key", "path to the private key (PeerID) file") + roomFlag := flag.String("room", DefaultRoom, "the gossipsub topic / room to join (mDNS only)") headless := flag.Bool("headless", false, "run without chat UI") + port := flag.String("port", "9095", "port to listen on") var addrsToConnectTo stringSlice flag.Var(&addrsToConnectTo, "connect", "address to connect to (can be used multiple times)") flag.Parse() - log.SetLogLevel("app", "debug") + err := log.SetLogLevel("*", "ERROR") + if err != nil { + fmt.Printf("failed to set log level: %s", err) + os.Exit(1) + } + + if !*headless { + err = log.SetLogLevel("app", "ERROR") + if err != nil { + fmt.Printf("failed to set log level: %s", err) + os.Exit(1) + } + } else { + err = log.SetLogLevel("app", "INFO") + if err != nil { + fmt.Printf("failed to set log level: %s", err) + os.Exit(1) + } + } ctx := context.Background() @@ -153,16 +174,16 @@ func main() { libp2p.Identity(privk), libp2p.NATPortMap(), libp2p.ListenAddrStrings( - "/ip4/0.0.0.0/tcp/9095", - "/ip4/0.0.0.0/udp/9095/quic-v1", - "/ip4/0.0.0.0/udp/9095/quic-v1/webtransport", - "/ip4/0.0.0.0/udp/9095/webrtc-direct", - "/ip6/::/tcp/9095", - "/ip6/::/udp/9095/quic-v1", - "/ip6/::/udp/9095/quic-v1/webtransport", - "/ip6/::/udp/9095/webrtc-direct", - fmt.Sprintf("/ip4/0.0.0.0/tcp/9095/tls/sni/*.%s/ws", p2pforge.DefaultForgeDomain), - fmt.Sprintf("/ip6/::/tcp/9095/tls/sni/*.%s/ws", p2pforge.DefaultForgeDomain), + "/ip4/0.0.0.0/tcp/"+*port, + "/ip4/0.0.0.0/udp/"+*port+"/quic-v1", + "/ip4/0.0.0.0/udp/"+*port+"/quic-v1/webtransport", + "/ip4/0.0.0.0/udp/"+*port+"/webrtc-direct", + "/ip6/::/tcp/"+*port, + "/ip6/::/udp/"+*port+"/quic-v1", + "/ip6/::/udp/"+*port+"/quic-v1/webtransport", + "/ip6/::/udp/"+*port+"/webrtc-direct", + fmt.Sprintf("/ip4/0.0.0.0/tcp/"+*port+"/tls/sni/*.%s/ws", p2pforge.DefaultForgeDomain), + fmt.Sprintf("/ip6/::/tcp/"+*port+"/tls/sni/*.%s/ws", p2pforge.DefaultForgeDomain), ), libp2p.ResourceManager(getResourceManager()), libp2p.Transport(webtransport.New), @@ -206,12 +227,12 @@ func main() { // use the nickname from the cli flag, or a default if blank nick := *nickFlag - if len(nick) == 0 { + if nick == "" { nick = defaultNick(h.ID()) } // join the chat room - cr, err := JoinChatRoom(ctx, h, ps, nick) + cr, err := JoinChatRoom(ctx, h, ps, nick, *roomFlag) if err != nil { panic(err) } @@ -220,16 +241,16 @@ func main() { // setup DHT with empty discovery peers // so this will be a discovery peer for others // this peer should run on cloud(with public ip address) - dht, err := NewDHT(ctx, h, nil) + hDHT, err := NewDHT(ctx, h, nil) if err != nil { panic(err) } // setup peer discovery - go Discover(ctx, h, dht) + go Discover(ctx, h, hDHT, *roomFlag) // setup local mDNS discovery - if err := setupDiscovery(h); err != nil { + if err := setupDiscovery(h, *roomFlag); err != nil { panic(err) } @@ -260,15 +281,19 @@ func main() { return case <-ticker.C: rm := h.Network().ResourceManager() - rm.ViewSystem( + + err = rm.ViewSystem( func(rs network.ResourceScope) error { - fmt.Printf("Stats: %+v\n", rs.Stat()) + LogMsgf("System Stats: %+v", rs.Stat()) if r, ok := rs.(interface{ Limit() rcmgr.Limit }); ok { - fmt.Printf("Limits: %+v\n", r.Limit()) + LogMsgf("System Limits: %+v", r.Limit()) } return nil }, ) + if err != nil { + LogMsgf("ViewSystem error: %s", err) + } } } }() @@ -305,7 +330,7 @@ func main() { } // printErr is like fmt.Printf, but writes to stderr. -func printErr(m string, args ...interface{}) { +func printErr(m string, args ...any) { fmt.Fprintf(os.Stderr, m, args...) } @@ -330,18 +355,27 @@ type discoveryNotifee struct { // the PubSub system will automatically start interacting with them if they also // support PubSub. func (n *discoveryNotifee) HandlePeerFound(pi peer.AddrInfo) { - LogMsgf("discovered new peer %s", pi.ID.String()) - err := n.h.Connect(context.Background(), pi) - if err != nil { - LogMsgf("error connecting to peer %s: %s", pi.ID.String(), err) - } + LogMsgf("mDNS discovered new peer %s", pi.ID.String()) + + go func(pi peer.AddrInfo) { + // add 1 second jitter to avoid all peers connecting at the same time + time.Sleep(time.Duration(1+rand.Intn(999)) * time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + err := n.h.Connect(ctx, pi) + if err != nil { + LogMsgf("error connecting to mDNS peer %s: %s", pi.ID.String(), err) + } + }(pi) } // setupDiscovery creates an mDNS discovery service and attaches it to the libp2p Host. // This lets us automatically discover peers on the same LAN and connect to them. -func setupDiscovery(h host.Host) error { +func setupDiscovery(h host.Host, serviceName string) error { // setup mDNS discovery to find local peers - s := mdns.NewMdnsService(h, DiscoveryServiceTag, &discoveryNotifee{h: h}) + s := mdns.NewMdnsService(h, serviceName, &discoveryNotifee{h: h}) return s.Start() } @@ -374,9 +408,11 @@ func getResourceManager() network.ResourceManager { StreamBaseLimit: baseLimits, } cl := scl.Scale(0, 0) - rcmgr, err := rcmgr.NewResourceManager(rcmgr.NewFixedLimiter(cl)) + + resourceMaanger, err := rcmgr.NewResourceManager(rcmgr.NewFixedLimiter(cl)) if err != nil { panic(err) } - return rcmgr + + return resourceMaanger } diff --git a/go-peer/ui.go b/go-peer/ui.go index 13d4d040..36eb42b3 100644 --- a/go-peer/ui.go +++ b/go-peer/ui.go @@ -6,7 +6,6 @@ import ( "time" "github.com/gdamore/tcell/v2" - "github.com/libp2p/go-libp2p/core/peer" "github.com/rivo/tview" ) @@ -18,43 +17,35 @@ type ChatUI struct { cr *ChatRoom app *tview.Application peersList *tview.TextView - - msgW io.Writer - sysW io.Writer - inputCh chan string - doneCh chan struct{} + msgBox *tview.TextView + sysBox *tview.TextView + + msgW io.Writer + sysW io.Writer + inputCh chan string + doneCh chan struct{} + uiUpdateChan chan func() } // NewChatUI returns a new ChatUI struct that controls the text UI. // It won't actually do anything until you call Run(). func NewChatUI(cr *ChatRoom) *ChatUI { app := tview.NewApplication() + app.EnableMouse(true) // make a text view to contain our chat messages msgBox := tview.NewTextView() msgBox.SetDynamicColors(true) msgBox.SetBorder(true) msgBox.SetTitle(fmt.Sprintf("Room: %s", cr.roomName)) - - // text views are io.Writers, but they don't automatically refresh. - // this sets a change handler to force the app to redraw when we get - // new messages to display. - msgBox.SetChangedFunc(func() { - app.Draw() - }) + msgBox.SetScrollable(true) // make a text view to contain our error messages sysBox := tview.NewTextView() sysBox.SetDynamicColors(true) sysBox.SetBorder(true) sysBox.SetTitle("System") - - // text views are io.Writers, but they don't automatically refresh. - // this sets a change handler to force the app to redraw when we get - // new messages to display. - sysBox.SetChangedFunc(func() { - app.Draw() - }) + sysBox.SetScrollable(true) // an input field for typing messages into inputCh := make(chan string, 32) @@ -70,7 +61,7 @@ func NewChatUI(cr *ChatRoom) *ChatUI { return } line := input.GetText() - if len(line) == 0 { + if line == "" { // ignore blank lines return } @@ -90,7 +81,6 @@ func NewChatUI(cr *ChatRoom) *ChatUI { peersList := tview.NewTextView() peersList.SetBorder(true) peersList.SetTitle("Peers") - peersList.SetChangedFunc(func() { app.Draw() }) // chatPanel is a horizontal box with messages on the left and peers on the right // the peers list takes 20 columns, and the messages take the remaining space @@ -99,7 +89,6 @@ func NewChatUI(cr *ChatRoom) *ChatUI { AddItem(peersList, 20, 1, false) // flex is a vertical box with the chatPanel on top and the input field at the bottom. - flex := tview.NewFlex(). SetDirection(tview.FlexRow). AddItem(chatPanel, 0, 3, false). @@ -109,13 +98,22 @@ func NewChatUI(cr *ChatRoom) *ChatUI { app.SetRoot(flex, true) return &ChatUI{ - cr: cr, - app: app, - peersList: peersList, - msgW: msgBox, - sysW: sysBox, - inputCh: inputCh, - doneCh: make(chan struct{}, 1), + cr: cr, + app: app, + peersList: peersList, + msgBox: msgBox, + sysBox: sysBox, + msgW: msgBox, + sysW: sysBox, + inputCh: inputCh, + doneCh: make(chan struct{}, 1), + uiUpdateChan: make(chan func(), 256), + } +} + +func (ui *ChatUI) uiUpdater() { + for f := range ui.uiUpdateChan { + ui.app.QueueUpdateDraw(f) } } @@ -123,6 +121,7 @@ func NewChatUI(cr *ChatRoom) *ChatUI { // the event loop for the text UI. func (ui *ChatUI) Run() error { go ui.handleEvents() + go ui.uiUpdater() defer ui.end() return ui.app.Run() @@ -130,6 +129,7 @@ func (ui *ChatUI) Run() error { // end signals the event loop to exit gracefully func (ui *ChatUI) end() { + close(ui.uiUpdateChan) ui.doneCh <- struct{}{} } @@ -144,16 +144,14 @@ func (ui *ChatUI) refreshPeers() { for _, p := range peers { fmt.Fprintln(ui.peersList, shortID(p)) } - - ui.app.Draw() } // displayChatMessage writes a ChatMessage from the room to the message window, // with the sender's nick highlighted in green. func (ui *ChatUI) displayChatMessage(cm *ChatMessage) { - p := peer.ID(cm.SenderID) - prompt := withColor("green", fmt.Sprintf("<%s>:", shortID(p))) + prompt := withColor("green", fmt.Sprintf("<%s>:", cm.SenderNick)) fmt.Fprintf(ui.msgW, "%s %s\n", prompt, cm.Message) + ui.msgBox.ScrollToEnd() } // displayChatMessage writes a ChatMessage from the room to the message window, @@ -161,6 +159,7 @@ func (ui *ChatUI) displayChatMessage(cm *ChatMessage) { func (ui *ChatUI) displaySysMessage(cm *ChatMessage) { fmt.Fprintf(ui.sysW, "%s\n", cm.Message) logger.Info(cm.Message) + ui.sysBox.ScrollToEnd() } // displaySelfMessage writes a message from ourself to the message window, @@ -168,6 +167,7 @@ func (ui *ChatUI) displaySysMessage(cm *ChatMessage) { func (ui *ChatUI) displaySelfMessage(msg string) { prompt := withColor("yellow", fmt.Sprintf("<%s>:", ui.cr.nick)) fmt.Fprintf(ui.msgW, "%s %s\n", prompt, msg) + ui.msgBox.ScrollToEnd() } // handleEvents runs an event loop that sends user input to the chat room @@ -185,19 +185,31 @@ func (ui *ChatUI) handleEvents() { if err != nil { printErr("publish error: %s", err) } - ui.displaySelfMessage(input) + + ui.app.QueueUpdateDraw(func() { + if err != nil { + fmt.Fprintf(ui.sysW, "[red]publish error: %s[-]\n", err) + } + ui.displaySelfMessage(input) + }) case m := <-ui.cr.Messages: // when we receive a message from the chat room, print it to the message window - ui.displayChatMessage(m) + ui.app.QueueUpdateDraw(func() { + ui.displayChatMessage(m) + }) case s := <-ui.cr.SysMessages: - // when we receive a message from the chat room, print it to the message window - ui.displaySysMessage(s) + // when we receive a system message, print it to the system window + ui.app.QueueUpdateDraw(func() { + ui.displaySysMessage(s) + }) case <-peerRefreshTicker.C: // refresh the list of peers in the chat room periodically - ui.refreshPeers() + ui.app.QueueUpdateDraw(func() { + ui.refreshPeers() + }) case <-ui.cr.ctx.Done(): return diff --git a/js-peer/package-lock.json b/js-peer/package-lock.json index 1148a1f6..aa4fec68 100644 --- a/js-peer/package-lock.json +++ b/js-peer/package-lock.json @@ -30,7 +30,7 @@ "it-pipe": "^3.0.1", "it-protobuf-stream": "^1.1.5", "libp2p": "^2.7.4", - "next": "14.2.13", + "next": "14.2.25", "protons-runtime": "^5.5.0", "react": "18.3.1", "react-18-blockies": "^1.0.6", @@ -3527,9 +3527,9 @@ "integrity": "sha512-HzdtdBwxsIkzpeXzhQ5mAhhuxcHbjEHH+JQoxt7hG/2HGFjjwyolLo7hbaexcnhoEuV4e0TNJ8kkpMjiEYY4VQ==" }, "node_modules/@next/env": { - "version": "14.2.13", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.13.tgz", - "integrity": "sha512-s3lh6K8cbW1h5Nga7NNeXrbe0+2jIIYK9YaA9T7IufDWnZpozdFUp6Hf0d5rNWUKu4fEuSX2rCKlGjCrtylfDw==", + "version": "14.2.25", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.25.tgz", + "integrity": "sha512-JnzQ2cExDeG7FxJwqAksZ3aqVJrHjFwZQAEJ9gQZSoEhIow7SNoKZzju/AwQ+PLIR4NY8V0rhcVozx/2izDO0w==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -3588,9 +3588,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.13", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.13.tgz", - "integrity": "sha512-IkAmQEa2Htq+wHACBxOsslt+jMoV3msvxCn0WFSfJSkv/scy+i/EukBKNad36grRxywaXUYJc9mxEGkeIs8Bzg==", + "version": "14.2.25", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.25.tgz", + "integrity": "sha512-09clWInF1YRd6le00vt750s3m7SEYNehz9C4PUcSu3bAdCTpjIV4aTYQZ25Ehrr83VR1rZeqtKUPWSI7GfuKZQ==", "cpu": [ "arm64" ], @@ -3604,9 +3604,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.13", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.13.tgz", - "integrity": "sha512-Dv1RBGs2TTjkwEnFMVL5XIfJEavnLqqwYSD6LXgTPdEy/u6FlSrLBSSfe1pcfqhFEXRAgVL3Wpjibe5wXJzWog==", + "version": "14.2.25", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.25.tgz", + "integrity": "sha512-V+iYM/QR+aYeJl3/FWWU/7Ix4b07ovsQ5IbkwgUK29pTHmq+5UxeDr7/dphvtXEq5pLB/PucfcBNh9KZ8vWbug==", "cpu": [ "x64" ], @@ -3620,9 +3620,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.13", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.13.tgz", - "integrity": "sha512-yB1tYEFFqo4ZNWkwrJultbsw7NPAAxlPXURXioRl9SdW6aIefOLS+0TEsKrWBtbJ9moTDgU3HRILL6QBQnMevg==", + "version": "14.2.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.25.tgz", + "integrity": "sha512-LFnV2899PJZAIEHQ4IMmZIgL0FBieh5keMnriMY1cK7ompR+JUd24xeTtKkcaw8QmxmEdhoE5Mu9dPSuDBgtTg==", "cpu": [ "arm64" ], @@ -3636,9 +3636,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.13", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.13.tgz", - "integrity": "sha512-v5jZ/FV/eHGoWhMKYrsAweQ7CWb8xsWGM/8m1mwwZQ/sutJjoFaXchwK4pX8NqwImILEvQmZWyb8pPTcP7htWg==", + "version": "14.2.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.25.tgz", + "integrity": "sha512-QC5y5PPTmtqFExcKWKYgUNkHeHE/z3lUsu83di488nyP0ZzQ3Yse2G6TCxz6nNsQwgAx1BehAJTZez+UQxzLfw==", "cpu": [ "arm64" ], @@ -3652,9 +3652,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.13", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.13.tgz", - "integrity": "sha512-aVc7m4YL7ViiRv7SOXK3RplXzOEe/qQzRA5R2vpXboHABs3w8vtFslGTz+5tKiQzWUmTmBNVW0UQdhkKRORmGA==", + "version": "14.2.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.25.tgz", + "integrity": "sha512-y6/ML4b9eQ2D/56wqatTJN5/JR8/xdObU2Fb1RBidnrr450HLCKr6IJZbPqbv7NXmje61UyxjF5kvSajvjye5w==", "cpu": [ "x64" ], @@ -3668,9 +3668,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.13", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.13.tgz", - "integrity": "sha512-4wWY7/OsSaJOOKvMsu1Teylku7vKyTuocvDLTZQq0TYv9OjiYYWt63PiE1nTuZnqQ4RPvME7Xai+9enoiN0Wrg==", + "version": "14.2.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.25.tgz", + "integrity": "sha512-sPX0TSXHGUOZFvv96GoBXpB3w4emMqKeMgemrSxI7A6l55VBJp/RKYLwZIB9JxSqYPApqiREaIIap+wWq0RU8w==", "cpu": [ "x64" ], @@ -3684,9 +3684,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.13", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.13.tgz", - "integrity": "sha512-uP1XkqCqV2NVH9+g2sC7qIw+w2tRbcMiXFEbMihkQ8B1+V6m28sshBwAB0SDmOe0u44ne1vFU66+gx/28RsBVQ==", + "version": "14.2.25", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.25.tgz", + "integrity": "sha512-ReO9S5hkA1DU2cFCsGoOEp7WJkhFzNbU/3VUF6XxNGUCQChyug6hZdYL/istQgfT/GWE6PNIg9cm784OI4ddxQ==", "cpu": [ "arm64" ], @@ -3700,9 +3700,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.13", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.13.tgz", - "integrity": "sha512-V26ezyjPqQpDBV4lcWIh8B/QICQ4v+M5Bo9ykLN+sqeKKBxJVDpEc6biDVyluTXTC40f5IqCU0ttth7Es2ZuMw==", + "version": "14.2.25", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.25.tgz", + "integrity": "sha512-DZ/gc0o9neuCDyD5IumyTGHVun2dCox5TfPQI/BJTYwpSNYM3CZDI4i6TOdjeq1JMo+Ug4kPSMuZdwsycwFbAw==", "cpu": [ "ia32" ], @@ -3716,9 +3716,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.13", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.13.tgz", - "integrity": "sha512-WwzOEAFBGhlDHE5Z73mNU8CO8mqMNLqaG+AO9ETmzdCQlJhVtWZnOl2+rqgVQS+YHunjOWptdFmNfbpwcUuEsw==", + "version": "14.2.25", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.25.tgz", + "integrity": "sha512-KSznmS6eFjQ9RJ1nEc66kJvtGIL1iZMYmGEXsZPh2YtnLtqrgdVvKXJY2ScjjoFnG6nGLyPFR0UiEvDwVah4Tw==", "cpu": [ "x64" ], @@ -10752,12 +10752,12 @@ } }, "node_modules/next": { - "version": "14.2.13", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.13.tgz", - "integrity": "sha512-BseY9YNw8QJSwLYD7hlZzl6QVDoSFHL/URN5K64kVEVpCsSOWeyjbIGK+dZUaRViHTaMQX8aqmnn0PHBbGZezg==", + "version": "14.2.25", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.25.tgz", + "integrity": "sha512-N5M7xMc4wSb4IkPvEV5X2BRRXUmhVHNyaXwEM86+voXthSZz8ZiRyQW4p9mwAoAPIm6OzuVZtn7idgEJeAJN3Q==", "license": "MIT", "dependencies": { - "@next/env": "14.2.13", + "@next/env": "14.2.25", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -10772,15 +10772,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.13", - "@next/swc-darwin-x64": "14.2.13", - "@next/swc-linux-arm64-gnu": "14.2.13", - "@next/swc-linux-arm64-musl": "14.2.13", - "@next/swc-linux-x64-gnu": "14.2.13", - "@next/swc-linux-x64-musl": "14.2.13", - "@next/swc-win32-arm64-msvc": "14.2.13", - "@next/swc-win32-ia32-msvc": "14.2.13", - "@next/swc-win32-x64-msvc": "14.2.13" + "@next/swc-darwin-arm64": "14.2.25", + "@next/swc-darwin-x64": "14.2.25", + "@next/swc-linux-arm64-gnu": "14.2.25", + "@next/swc-linux-arm64-musl": "14.2.25", + "@next/swc-linux-x64-gnu": "14.2.25", + "@next/swc-linux-x64-musl": "14.2.25", + "@next/swc-win32-arm64-msvc": "14.2.25", + "@next/swc-win32-ia32-msvc": "14.2.25", + "@next/swc-win32-x64-msvc": "14.2.25" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/js-peer/package.json b/js-peer/package.json index d4bc8c63..baf3644f 100644 --- a/js-peer/package.json +++ b/js-peer/package.json @@ -34,7 +34,7 @@ "it-pipe": "^3.0.1", "it-protobuf-stream": "^1.1.5", "libp2p": "^2.7.4", - "next": "14.2.13", + "next": "14.2.25", "protons-runtime": "^5.5.0", "react": "18.3.1", "react-18-blockies": "^1.0.6", diff --git a/nim-peer/.keep b/nim-peer/.keep new file mode 100644 index 00000000..c0971430 --- /dev/null +++ b/nim-peer/.keep @@ -0,0 +1 @@ +8e5c0893-9cae-4ed2-8e12-98a64ff72b74 diff --git a/nim-peer/config.nims b/nim-peer/config.nims new file mode 100644 index 00000000..06f10ae0 --- /dev/null +++ b/nim-peer/config.nims @@ -0,0 +1,8 @@ +# begin Nimble config (version 2) +when withDir(thisDir(), system.fileExists("nimble.paths")): + include "nimble.paths" +--define: + "chronicles_sinks=json[dynamic]" +--define: + "chronicles_log_level=DEBUG" +# end Nimble config diff --git a/nim-peer/nim_peer.nimble b/nim-peer/nim_peer.nimble new file mode 100644 index 00000000..667aca8d --- /dev/null +++ b/nim-peer/nim_peer.nimble @@ -0,0 +1,13 @@ +# Package + +version = "0.1.0" +author = "Status Research & Development GmbH" +description = "universal-connectivity nim peer" +license = "MIT" +srcDir = "src" +bin = @["nim_peer"] + + +# Dependencies + +requires "nim >= 2.2.0", "nimwave", "chronos", "chronicles", "libp2p", "illwill", "cligen", "stew" diff --git a/nim-peer/src/file_exchange.nim b/nim-peer/src/file_exchange.nim new file mode 100644 index 00000000..824de18d --- /dev/null +++ b/nim-peer/src/file_exchange.nim @@ -0,0 +1,33 @@ +import os +import libp2p, chronos, chronicles, stew/byteutils + +const + MaxFileSize: int = 1024 # 1KiB + MaxFileIdSize: int = 1024 # 1KiB + FileExchangeCodec*: string = "/universal-connectivity-file/1" + +type FileExchange* = ref object of LPProtocol + +proc new*(T: typedesc[FileExchange]): T = + proc handle(conn: Connection, proto: string) {.async: (raises: [CancelledError]).} = + try: + let fileId = string.fromBytes(await conn.readLp(MaxFileIdSize)) + # filename is /tmp/{fileid} + let filename = getTempDir().joinPath(fileId) + if filename.fileExists: + let fileContent = cast[seq[byte]](readFile(filename)) + await conn.writeLp(fileContent) + except CancelledError as e: + raise e + except CatchableError as e: + error "Exception in handler", error = e.msg + finally: + await conn.close() + + return T.new(codecs = @[FileExchangeCodec], handler = handle) + +proc requestFile*( + p: FileExchange, conn: Connection, fileId: string +): Future[seq[byte]] {.async.} = + await conn.writeLp(cast[seq[byte]](fileId)) + await conn.readLp(MaxFileSize) diff --git a/nim-peer/src/nim_peer.nim b/nim-peer/src/nim_peer.nim new file mode 100644 index 00000000..34e16c3e --- /dev/null +++ b/nim-peer/src/nim_peer.nim @@ -0,0 +1,259 @@ +{.push raises: [Exception].} + +import tables, deques, strutils, os, streams + +import libp2p, chronos, cligen, chronicles +from libp2p/protocols/pubsub/rpc/message import Message + +from illwave as iw import nil, `[]`, `[]=`, `==`, width, height +from terminal import nil + +import ./ui/root +import ./utils +import ./file_exchange + +const + KeyFile: string = "local.key" + PeerIdFile: string = "local.peerid" + MaxKeyLen: int = 4096 + ListenPort: int = 9093 + +proc cleanup() {.noconv: (raises: []).} = + try: + iw.deinit() + except: + discard + try: + terminal.resetAttributes() + terminal.showCursor() + # Clear screen and move cursor to top-left + stdout.write("\e[2J\e[H") # ANSI escape: clear screen & home + stdout.flushFile() + quit(130) # SIGINT conventional exit code + except IOError as exc: + echo "Unexpected error: " & exc.msg + quit(1) + +proc readKeyFile( + filename: string +): PrivateKey {.raises: [OSError, IOError, ResultError[crypto.CryptoError]].} = + let size = getFileSize(filename) + + if size == 0: + raise newException(OSError, "Empty key file") + + var buf: seq[byte] + buf.setLen(size) + + var fs = openFileStream(filename, fmRead) + defer: + fs.close() + + discard fs.readData(buf[0].addr, size.int) + PrivateKey.init(buf).tryGet() + +proc writeKeyFile( + filename: string, key: PrivateKey +) {.raises: [OSError, IOError, ResultError[crypto.CryptoError]].} = + var fs = openFileStream(filename, fmWrite) + defer: + fs.close() + + let buf = key.getBytes().tryGet() + fs.writeData(buf[0].addr, buf.len) + +proc loadOrCreateKey(rng: var HmacDrbgContext): PrivateKey = + if fileExists(KeyFile): + try: + return readKeyFile(KeyFile) + except: + discard # overwrite file + try: + let k = PrivateKey.random(rng).tryGet() + writeKeyFile(KeyFile, k) + k + except: + echo "Could not create new key" + quit(1) + +proc start( + addrs: Opt[MultiAddress], headless: bool, room: string, port: int +) {.async: (raises: [CancelledError]).} = + # Handle Ctrl+C + setControlCHook(cleanup) + + # Pick the correct string type for your Chronicles version + when declared(OutStr): + type WriterStr = OutStr + else: + type WriterStr = LogOutputStr + + # Early (bootstrap) writer: mirror logs to stdout so nothing is dropped + defaultChroniclesStream.output.writer = + proc (lvl: LogLevel, rec: WriterStr) {.closure, gcsafe, raises: [].} = + let s = cast[string](rec) + try: + for line in s.splitLines(): + stdout.writeLine(line) + stdout.flushFile() + except IOError: + discard + + + var rng = newRng() + + let switch = + try: + SwitchBuilder + .new() + .withRng(rng) + .withTcpTransport() + .withAddresses(@[MultiAddress.init("/ip4/0.0.0.0/tcp/" & $port).tryGet()]) + .withYamux() + .withNoise() + .withPrivateKey(loadOrCreateKey(rng[])) + .build() + except LPError as exc: + echo "Could not start switch: " & $exc.msg + quit(1) + except Exception as exc: + echo "Could not start switch: " & $exc.msg + quit(1) + + try: + writeFile(PeerIdFile, $switch.peerInfo.peerId) + except IOError as exc: + error "Could not write PeerId to file", description = exc.msg + + let (gossip, fileExchange) = + try: + (GossipSub.init(switch = switch, triggerSelf = true), FileExchange.new()) + except InitializationError as exc: + echo "Could not initialize gossipsub: " & $exc.msg + quit(1) + + try: + switch.mount(gossip) + switch.mount(fileExchange) + await switch.start() + except LPError as exc: + echo "Could start switch: " & $exc.msg + + info "Started switch", peerId = $switch.peerInfo.peerId + + let + recvQ = newAsyncQueue[string]() + peerQ = newAsyncQueue[(PeerId, PeerEventKind)]() + systemQ = newAsyncQueue[string]() + + # if --connect was specified, connect to peer + if addrs.isSome(): + try: + discard await switch.connect(addrs.get()) + except Exception as exc: + error "Connection error", description = exc.msg + + # wait so that gossipsub can form mesh + await sleepAsync(3.seconds) + + # topic handlers + # chat and file handlers actually need to be validators instead of regular handlers + # validators allow us to get information about which peer sent a message + let onChatMsg = proc( + topic: string, msg: Message + ): Future[ValidationResult] {.async, gcsafe.} = + let strMsg = cast[string](msg.data) + await recvQ.put(shortPeerId(msg.fromPeer) & ": " & strMsg) + await systemQ.put("Received message") + await systemQ.put(" Source: " & $msg.fromPeer) + await systemQ.put(" Topic: " & $topic) + await systemQ.put(" Seqno: " & $seqnoToUint64(msg.seqno)) + await systemQ.put(" ") # empty line + return ValidationResult.Accept + + # when a new file is announced, download it + let onNewFile = proc( + topic: string, msg: Message + ): Future[ValidationResult] {.async, gcsafe.} = + let fileId = sanitizeFileId(cast[string](msg.data)) + # this will only work if we're connected to `fromPeer` (since we don't have kad-dht) + let conn = await switch.dial(msg.fromPeer, FileExchangeCodec) + let filePath = getTempDir() / fileId + let fileContents = await fileExchange.requestFile(conn, fileId) + writeFile(filePath, fileContents) + await conn.close() + # Save file in /tmp/fileId + await systemQ.put("Downloaded file to " & filePath) + await systemQ.put(" ") # empty line + return ValidationResult.Accept + + # when a new peer is announced + let onNewPeer = proc(topic: string, data: seq[byte]) {.async, gcsafe.} = + let peerId = PeerId.init(data).valueOr: + error "Could not parse PeerId from data", data = $data + return + await peerQ.put((peerId, PeerEventKind.Joined)) + + # register validators and handlers + + # receive chat messages + gossip.subscribe(room, nil) + gossip.addValidator(room, onChatMsg) + + # receive files offerings + gossip.subscribe(ChatFileTopic, nil) + gossip.addValidator(ChatFileTopic, onNewFile) + + # receive newly connected peers through gossipsub + gossip.subscribe(PeerDiscoveryTopic, onNewPeer) + + let onPeerJoined = proc( + peer: PeerId, peerEvent: PeerEvent + ) {.gcsafe, async: (raises: [CancelledError]).} = + await peerQ.put((peer, PeerEventKind.Joined)) + + let onPeerLeft = proc( + peer: PeerId, peerEvent: PeerEvent + ) {.gcsafe, async: (raises: [CancelledError]).} = + await peerQ.put((peer, PeerEventKind.Left)) + + # receive newly connected peers through direct connections + switch.addPeerEventHandler(onPeerJoined, PeerEventKind.Joined) + switch.addPeerEventHandler(onPeerLeft, PeerEventKind.Left) + + # add already connected peers + for peerId in switch.peerStore[AddressBook].book.keys: + await peerQ.put((peerId, PeerEventKind.Joined)) + + if headless: + runForever() + else: + try: + await runUI(gossip, room, recvQ, peerQ, systemQ, switch.peerInfo.peerId) + except Exception as exc: + error "Unexpected error", description = exc.msg + finally: + if switch != nil: + await switch.stop() + try: + cleanup() + except: + discard + +proc cli(connect = "", room = ChatTopic, port = ListenPort, headless = false) = + var addrs = Opt.none(MultiAddress) + if connect.len > 0: + addrs = Opt.some(MultiAddress.init(connect).get()) + try: + waitFor start(addrs, headless, room, port) + except CancelledError: + echo "Operation cancelled" + +when isMainModule: + dispatch cli, + help = { + "connect": "full multiaddress (with /p2p/ peerId) of the node to connect to", + "room": "Room name", + "port": "TCP listen port", + "headless": "No UI, can only receive messages", + } diff --git a/nim-peer/src/ui/context.nim b/nim-peer/src/ui/context.nim new file mode 100644 index 00000000..45062e48 --- /dev/null +++ b/nim-peer/src/ui/context.nim @@ -0,0 +1,4 @@ +type State* = object + inputBuffer*: string + +include nimwave/prelude diff --git a/nim-peer/src/ui/root.nim b/nim-peer/src/ui/root.nim new file mode 100644 index 00000000..2a21ed68 --- /dev/null +++ b/nim-peer/src/ui/root.nim @@ -0,0 +1,196 @@ +import chronos, chronicles, deques, strutils, os +from illwave as iw import nil, `[]`, `[]=`, `==`, width, height +from nimwave as nw import nil +from terminal import nil +import libp2p + +import ./scrollingtextbox +import ./context +import ../utils + +const + InputPanelHeight: int = 3 + ScrollSpeed: int = 2 + +type InputPanel = ref object of nw.Node + +method render(node: InputPanel, ctx: var nw.Context[State]) = + ctx = nw.slice(ctx, 0, 0, iw.width(ctx.tb), InputPanelHeight) + render( + nw.Box( + border: nw.Border.Single, + direction: nw.Direction.Vertical, + children: nw.seq("> " & ctx.data.inputBuffer), + ), + ctx, + ) + +proc resizePanels( + chatPanel: ScrollingTextBox, + peersPanel: ScrollingTextBox, + systemPanel: ScrollingTextBox, + newWidth: int, + newHeight: int, +) = + let + peersPanelWidth = (newWidth / 4).int + topHeight = (newHeight / 2).int + chatPanel.resize(newWidth - peersPanelWidth, topHeight) + peersPanel.resize(peersPanelWidth, topHeight) + systemPanel.resize(newWidth, newHeight - topHeight - InputPanelHeight) + +proc runUI*( + gossip: GossipSub, + room: string, + recvQ: AsyncQueue[string], + peerQ: AsyncQueue[(PeerId, PeerEventKind)], + systemQ: AsyncQueue[string], + myPeerId: PeerId, +) {.async: (raises: [Exception]).} = + var + ctx = nw.initContext[State]() + prevTb: iw.TerminalBuffer + mouse: iw.MouseInfo + key: iw.Key + terminal.enableTrueColors() + terminal.hideCursor() + try: + iw.init() + except: + return + + ctx.tb = iw.initTerminalBuffer(terminal.terminalWidth(), terminal.terminalHeight()) + + # TODO: publish my peerid in peerid topic + let + peersPanelWidth = (iw.width(ctx.tb) / 4).int + topHeight = (iw.height(ctx.tb) / 2).int + chatPanel = ScrollingTextBox.new( + title = "Chat", width = iw.width(ctx.tb) - peersPanelWidth, height = topHeight + ) + peersPanel = ScrollingTextBox.new( + title = "Peers", + width = peersPanelWidth, + height = topHeight, + text = @[shortPeerId(myPeerId) & " (You)"], + ) + systemPanel = ScrollingTextBox.new( + title = "System", + width = iw.width(ctx.tb), + height = iw.height(ctx.tb) - topHeight - InputPanelHeight, + ) + + # Send chronicle logs to systemPanel + defaultChroniclesStream.output.writer = proc( + logLevel: LogLevel, msg: LogOutputStr + ) {.gcsafe.} = + for line in msg.replace("\t", " ").splitLines(): + systemPanel.push(line) + + ctx.data.inputBuffer = "" + let focusAreas = @[chatPanel, peersPanel, systemPanel] + var focusIndex = 0 + var focusedPanel: ScrollingTextBox + + while true: + focusedPanel = focusAreas[focusIndex] + focusedPanel.border = nw.Border.Double + key = iw.getKey(mouse) + if key == iw.Key.Mouse: + case mouse.scrollDir + of iw.ScrollDirection.sdUp: + focusedPanel.scrollUp(ScrollSpeed) + of iw.ScrollDirection.sdDown: + focusedPanel.scrollDown(ScrollSpeed) + else: + discard + elif key == iw.Key.Tab: + # unfocus previous panel + focusedPanel.border = nw.Border.Single + focusIndex += 1 + if focusIndex >= focusAreas.len: + focusIndex = 0 # wrap around + elif key in {iw.Key.Space .. iw.Key.Tilde}: + ctx.data.inputBuffer.add(cast[char](key.ord)) + elif key == iw.Key.Backspace and ctx.data.inputBuffer.len > 0: + ctx.data.inputBuffer.setLen(ctx.data.inputBuffer.len - 1) + elif key == iw.Key.Enter: + # handle /file command to send/publish files + if ctx.data.inputBuffer.startsWith("/file"): + let parts = ctx.data.inputBuffer.split(" ") + if parts.len < 2: + systemPanel.push("Invalid /file command, missing file name") + else: + for path in parts[1 ..^ 1]: + if not fileExists(path): + systemPanel.push("Unable to find file '" & path & "', skipping") + continue + let fileId = path.splitFile().name + # copy file to /tmp/{filename} + copyFile(path, getTempDir().joinPath(fileId)) + # publish /tmp/{filename} + try: + discard await gossip.publish(ChatFileTopic, cast[seq[byte]](@(fileId))) + systemPanel.push("Offering file " & fileId) + except Exception as exc: + systemPanel.push("Unable to offer file: " & exc.msg) + else: + try: + discard await gossip.publish(room, cast[seq[byte]](@(ctx.data.inputBuffer))) + chatPanel.push("You: " & ctx.data.inputBuffer) # show message in ui + systemPanel.push("Sent chat message") + except Exception as exc: + systemPanel.push("Unable to send chat message: " & exc.msg) + ctx.data.inputBuffer = "" # clear input buffer + elif key != iw.Key.None: + discard + + # update peer list if there's a new peer from peerQ + if not peerQ.empty(): + let (newPeer, eventKind) = await peerQ.get() + + if eventKind == PeerEventKind.Joined and + not peersPanel.text.contains(shortPeerId(newPeer)): + systemPanel.push("Adding peer " & shortPeerId(newPeer)) + peersPanel.push(shortPeerId(newPeer)) + + if eventKind == PeerEventKind.Left and + peersPanel.text.contains(shortPeerId(newPeer)): + systemPanel.push("Removing peer " & shortPeerId(newPeer)) + peersPanel.remove(shortPeerId(newPeer)) + + # update messages if there's a new message from recvQ + if not recvQ.empty(): + let msg = await recvQ.get() + chatPanel.push(msg) # show message in ui + + # update messages if there's a new message from recvQ + if not systemQ.empty(): + let msg = await systemQ.get() + if msg.len > 0: + systemPanel.push(msg) # show message in ui + + renderRoot( + nw.Box( + direction: nw.Direction.Vertical, + children: nw.seq( + nw.Box( + direction: nw.Direction.Horizontal, children: nw.seq(chatPanel, peersPanel) + ), + systemPanel, + InputPanel(), + ), + ), + ctx, + ) + + # render + iw.display(ctx.tb, prevTb) + prevTb = ctx.tb + ctx.tb = iw.initTerminalBuffer(terminal.terminalWidth(), terminal.terminalHeight()) + if iw.width(prevTb) != iw.width(ctx.tb) or iw.height(prevTb) != iw.height(ctx.tb): + resizePanels( + chatPanel, peersPanel, systemPanel, iw.width(ctx.tb), iw.height(ctx.tb) + ) + + await sleepAsync(5.milliseconds) diff --git a/nim-peer/src/ui/scrollingtextbox.nim b/nim-peer/src/ui/scrollingtextbox.nim new file mode 100644 index 00000000..77c77db1 --- /dev/null +++ b/nim-peer/src/ui/scrollingtextbox.nim @@ -0,0 +1,112 @@ +import unicode +from nimwave as nw import nil + +import ./context + +type ScrollingTextBox* = ref object of nw.Node + title*: string + text*: seq[string] + width*: int + height*: int + startingLine: int + border*: nw.Border + +proc new*( + T: typedesc[ScrollingTextBox], + title: string = "", + width: int = 3, + height: int = 3, + text: seq[string] = @[], +): T = + # width and height cannot be less than 3 (2 for borders + 1 for content) + let height = max(height, 3) + let width = max(width, 3) + # height and width - 2 to account for size of box lines (top and botton) + ScrollingTextBox( + title: title, + width: width - 2, + height: height - 2, + text: text, + startingLine: 0, + border: nw.Border.Single, + ) + +proc resize*(node: ScrollingTextBox, width: int, height: int) = + let height = max(height, 3) + let width = max(width, 3) + node.width = width - 2 + node.height = height - 2 + +proc formatText(node: ScrollingTextBox): seq[string] = + result = @[] + result.add(node.title.alignLeft(node.width)) + # empty line after title + result.add(" ".alignLeft(node.width)) + for i in node.startingLine ..< max(node.startingLine + node.height - 2, 0): + if i < node.text.len: + result.add(node.text[i].alignLeft(node.width)) + else: + result.add(" ".alignLeft(node.width)) + +proc scrollUp*(node: ScrollingTextBox, speed: int) = + node.startingLine = max(node.startingLine - speed, 0) + +proc scrollDown*(node: ScrollingTextBox, speed: int) = + let lastStartingLine = max(0, node.text.len - node.height + 2) + node.startingLine = min(node.startingLine + speed, lastStartingLine) + +proc tail(node: ScrollingTextBox) = + ## focuses window in lowest frame + node.startingLine = max(0, node.text.len - node.height + 2) + +proc isAnsiEscapeSequence(s: string, idx: int): bool = + ## Check if the substring starting at `idx` is an ANSI escape sequence + if idx < 0 or idx + 2 >= s.len: # Need at least 3 characters for "\e[" + return false + if s[idx] == '\e' and s[idx + 1] == '[': # Must start with "\e[" + var i = idx + 2 + while i < s.len and (s[i] in '0' .. '9' or s[i] == ';' or s[i] == 'm'): + i.inc + return s[i - 1] == 'm' # Ends with 'm' + return false + +proc chunkString(s: string, chunkSize: int): seq[string] = + var result: seq[string] = @[] + var i = 0 + + while i < s.len: + var endIdx = min(i + chunkSize - 1, s.len - 1) + + # Avoid splitting escape sequences + while endIdx > i and isAnsiEscapeSequence(s, endIdx): + dec endIdx + + result.add(s[i .. endIdx]) + i = endIdx + 1 + + return result + +proc push*(node: ScrollingTextBox, newLine: string) = + if newLine.len == 0 or node.width <= 0: + return + for chunk in chunkString(newLine, node.width): + node.text.add(chunk) + node.tail() + +proc remove*(node: ScrollingTextBox, lineToRemove: string) = + let idx = node.text.find(lineToRemove) + if idx >= 0: + node.text.delete(idx) + if idx <= node.startingLine: + node.scrollUp(1) + +method render(node: ScrollingTextBox, ctx: var nw.Context[State]) = + ctx = nw.slice(ctx, 0, 0, node.width + 2, node.height + 2) + render( + nw.Box( + border: node.border, + direction: nw.Direction.Vertical, + children: nw.seq(node.formatText()), + ), + ctx, + ) diff --git a/nim-peer/src/utils.nim b/nim-peer/src/utils.nim new file mode 100644 index 00000000..938c94f3 --- /dev/null +++ b/nim-peer/src/utils.nim @@ -0,0 +1,47 @@ +import strutils + +import libp2p + +const + ChatTopic*: string = "universal-connectivity" + ChatFileTopic*: string = "universal-connectivity-file" + PeerDiscoveryTopic*: string = "universal-connectivity-browser-peer-discovery" + +const SanitizationRules = [ + ({'\0' .. '\31'}, ' '), # Control chars -> space + ({'"'}, '\''), # Double quote -> single quote + ({'/', '\\', ':', '|'}, '-'), # Slash, backslash, colon, pipe -> dash + ({'*', '?', '<', '>'}, '_'), # Asterisk, question, angle brackets -> underscore +] + +proc shortPeerId*(peerId: PeerId): string {.raises: [ValueError].} = + let strPeerId = $peerId + if strPeerId.len < 7: + raise newException(ValueError, "PeerId too short") + strPeerId[^7 ..^ 1] + +proc sanitizeFileId*(fileId: string): string = + ## Sanitize a filename for Windows, macOS, and Linux + result = fileId + for (chars, replacement) in SanitizationRules: + for ch in chars: + result = result.multiReplace(($ch, $replacement)) + result = result.strip() + # Avoid reserved Windows filenames (CON, PRN, AUX, NUL, COM1..COM9, LPT1..LPT9) + var reserved = @["CON", "PRN", "AUX", "NUL"] + for i in 1 .. 9: + reserved.add("COM" & $i) + reserved.add("LPT" & $i) + if result.toUpperAscii() in reserved: + result = "_" & result + # Avoid empty filenames + if result.len == 0: + result = "_" + +proc seqnoToUint64*(bytes: seq[byte]): uint64 = + if bytes.len != 8: + return 0 + var seqno: uint64 = 0 + for i in 0 ..< 8: + seqno = seqno or (uint64(bytes[i]) shl (8 * (7 - i))) + seqno diff --git a/node-js-peer/.gitignore b/node-js-peer/.gitignore new file mode 100644 index 00000000..5dc9beb3 --- /dev/null +++ b/node-js-peer/.gitignore @@ -0,0 +1 @@ +.dist diff --git a/node-js-peer/README.md b/node-js-peer/README.md new file mode 100644 index 00000000..e47c6eab --- /dev/null +++ b/node-js-peer/README.md @@ -0,0 +1,40 @@ +# Node.js peer + +This is a JavaScript peer for the Universal Connectivity app implemented as a +command line app using a Terminal User Interface aimed at Node.js. + +The TUI is implemented using [react-curse](https://www.npmjs.com/package/react-curse), +a JavaScript so should be familiar to anyone who has used [React](https://react.dev/) before. + +## Getting Started + +To start the app run: + +```bash +npm start +# or +yarn start +# or +pnpm start +``` + +You should see a terminal user interface similar to this: + +![Node.js peer terminal user interface](./assets/tui.png) + +Use `CTRL-C` to exit the app. + +## Hacking + +You can start editing the app by modifying [./App.tsx](./App.tsx) and restarting the app. + +The libp2p configuration can be found in [./lib/libp2p.ts](./lib/libp2p.ts). + +## Learn More + +To learn more about libp2p, take a look at the following resources: + +- [js-libp2p on GitHub](https://github.com/libp2p/js-libp2p) - The js-libp2p repo +- [API docs](https://libp2p.github.io/js-libp2p/) - API documentation +- [Docs](https://github.com/libp2p/js-libp2p/tree/main/doc) - Longer form docs +- [Examples](https://github.com/libp2p/js-libp2p-examples) - How to do almost anything with your libp2p node diff --git a/node-js-peer/assets/tui.png b/node-js-peer/assets/tui.png new file mode 100644 index 00000000..88b9ecd3 Binary files /dev/null and b/node-js-peer/assets/tui.png differ diff --git a/node-js-peer/package-lock.json b/node-js-peer/package-lock.json new file mode 100644 index 00000000..5dc42e13 --- /dev/null +++ b/node-js-peer/package-lock.json @@ -0,0 +1,7696 @@ +{ + "name": "universal-connectivity-node-js-peer", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "universal-connectivity-node-js-peer", + "dependencies": { + "@chainsafe/libp2p-gossipsub": "^14.1.1", + "@chainsafe/libp2p-noise": "^16.1.0", + "@chainsafe/libp2p-quic": "^1.1.1", + "@chainsafe/libp2p-yamux": "^7.0.1", + "@helia/delegated-routing-v1-http-api-client": "^4.2.2", + "@libp2p/bootstrap": "^11.0.33", + "@libp2p/circuit-relay-v2": "^3.2.9", + "@libp2p/identify": "^3.0.28", + "@libp2p/interface": "^2.8.0", + "@libp2p/interface-internal": "^2.3.10", + "@libp2p/kad-dht": "^15.0.0", + "@libp2p/ping": "^2.0.28", + "@libp2p/pubsub-peer-discovery": "^11.0.1", + "@libp2p/tcp": "^10.1.9", + "@libp2p/webrtc": "^5.2.10", + "@libp2p/websockets": "^9.2.9", + "it-protobuf-stream": "^2.0.1", + "libp2p": "^2.8.3", + "multiformats": "^13.3.2", + "protons-runtime": "^5.5.0", + "react": "^18.3.1", + "react-curse": "1.0.15", + "uint8arraylist": "^2.4.8" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@types/react": "^18.0.27", + "protons": "^7.6.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse--for-generate-function-map": { + "name": "@babel/traverse", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@chainsafe/as-chacha20poly1305": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@chainsafe/as-chacha20poly1305/-/as-chacha20poly1305-0.1.0.tgz", + "integrity": "sha512-BpNcL8/lji/GM3+vZ/bgRWqJ1q5kwvTFmGPk7pxm/QQZDbaMI98waOHjEymTjq2JmdD/INdNBFOVSyJofXg7ew==", + "license": "Apache-2.0" + }, + "node_modules/@chainsafe/as-sha256": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-1.2.0.tgz", + "integrity": "sha512-H2BNHQ5C3RS+H0ZvOdovK6GjFAyq5T6LClad8ivwj9Oaiy28uvdsGVS7gNJKuZmg0FGHAI+n7F0Qju6U0QkKDA==", + "license": "Apache-2.0" + }, + "node_modules/@chainsafe/is-ip": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chainsafe/is-ip/-/is-ip-2.1.0.tgz", + "integrity": "sha512-KIjt+6IfysQ4GCv66xihEitBjvhU/bixbbbFxdJ1sqCp4uJ0wuZiYBPhksZoy4lfaF0k9cwNzY5upEW/VWdw3w==", + "license": "MIT" + }, + "node_modules/@chainsafe/libp2p-gossipsub": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@chainsafe/libp2p-gossipsub/-/libp2p-gossipsub-14.1.1.tgz", + "integrity": "sha512-EUs2C+xHXXbw0pQQF2AN/ih4qB6BBWOGkDhvHz1VN52o2m/827IBEMT8RHdXMNZciQc90to1L57BKmhXkvztDw==", + "license": "Apache-2.0", + "dependencies": { + "@libp2p/crypto": "^5.0.0", + "@libp2p/interface": "^2.0.0", + "@libp2p/interface-internal": "^2.0.0", + "@libp2p/peer-id": "^5.0.0", + "@libp2p/pubsub": "^10.0.0", + "@multiformats/multiaddr": "^12.1.14", + "denque": "^2.1.0", + "it-length-prefixed": "^9.0.4", + "it-pipe": "^3.0.1", + "it-pushable": "^3.2.3", + "multiformats": "^13.0.1", + "protons-runtime": "^5.5.0", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.0.1" + }, + "engines": { + "npm": ">=8.7.0" + } + }, + "node_modules/@chainsafe/libp2p-noise": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@chainsafe/libp2p-noise/-/libp2p-noise-16.1.4.tgz", + "integrity": "sha512-f4FlyRVndcs4PoioOIZWrFc6wfO/mrAj7H63o0+eA0O2xhcoRkxHh6zna4W+WtScaF/Ua/UULgiNGuKNpLvLlQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@chainsafe/as-chacha20poly1305": "^0.1.0", + "@chainsafe/as-sha256": "^1.0.0", + "@libp2p/crypto": "^5.0.0", + "@libp2p/interface": "^2.9.0", + "@libp2p/peer-id": "^5.0.0", + "@noble/ciphers": "^1.1.3", + "@noble/curves": "^1.1.0", + "@noble/hashes": "^1.3.1", + "it-length-prefixed": "^10.0.1", + "it-length-prefixed-stream": "^2.0.1", + "it-pair": "^2.0.6", + "it-pipe": "^3.0.1", + "it-stream-types": "^2.0.1", + "protons-runtime": "^5.5.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^5.0.0", + "wherearewe": "^2.0.1" + } + }, + "node_modules/@chainsafe/libp2p-noise/node_modules/it-length-prefixed": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/it-length-prefixed/-/it-length-prefixed-10.0.1.tgz", + "integrity": "sha512-BhyluvGps26u9a7eQIpOI1YN7mFgi8lFwmiPi07whewbBARKAG9LE09Odc8s1Wtbt2MB6rNUrl7j9vvfXTJwdQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "it-reader": "^6.0.1", + "it-stream-types": "^2.0.1", + "uint8-varint": "^2.0.1", + "uint8arraylist": "^2.0.0", + "uint8arrays": "^5.0.1" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@chainsafe/libp2p-quic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@chainsafe/libp2p-quic/-/libp2p-quic-1.1.3.tgz", + "integrity": "sha512-Y9F2vGPW5ZhvYYAcDC4dF6i92h+pch+BAXC1yfO2AX2KLyg8rVlECOkEffeStp06DL4knPZLN+Qi10EgOVfwwA==", + "license": "MIT", + "dependencies": { + "@libp2p/crypto": "^5.1.7", + "@libp2p/interface": "^2.10.5", + "@libp2p/utils": "^6.7.1", + "@multiformats/multiaddr": "^12.4.0", + "@multiformats/multiaddr-matcher": "^2.0.1", + "it-stream-types": "^2.0.2", + "race-signal": "^1.1.3", + "uint8arraylist": "^2.4.8" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@chainsafe/libp2p-quic-darwin-arm64": "1.1.3", + "@chainsafe/libp2p-quic-darwin-x64": "1.1.3", + "@chainsafe/libp2p-quic-linux-arm64-gnu": "1.1.3", + "@chainsafe/libp2p-quic-linux-arm64-musl": "1.1.3", + "@chainsafe/libp2p-quic-linux-x64-gnu": "1.1.3", + "@chainsafe/libp2p-quic-linux-x64-musl": "1.1.3", + "@chainsafe/libp2p-quic-win32-x64-msvc": "1.1.3" + } + }, + "node_modules/@chainsafe/libp2p-quic-darwin-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@chainsafe/libp2p-quic-darwin-arm64/-/libp2p-quic-darwin-arm64-1.1.3.tgz", + "integrity": "sha512-L9Ta/CalkCiKC910thxR6GfqD0Tmm8QfSbZ5eTY7sGUuYYeE5/73UOlNzVHZxEWid7uceYHBYETTAUkdSsy+RQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@chainsafe/libp2p-quic-darwin-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@chainsafe/libp2p-quic-darwin-x64/-/libp2p-quic-darwin-x64-1.1.3.tgz", + "integrity": "sha512-hxE4wL/PQop/r6OLpzeJJ3c4WDtfk7zWBKhX2Zjul0jHc5v04a1DRTEmugooaqeU7UNnBQkBsqiHcA8efuhNqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@chainsafe/libp2p-quic-linux-arm64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@chainsafe/libp2p-quic-linux-arm64-gnu/-/libp2p-quic-linux-arm64-gnu-1.1.3.tgz", + "integrity": "sha512-XUyafb32UHdkuhgNYATnoBj81YfRlVl1MDW+OPHD3XPsIYkloUHlPD9Y2cBH9m17K0lvhe/3KndeQ3WLZ2syNA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@chainsafe/libp2p-quic-linux-arm64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@chainsafe/libp2p-quic-linux-arm64-musl/-/libp2p-quic-linux-arm64-musl-1.1.3.tgz", + "integrity": "sha512-cmYfa3heaSUN/ts4P1Y1N72Oi7frQdufDC8KzBOgD5WVSLnpXw4Nq8mVt6kf7WU1FLC6FUffKuz3mRWse1gGVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@chainsafe/libp2p-quic-linux-x64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@chainsafe/libp2p-quic-linux-x64-gnu/-/libp2p-quic-linux-x64-gnu-1.1.3.tgz", + "integrity": "sha512-ZS4CtINANQeBvqVHAoWqW9SRfxZ9R5xbM1bQUPjjPsNWdIgu0vCjiIkRYqkaL9cJvVHJPguNhu/NwC6whkdWww==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@chainsafe/libp2p-quic-linux-x64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@chainsafe/libp2p-quic-linux-x64-musl/-/libp2p-quic-linux-x64-musl-1.1.3.tgz", + "integrity": "sha512-LRi33YOHa/s/KSVRV4iCK+Cz8VBg2J8j3KrUEPtUp3aQvxYYvA/YkbRBcRNOyvEz6natzYA8LOycQsJTrVt4MA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@chainsafe/libp2p-quic-win32-x64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@chainsafe/libp2p-quic-win32-x64-msvc/-/libp2p-quic-win32-x64-msvc-1.1.3.tgz", + "integrity": "sha512-zm2h1lYkhHEcVrzO/D0NyPwf5yj0/4zWwltaHXl4fdQMy8kqJCm8zcyZmBRfniDX8/03a2svbYPZdTDtb7FSTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@chainsafe/libp2p-yamux": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@chainsafe/libp2p-yamux/-/libp2p-yamux-7.0.4.tgz", + "integrity": "sha512-Qw+EB9ew/9hRCq9V702gkm5xXThFHQ3Bdvh01M+enI1RScriSDWFGod02dwNHUxsYRc743i49sLlHp0edC7hSQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface": "^2.0.0", + "@libp2p/utils": "^6.0.0", + "get-iterator": "^2.0.1", + "it-foreach": "^2.0.6", + "it-pushable": "^3.2.3", + "it-stream-types": "^2.0.1", + "race-signal": "^1.1.3", + "uint8arraylist": "^2.4.8" + } + }, + "node_modules/@chainsafe/netmask": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@chainsafe/netmask/-/netmask-2.0.0.tgz", + "integrity": "sha512-I3Z+6SWUoaljh3TBzCnCxjlUyN8tA+NAk5L6m9IxvCf1BENQTePzPMis97CoN/iMW1St3WN+AWCCRp+TTBRiDg==", + "license": "MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.0.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@helia/delegated-routing-v1-http-api-client": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@helia/delegated-routing-v1-http-api-client/-/delegated-routing-v1-http-api-client-4.2.5.tgz", + "integrity": "sha512-fFqVhs7a4TnpKQ1cZ4im3tj53v+8UZLFkQo85otl/GpbIVBmBoGbjkDHGPv4UdjJ2lmYM/cRdnHsYbfjuc5pwA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface": "^2.2.0", + "@libp2p/logger": "^5.0.1", + "@libp2p/peer-id": "^5.0.1", + "@multiformats/multiaddr": "^12.3.1", + "any-signal": "^4.1.1", + "browser-readablestream-to-it": "^2.0.7", + "ipns": "^10.0.0", + "it-first": "^3.0.6", + "it-map": "^3.1.1", + "it-ndjson": "^1.0.7", + "multiformats": "^13.3.0", + "p-defer": "^4.0.1", + "p-queue": "^8.0.1", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@ipshipyard/node-datachannel": { + "version": "0.26.6", + "resolved": "https://registry.npmjs.org/@ipshipyard/node-datachannel/-/node-datachannel-0.26.6.tgz", + "integrity": "sha512-70HdhYMyAGXEMuCUq9ATO1Rx/JmiENM5LrGN94KT/q/Et2VsMjJpOWbyFzgodtkQJjDG5saNXTOiQpYZ1AnvEg==", + "hasInstallScript": true, + "license": "MPL 2.0", + "dependencies": { + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", + "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=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", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@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", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT", + "peer": true + }, + "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", + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdoc/salty": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz", + "integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT" + }, + "node_modules/@libp2p/bootstrap": { + "version": "11.0.47", + "resolved": "https://registry.npmjs.org/@libp2p/bootstrap/-/bootstrap-11.0.47.tgz", + "integrity": "sha512-D3V8AHZvX9R0cFD0BKmDp7fKTo+GgPFFWa55z62VP14yA3f3kSAELyZ7NujrD0QXEtUdd/d7suqeJ4tKGsnZ5A==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface": "^2.11.0", + "@libp2p/interface-internal": "^2.3.19", + "@libp2p/peer-id": "^5.1.9", + "@multiformats/mafmt": "^12.1.6", + "@multiformats/multiaddr": "^12.4.4", + "main-event": "^1.0.1" + } + }, + "node_modules/@libp2p/circuit-relay-v2": { + "version": "3.2.24", + "resolved": "https://registry.npmjs.org/@libp2p/circuit-relay-v2/-/circuit-relay-v2-3.2.24.tgz", + "integrity": "sha512-JXSm0dOOpPC+Yax0ngLYWqELHLFmaYPNZOhZkrr0iEtbiXwhDXuDuwT71pwmozp7h/rYd2YFoIk178AhZ4711Q==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/crypto": "^5.1.8", + "@libp2p/interface": "^2.11.0", + "@libp2p/interface-internal": "^2.3.19", + "@libp2p/peer-collections": "^6.0.35", + "@libp2p/peer-id": "^5.1.9", + "@libp2p/peer-record": "^8.0.35", + "@libp2p/utils": "^6.7.2", + "@multiformats/multiaddr": "^12.4.4", + "@multiformats/multiaddr-matcher": "^2.0.0", + "any-signal": "^4.1.1", + "it-protobuf-stream": "^2.0.2", + "it-stream-types": "^2.0.2", + "main-event": "^1.0.1", + "multiformats": "^13.3.6", + "nanoid": "^5.1.5", + "progress-events": "^1.0.1", + "protons-runtime": "^5.5.0", + "retimeable-signal": "^1.0.1", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/crypto": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@libp2p/crypto/-/crypto-5.1.8.tgz", + "integrity": "sha512-zkfWd2x12E0NbSRU52Wb0A5I9v5a1uLgCauR8uuTqnC21OVznXUGkMg4A2Xoj90M98lReDHo+Khc/hlQFbJ5Vw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface": "^2.11.0", + "@noble/curves": "^1.9.1", + "@noble/hashes": "^1.8.0", + "multiformats": "^13.3.6", + "protons-runtime": "^5.5.0", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/identify": { + "version": "3.0.39", + "resolved": "https://registry.npmjs.org/@libp2p/identify/-/identify-3.0.39.tgz", + "integrity": "sha512-302y1LAGuPy8im+LUiB5+2sUOa/VZuAphOAKLsAQ/74EglWlSrw0Q7f09WUQvfNXmn7XpQnDh7GEI3NZBl54Jw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/crypto": "^5.1.8", + "@libp2p/interface": "^2.11.0", + "@libp2p/interface-internal": "^2.3.19", + "@libp2p/peer-id": "^5.1.9", + "@libp2p/peer-record": "^8.0.35", + "@libp2p/utils": "^6.7.2", + "@multiformats/multiaddr": "^12.4.4", + "@multiformats/multiaddr-matcher": "^2.0.0", + "it-drain": "^3.0.9", + "it-parallel": "^3.0.11", + "it-protobuf-stream": "^2.0.2", + "main-event": "^1.0.1", + "protons-runtime": "^5.5.0", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/interface": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@libp2p/interface/-/interface-2.11.0.tgz", + "integrity": "sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@multiformats/dns": "^1.0.6", + "@multiformats/multiaddr": "^12.4.4", + "it-pushable": "^3.2.3", + "it-stream-types": "^2.0.2", + "main-event": "^1.0.1", + "multiformats": "^13.3.6", + "progress-events": "^1.0.1", + "uint8arraylist": "^2.4.8" + } + }, + "node_modules/@libp2p/interface-internal": { + "version": "2.3.19", + "resolved": "https://registry.npmjs.org/@libp2p/interface-internal/-/interface-internal-2.3.19.tgz", + "integrity": "sha512-v335EB0i5CaNF+0SqT01CTBp0VyjJizpy46KprcshFFjX16UQ8+/QzoTZqmot9WiAmAzwR0b87oKmlAE9cpxzQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface": "^2.11.0", + "@libp2p/peer-collections": "^6.0.35", + "@multiformats/multiaddr": "^12.4.4", + "progress-events": "^1.0.1" + } + }, + "node_modules/@libp2p/kad-dht": { + "version": "15.1.11", + "resolved": "https://registry.npmjs.org/@libp2p/kad-dht/-/kad-dht-15.1.11.tgz", + "integrity": "sha512-a5sdnkztx8AVRDG/+llboRfTLkjQJpSPsSD6F/q6xlI2GbAyEf+JnNKJv0GAUZe9UJCl+g4htM1jA/Rjl0IuCg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/crypto": "^5.1.8", + "@libp2p/interface": "^2.11.0", + "@libp2p/interface-internal": "^2.3.19", + "@libp2p/peer-collections": "^6.0.35", + "@libp2p/peer-id": "^5.1.9", + "@libp2p/ping": "^2.0.37", + "@libp2p/record": "^4.0.7", + "@libp2p/utils": "^6.7.2", + "@multiformats/multiaddr": "^12.4.4", + "any-signal": "^4.1.1", + "interface-datastore": "^8.3.1", + "it-all": "^3.0.8", + "it-drain": "^3.0.9", + "it-length": "^3.0.8", + "it-map": "^3.1.3", + "it-merge": "^3.0.11", + "it-parallel": "^3.0.11", + "it-pipe": "^3.0.1", + "it-protobuf-stream": "^2.0.2", + "it-pushable": "^3.2.3", + "it-take": "^3.0.8", + "main-event": "^1.0.1", + "multiformats": "^13.3.6", + "p-defer": "^4.0.1", + "p-event": "^6.0.1", + "progress-events": "^1.0.1", + "protons-runtime": "^5.5.0", + "race-signal": "^1.1.3", + "uint8-varint": "^2.0.4", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/keychain": { + "version": "5.2.9", + "resolved": "https://registry.npmjs.org/@libp2p/keychain/-/keychain-5.2.9.tgz", + "integrity": "sha512-BgZKMqQCu3Xzd7YFIdwWqG2xXtvsO6RVHJKS8VOw6Dg5tuPAWcQhs0T84TZ5PCg5r6NNBwwI8fWdZGVtu/pPfQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/crypto": "^5.1.8", + "@libp2p/interface": "^2.11.0", + "@libp2p/utils": "^6.7.2", + "@noble/hashes": "^1.8.0", + "asn1js": "^3.0.6", + "interface-datastore": "^8.3.1", + "multiformats": "^13.3.6", + "sanitize-filename": "^1.6.3", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/logger": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@libp2p/logger/-/logger-5.2.0.tgz", + "integrity": "sha512-OEFS529CnIKfbWEHmuCNESw9q0D0hL8cQ8klQfjIVPur15RcgAEgc1buQ7Y6l0B6tCYg120bp55+e9tGvn8c0g==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface": "^2.11.0", + "@multiformats/multiaddr": "^12.4.4", + "interface-datastore": "^8.3.1", + "multiformats": "^13.3.6", + "weald": "^1.0.4" + } + }, + "node_modules/@libp2p/multistream-select": { + "version": "6.0.29", + "resolved": "https://registry.npmjs.org/@libp2p/multistream-select/-/multistream-select-6.0.29.tgz", + "integrity": "sha512-SWQbPcABOIpznEY7+vAp0Y3HNrE2PlaVY4EywN0lUZ7zvTv9VnAb7av3/gMvfaLI+YrOvhCr1mZ9qbSB93k4kA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface": "^2.11.0", + "it-length-prefixed": "^10.0.1", + "it-length-prefixed-stream": "^2.0.2", + "it-stream-types": "^2.0.2", + "p-defer": "^4.0.1", + "race-signal": "^1.1.3", + "uint8-varint": "^2.0.4", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/multistream-select/node_modules/it-length-prefixed": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/it-length-prefixed/-/it-length-prefixed-10.0.1.tgz", + "integrity": "sha512-BhyluvGps26u9a7eQIpOI1YN7mFgi8lFwmiPi07whewbBARKAG9LE09Odc8s1Wtbt2MB6rNUrl7j9vvfXTJwdQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "it-reader": "^6.0.1", + "it-stream-types": "^2.0.1", + "uint8-varint": "^2.0.1", + "uint8arraylist": "^2.0.0", + "uint8arrays": "^5.0.1" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@libp2p/peer-collections": { + "version": "6.0.35", + "resolved": "https://registry.npmjs.org/@libp2p/peer-collections/-/peer-collections-6.0.35.tgz", + "integrity": "sha512-QiloK3T7DXW7R2cpL38dBnALCHf5pMzs/TyFzlEK33WezA2YFVoj7CtOJKqbn29bmV9uspWOxMgfmLUXf8ALvA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface": "^2.11.0", + "@libp2p/peer-id": "^5.1.9", + "@libp2p/utils": "^6.7.2", + "multiformats": "^13.3.6" + } + }, + "node_modules/@libp2p/peer-id": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@libp2p/peer-id/-/peer-id-5.1.9.tgz", + "integrity": "sha512-cVDp7lX187Epmi/zr0Qq2RsEMmueswP9eIxYSFoMcHL/qcvRFhsxOfUGB8361E26s2WJvC9sXZ0oJS9XVueJhQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/crypto": "^5.1.8", + "@libp2p/interface": "^2.11.0", + "multiformats": "^13.3.6", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/peer-record": { + "version": "8.0.35", + "resolved": "https://registry.npmjs.org/@libp2p/peer-record/-/peer-record-8.0.35.tgz", + "integrity": "sha512-0818zvjKbucq5XBnusG8oSWxJ992rVry/2qlfcn/nyK/uDrZ12tjDYHNMCoOWTNeFvFUVkMg9pRkvXvTNp6Yiw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/crypto": "^5.1.8", + "@libp2p/interface": "^2.11.0", + "@libp2p/peer-id": "^5.1.9", + "@libp2p/utils": "^6.7.2", + "@multiformats/multiaddr": "^12.4.4", + "multiformats": "^13.3.6", + "protons-runtime": "^5.5.0", + "uint8-varint": "^2.0.4", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/peer-store": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/@libp2p/peer-store/-/peer-store-11.2.7.tgz", + "integrity": "sha512-dwTM+0i7mAgAnZvMHghgGcFoWPGaTbKx2nBueMd2Yg38mCs9WeambmR6gQdjwvYpybvNgFDAA+XesCKCotuczg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/crypto": "^5.1.8", + "@libp2p/interface": "^2.11.0", + "@libp2p/peer-collections": "^6.0.35", + "@libp2p/peer-id": "^5.1.9", + "@libp2p/peer-record": "^8.0.35", + "@multiformats/multiaddr": "^12.4.4", + "interface-datastore": "^8.3.1", + "it-all": "^3.0.8", + "main-event": "^1.0.1", + "mortice": "^3.2.1", + "multiformats": "^13.3.6", + "protons-runtime": "^5.5.0", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/ping": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/@libp2p/ping/-/ping-2.0.37.tgz", + "integrity": "sha512-SvCYM/tHvK3LQzCEa4eflQmrHEL5EAPWPxbIclqJ6SA0mi7jW3xO21AIsHkQDxfFVevIRWKaKoLj6MAythrNcg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/crypto": "^5.1.8", + "@libp2p/interface": "^2.11.0", + "@libp2p/interface-internal": "^2.3.19", + "@multiformats/multiaddr": "^12.4.4", + "it-byte-stream": "^2.0.2", + "main-event": "^1.0.1", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/pubsub": { + "version": "10.1.18", + "resolved": "https://registry.npmjs.org/@libp2p/pubsub/-/pubsub-10.1.18.tgz", + "integrity": "sha512-Bxa0cwkaQvadyJNlJlzH0m1eo7m03G2nCpuKbcv+i0qNbyyTOydBcuoslG/UWFYhRBB9Js9R6zNIsaIgpo+iGw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/crypto": "^5.1.8", + "@libp2p/interface": "^2.11.0", + "@libp2p/interface-internal": "^2.3.19", + "@libp2p/peer-collections": "^6.0.35", + "@libp2p/peer-id": "^5.1.9", + "@libp2p/utils": "^6.7.2", + "it-length-prefixed": "^10.0.1", + "it-pipe": "^3.0.1", + "it-pushable": "^3.2.3", + "main-event": "^1.0.1", + "multiformats": "^13.3.6", + "p-queue": "^8.1.0", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/pubsub-peer-discovery": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@libp2p/pubsub-peer-discovery/-/pubsub-peer-discovery-11.0.2.tgz", + "integrity": "sha512-qN3bP7QX6KepuSBIqvbkoGnMJ6Oc5gV86+QXtNJh3T2lUV34kTsuIcROkMeBeYDOfzUi/99Y4LIOrtBOC3M3yA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/crypto": "^5.0.0", + "@libp2p/interface": "^2.0.0", + "@libp2p/interface-internal": "^2.0.0", + "@libp2p/peer-id": "^5.0.0", + "@multiformats/multiaddr": "^12.0.0", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^5.0.2" + } + }, + "node_modules/@libp2p/pubsub/node_modules/it-length-prefixed": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/it-length-prefixed/-/it-length-prefixed-10.0.1.tgz", + "integrity": "sha512-BhyluvGps26u9a7eQIpOI1YN7mFgi8lFwmiPi07whewbBARKAG9LE09Odc8s1Wtbt2MB6rNUrl7j9vvfXTJwdQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "it-reader": "^6.0.1", + "it-stream-types": "^2.0.1", + "uint8-varint": "^2.0.1", + "uint8arraylist": "^2.0.0", + "uint8arrays": "^5.0.1" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@libp2p/record": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@libp2p/record/-/record-4.0.7.tgz", + "integrity": "sha512-9JFfOytFS730Z79azWi3Ozlb7IufpwbjC/frAv1yZUCLPp7flT9HNsNB+JQwi+V7z68MaNUYeAFE86VQaq2ccA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "protons-runtime": "^5.5.0", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/tcp": { + "version": "10.1.19", + "resolved": "https://registry.npmjs.org/@libp2p/tcp/-/tcp-10.1.19.tgz", + "integrity": "sha512-Z+s1n7gBexc32d+DUhOGgQpA8HVukubmNJHzolzZPqly5DYkG2f6SIelitp+M5tDYOPH/43EH9pPSSZ3vUuOwQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface": "^2.11.0", + "@libp2p/utils": "^6.7.2", + "@multiformats/multiaddr": "^12.4.4", + "@multiformats/multiaddr-matcher": "^2.0.0", + "@types/sinon": "^17.0.4", + "main-event": "^1.0.1", + "p-defer": "^4.0.1", + "p-event": "^6.0.1", + "progress-events": "^1.0.1", + "race-event": "^1.3.0", + "stream-to-it": "^1.0.1" + } + }, + "node_modules/@libp2p/utils": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@libp2p/utils/-/utils-6.7.2.tgz", + "integrity": "sha512-yglVPcYErb4al3MMTdedVLLsdUvr5KaqrrxohxTl/FXMFBvBs0o3w8lo29nfnTUpnNSHFhWZ9at0ZGNnpT/C/w==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.1.0", + "@chainsafe/netmask": "^2.0.0", + "@libp2p/crypto": "^5.1.8", + "@libp2p/interface": "^2.11.0", + "@libp2p/logger": "^5.2.0", + "@multiformats/multiaddr": "^12.4.4", + "@sindresorhus/fnv1a": "^3.1.0", + "any-signal": "^4.1.1", + "delay": "^6.0.0", + "get-iterator": "^2.0.1", + "is-loopback-addr": "^2.0.2", + "is-plain-obj": "^4.1.0", + "it-foreach": "^2.1.3", + "it-pipe": "^3.0.1", + "it-pushable": "^3.2.3", + "it-stream-types": "^2.0.2", + "main-event": "^1.0.1", + "netmask": "^2.0.2", + "p-defer": "^4.0.1", + "race-event": "^1.3.0", + "race-signal": "^1.1.3", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/webrtc": { + "version": "5.2.24", + "resolved": "https://registry.npmjs.org/@libp2p/webrtc/-/webrtc-5.2.24.tgz", + "integrity": "sha512-0Ne/GDR0FBnKQ/KpJDdQ0Ohii1jyhSCJvbKRxLISm8XItCuJtE1WA2awiWZddZwp2lPDPh9iQmFaYUvv8Zel2w==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.1.0", + "@chainsafe/libp2p-noise": "^16.1.3", + "@ipshipyard/node-datachannel": "^0.26.6", + "@libp2p/crypto": "^5.1.8", + "@libp2p/interface": "^2.11.0", + "@libp2p/interface-internal": "^2.3.19", + "@libp2p/keychain": "^5.2.9", + "@libp2p/peer-id": "^5.1.9", + "@libp2p/utils": "^6.7.2", + "@multiformats/multiaddr": "^12.4.4", + "@multiformats/multiaddr-matcher": "^2.0.0", + "@peculiar/webcrypto": "^1.5.0", + "@peculiar/x509": "^1.12.3", + "any-signal": "^4.1.1", + "detect-browser": "^5.3.0", + "get-port": "^7.1.0", + "interface-datastore": "^8.3.1", + "it-length-prefixed": "^10.0.1", + "it-protobuf-stream": "^2.0.2", + "it-pushable": "^3.2.3", + "it-stream-types": "^2.0.2", + "main-event": "^1.0.1", + "multiformats": "^13.3.6", + "p-defer": "^4.0.1", + "p-timeout": "^6.1.4", + "p-wait-for": "^5.0.2", + "progress-events": "^1.0.1", + "protons-runtime": "^5.5.0", + "race-event": "^1.3.0", + "race-signal": "^1.1.3", + "react-native-webrtc": "^124.0.5", + "uint8-varint": "^2.0.4", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/webrtc/node_modules/@react-native/virtualized-lists": { + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.1.tgz", + "integrity": "sha512-yG+zcMtyApW1yRwkNFvlXzEg3RIFdItuwr/zEvPCSdjaL+paX4rounpL0YX5kS9MsDIE5FXfcqINXg7L0xuwPg==", + "license": "MIT", + "peer": true, + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@types/react": "^19.1.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@libp2p/webrtc/node_modules/@types/react": { + "version": "19.1.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", + "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@libp2p/webrtc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@libp2p/webrtc/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@libp2p/webrtc/node_modules/event-target-shim": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz", + "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/@libp2p/webrtc/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@libp2p/webrtc/node_modules/it-length-prefixed": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/it-length-prefixed/-/it-length-prefixed-10.0.1.tgz", + "integrity": "sha512-BhyluvGps26u9a7eQIpOI1YN7mFgi8lFwmiPi07whewbBARKAG9LE09Odc8s1Wtbt2MB6rNUrl7j9vvfXTJwdQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "it-reader": "^6.0.1", + "it-stream-types": "^2.0.1", + "uint8-varint": "^2.0.1", + "uint8arraylist": "^2.0.0", + "uint8arrays": "^5.0.1" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@libp2p/webrtc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@libp2p/webrtc/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/@libp2p/webrtc/node_modules/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@libp2p/webrtc/node_modules/react-native": { + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.1.tgz", + "integrity": "sha512-k2QJzWc/CUOwaakmD1SXa4uJaLcwB2g2V9BauNIjgtXYYAeyFjx9jlNz/+wAEcHLg9bH5mgMdeAwzvXqjjh9Hg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/create-cache-key-function": "^29.7.0", + "@react-native/assets-registry": "0.81.1", + "@react-native/codegen": "0.81.1", + "@react-native/community-cli-plugin": "0.81.1", + "@react-native/gradle-plugin": "0.81.1", + "@react-native/js-polyfills": "0.81.1", + "@react-native/normalize-colors": "0.81.1", + "@react-native/virtualized-lists": "0.81.1", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "babel-jest": "^29.7.0", + "babel-plugin-syntax-hermes-parser": "0.29.1", + "base64-js": "^1.5.1", + "commander": "^12.0.0", + "flow-enums-runtime": "^0.0.6", + "glob": "^7.1.1", + "invariant": "^2.2.4", + "jest-environment-node": "^29.7.0", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.83.1", + "metro-source-map": "^0.83.1", + "nullthrows": "^1.1.1", + "pretty-format": "^29.7.0", + "promise": "^8.3.0", + "react-devtools-core": "^6.1.5", + "react-refresh": "^0.14.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.26.0", + "semver": "^7.1.3", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.3", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@types/react": "^19.1.0", + "react": "^19.1.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@libp2p/webrtc/node_modules/react-native-webrtc": { + "version": "124.0.6", + "resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-124.0.6.tgz", + "integrity": "sha512-5GviOGK19vujT7sGvSYdZE+bBlh0KC9g1JLharzajpCDVrNdCSpYxveOJUINSRevLsmL12FgNJJgnTjFKn7Aqw==", + "license": "MIT", + "dependencies": { + "base64-js": "1.5.1", + "debug": "4.3.4", + "event-target-shim": "6.0.2" + }, + "peerDependencies": { + "react-native": ">=0.60.0" + } + }, + "node_modules/@libp2p/webrtc/node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT", + "peer": true + }, + "node_modules/@libp2p/webrtc/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "peer": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/@libp2p/websockets": { + "version": "9.2.19", + "resolved": "https://registry.npmjs.org/@libp2p/websockets/-/websockets-9.2.19.tgz", + "integrity": "sha512-+g2qI9Lgvyofoc6GFztPoPVZV+z/lg9pIUfneHht6j88Y7tH3NrAQ7Ki+9lqS5XBX2h1O1bHULWbCaVYCj9TZg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface": "^2.11.0", + "@libp2p/utils": "^6.7.2", + "@multiformats/multiaddr": "^12.4.4", + "@multiformats/multiaddr-matcher": "^2.0.0", + "@multiformats/multiaddr-to-uri": "^11.0.0", + "@types/ws": "^8.18.1", + "it-ws": "^6.1.5", + "main-event": "^1.0.1", + "p-defer": "^4.0.1", + "p-event": "^6.0.1", + "progress-events": "^1.0.1", + "race-signal": "^1.1.3", + "ws": "^8.18.2" + } + }, + "node_modules/@multiformats/dns": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@multiformats/dns/-/dns-1.0.9.tgz", + "integrity": "sha512-Ja4hevWI9p96ICx11K3suFvFirnMmXILzS7FpsR2KG3FoKF/XJijm8ylf3vY6kRFGr98yfZYM+zIn18KaINs3A==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "buffer": "^6.0.3", + "dns-packet": "^5.6.1", + "hashlru": "^2.3.0", + "p-queue": "^8.0.1", + "progress-events": "^1.0.0", + "uint8arrays": "^5.0.2" + } + }, + "node_modules/@multiformats/mafmt": { + "version": "12.1.6", + "resolved": "https://registry.npmjs.org/@multiformats/mafmt/-/mafmt-12.1.6.tgz", + "integrity": "sha512-tlJRfL21X+AKn9b5i5VnaTD6bNttpSpcqwKVmDmSHLwxoz97fAHaepqFOk/l1fIu94nImIXneNbhsJx/RQNIww==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@multiformats/multiaddr": "^12.0.0" + } + }, + "node_modules/@multiformats/multiaddr": { + "version": "12.5.1", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr/-/multiaddr-12.5.1.tgz", + "integrity": "sha512-+DDlr9LIRUS8KncI1TX/FfUn8F2dl6BIxJgshS/yFQCNB5IAF0OGzcwB39g5NLE22s4qqDePv0Qof6HdpJ/4aQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.0.1", + "@chainsafe/netmask": "^2.0.0", + "@multiformats/dns": "^1.0.3", + "abort-error": "^1.0.1", + "multiformats": "^13.0.0", + "uint8-varint": "^2.0.1", + "uint8arrays": "^5.0.0" + } + }, + "node_modules/@multiformats/multiaddr-matcher": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr-matcher/-/multiaddr-matcher-2.0.2.tgz", + "integrity": "sha512-si7EZCI93mfBJKKRkh+u2bB9W6W5APVN3XfdwuseEJ0OS7ysg0Jno9SuAi0bRzsl5OEFESoF71SjsRqgp8PXAA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@multiformats/multiaddr": "^12.0.0" + } + }, + "node_modules/@multiformats/multiaddr-to-uri": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr-to-uri/-/multiaddr-to-uri-11.0.2.tgz", + "integrity": "sha512-SiLFD54zeOJ0qMgo9xv1Tl9O5YktDKAVDP4q4hL16mSq4O4sfFNagNADz8eAofxd6TfQUzGQ3TkRRG9IY2uHRg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@multiformats/multiaddr": "^12.3.0" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.5.0.tgz", + "integrity": "sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "@peculiar/asn1-x509-attr": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.5.0.tgz", + "integrity": "sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.5.0.tgz", + "integrity": "sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.5.0.tgz", + "integrity": "sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.5.0", + "@peculiar/asn1-pkcs8": "^2.5.0", + "@peculiar/asn1-rsa": "^2.5.0", + "@peculiar/asn1-schema": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.5.0.tgz", + "integrity": "sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.5.0.tgz", + "integrity": "sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.5.0", + "@peculiar/asn1-pfx": "^2.5.0", + "@peculiar/asn1-pkcs8": "^2.5.0", + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "@peculiar/asn1-x509-attr": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.5.0.tgz", + "integrity": "sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.5.0.tgz", + "integrity": "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ==", + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.5.0.tgz", + "integrity": "sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.5.0.tgz", + "integrity": "sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.5.0.tgz", + "integrity": "sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/json-schema": "^1.1.12", + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2", + "webcrypto-core": "^1.8.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.0.tgz", + "integrity": "sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.5.0", + "@peculiar/asn1-csr": "^2.5.0", + "@peculiar/asn1-ecc": "^2.5.0", + "@peculiar/asn1-pkcs9": "^2.5.0", + "@peculiar/asn1-rsa": "^2.5.0", + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@react-native/assets-registry": { + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.1.tgz", + "integrity": "sha512-o/AeHeoiPW8x9MzxE1RSnKYc+KZMW9b7uaojobEz0G8fKgGD1R8n5CJSOiQ/0yO2fJdC5wFxMMOgy2IKwRrVgw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/codegen": { + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.81.1.tgz", + "integrity": "sha512-8KoUE1j65fF1PPHlAhSeUHmcyqpE+Z7Qv27A89vSZkz3s8eqWSRu2hZtCl0D3nSgS0WW0fyrIsFaRFj7azIiPw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/parser": "^7.25.3", + "glob": "^7.1.1", + "hermes-parser": "0.29.1", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/codegen/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@react-native/codegen/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@react-native/codegen/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@react-native/community-cli-plugin": { + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.81.1.tgz", + "integrity": "sha512-FuIpZcjBiiYcVMNx+1JBqTPLs2bUIm6X4F5enYGYcetNE2nfSMUVO8SGUtTkBdbUTfKesXYSYN8wungyro28Ag==", + "license": "MIT", + "peer": true, + "dependencies": { + "@react-native/dev-middleware": "0.81.1", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "metro": "^0.83.1", + "metro-config": "^0.83.1", + "metro-core": "^0.83.1", + "semver": "^7.1.3" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@react-native-community/cli": "*", + "@react-native/metro-config": "*" + }, + "peerDependenciesMeta": { + "@react-native-community/cli": { + "optional": true + }, + "@react-native/metro-config": { + "optional": true + } + } + }, + "node_modules/@react-native/debugger-frontend": { + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.81.1.tgz", + "integrity": "sha512-dwKv1EqKD+vONN4xsfyTXxn291CNl1LeBpaHhNGWASK1GO4qlyExMs4TtTjN57BnYHikR9PzqPWcUcfzpVRaLg==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.81.1.tgz", + "integrity": "sha512-hy3KlxNOfev3O5/IuyZSstixWo7E9FhljxKGHdvVtZVNjQdM+kPMh66mxeJbB2TjdJGAyBT4DjIwBaZnIFOGHQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.81.1", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^6.2.3" + }, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "peer": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.81.1.tgz", + "integrity": "sha512-RpRxs/LbWVM9Zi5jH1qBLgTX746Ei+Ui4vj3FmUCd9EXUSECM5bJpphcsvqjxM5Vfl/o2wDLSqIoFkVP/6Te7g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.81.1.tgz", + "integrity": "sha512-w093OkHFfCnJKnkiFizwwjgrjh5ra53BU0ebPM3uBLkIQ6ZMNSCTZhG8ZHIlAYeIGtEinvmnSUi3JySoxuDCAQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.81.1", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.1.tgz", + "integrity": "sha512-TsaeZlE8OYFy3PSWc+1VBmAzI2T3kInzqxmwXoGU4w1d4XFkQFg271Ja9GmDi9cqV3CnBfqoF9VPwRxVlc/l5g==", + "license": "MIT", + "peer": true + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "license": "MIT", + "peer": true + }, + "node_modules/@sindresorhus/fnv1a": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/fnv1a/-/fnv1a-3.1.0.tgz", + "integrity": "sha512-KV321z5m/0nuAg83W1dPLy85HpHDk7Sdi4fJbwvacWsEhAh+rZUW4ZfGcXmUIvjZg4ss2bcwNlRhJ7GBEUG08w==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.18.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.1.tgz", + "integrity": "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", + "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "license": "MIT" + }, + "node_modules/@types/sinon": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", + "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT", + "peer": true + }, + "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", + "peer": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abort-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/abort-error/-/abort-error-1.0.1.tgz", + "integrity": "sha512-fxqCblJiIPdSXIUrxI0PL+eJG49QdP9SQ70qtB65MVAoMr2rASlOyAbJFOylfB467F/f+5BCLJJq58RYi7mGfg==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/anser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", + "license": "MIT", + "peer": true + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-signal": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/any-signal/-/any-signal-4.1.1.tgz", + "integrity": "sha512-iADenERppdC+A2YKbOXXB2WUeABLaM6qnpZ70kZbPZ1cZMMJ7eF+3CaYm+/PhBizgkzlvssC7QuHS30oOiQYWA==", + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "peer": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT", + "peer": true + }, + "node_modules/asn1js": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", + "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT", + "peer": true + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.29.1.tgz", + "integrity": "sha512-2WFYnoWGdmih1I1J5eIqxATOeycOqRwYxAQBu3cUu/rhwInwHUg7k60AFNbuGjSDL8tje5GDrAnxzRLcu2pYcA==", + "license": "MIT", + "peer": true, + "dependencies": { + "hermes-parser": "0.29.1" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "license": "MIT", + "peer": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "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/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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.1.13" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-readablestream-to-it": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/browser-readablestream-to-it/-/browser-readablestream-to-it-2.0.10.tgz", + "integrity": "sha512-I/9hEcRtjct8CzD9sVo9Mm4ntn0D+7tOVrjbPl69XAoOfgJ8NBdOQU+WX+5SHhcELJDb14mWt7zuvyqha+MEAQ==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/browserslist": { + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "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/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT", + "peer": true + }, + "node_modules/caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "callsites": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", + "license": "MIT", + "peer": true, + "dependencies": { + "caller-callsite": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0", + "peer": true + }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cborg": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.2.15.tgz", + "integrity": "sha512-T+YVPemWyXcBVQdp0k61lQp2hJniRNmul0lAwTj2DTS/6dI4eCq/MRMucGqqvFqMBfmnD8tJ9aFtPu5dEGAbgw==", + "license": "Apache-2.0", + "bin": { + "cborg": "lib/bin.js" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chrome-launcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chromium-edge-launcher": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", + "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "node_modules/chromium-edge-launcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "peer": true + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "peer": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT", + "peer": true + }, + "node_modules/cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "license": "MIT", + "peer": true, + "dependencies": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/datastore-core": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/datastore-core/-/datastore-core-10.0.4.tgz", + "integrity": "sha512-IctgCO0GA7GHG7aRm3JRruibCsfvN4EXNnNIlLCZMKIv0TPkdAL5UFV3/xTYFYrrZ1jRNrXZNZRvfcVf/R+rAw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/logger": "^5.1.18", + "interface-datastore": "^8.0.0", + "interface-store": "^6.0.0", + "it-drain": "^3.0.9", + "it-filter": "^3.1.3", + "it-map": "^3.1.3", + "it-merge": "^3.0.11", + "it-pipe": "^3.0.1", + "it-sort": "^3.0.8", + "it-take": "^3.0.8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delay": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-6.0.0.tgz", + "integrity": "sha512-2NJozoOHQ4NuZuVIr5CWd0iiLVIRSDepakaovIN+9eIDHEhdCAEvSy2cuf1DCrPPQLvHmbqTHODlhHg8UCy4zw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-browser": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", + "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.217", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.217.tgz", + "integrity": "sha512-Pludfu5iBxp9XzNl0qq2G87hdD17ZV7h5T4n6rQXDi3nCyloBV3jreE9+8GC6g4X/5yxqVgXEURpcLtM0WS4jA==", + "license": "ISC", + "peer": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "peer": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.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", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT", + "peer": true + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-iterator": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/event-iterator/-/event-iterator-2.0.0.tgz", + "integrity": "sha512-KGft0ldl31BZVV//jj+IAIGCxkvvUkkON+ScH6zfoX+l+omX6001ggyRSpI0Io2Hlro0ThXotswCtfzS8UkIiQ==", + "license": "MIT" + }, + "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", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT", + "peer": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "peer": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "peer": true + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flow-enums-runtime": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", + "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", + "license": "MIT", + "peer": true + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "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", + "peer": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-iterator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/get-iterator/-/get-iterator-2.0.1.tgz", + "integrity": "sha512-7HuY/hebu4gryTDT7O/XY/fvY9wRByEGdK6QOa4of8npTcv0+NS6frFKABcf6S9EBAsveTuKTsZQQBFMMNILIg==", + "license": "MIT" + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hashlru": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/hashlru/-/hashlru-2.3.0.tgz", + "integrity": "sha512-0cMsjjIC8I+D3M44pOQdsy0OHXGLVz6Z0beRuufhKa0KfaD2wGwAev6jILzXsd3/vpnNQJmWyZtIILqM1N+n5A==", + "license": "MIT" + }, + "node_modules/hermes-estree": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz", + "integrity": "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==", + "license": "MIT", + "peer": true + }, + "node_modules/hermes-parser": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.1.tgz", + "integrity": "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==", + "license": "MIT", + "peer": true, + "dependencies": { + "hermes-estree": "0.29.1" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "peer": true, + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "license": "MIT", + "peer": true, + "dependencies": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/interface-datastore": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/interface-datastore/-/interface-datastore-8.3.2.tgz", + "integrity": "sha512-R3NLts7pRbJKc3qFdQf+u40hK8XWc0w4Qkx3OFEstC80VoaDUABY/dXA2EJPhtNC+bsrf41Ehvqb6+pnIclyRA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "interface-store": "^6.0.0", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/interface-store": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/interface-store/-/interface-store-6.0.3.tgz", + "integrity": "sha512-+WvfEZnFUhRwFxgz+QCQi7UC6o9AM0EHM9bpIe2Nhqb100NHCsTvNAn4eJgvgV2/tmLo1MP9nGxQKEcZTAueLA==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ipns": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/ipns/-/ipns-10.1.2.tgz", + "integrity": "sha512-RKAX20vZSHWEobmUw4zpU8t/kw+0CkrJYMA5ou39kNW5B4sAPGOiR/wGK9c51tQKA3rb8SeKs5g7ndNvNiS/vg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/crypto": "^5.0.0", + "@libp2p/interface": "^2.0.0", + "@libp2p/logger": "^5.0.0", + "cborg": "^4.2.3", + "interface-datastore": "^8.3.0", + "multiformats": "^13.2.2", + "protons-runtime": "^5.5.0", + "timestamp-nano": "^1.0.1", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT", + "peer": true + }, + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "peer": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-loopback-addr": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-loopback-addr/-/is-loopback-addr-2.0.2.tgz", + "integrity": "sha512-26POf2KRCno/KTNL5Q0b/9TYnL00xEsSaLfiFRmjM7m7Lw7ZMmFybzzuX4CcsLAluZGd+niLUiMRxEooVE3aqg==", + "license": "MIT" + }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/it-all": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/it-all/-/it-all-3.0.9.tgz", + "integrity": "sha512-fz1oJJ36ciGnu2LntAlE6SA97bFZpW7Rnt0uEc1yazzR2nKokZLr8lIRtgnpex4NsmaBcvHF+Z9krljWFy/mmg==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/it-byte-stream": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/it-byte-stream/-/it-byte-stream-2.0.3.tgz", + "integrity": "sha512-h7FFcn4DWiWsJw1dCJhuPdiY8cGi1z8g4aLAfFspTaJbwQxvEMlEBFG/f8lIVGwM8YK26ClM4/9lxLVhF33b8g==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "abort-error": "^1.0.1", + "it-queueless-pushable": "^2.0.0", + "it-stream-types": "^2.0.2", + "race-signal": "^1.1.3", + "uint8arraylist": "^2.4.8" + } + }, + "node_modules/it-drain": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/it-drain/-/it-drain-3.0.10.tgz", + "integrity": "sha512-0w/bXzudlyKIyD1+rl0xUKTI7k4cshcS43LTlBiGFxI8K1eyLydNPxGcsVLsFVtKh1/ieS8AnVWt6KwmozxyEA==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/it-filter": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/it-filter/-/it-filter-3.1.4.tgz", + "integrity": "sha512-80kWEKgiFEa4fEYD3mwf2uygo1dTQ5Y5midKtL89iXyjinruA/sNXl6iFkTcdNedydjvIsFhWLiqRPQP4fAwWQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "it-peekable": "^3.0.0" + } + }, + "node_modules/it-first": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/it-first/-/it-first-3.0.9.tgz", + "integrity": "sha512-ZWYun273Gbl7CwiF6kK5xBtIKR56H1NoRaiJek2QzDirgen24u8XZ0Nk+jdnJSuCTPxC2ul1TuXKxu/7eK6NuA==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/it-foreach": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/it-foreach/-/it-foreach-2.1.4.tgz", + "integrity": "sha512-gFntBbNLpVK9uDmaHusugICD8/Pp+OCqbF5q1Z8K+B8WaG20YgMePWbMxI1I25+JmNWWr3hk0ecKyiI9pOLgeA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "it-peekable": "^3.0.0" + } + }, + "node_modules/it-length": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/it-length/-/it-length-3.0.9.tgz", + "integrity": "sha512-cPhRPzyulYqyL7x4sX4MOjG/xu3vvEIFAhJ1aCrtrnbfxloCOtejOONib5oC3Bz8tLL6b6ke6+YHu4Bm6HCG7A==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/it-length-prefixed": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/it-length-prefixed/-/it-length-prefixed-9.1.1.tgz", + "integrity": "sha512-O88nBweT6M9ozsmok68/auKH7ik/slNM4pYbM9lrfy2z5QnpokW5SlrepHZDKtN71llhG2sZvd6uY4SAl+lAQg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "it-reader": "^6.0.1", + "it-stream-types": "^2.0.1", + "uint8-varint": "^2.0.1", + "uint8arraylist": "^2.0.0", + "uint8arrays": "^5.0.1" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/it-length-prefixed-stream": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/it-length-prefixed-stream/-/it-length-prefixed-stream-2.0.3.tgz", + "integrity": "sha512-Ns3jNFy2mcFnV59llCYitJnFHapg8wIcOsWkEaAwOkG9v4HBCk24nze/zGDQjiJdDTyFXTT5GOY3M/uaksot3w==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "abort-error": "^1.0.1", + "it-byte-stream": "^2.0.0", + "it-stream-types": "^2.0.2", + "uint8-varint": "^2.0.4", + "uint8arraylist": "^2.4.8" + } + }, + "node_modules/it-map": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/it-map/-/it-map-3.1.4.tgz", + "integrity": "sha512-QB9PYQdE9fUfpVFYfSxBIyvKynUCgblb143c+ktTK6ZuKSKkp7iH58uYFzagqcJ5HcqIfn1xbfaralHWam+3fg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "it-peekable": "^3.0.0" + } + }, + "node_modules/it-merge": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/it-merge/-/it-merge-3.0.12.tgz", + "integrity": "sha512-nnnFSUxKlkZVZD7c0jYw6rDxCcAQYcMsFj27thf7KkDhpj0EA0g9KHPxbFzHuDoc6US2EPS/MtplkNj8sbCx4Q==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "it-queueless-pushable": "^2.0.0" + } + }, + "node_modules/it-ndjson": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/it-ndjson/-/it-ndjson-1.1.4.tgz", + "integrity": "sha512-ZMgTUrNo/UQCeRUT3KqnC0UaClzU6D+ItSmzVt7Ks7pcJ7DboYeYBSPeFLAaEthf5zlvaApDuACLmOWepgkrRg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "uint8arraylist": "^2.4.8" + } + }, + "node_modules/it-pair": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/it-pair/-/it-pair-2.0.6.tgz", + "integrity": "sha512-5M0t5RAcYEQYNG5BV7d7cqbdwbCAp5yLdzvkxsZmkuZsLbTdZzah6MQySYfaAQjNDCq6PUnDt0hqBZ4NwMfW6g==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "it-stream-types": "^2.0.1", + "p-defer": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/it-parallel": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/it-parallel/-/it-parallel-3.0.13.tgz", + "integrity": "sha512-85PPJ/O8q97Vj9wmDTSBBXEkattwfQGruXitIzrh0RLPso6RHfiVqkuTqBNufYYtB1x6PSkh0cwvjmMIkFEPHA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "p-defer": "^4.0.1" + } + }, + "node_modules/it-peekable": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/it-peekable/-/it-peekable-3.0.8.tgz", + "integrity": "sha512-7IDBQKSp/dtBxXV3Fj0v3qM1jftJ9y9XrWLRIuU1X6RdKqWiN60syNwP0fiDxZD97b8SYM58dD3uklIk1TTQAw==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/it-pipe": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/it-pipe/-/it-pipe-3.0.1.tgz", + "integrity": "sha512-sIoNrQl1qSRg2seYSBH/3QxWhJFn9PKYvOf/bHdtCBF0bnghey44VyASsWzn5dAx0DCDDABq1hZIuzKmtBZmKA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "it-merge": "^3.0.0", + "it-pushable": "^3.1.2", + "it-stream-types": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/it-protobuf-stream": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/it-protobuf-stream/-/it-protobuf-stream-2.0.3.tgz", + "integrity": "sha512-Dus9qyylOSnC7l75/3qs6j3Fe9MCM2K5luXi9o175DYijFRne5FPucdOGIYdwaDBDQ4Oy34dNCuFobOpcusvEQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "abort-error": "^1.0.1", + "it-length-prefixed-stream": "^2.0.0", + "it-stream-types": "^2.0.2", + "uint8arraylist": "^2.4.8" + } + }, + "node_modules/it-pushable": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/it-pushable/-/it-pushable-3.2.3.tgz", + "integrity": "sha512-gzYnXYK8Y5t5b/BnJUr7glfQLO4U5vyb05gPx/TyTw+4Bv1zM9gFk4YsOrnulWefMewlphCjKkakFvj1y99Tcg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "p-defer": "^4.0.0" + } + }, + "node_modules/it-queue": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/it-queue/-/it-queue-1.1.0.tgz", + "integrity": "sha512-aK9unJRIaJc9qiv53LByhF7/I2AuD7Ro4oLfLieVLL9QXNvRx++ANMpv8yCp2UO0KAtBuf70GOxSYb6ElFVRpQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "abort-error": "^1.0.1", + "it-pushable": "^3.2.3", + "main-event": "^1.0.0", + "race-event": "^1.3.0", + "race-signal": "^1.1.3" + } + }, + "node_modules/it-queueless-pushable": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/it-queueless-pushable/-/it-queueless-pushable-2.0.2.tgz", + "integrity": "sha512-2BqIt7XvDdgEgudLAdJkdseAwbVSBc0yAd8yPVHrll4eBuJPWIj9+8C3OIxzEKwhswLtd3bi+yLrzgw9gCyxMA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "abort-error": "^1.0.1", + "p-defer": "^4.0.1", + "race-signal": "^1.1.3" + } + }, + "node_modules/it-reader": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/it-reader/-/it-reader-6.0.4.tgz", + "integrity": "sha512-XCWifEcNFFjjBHtor4Sfaj8rcpt+FkY0L6WdhD578SCDhV4VUm7fCkF3dv5a+fTcfQqvN9BsxBTvWbYO6iCjTg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "it-stream-types": "^2.0.1", + "uint8arraylist": "^2.0.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/it-sort": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/it-sort/-/it-sort-3.0.9.tgz", + "integrity": "sha512-jsM6alGaPiQbcAJdzMsuMh00uJcI+kD9TBoScB8TR75zUFOmHvhSsPi+Dmh2zfVkcoca+14EbfeIZZXTUGH63w==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "it-all": "^3.0.0" + } + }, + "node_modules/it-stream-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/it-stream-types/-/it-stream-types-2.0.2.tgz", + "integrity": "sha512-Rz/DEZ6Byn/r9+/SBCuJhpPATDF9D+dz5pbgSUyBsCDtza6wtNATrz/jz1gDyNanC3XdLboriHnOC925bZRBww==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/it-take": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/it-take/-/it-take-3.0.9.tgz", + "integrity": "sha512-XMeUbnjOcgrhFXPUqa7H0VIjYSV/BvyxxjCp76QHVAFDJw2LmR1SHxUFiqyGeobgzJr7P2ZwSRRJQGn4D2BVlA==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/it-ws": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/it-ws/-/it-ws-6.1.5.tgz", + "integrity": "sha512-uWjMtpy5HqhSd/LlrlP3fhYrr7rUfJFFMABv0F5d6n13Q+0glhZthwUKpEAVhDrXY95Tb1RB5lLqqef+QbVNaw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@types/ws": "^8.2.2", + "event-iterator": "^2.0.0", + "it-stream-types": "^2.0.1", + "uint8arrays": "^5.0.0", + "ws": "^8.4.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js-yaml/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsc-safe-url": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", + "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", + "license": "0BSD", + "peer": true + }, + "node_modules/jsdoc": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz", + "integrity": "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "peer": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "license": "MIT", + "peer": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libp2p": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/libp2p/-/libp2p-2.10.0.tgz", + "integrity": "sha512-tgDz7YuGg1XX7UfxebCUii+IGsly/8V0ZRZdFJSDySY2i3UuqpCTsEbRApH3cBKFhcAf00nx9xj8GL9zfo+XWw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.1.0", + "@chainsafe/netmask": "^2.0.0", + "@libp2p/crypto": "^5.1.8", + "@libp2p/interface": "^2.11.0", + "@libp2p/interface-internal": "^2.3.19", + "@libp2p/logger": "^5.2.0", + "@libp2p/multistream-select": "^6.0.29", + "@libp2p/peer-collections": "^6.0.35", + "@libp2p/peer-id": "^5.1.9", + "@libp2p/peer-store": "^11.2.7", + "@libp2p/utils": "^6.7.2", + "@multiformats/dns": "^1.0.6", + "@multiformats/multiaddr": "^12.4.4", + "@multiformats/multiaddr-matcher": "^2.0.0", + "any-signal": "^4.1.1", + "datastore-core": "^10.0.2", + "interface-datastore": "^8.3.1", + "it-byte-stream": "^2.0.2", + "it-merge": "^3.0.11", + "it-parallel": "^3.0.11", + "main-event": "^1.0.1", + "multiformats": "^13.3.6", + "p-defer": "^4.0.1", + "p-retry": "^6.2.1", + "progress-events": "^1.0.1", + "race-event": "^1.3.0", + "race-signal": "^1.1.3", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "peer": true + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT", + "peer": true + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/main-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/main-event/-/main-event-1.0.1.tgz", + "integrity": "sha512-NWtdGrAca/69fm6DIVd8T9rtfDII4Q8NQbIbsKQq2VzS9eqOGYs8uaNQjcuaCq/d9H/o625aOTJX2Qoxzqw0Pw==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "license": "Unlicense", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT", + "peer": true + }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT", + "peer": true + }, + "node_modules/metro": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.1.tgz", + "integrity": "sha512-UGKepmTxoGD4HkQV8YWvpvwef7fUujNtTgG4Ygf7m/M0qjvb9VuDmAsEU+UdriRX7F61pnVK/opz89hjKlYTXA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "@babel/types": "^7.25.2", + "accepts": "^1.3.7", + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "error-stack-parser": "^2.0.6", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "hermes-parser": "0.29.1", + "image-size": "^1.0.2", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "jsc-safe-url": "^0.2.2", + "lodash.throttle": "^4.1.1", + "metro-babel-transformer": "0.83.1", + "metro-cache": "0.83.1", + "metro-cache-key": "0.83.1", + "metro-config": "0.83.1", + "metro-core": "0.83.1", + "metro-file-map": "0.83.1", + "metro-resolver": "0.83.1", + "metro-runtime": "0.83.1", + "metro-source-map": "0.83.1", + "metro-symbolicate": "0.83.1", + "metro-transform-plugins": "0.83.1", + "metro-transform-worker": "0.83.1", + "mime-types": "^2.1.27", + "nullthrows": "^1.1.1", + "serialize-error": "^2.1.0", + "source-map": "^0.5.6", + "throat": "^5.0.0", + "ws": "^7.5.10", + "yargs": "^17.6.2" + }, + "bin": { + "metro": "src/cli.js" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-babel-transformer": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.1.tgz", + "integrity": "sha512-r3xAD3964E8dwDBaZNSO2aIIvWXjIK80uO2xo0/pi3WI8XWT9h5SCjtGWtMtE5PRWw+t20TN0q1WMRsjvhC1rQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "hermes-parser": "0.29.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-cache": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.1.tgz", + "integrity": "sha512-7N/Ad1PHa1YMWDNiyynTPq34Op2qIE68NWryGEQ4TSE3Zy6a8GpsYnEEZE4Qi6aHgsE+yZHKkRczeBgxhnFIxQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "exponential-backoff": "^3.1.1", + "flow-enums-runtime": "^0.0.6", + "https-proxy-agent": "^7.0.5", + "metro-core": "0.83.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-cache-key": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.1.tgz", + "integrity": "sha512-ZUs+GD5CNeDLxx5UUWmfg26IL+Dnbryd+TLqTlZnDEgehkIa11kUSvgF92OFfJhONeXzV4rZDRGNXoo6JT+8Gg==", + "license": "MIT", + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-config": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.1.tgz", + "integrity": "sha512-HJhpZx3wyOkux/jeF1o7akFJzZFdbn6Zf7UQqWrvp7gqFqNulQ8Mju09raBgPmmSxKDl4LbbNeigkX0/nKY1QA==", + "license": "MIT", + "peer": true, + "dependencies": { + "connect": "^3.6.5", + "cosmiconfig": "^5.0.5", + "flow-enums-runtime": "^0.0.6", + "jest-validate": "^29.7.0", + "metro": "0.83.1", + "metro-cache": "0.83.1", + "metro-core": "0.83.1", + "metro-runtime": "0.83.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-core": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.1.tgz", + "integrity": "sha512-uVL1eAJcMFd2o2Q7dsbpg8COaxjZBBGaXqO2OHnivpCdfanraVL8dPmY6It9ZeqWLOihUKZ2yHW4b6soVCzH/Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "lodash.throttle": "^4.1.1", + "metro-resolver": "0.83.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-file-map": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.1.tgz", + "integrity": "sha512-Yu429lnexKl44PttKw3nhqgmpBR+6UQ/tRaYcxPeEShtcza9DWakCn7cjqDTQZtWR2A8xSNv139izJMyQ4CG+w==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "fb-watchman": "^2.0.0", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "nullthrows": "^1.1.1", + "walker": "^1.0.7" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-minify-terser": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.1.tgz", + "integrity": "sha512-kmooOxXLvKVxkh80IVSYO4weBdJDhCpg5NSPkjzzAnPJP43u6+usGXobkTWxxrAlq900bhzqKek4pBsUchlX6A==", + "license": "MIT", + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "terser": "^5.15.0" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-resolver": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.1.tgz", + "integrity": "sha512-t8j46kiILAqqFS5RNa+xpQyVjULxRxlvMidqUswPEk5nQVNdlJslqizDm/Et3v/JKwOtQGkYAQCHxP1zGStR/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-runtime": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.1.tgz", + "integrity": "sha512-3Ag8ZS4IwafL/JUKlaeM6/CbkooY+WcVeqdNlBG0m4S0Qz0om3rdFdy1y6fYBpl6AwXJwWeMuXrvZdMuByTcRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.25.0", + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-source-map": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.1.tgz", + "integrity": "sha512-De7Vbeo96fFZ2cqmI0fWwVJbtHIwPZv++LYlWSwzTiCzxBDJORncN0LcT48Vi2UlQLzXJg+/CuTAcy7NBVh69A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/traverse": "^7.25.3", + "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-symbolicate": "0.83.1", + "nullthrows": "^1.1.1", + "ob1": "0.83.1", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-source-map/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro-symbolicate": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.1.tgz", + "integrity": "sha512-wPxYkONlq/Sv8Ji7vHEx5OzFouXAMQJjpcPW41ySKMLP/Ir18SsiJK2h4YkdKpYrTS1+0xf8oqF6nxCsT3uWtg==", + "license": "MIT", + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-source-map": "0.83.1", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-symbolicate/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro-transform-plugins": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.1.tgz", + "integrity": "sha512-1Y+I8oozXwhuS0qwC+ezaHXBf0jXW4oeYn4X39XWbZt9X2HfjodqY9bH9r6RUTsoiK7S4j8Ni2C91bUC+sktJQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "flow-enums-runtime": "^0.0.6", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-transform-worker": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.1.tgz", + "integrity": "sha512-owCrhPyUxdLgXEEEAL2b14GWTPZ2zYuab1VQXcfEy0sJE71iciD7fuMcrngoufh7e7UHDZ56q4ktXg8wgiYA1Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "metro": "0.83.1", + "metro-babel-transformer": "0.83.1", + "metro-cache": "0.83.1", + "metro-cache-key": "0.83.1", + "metro-minify-terser": "0.83.1", + "metro-source-map": "0.83.1", + "metro-transform-plugins": "0.83.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "license": "MIT", + "peer": true + }, + "node_modules/metro/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mortice": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/mortice/-/mortice-3.3.1.tgz", + "integrity": "sha512-t3oESfijIPGsmsdLEKjF+grHfrbnKSXflJtgb1wY14cjxZpS6GnhHRXTxxzCAoCCnq1YYfpEPwY3gjiCPhOufQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "abort-error": "^1.0.0", + "it-queue": "^1.1.0", + "main-event": "^1.0.0" + } + }, + "node_modules/ms": { + "version": "3.0.0-canary.202508261828", + "resolved": "https://registry.npmjs.org/ms/-/ms-3.0.0-canary.202508261828.tgz", + "integrity": "sha512-NotsCoUCIUkojWCzQff4ttdCfIPoA1UGZsyQbi7KmqkNRfKCrvga8JJi2PknHymHOuor0cJSn/ylj52Cbt2IrQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/multiformats": { + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.0.tgz", + "integrity": "sha512-Mkb/QcclrJxKC+vrcIFl297h52QcKh2Az/9A5vbWytbQt4225UWWWmIuSsKksdww9NkIeYcA7DkfftyLuC/JSg==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/nanoid": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-abi": { + "version": "3.77.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", + "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT", + "peer": true + }, + "node_modules/node-releases": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz", + "integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==", + "license": "MIT", + "peer": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "license": "MIT", + "peer": true + }, + "node_modules/ob1": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.1.tgz", + "integrity": "sha512-ngwqewtdUzFyycomdbdIhFLjePPSOt1awKMUXQ0L7iLHgWEPF3DsCerblzjzfAUHaXuvE9ccJymWQ/4PNNqvnQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-defer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-4.0.1.tgz", + "integrity": "sha512-Mr5KC5efvAK5VUptYEIopP1bakB85k2IWXaRC0rsh1uwn1L6M0LVml8OIQ4Gudg4oyZakf7FmeRLkMMtZW1i5A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-event": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", + "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", + "license": "MIT", + "dependencies": { + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-queue": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", + "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-wait-for": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/p-wait-for/-/p-wait-for-5.0.2.tgz", + "integrity": "sha512-lwx6u1CotQYPVju77R+D0vFomni/AqRfqLmqQ8hekklqZ6gAY9rONh7lBQ0uxWMkC2AuX9b2DVAl8To0NyP1JA==", + "license": "MIT", + "dependencies": { + "p-timeout": "^6.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "license": "MIT", + "peer": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC", + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/progress-events": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/progress-events/-/progress-events-1.0.1.tgz", + "integrity": "sha512-MOzLIwhpt64KIVN64h1MwdKWiyKFNc/S6BoYKPIVUHFg0/eIEyBulhWCgn678v/4c0ri3FdGuzXymNCv02MUIw==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "license": "MIT", + "peer": true, + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs-cli": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.3.tgz", + "integrity": "sha512-MqD10lqF+FMsOayFiNOdOGNlXc4iKDCf0ZQPkPR+gizYh9gqUeGTWulABUCdI+N67w5RfJ6xhgX4J8pa8qmMXQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/protons": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/protons/-/protons-7.7.0.tgz", + "integrity": "sha512-M17M/UBT+jHeleDioZf53FhdoXJdP7L1LUkjEqhPHK7aSeYW7FDoCZETEjzTap9Knq4KiMLMJk5aIxVrlYgHJw==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "dependencies": { + "meow": "^13.1.0", + "protobufjs-cli": "^1.0.0", + "protons-runtime": "^5.0.0" + }, + "bin": { + "protons": "dist/bin/protons.js" + } + }, + "node_modules/protons-runtime": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-5.6.0.tgz", + "integrity": "sha512-/Kde+sB9DsMFrddJT/UZWe6XqvL7SL5dbag/DBCElFKhkwDj7XKt53S+mzLyaDP5OqS0wXjV5SA572uWDaT0Hg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "uint8-varint": "^2.0.2", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^5.0.1" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/race-event": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/race-event/-/race-event-1.6.1.tgz", + "integrity": "sha512-vi7WH5g5KoTFpu2mme/HqZiWH14XSOtg5rfp6raBskBHl7wnmy3F/biAIyY5MsK+BHWhoPhxtZ1Y2R7OHHaWyQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "abort-error": "^1.0.1" + } + }, + "node_modules/race-signal": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/race-signal/-/race-signal-1.1.3.tgz", + "integrity": "sha512-Mt2NznMgepLfORijhQMncE26IhkmjEphig+/1fKC0OtaKwys/gpvpmswSjoN01SS+VO951mj0L4VIDXdXsjnfA==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-curse": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/react-curse/-/react-curse-1.0.15.tgz", + "integrity": "sha512-7yJxsv3tDXEsPHxeCuJnvCJ+1rRI5fwlXlm7gQqTwhnP+ki2STnNCxgE8yk4JlY8/5aIgciHp25Tqstr0M22Eg==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.24.0", + "react": "^18.3.1", + "react-reconciler": "^0.29.2" + } + }, + "node_modules/react-devtools-core": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz", + "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", + "license": "MIT", + "peer": true, + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT", + "peer": true + }, + "node_modules/react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "peer": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/retimeable-signal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/retimeable-signal/-/retimeable-signal-1.0.1.tgz", + "integrity": "sha512-Cy26CYfbWnYu8HMoJeDhaMpW/EYFIbne3vMf6G9RSrOyWYXbPehja/BEdzpqmM84uy2bfBD7NPZhoQ4GZEtgvg==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "peer": true + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC", + "peer": true + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "peer": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT", + "peer": true + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "license": "MIT", + "peer": true, + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-to-it": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-to-it/-/stream-to-it-1.0.1.tgz", + "integrity": "sha512-AqHYAYPHcmvMrcLNgncE/q0Aj/ajP6A4qGhxP6EVn7K3YTNs0bJpJyk57wc2Heb7MUL64jurvmnmui8D9kjZgA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "it-stream-types": "^2.0.1" + } + }, + "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": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", + "peer": true + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "license": "ISC", + "peer": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "license": "MIT", + "peer": true + }, + "node_modules/timestamp-nano": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/timestamp-nano/-/timestamp-nano-1.0.1.tgz", + "integrity": "sha512-4oGOVZWTu5sl89PtCDnhQBSt7/vL1zVEwAfxH1p49JhTosxzVQWYBYFRFZ8nJmo0G6f824iyP/44BFAwIoKvIA==", + "license": "MIT", + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "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" + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uint8-varint": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/uint8-varint/-/uint8-varint-2.0.4.tgz", + "integrity": "sha512-FwpTa7ZGA/f/EssWAb5/YV6pHgVF1fViKdW8cWaEarjB8t7NyofSWBdOTyFPaGuUG4gx3v1O3PQ8etsiOs3lcw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "uint8arraylist": "^2.0.0", + "uint8arrays": "^5.0.0" + } + }, + "node_modules/uint8arraylist": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/uint8arraylist/-/uint8arraylist-2.4.8.tgz", + "integrity": "sha512-vc1PlGOzglLF0eae1M8mLRTBivsvrGsdmJ5RbK3e+QRvRLOZfZhQROTwH/OfyF3+ZVUg9/8hE8bmKP2CvP9quQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "uint8arrays": "^5.0.1" + } + }, + "node_modules/uint8arrays": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.0.tgz", + "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0" + } + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vlq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", + "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", + "license": "MIT", + "peer": true + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/weald": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/weald/-/weald-1.0.6.tgz", + "integrity": "sha512-sX1PzkcMJZUJ848JbFzB6aKHHglTxqACEnq2KgI75b7vWYvfXFBNbOuDKqFKwCT44CrP6c5r+L4+5GmPnb5/SQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "ms": "^3.0.0-canary.1", + "supports-color": "^10.0.0" + } + }, + "node_modules/weald/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/webcrypto-core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.8.1.tgz", + "integrity": "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/json-schema": "^1.1.12", + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.5", + "tslib": "^2.7.0" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT", + "peer": true + }, + "node_modules/wherearewe": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wherearewe/-/wherearewe-2.0.1.tgz", + "integrity": "sha512-XUguZbDxCA2wBn2LoFtcEhXL6AXo+hVjGonwhSTTTU9SzbWG8Xu3onNIpzf9j/mYUcJQ0f+m37SzG77G851uFw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "is-electron": "^2.2.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "license": "ISC", + "peer": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true, + "license": "Apache-2.0" + }, + "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", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC", + "peer": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + } + } +} diff --git a/node-js-peer/package.json b/node-js-peer/package.json new file mode 100644 index 00000000..20718de7 --- /dev/null +++ b/node-js-peer/package.json @@ -0,0 +1,40 @@ +{ + "name": "universal-connectivity-node-js-peer", + "type": "module", + "scripts": { + "start": "npx esbuild src/App.tsx --outfile=.dist/index.js --bundle --platform=node --format=esm --external:'./node_modules/*' --sourcemap && node --enable-source-maps .dist", + "dist": "npx esbuild src/App.tsx --outfile=.dist/index.cjs --bundle --platform=node --define:'process.env.NODE_ENV=\"production\"' --minify --tree-shaking=true", + "generate": "protons protobuf/*.proto", + "format": "prettier --write src", + "lint": "prettier . --check" + }, + "dependencies": { + "@chainsafe/libp2p-gossipsub": "^14.1.1", + "@chainsafe/libp2p-noise": "^16.1.0", + "@chainsafe/libp2p-quic": "^1.1.1", + "@chainsafe/libp2p-yamux": "^7.0.1", + "@helia/delegated-routing-v1-http-api-client": "^4.2.2", + "@libp2p/bootstrap": "^11.0.33", + "@libp2p/circuit-relay-v2": "^3.2.9", + "@libp2p/identify": "^3.0.28", + "@libp2p/interface": "^2.8.0", + "@libp2p/interface-internal": "^2.3.10", + "@libp2p/ping": "^2.0.28", + "@libp2p/pubsub-peer-discovery": "^11.0.1", + "@libp2p/tcp": "^10.1.9", + "@libp2p/webrtc": "^5.2.10", + "@libp2p/websockets": "^9.2.9", + "it-protobuf-stream": "^2.0.1", + "libp2p": "^2.8.3", + "multiformats": "^13.3.2", + "protons-runtime": "^5.5.0", + "react": "^18.3.1", + "react-curse": "1.0.15", + "uint8arraylist": "^2.4.8" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@types/react": "^18.0.27", + "protons": "^7.6.0" + } +} diff --git a/node-js-peer/src/App.tsx b/node-js-peer/src/App.tsx new file mode 100644 index 00000000..bfa068a9 --- /dev/null +++ b/node-js-peer/src/App.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import ReactCurse, { Banner, useSize } from 'react-curse' +import { AppWrapper } from './context/index.js' +import { PeerList } from './components/peer-list.js' +import { layout } from './lib/position.js' +import { SendMessage } from './components/send-message.js' +import Messages from './components/messages.js' + +const App = () => { + const dims = useSize() + + let title = 'Universal Connectivity Node.js Peer' + + if (dims.width < 140) { + title = 'UC Node.js Peer' + } + + if (dims.width < 50) { + dims.width = 50 + } + + if (dims.height < 30) { + dims.height = 30 + } + + return ( + <> + {title} + + + + + ) +} + +ReactCurse.render(( + + + +)) diff --git a/node-js-peer/src/components/booting.tsx b/node-js-peer/src/components/booting.tsx new file mode 100644 index 00000000..119d78fe --- /dev/null +++ b/node-js-peer/src/components/booting.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { Text } from 'react-curse' + +interface Props { + error?: string +} + +export function Booting({ error }: Props) { + if (error) { + return ( + Failed to start - {error} + ) + } + + return ( + ...libp2p is starting + ) +} diff --git a/node-js-peer/src/components/message.tsx b/node-js-peer/src/components/message.tsx new file mode 100644 index 00000000..6d325124 --- /dev/null +++ b/node-js-peer/src/components/message.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { Text } from 'react-curse' +import { useLibp2pContext } from '../context/index.js' +import { ChatMessage } from '../context/chat.js' +import { useMarkAsRead } from '../hooks/mark-as-read.js' +import { peerIdFromString } from '@libp2p/peer-id' +import { peerColor } from '../lib/peer-color.js' + +interface Props extends ChatMessage { + dm: boolean + children: any +} + +export const Message = ({ msgId, msg, peerId, read, dm, receivedAt }: Props) => { + const { libp2p } = useLibp2pContext() + + const p = peerIdFromString(peerId) + + const isSelf = libp2p.peerId.equals(p) + const timestamp = new Date(receivedAt).toLocaleString() + + useMarkAsRead(msgId, peerId, read, dm) + + const color = isSelf ? undefined : `#${peerColor(p)}` + + return ( + <> + {timestamp} - {peerId.substring(peerId.length - 7)} - {msg} + + ) +} diff --git a/node-js-peer/src/components/messages.tsx b/node-js-peer/src/components/messages.tsx new file mode 100644 index 00000000..f54a1abb --- /dev/null +++ b/node-js-peer/src/components/messages.tsx @@ -0,0 +1,58 @@ +import React, { useEffect, useState } from 'react' +import { Text, Frame, View } from 'react-curse' +import { useLibp2pContext } from '../context/index.js' +import { PUBLIC_CHAT_ROOM_ID } from '../constants.js' +import { ChatMessage, useChatContext } from '../context/chat.js' +import { Message } from './message.js' +import { Logger } from '@libp2p/interface' +import { layout, PositionProps } from '../lib/position.js' +import { shortPeerId } from '../lib/short-peer-id.js' + +let log: Logger + +export default function Messages(props: PositionProps) { + const { libp2p } = useLibp2pContext() + const { roomId, setRoomId } = useChatContext() + const { messageHistory, setMessageHistory, directMessages, setDirectMessages } = useChatContext() + const [ messages, setMessages ] = useState([]) + + log = log ?? libp2p.logger.forComponent('chat') + + const handleBackToPublic = () => { + setRoomId(PUBLIC_CHAT_ROOM_ID) + setMessages(messageHistory) + } + + useEffect(() => { + // assumes a chat room is a peerId thus a direct message + if (roomId === PUBLIC_CHAT_ROOM_ID) { + setMessages(messageHistory) + } else { + setMessages(directMessages[roomId] || []) + } + }, [roomId, directMessages, messageHistory]) + + const title = roomId === PUBLIC_CHAT_ROOM_ID ? `Public chat (${shortPeerId(libp2p.peerId)})` : `DM (${shortPeerId(libp2p.peerId)} x ${shortPeerId(roomId)})` + + return ( + <> + + { + messages.map(({ msgId, msg, peerId, read, receivedAt }) => ( + + )) + } + + {title} + + ) +} diff --git a/node-js-peer/src/components/peer-list.tsx b/node-js-peer/src/components/peer-list.tsx new file mode 100644 index 00000000..cffbef20 --- /dev/null +++ b/node-js-peer/src/components/peer-list.tsx @@ -0,0 +1,42 @@ +import { useLibp2pContext } from '../context/index.js' +import { CHAT_TOPIC } from '../constants.js' +import React, { useEffect, useState } from 'react' +import { Peer } from './peer.js' +import type { PeerId } from '@libp2p/interface' +import { Text, Frame, View } from 'react-curse' +import { PositionProps } from '../index.js' + +export function PeerList(props: PositionProps) { + const { libp2p } = useLibp2pContext() + const [subscribers, setSubscribers] = useState([]) + + useEffect(() => { + const onSubscriptionChange = () => { + setSubscribers(libp2p.services.pubsub.getSubscribers(CHAT_TOPIC)) + } + onSubscriptionChange() + + libp2p.services.pubsub.addEventListener('subscription-change', onSubscriptionChange) + + return () => { + libp2p.services.pubsub.removeEventListener('subscription-change', onSubscriptionChange) + } + }, [libp2p, setSubscribers]) + + return ( + <> + + + {/* Have to specify empty children prop - https://github.com/infely/react-curse/pull/9 */} + + {subscribers.map((p) => ( + + ))} + + + + Topic Peers ({subscribers.length}) + + + ) +} diff --git a/node-js-peer/src/components/peer.tsx b/node-js-peer/src/components/peer.tsx new file mode 100644 index 00000000..810d4a5f --- /dev/null +++ b/node-js-peer/src/components/peer.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { PeerId } from '@libp2p/interface' +import { useChatContext } from '../context/chat.js' +import { Text } from 'react-curse' +import { shortPeerId } from '../lib/short-peer-id.js' +import { peerColor } from '../lib/peer-color.js' + +export interface PeerProps { + peer: PeerId + self: boolean + children: any +} + +export function Peer({ peer, self }: PeerProps) { + const { directMessages } = useChatContext() + const dmCount = directMessages[peer.toString()]?.length + + const color = peerColor(peer) + return ( + + {shortPeerId(peer)} {self ? '(You)' : ''} {dmCount ? `(${dmCount})` : ''} + + ) +} diff --git a/node-js-peer/src/components/send-message.tsx b/node-js-peer/src/components/send-message.tsx new file mode 100644 index 00000000..b62f7f7d --- /dev/null +++ b/node-js-peer/src/components/send-message.tsx @@ -0,0 +1,86 @@ +import React, { useCallback } from 'react' +import { Text, Frame, Input } from 'react-curse' +import { useLibp2pContext } from '../context/index.js' +import { CHAT_TOPIC, PUBLIC_CHAT_ROOM_ID } from '../constants.js' +import { layout, PositionProps } from '../lib/position.js' +import { ChatMessage, useChatContext } from '../context/chat.js' +import { peerIdFromString } from '@libp2p/peer-id' +import { Logger } from '@libp2p/interface' + +let log: Logger + +export function SendMessage(props: PositionProps) { + const { libp2p } = useLibp2pContext() + const { roomId, messageHistory, setMessageHistory, directMessages, setDirectMessages } = useChatContext() + + log ??= libp2p.logger.forComponent('chat:send-message') + + const onSubmit = (text) => { + if (roomId === PUBLIC_CHAT_ROOM_ID) { + sendPublicMessage(text) + } else { + sendDirectMessage(text) + } + } + + // Send message to public chat over gossipsub + const sendPublicMessage = useCallback(async (input: string) => { + if (input === '') { + return + } + + await libp2p.services.pubsub.publish(CHAT_TOPIC, new TextEncoder().encode(input)) + const myPeerId = libp2p.peerId.toString() + + setMessageHistory([ + ...messageHistory, + { + msgId: crypto.randomUUID(), + msg: input, + peerId: myPeerId, + read: true, + receivedAt: Date.now(), + }, + ]) + }, [messageHistory, libp2p, setMessageHistory]) + + // Send direct message over custom protocol + const sendDirectMessage = useCallback(async (input: string) => { + try { + const res = await libp2p.services.directMessage.send(peerIdFromString(roomId), input) + + if (!res) { + log('Failed to send message') + return + } + + const myPeerId = libp2p.peerId.toString() + + const newMessage: ChatMessage = { + msgId: crypto.randomUUID(), + msg: input, + peerId: myPeerId, + read: true, + receivedAt: Date.now(), + } + + const updatedMessages = directMessages[roomId] ? [...directMessages[roomId], newMessage] : [newMessage] + + setDirectMessages({ + ...directMessages, + [roomId]: updatedMessages, + }) + } catch (e: any) { + log.error('error sending message - %e', e) + } + }, [libp2p, setDirectMessages, directMessages, roomId]) + + return ( + <> + + + + Send message + + ) +} diff --git a/node-js-peer/src/constants.ts b/node-js-peer/src/constants.ts new file mode 100644 index 00000000..ae343abb --- /dev/null +++ b/node-js-peer/src/constants.ts @@ -0,0 +1,13 @@ +export const CHAT_TOPIC = 'universal-connectivity' +export const CHAT_FILE_TOPIC = 'universal-connectivity-file' +export const PUBSUB_PEER_DISCOVERY = 'universal-connectivity-browser-peer-discovery' +export const DIRECT_MESSAGE_PROTOCOL = '/universal-connectivity/dm/1.0.0' +export const CIRCUIT_RELAY_CODE = 290 +export const MIME_TEXT_PLAIN = 'text/plain' +export const PUBLIC_CHAT_ROOM_ID = '' + +// 👇 App specific dedicated bootstrap PeerIDs +// Their multiaddrs are ephemeral so peer routing is used to resolve multiaddr +export const BOOTSTRAP_PEER_IDS = [ + '12D3KooWFhXabKDwALpzqMbto94sB7rvmZ6M28hs9Y9xSopDKwQr' +] diff --git a/node-js-peer/src/context/chat.tsx b/node-js-peer/src/context/chat.tsx new file mode 100644 index 00000000..4dc5c2be --- /dev/null +++ b/node-js-peer/src/context/chat.tsx @@ -0,0 +1,149 @@ +import React, { createContext, useContext, useEffect, useState } from 'react' +import { useLibp2pContext } from './index.js' +import type { Logger, Message } from '@libp2p/interface' +import { + CHAT_TOPIC, + MIME_TEXT_PLAIN, + PUBSUB_PEER_DISCOVERY, +} from '../constants.js' +import { DirectMessageEvent, directMessageEvent } from '../lib/direct-message.js' + +let log: Logger + +export interface ChatMessage { + msgId: string + msg: string + peerId: string + read: boolean + receivedAt: number +} + +export interface DirectMessages { + [peerId: string]: ChatMessage[] +} + +type Chatroom = string + +export interface ChatContextInterface { + messageHistory: ChatMessage[] + setMessageHistory: (messageHistory: ChatMessage[] | ((prevMessages: ChatMessage[]) => ChatMessage[])) => void + directMessages: DirectMessages + setDirectMessages: (directMessages: DirectMessages | ((prevMessages: DirectMessages) => DirectMessages)) => void + roomId: Chatroom + setRoomId: (chatRoom: Chatroom) => void +} + +export const chatContext = createContext({ + messageHistory: [], + setMessageHistory: () => {}, + directMessages: {}, + setDirectMessages: () => {}, + roomId: '', + setRoomId: () => {} +}) + +export const useChatContext = () => { + return useContext(chatContext) +} + +export const ChatProvider = ({ children }: any) => { + const [messageHistory, setMessageHistory] = useState([]) + const [directMessages, setDirectMessages] = useState({}) + const [roomId, setRoomId] = useState('') + + const { libp2p } = useLibp2pContext() + log = log ?? libp2p.logger.forComponent('chat-context') + + const messageCB = (evt: CustomEvent) => { + const { topic, data } = evt.detail + + switch (topic) { + case CHAT_TOPIC: { + chatMessageCB(evt, topic, data) + break + } + case PUBSUB_PEER_DISCOVERY: { + break + } + default: { + log.error('Unexpected event %o on gossipsub topic: %s', evt, topic) + } + } + } + + const chatMessageCB = (evt: CustomEvent, topic: string, data: Uint8Array) => { + const msg = new TextDecoder().decode(data) + log(`${topic}: ${msg}`) + + // Append signed messages, otherwise discard + if (evt.detail.type === 'signed') { + setMessageHistory([ + ...messageHistory, + { + msgId: crypto.randomUUID(), + msg, + peerId: evt.detail.from.toString(), + read: false, + receivedAt: Date.now(), + }, + ]) + } + } + + useEffect(() => { + const handleDirectMessage = (evt: CustomEvent) => { + const peerId = evt.detail.connection.remotePeer.toString() + + if (evt.detail.type !== MIME_TEXT_PLAIN) { + throw new Error(`unexpected message type: ${evt.detail.type}`) + } + + const message: ChatMessage = { + msg: evt.detail.content, + read: false, + msgId: crypto.randomUUID(), + peerId: peerId, + receivedAt: Date.now(), + } + + const updatedMessages = directMessages[peerId] ? [...directMessages[peerId], message] : [message] + + setDirectMessages({ + ...directMessages, + [peerId]: updatedMessages, + }) + } + + libp2p.services.directMessage.addEventListener(directMessageEvent, handleDirectMessage) + + return () => { + libp2p.services.directMessage.removeEventListener(directMessageEvent, handleDirectMessage) + } + }, [directMessages, libp2p.services.directMessage, setDirectMessages]) + + useEffect(() => { + libp2p.services.pubsub.addEventListener('message', messageCB) + + return () => { + ;(async () => { + // Cleanup handlers 👇 + libp2p.services.pubsub.removeEventListener('message', messageCB) + })() + } + }) + + return ( + + {children} + + ) +} diff --git a/node-js-peer/src/context/index.tsx b/node-js-peer/src/context/index.tsx new file mode 100644 index 00000000..6b6649a1 --- /dev/null +++ b/node-js-peer/src/context/index.tsx @@ -0,0 +1,71 @@ +import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react' +import { startLibp2p } from '../lib/libp2p.js' +import { ChatProvider } from './chat.js' +import type { Libp2p, PubSub } from '@libp2p/interface' +import type { Identify } from '@libp2p/identify' +import type { DirectMessage } from '../lib/direct-message.js' +import type { DelegatedRoutingV1HttpApiClient } from '@helia/delegated-routing-v1-http-api-client' +import { Booting } from '../components/booting.js' + +export type Libp2pType = Libp2p<{ + pubsub: PubSub + identify: Identify + directMessage: DirectMessage + delegatedRouting: DelegatedRoutingV1HttpApiClient +}> + +export const libp2pContext = createContext<{ libp2p: Libp2pType }>({ + // @ts-ignore to avoid having to check isn't undefined everywhere. Can't be undefined because children are conditionally rendered + libp2p: undefined, +}) + +interface WrapperProps { + children?: ReactNode +} + +// This is needed to prevent libp2p from instantiating more than once +let loaded = false +export function AppWrapper({ children }: WrapperProps) { + const [libp2p, setLibp2p] = useState(undefined) + const [error, setError] = useState('') + + useEffect(() => { + const init = async () => { + if (loaded) return + try { + loaded = true + const libp2p = await startLibp2p() + + if (!libp2p) { + throw new Error('failed to start libp2p') + } + + // @ts-ignore + globalThis.libp2p = libp2p + + setLibp2p(libp2p as Libp2pType) + } catch (e) { + console.error('failed to start libp2p', e) + setError(`failed to start libp2p ${e}`) + } + } + + init() + }, []) + + if (!libp2p) { + return + } + + return ( + <> + + {{children}} + + + ) +} + +export function useLibp2pContext() { + return useContext(libp2pContext) +} diff --git a/node-js-peer/src/hooks/mark-as-read.ts b/node-js-peer/src/hooks/mark-as-read.ts new file mode 100644 index 00000000..5088e61f --- /dev/null +++ b/node-js-peer/src/hooks/mark-as-read.ts @@ -0,0 +1,31 @@ +import { useEffect, useCallback } from 'react' +import { ChatMessage, useChatContext } from '../context/chat.js' + +export const useMarkAsRead = (msgId: string, peerId: string, read: boolean, dm: boolean) => { + const { messageHistory, setMessageHistory, directMessages, setDirectMessages } = useChatContext() + + const markAsRead = useCallback((messages: ChatMessage[], msgId: string): ChatMessage[] => { + return messages.map((m) => (m.msgId === msgId ? { ...m, read: true } : m)) + }, []) + + useEffect(() => { + if (read) { + return + } + + if (dm) { + const updatedDMs = directMessages[peerId] + + if (updatedDMs.some((m) => m.msgId === msgId && !m.read)) { + setDirectMessages((prev) => ({ + ...prev, + [peerId]: markAsRead(updatedDMs, msgId), + })) + } + } else { + if (messageHistory.some((m) => m.msgId === msgId && !m.read)) { + setMessageHistory((prev) => markAsRead(prev, msgId)) + } + } + }, [dm, directMessages, messageHistory, msgId, peerId, read, setDirectMessages, setMessageHistory, markAsRead]) +} diff --git a/node-js-peer/src/lib/direct-message.ts b/node-js-peer/src/lib/direct-message.ts new file mode 100644 index 00000000..7ae2983f --- /dev/null +++ b/node-js-peer/src/lib/direct-message.ts @@ -0,0 +1,201 @@ +import { PeerId, Stream, Connection, TypedEventEmitter, Startable } from '@libp2p/interface' +import { DIRECT_MESSAGE_PROTOCOL, MIME_TEXT_PLAIN } from '../constants.js' +import { serviceCapabilities, serviceDependencies } from '@libp2p/interface' +import type { ConnectionManager } from '@libp2p/interface-internal' +import type { Registrar } from '@libp2p/interface-internal' +import { dm } from '../protobuf/direct-message.js' +import { pbStream } from 'it-protobuf-stream' + +export const dmClientVersion = '0.0.1' +export const directMessageEvent = 'message' + +const ERRORS = { + EMPTY_MESSAGE: 'Message cannot be empty', + NO_CONNECTION: 'Failed to create connection', + NO_STREAM: 'Failed to create stream', + NO_RESPONSE: 'No response received', + NO_METADATA: 'No metadata in response', + STATUS_NOT_OK: (status: dm.Status) => `Received status: ${status}, expected OK`, +} + +export interface DirectMessageEvent { + content: string + type: string + stream: Stream + connection: Connection +} + +export interface DirectMessageEvents { + message: CustomEvent +} + +interface DirectMessageComponents { + registrar: Registrar + connectionManager: ConnectionManager +} + +export class DirectMessage extends TypedEventEmitter implements Startable { + readonly [serviceDependencies]: string[] = [ + '@libp2p/identify', + '@libp2p/connection-encryption', + '@libp2p/transport', + '@libp2p/stream-multiplexing', + ] + + readonly [serviceCapabilities]: string[] = ['@universal-connectivity/direct-message'] + + private topologyId?: string + private readonly components: DirectMessageComponents + private dmPeers: Set = new Set() + + constructor(components: DirectMessageComponents) { + super() + this.components = components + } + + async start(): Promise { + this.topologyId = await this.components.registrar.register(DIRECT_MESSAGE_PROTOCOL, { + onConnect: this.handleConnect.bind(this), + onDisconnect: this.handleDisconnect.bind(this), + }) + } + + async afterStart(): Promise { + await this.components.registrar.handle(DIRECT_MESSAGE_PROTOCOL, async ({ stream, connection }) => { + await this.receive(stream, connection) + }) + } + + stop(): void { + if (this.topologyId != null) { + this.components.registrar.unregister(this.topologyId) + } + } + + private handleConnect(peerId: PeerId): void { + this.dmPeers.add(peerId.toString()) + } + + private handleDisconnect(peerId: PeerId): void { + this.dmPeers.delete(peerId.toString()) + } + + isDMPeer(peerId: PeerId): boolean { + return this.dmPeers.has(peerId.toString()) + } + + async send(peerId: PeerId, message: string): Promise { + if (!message) { + throw new Error(ERRORS.EMPTY_MESSAGE) + } + + let stream: Stream | undefined + + try { + // openConnection will return the current open connection if it already exists, or create a new one + const conn = await this.components.connectionManager.openConnection(peerId, { signal: AbortSignal.timeout(5000) }) + if (!conn) { + throw new Error(ERRORS.NO_CONNECTION) + } + + // Single protocols can skip full negotiation + const stream = await conn.newStream(DIRECT_MESSAGE_PROTOCOL, { + negotiateFully: false, + }) + + if (!stream) { + throw new Error(ERRORS.NO_STREAM) + } + + const datastream = pbStream(stream) + + const req: dm.DirectMessageRequest = { + content: message, + type: MIME_TEXT_PLAIN, + metadata: { + clientVersion: dmClientVersion, + timestamp: BigInt(Date.now()), + }, + } + + const signal = AbortSignal.timeout(5000) + + await datastream.write(req, dm.DirectMessageRequest, { signal }) + + const res = await datastream.read(dm.DirectMessageResponse, { signal }) + + if (!res) { + throw new Error(ERRORS.NO_RESPONSE) + } + + if (!res.metadata) { + throw new Error(ERRORS.NO_METADATA) + } + + if (res.status !== dm.Status.OK) { + throw new Error(ERRORS.STATUS_NOT_OK(res.status)) + } + } catch (e: any) { + stream?.abort(e) + throw e + } finally { + try { + await stream?.close({ + signal: AbortSignal.timeout(5000), + }) + } catch (err: any) { + stream?.abort(err) + throw err + } + } + + return true + } + + async receive(stream: Stream, connection: Connection): Promise { + try { + const datastream = pbStream(stream) + + const signal = AbortSignal.timeout(5000) + + const req = await datastream.read(dm.DirectMessageRequest, { signal }) + + const res: dm.DirectMessageResponse = { + status: dm.Status.OK, + metadata: { + clientVersion: dmClientVersion, + timestamp: BigInt(Date.now()), + }, + } + + await datastream.write(res, dm.DirectMessageResponse, { signal }) + + const detail: DirectMessageEvent = { + content: req.content, + type: req.type, + stream: stream, + connection: connection, + } + + this.dispatchEvent(new CustomEvent(directMessageEvent, { detail })) + } catch (e: any) { + stream?.abort(e) + throw e + } finally { + try { + await stream?.close({ + signal: AbortSignal.timeout(5000), + }) + } catch (err: any) { + stream?.abort(err) + throw err + } + } + } +} + +export function directMessage() { + return (components: DirectMessageComponents) => { + return new DirectMessage(components) + } +} diff --git a/node-js-peer/src/lib/libp2p.ts b/node-js-peer/src/lib/libp2p.ts new file mode 100644 index 00000000..da76e3c5 --- /dev/null +++ b/node-js-peer/src/lib/libp2p.ts @@ -0,0 +1,113 @@ +import { + createDelegatedRoutingV1HttpApiClient, +} from '@helia/delegated-routing-v1-http-api-client' +import { createLibp2p } from 'libp2p' +import { identify } from '@libp2p/identify' +import { noise } from '@chainsafe/libp2p-noise' +import { yamux } from '@chainsafe/libp2p-yamux' +import { sha256 } from 'multiformats/hashes/sha2' +import type { Message, SignedMessage } from '@libp2p/interface' +import { gossipsub } from '@chainsafe/libp2p-gossipsub' +import { webSockets } from '@libp2p/websockets' +import { webRTC, webRTCDirect } from '@libp2p/webrtc' +import { circuitRelayTransport } from '@libp2p/circuit-relay-v2' +import { pubsubPeerDiscovery } from '@libp2p/pubsub-peer-discovery' +import { ping } from '@libp2p/ping' +import { BOOTSTRAP_PEER_IDS, CHAT_TOPIC, PUBSUB_PEER_DISCOVERY } from '../constants.js' +import { directMessage } from './direct-message.js' +import { quic } from '@chainsafe/libp2p-quic' +import { tcp } from '@libp2p/tcp' +import { peerIdFromString } from '@libp2p/peer-id' + +// message IDs are used to dedupe inbound messages +// every agent in network should use the same message id function +// messages could be perceived as duplicate if this isn't added (as opposed to +// rust peer which has unique message ids) +export async function msgIdFnStrictNoSign(msg: Message): Promise { + var enc = new TextEncoder() + + const signedMessage = msg as SignedMessage + const encodedSeqNum = enc.encode(signedMessage.sequenceNumber.toString()) + return await sha256.encode(encodedSeqNum) +} + +export async function startLibp2p () { + const delegatedClient = createDelegatedRoutingV1HttpApiClient('https://delegated-ipfs.dev') + const node = await createLibp2p({ + addresses: { + listen: [ + '/webrtc-direct', + '/ip4/0.0.0.0/tcp/0', + '/ip4/0.0.0.0/udp/0/quic-v1' + ] + }, + transports: [ + webSockets(), + webRTC(), + webRTCDirect(), + circuitRelayTransport(), + quic(), + tcp() + ], + connectionEncrypters: [noise()], + streamMuxers: [yamux()], + connectionGater: { + denyDialMultiaddr: async () => false + }, + peerDiscovery: [ + pubsubPeerDiscovery({ + interval: 10_000, + topics: [PUBSUB_PEER_DISCOVERY], + listenOnly: false + }) + ], + services: { + pubsub: gossipsub({ + allowPublishToZeroTopicPeers: true, + msgIdFn: msgIdFnStrictNoSign, + ignoreDuplicatePublishError: true, + }), + // Delegated routing helps us discover the ephemeral multiaddrs of the + // dedicated go and rust bootstrap peers + // This relies on the public delegated routing endpoint + // See https://docs.ipfs.tech/concepts/public-utilities/#delegated-routing + delegatedRouting: () => delegatedClient, + identify: identify(), + // Custom protocol for direct messaging + directMessage: directMessage(), + ping: ping() + } + }) + + // subscribe to incoming chat messages + node.services.pubsub.subscribe(CHAT_TOPIC) + + // find and dial the bootstrap peers + Promise.resolve().then(async () => { + for (const id of BOOTSTRAP_PEER_IDS) { + const peerId = peerIdFromString(id) + const peer = await node.peerRouting.findPeer(peerId, { + useCache: false + }) + await node.dial(peer.id) + } + }) + .catch(err => { + console.error('bootstrap error', err) + }) + + // try to dial topic peers - this is a hack to make them appear in the chat + // peer list. + // + // Note that we do not need a connection to a peer to receive its messages + // since they will be forwarded on by mesh peers. For more info see the spec: + // https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/README.md + node.services.pubsub.addEventListener('message', (evt) => { + if (evt.detail.topic === CHAT_TOPIC && evt.detail.type === 'signed') { + node.dial(evt.detail.from) + .catch(() => {}) + } + }) + + return node +} diff --git a/node-js-peer/src/lib/peer-color.ts b/node-js-peer/src/lib/peer-color.ts new file mode 100644 index 00000000..ad232e02 --- /dev/null +++ b/node-js-peer/src/lib/peer-color.ts @@ -0,0 +1,16 @@ +import { PeerId } from '@libp2p/interface' +import { peerIdFromString } from '@libp2p/peer-id' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' + +/** + * use the last 6 chars of the peer id as a hex code to create a deterministic + * color + */ +export function peerColor (peerId: PeerId | string): string { + if (typeof peerId === 'string') { + peerId = peerIdFromString(peerId) + } + + const peerIdString = uint8ArrayToString(peerId.toCID().bytes, 'base16') + return peerIdString.substring(peerIdString.length - 6).toUpperCase() +} diff --git a/node-js-peer/src/lib/position.ts b/node-js-peer/src/lib/position.ts new file mode 100644 index 00000000..47f8ea02 --- /dev/null +++ b/node-js-peer/src/lib/position.ts @@ -0,0 +1,13 @@ +export interface PositionProps { + x: number + y: number + height: number + width: number +} + +export const layout = { + margin: 2, + bannerHeight: 3, + peerListWidth: 40, + inputHeight: 2 +} diff --git a/node-js-peer/src/lib/short-peer-id.ts b/node-js-peer/src/lib/short-peer-id.ts new file mode 100644 index 00000000..b83f7c70 --- /dev/null +++ b/node-js-peer/src/lib/short-peer-id.ts @@ -0,0 +1,12 @@ +import { PeerId, isPeerId } from '@libp2p/interface' + +/** + * Returns the last `length` characters of the peer id + */ +export function shortPeerId (peerId: PeerId | string, length = 7): string { + if (isPeerId(peerId)) { + peerId = peerId.toString() + } + + return peerId.substring(peerId.length - length) +} diff --git a/node-js-peer/src/protobuf/direct-message.proto b/node-js-peer/src/protobuf/direct-message.proto new file mode 100644 index 00000000..f2d018ac --- /dev/null +++ b/node-js-peer/src/protobuf/direct-message.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package dm; + +service DirectMessage { + rpc DirectMessage (DirectMessageRequest) returns (DirectMessageResponse) {} +} + +message Metadata { + string clientVersion = 1; // client version + int64 timestamp = 2; // unix time +} + +enum Status { + UNKNOWN = 0; + OK = 200; + ERROR = 500; +} + +message DirectMessageRequest { + Metadata metadata = 1; + string content = 2; + string type = 3; +} + +message DirectMessageResponse{ + Metadata metadata = 1; + Status status = 2; + optional string statusText = 3; +} diff --git a/node-js-peer/src/protobuf/direct-message.ts b/node-js-peer/src/protobuf/direct-message.ts new file mode 100644 index 00000000..7fb5bd0d --- /dev/null +++ b/node-js-peer/src/protobuf/direct-message.ts @@ -0,0 +1,356 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { type Codec, decodeMessage, type DecodeOptions, encodeMessage, enumeration, message } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface dm {} + +export namespace dm { + export interface DirectMessage {} + + export namespace DirectMessage { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length, opts = {}) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: { + reader.skipType(tag & 7) + break + } + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DirectMessage.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions): DirectMessage => { + return decodeMessage(buf, DirectMessage.codec(), opts) + } + } + + export interface Metadata { + clientVersion: string + timestamp: bigint + } + + export namespace Metadata { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.clientVersion != null && obj.clientVersion !== '')) { + w.uint32(10) + w.string(obj.clientVersion) + } + + if ((obj.timestamp != null && obj.timestamp !== 0n)) { + w.uint32(16) + w.int64(obj.timestamp) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length, opts = {}) => { + const obj: any = { + clientVersion: '', + timestamp: 0n + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: { + obj.clientVersion = reader.string() + break + } + case 2: { + obj.timestamp = reader.int64() + break + } + default: { + reader.skipType(tag & 7) + break + } + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Metadata.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions): Metadata => { + return decodeMessage(buf, Metadata.codec(), opts) + } + } + + export enum Status { + UNKNOWN = 'UNKNOWN', + OK = 'OK', + ERROR = 'ERROR' + } + + enum __StatusValues { + UNKNOWN = 0, + OK = 200, + ERROR = 500 + } + + export namespace Status { + export const codec = (): Codec => { + return enumeration(__StatusValues) + } + } + + export interface DirectMessageRequest { + metadata?: dm.Metadata + content: string + type: string + } + + export namespace DirectMessageRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.metadata != null) { + w.uint32(10) + dm.Metadata.codec().encode(obj.metadata, w) + } + + if ((obj.content != null && obj.content !== '')) { + w.uint32(18) + w.string(obj.content) + } + + if ((obj.type != null && obj.type !== '')) { + w.uint32(26) + w.string(obj.type) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length, opts = {}) => { + const obj: any = { + content: '', + type: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: { + obj.metadata = dm.Metadata.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.metadata + }) + break + } + case 2: { + obj.content = reader.string() + break + } + case 3: { + obj.type = reader.string() + break + } + default: { + reader.skipType(tag & 7) + break + } + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DirectMessageRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions): DirectMessageRequest => { + return decodeMessage(buf, DirectMessageRequest.codec(), opts) + } + } + + export interface DirectMessageResponse { + metadata?: dm.Metadata + status: dm.Status + statusText?: string + } + + export namespace DirectMessageResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.metadata != null) { + w.uint32(10) + dm.Metadata.codec().encode(obj.metadata, w) + } + + if (obj.status != null && __StatusValues[obj.status] !== 0) { + w.uint32(16) + dm.Status.codec().encode(obj.status, w) + } + + if (obj.statusText != null) { + w.uint32(26) + w.string(obj.statusText) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length, opts = {}) => { + const obj: any = { + status: Status.UNKNOWN + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: { + obj.metadata = dm.Metadata.codec().decode(reader, reader.uint32(), { + limits: opts.limits?.metadata + }) + break + } + case 2: { + obj.status = dm.Status.codec().decode(reader) + break + } + case 3: { + obj.statusText = reader.string() + break + } + default: { + reader.skipType(tag & 7) + break + } + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DirectMessageResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions): DirectMessageResponse => { + return decodeMessage(buf, DirectMessageResponse.codec(), opts) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length, opts = {}) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: { + reader.skipType(tag & 7) + break + } + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, dm.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions): dm => { + return decodeMessage(buf, dm.codec(), opts) + } +} diff --git a/py-peer/.gitignore b/py-peer/.gitignore new file mode 100644 index 00000000..6be062f1 --- /dev/null +++ b/py-peer/.gitignore @@ -0,0 +1,3 @@ +*.pyc +.bin +.env \ No newline at end of file diff --git a/py-peer/API_REFERENCE.md b/py-peer/API_REFERENCE.md new file mode 100644 index 00000000..3bad1633 --- /dev/null +++ b/py-peer/API_REFERENCE.md @@ -0,0 +1,1653 @@ +# py-peer Tornado API Reference + +**Base URL:** `http://localhost:8765` +**API Version:** `v1` +**Framework:** [Tornado](https://www.tornadoweb.org/) 6.5+ +**Python:** 3.12 · **libp2p:** py-libp2p 0.6.0 + +--- + +## Overview + +The py-peer Tornado API exposes the full capabilities of a running libp2p node over HTTP REST and WebSocket. It allows any HTTP client — browser, CLI, frontend app, or another service — to interact with the peer without a UI. + +### Starting the API server + +```bash +python main.py --nick --api --api-port 8765 +``` + +| Flag | Default | Description | +|---|---|---| +| `--api` | — | Enable Tornado REST + WebSocket server | +| `--api-port` | `8765` | Port for the API server | +| `--nick` | auto | Nickname shown in chat | +| `--port` | auto | libp2p TCP listen port | +| `--no-strict-signing` | off | Disable message signature verification | +| `--api-routes` | — | Print all routes and exit immediately | + +```bash +# Print all routes without starting +python main.py --api-routes +``` + +--- + +## Response Envelope + +Most REST endpoints return the same JSON envelope: + +**Success:** +```json +{ + "success": true, + "data": { ... }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +> **Exception:** `POST /api/v1/ask` uses a dedicated RAG response shape and does not use the `data/error/timestamp` envelope from `BaseHandler`. + +**Error:** +```json +{ + "success": false, + "data": null, + "error": { + "code": 400, + "message": "Human-readable error message", + "detail": "Optional traceback or extra context" + }, + "timestamp": 1772532115.19 +} +``` + +### HTTP Status Codes + +| Code | Meaning | +|---|---| +| `200` | Success | +| `202` | Request accepted and queued (async libp2p operations) | +| `204` | No content (CORS preflight) | +| `400` | Bad request — missing or invalid field | +| `404` | Resource not found | +| `409` | Conflict — e.g. already subscribed to topic | +| `500` | Internal server error | +| `502` | Upstream/model backend error | +| `503` | Service not ready yet or queue unavailable | + +### CORS + +All endpoints return: +``` +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS +Access-Control-Allow-Headers: Content-Type, X-API-Key +``` + +--- + +## Endpoint Index + +| Method | Path | Description | +|---|---|---| +| GET | [`/api/v1/node/info`](#get-apiv1nodeinfo) | Local peer identity | +| GET | [`/api/v1/node/status`](#get-apiv1nodestatus) | Readiness and uptime | +| GET | [`/api/v1/node/multiaddr`](#get-apiv1nodemultiaddr) | Full multiaddress | +| GET | [`/api/v1/peers`](#get-apiv1peers) | Connected peers | +| GET | [`/api/v1/peers/count`](#get-apiv1peerscount) | Connected peer count | +| GET | [`/api/v1/peers/known`](#get-apiv1peersknown) | All peers in peerstore | +| POST | [`/api/v1/peers/connect`](#post-apiv1peersconnect) | Connect via multiaddr | +| GET | [`/api/v1/peers/{peer_id}`](#get-apiv1peerspeer_id) | Single peer info | +| GET | [`/api/v1/peers/{peer_id}/identify`](#get-apiv1peerspeer_ididentify) | Cached identify data | +| POST | [`/api/v1/messages`](#post-apiv1messages) | Send to default topic | +| GET | [`/api/v1/messages/{topic}`](#get-apiv1messagestopic) | List stored messages | +| POST | [`/api/v1/messages/{topic}`](#post-apiv1messagestopic) | Send to specific topic | +| GET | [`/api/v1/messages/{topic}/unread`](#get-apiv1messagestopicunread) | Unread count | +| PUT | [`/api/v1/messages/{topic}/read`](#put-apiv1messagestopicread) | Mark all as read | +| GET | [`/api/v1/topics`](#get-apiv1topics) | List subscribed topics | +| POST | [`/api/v1/topics`](#post-apiv1topics) | Subscribe to new topic | +| GET | [`/api/v1/topics/{topic}/info`](#get-apiv1topicstopicinfo) | Topic details | +| GET | [`/api/v1/topics/{topic}/peers`](#get-apiv1topicstopicpeers) | Mesh peers for topic | +| GET | [`/api/v1/files/shared`](#get-apiv1filesshared) | List shared files | +| GET | [`/api/v1/files/shared/{cid}`](#get-apiv1filessharedcid) | Shared file detail | +| POST | [`/api/v1/files/share`](#post-apiv1filesshare) | Share a local file | +| POST | [`/api/v1/files/download`](#post-apiv1filesdownload) | Download file by CID | +| POST | [`/api/v1/files/upload`](#post-apiv1filesupload) | Upload + share (multipart) | +| GET | [`/api/v1/dht/status`](#get-apiv1dhtstatus) | DHT mode and table size | +| GET | [`/api/v1/dht/peers`](#get-apiv1dhtpeers) | DHT routing table peers | +| GET | [`/api/v1/dht/routing-table`](#get-apiv1dhtrouting-table) | Full routing table | +| GET | [`/api/v1/pubsub/peers`](#get-apiv1pubsubpeers) | PubSub connected peers | +| GET | [`/api/v1/pubsub/mesh`](#get-apiv1pubsubmesh) | GossipSub mesh state | +| GET | [`/api/v1/pubsub/fanout`](#get-apiv1pubsubfanout) | GossipSub fanout | +| GET | [`/api/v1/pubsub/config`](#get-apiv1pubsubconfig) | GossipSub configuration | +| GET | [`/api/v1/pubsub/subscriptions`](#get-apiv1pubsubsubscriptions) | Active subscriptions | +| GET | [`/api/v1/identity/cache`](#get-apiv1identitycache) | All cached identify entries | +| GET | [`/api/v1/identity/{peer_id}`](#get-apiv1identitypeer_id) | Cached identify info | +| GET | [`/api/v1/identity/{peer_id}/pubkey`](#get-apiv1identitypeer_idpubkey) | Public key (hex) | +| DELETE | [`/api/v1/identity/{peer_id}/cache`](#delete-apiv1identitypeer_idcache) | Invalidate cache entry | +| GET | [`/api/v1/service/status`](#get-apiv1servicestatus) | Service health | +| GET | [`/api/v1/service/config`](#get-apiv1serviceconfig) | Service configuration | +| POST | [`/api/v1/service/stop`](#post-apiv1servicestop) | Graceful shutdown | +| POST | [`/api/v1/service/bootstrap`](#post-apiv1servicebootstrap) | Re-trigger bootstrap | +| POST | [`/api/v1/ask`](#post-apiv1ask) | RAG assistant for py-libp2p/spec questions | +| WS | [`/ws/messages`](#ws-wsmessages) | Real-time message stream | +| WS | [`/ws/system`](#ws-wssystem) | Real-time system events | +| WS | [`/ws/peers`](#ws-wspeers) | Real-time peer list updates | +| WS | [`/ws/pubsub/mesh`](#ws-wspubsubmesh) | Real-time mesh topology | + +--- + +## Node + +### GET /api/v1/node/info + +Returns the local peer's identity, multiaddress, nickname, and readiness. + +**Request:** +```bash +curl http://localhost:8765/api/v1/node/info +``` + +**Response:** +```json +{ + "success": true, + "data": { + "peer_id": "QmcfYkkx45zyxRdRw333WnNssTMQ9AAVMF2wUB7nmQMiow", + "nickname": "alice", + "multiaddr": "/ip4/0.0.0.0/tcp/54770/p2p/QmcfYkkx45zyxRdRw333WnNssTMQ9AAVMF2wUB7nmQMiow", + "port": 54770, + "ready": true, + "uptime_seconds": 42.3 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/node/status + +Returns the current running state and uptime. This endpoint works **even before** the service is fully ready (unlike most others). + +**Request:** +```bash +curl http://localhost:8765/api/v1/node/status +``` + +**Response:** +```json +{ + "success": true, + "data": { + "ready": true, + "running": true, + "uptime_seconds": 507.2, + "port": 54770, + "strict_signing": false, + "nickname": "alice", + "topic": null + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/node/multiaddr + +Returns just the full multiaddress string. Useful for sharing with other peers. + +**Request:** +```bash +curl http://localhost:8765/api/v1/node/multiaddr +``` + +**Response:** +```json +{ + "success": true, + "data": { + "multiaddr": "/ip4/0.0.0.0/tcp/54770/p2p/QmcfYkkx45zyxRdRw333WnNssTMQ9AAVMF2wUB7nmQMiow" + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +## Peers + +### GET /api/v1/peers + +Returns the list of currently connected peers (active libp2p connections). + +**Request:** +```bash +curl http://localhost:8765/api/v1/peers +``` + +**Response:** +```json +{ + "success": true, + "data": { + "peers": [ + "12D3KooWRogVw8icxSguqjKonoTsbVymkbRoBtyr8Zz2bFdytJh7" + ], + "count": 1 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/peers/count + +Returns just the count of currently connected peers. + +**Request:** +```bash +curl http://localhost:8765/api/v1/peers/count +``` + +**Response:** +```json +{ + "success": true, + "data": { "count": 1 }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/peers/known + +Returns all peers the node has ever seen, including disconnected ones stored in the peerstore. + +**Request:** +```bash +curl http://localhost:8765/api/v1/peers/known +``` + +**Response:** +```json +{ + "success": true, + "data": { + "peers": [ + "QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa", + "12D3KooWRogVw8icxSguqjKonoTsbVymkbRoBtyr8Zz2bFdytJh7" + ], + "count": 78 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### POST /api/v1/peers/connect + +Queues a connection request to a remote peer using its multiaddress. + +> **Note:** Returns `202 Accepted` immediately. The actual TCP connection happens asynchronously in the libp2p trio thread. + +**Request:** +```bash +curl -X POST http://localhost:8765/api/v1/peers/connect \ + -H "Content-Type: application/json" \ + -d '{"multiaddr": "/ip4/139.178.65.157/tcp/4001/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa"}' +``` + +**Body:** +| Field | Type | Required | Description | +|---|---|---|---| +| `multiaddr` | string | ✅ | Full libp2p multiaddress of the peer to connect to | + +**Response `202`:** +```json +{ + "success": true, + "data": { + "message": "Connection request queued", + "multiaddr": "/ip4/139.178.65.157/tcp/4001/p2p/QmQCU2Ec..." + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +**Error `400` — missing field:** +```json +{ + "success": false, + "data": null, + "error": { "code": 400, "message": "'multiaddr' field is required.", "detail": null }, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/peers/{peer_id} + +Returns peerstore information (known addresses) for a specific peer. + +**Request:** +```bash +curl http://localhost:8765/api/v1/peers/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa +``` + +**Response:** +```json +{ + "success": true, + "data": { + "peer_id": "QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa", + "addrs": ["/ip4/139.178.65.157/tcp/4001"] + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +**Error `404`** — peer not in peerstore. + +--- + +### GET /api/v1/peers/{peer_id}/identify + +Returns **cached** identify protocol data for the peer (agent version, protocols, listen addresses). The data is populated automatically when the node connects to the peer. + +**Request:** +```bash +curl http://localhost:8765/api/v1/peers/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa/identify +``` + +**Response:** +```json +{ + "success": true, + "data": { + "peer_id": "QmQCU2Ec...", + "protocol_version": "ipfs/0.1.0", + "agent_version": "go-libp2p/0.31.0", + "listen_addrs": ["/ip4/139.178.65.157/tcp/4001"], + "protocols": ["/ipfs/id/1.0.0", "/meshsub/1.1.0", "/kad/1.0.0"], + "cached_at": 1772532115.19 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +**Error `404`** — connect to the peer first to populate the cache. + +--- + +## Messaging + +### POST /api/v1/messages + +Sends a message to the node's **default chat topic** (`universal-connectivity` or the one set via `--topic`). + +**Request:** +```bash +curl -X POST http://localhost:8765/api/v1/messages \ + -H "Content-Type: application/json" \ + -d '{"message": "Hello everyone!"}' +``` + +**Body:** +| Field | Type | Required | Description | +|---|---|---|---| +| `message` | string | ✅ | The message text to publish | + +**Response `202`:** +```json +{ + "success": true, + "data": { "message": "Message queued for delivery" }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/messages/{topic} + +Returns stored messages for a topic. Supports pagination with `limit` and `offset` query params. + +**Request:** +```bash +curl "http://localhost:8765/api/v1/messages/universal-connectivity?limit=50&offset=0" +``` + +**Query Parameters:** +| Param | Type | Default | Description | +|---|---|---|---| +| `limit` | integer | `100` | Max messages to return | +| `offset` | integer | `0` | Number of messages to skip | + +**Response:** +```json +{ + "success": true, + "data": { + "topic": "universal-connectivity", + "messages": [ + { + "type": "chat_message", + "message": "Hello from Tornado API!", + "sender_nick": "alice", + "sender_id": "QmcfYkkx45zyxRdRw333WnNssTMQ9AAVMF2wUB7nmQMiow", + "timestamp": 1772532166.99, + "topic": "universal-connectivity", + "read": false + } + ], + "total": 1, + "limit": 50, + "offset": 0 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +Each message object fields: + +| Field | Description | +|---|---| +| `type` | `"chat_message"` or `"file_message"` | +| `message` | Message text | +| `sender_nick` | Display name of the sender | +| `sender_id` | libp2p peer ID of the sender | +| `timestamp` | Unix timestamp | +| `topic` | Topic the message was received on | +| `read` | Whether the message has been marked as read | + +--- + +### POST /api/v1/messages/{topic} + +Sends a message to a **specific topic**. The topic must already be subscribed. + +**Request:** +```bash +curl -X POST http://localhost:8765/api/v1/messages/universal-connectivity \ + -H "Content-Type: application/json" \ + -d '{"message": "Hello from the API!"}' +``` + +**Body:** +| Field | Type | Required | Description | +|---|---|---|---| +| `message` | string | ✅ | The message text | + +**Response `202`:** +```json +{ + "success": true, + "data": { + "message": "Message queued for delivery", + "topic": "universal-connectivity" + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +**Error `400`** — topic not subscribed (subscribe first via `POST /api/v1/topics`). + +--- + +### GET /api/v1/messages/{topic}/unread + +Returns the count of unread messages in a topic. + +**Request:** +```bash +curl http://localhost:8765/api/v1/messages/universal-connectivity/unread +``` + +**Response:** +```json +{ + "success": true, + "data": { + "topic": "universal-connectivity", + "unread_count": 3 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### PUT /api/v1/messages/{topic}/read + +Marks all messages in a topic as read, resetting the unread count to `0`. + +**Request:** +```bash +curl -X PUT http://localhost:8765/api/v1/messages/universal-connectivity/read +``` + +**Response:** +```json +{ + "success": true, + "data": { + "topic": "universal-connectivity", + "message": "All messages marked as read" + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +## Topics + +### GET /api/v1/topics + +Returns all currently subscribed topics with metadata. + +**Request:** +```bash +curl http://localhost:8765/api/v1/topics +``` + +**Response:** +```json +{ + "success": true, + "data": { + "topics": { + "universal-connectivity": { + "unread_count": 1, + "total_count": 5, + "last_message": { + "type": "chat_message", + "message": "Hello!", + "sender_nick": "bob", + "timestamp": 1772532166.99 + } + }, + "universal-connectivity-browser-peer-discovery": { + "unread_count": 0, + "total_count": 0, + "last_message": null + } + }, + "count": 2 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### POST /api/v1/topics + +Subscribes the node to a new GossipSub topic. + +> Returns `202 Accepted` immediately. The actual subscription happens asynchronously. + +**Request:** +```bash +curl -X POST http://localhost:8765/api/v1/topics \ + -H "Content-Type: application/json" \ + -d '{"topic": "my-custom-channel"}' +``` + +**Body:** +| Field | Type | Required | Description | +|---|---|---|---| +| `topic` | string | ✅ | Topic name to subscribe to | + +**Response `202`:** +```json +{ + "success": true, + "data": { + "message": "Subscription request queued for 'my-custom-channel'", + "topic": "my-custom-channel" + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +**Error `409`** — already subscribed: +```json +{ + "success": false, + "data": null, + "error": { + "code": 409, + "message": "Already subscribed to topic 'universal-connectivity'.", + "detail": null + }, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/topics/{topic}/info + +Returns detailed info for a single topic. + +**Request:** +```bash +curl http://localhost:8765/api/v1/topics/universal-connectivity/info +``` + +**Response:** +```json +{ + "success": true, + "data": { + "topic": "universal-connectivity", + "unread_count": 1, + "total_count": 5, + "last_message": { "message": "hi", "sender_nick": "bob", "timestamp": 1772532166.99 } + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/topics/{topic}/peers + +Returns peer IDs of nodes currently in the GossipSub mesh for this topic. + +**Request:** +```bash +curl http://localhost:8765/api/v1/topics/universal-connectivity/peers +``` + +**Response:** +```json +{ + "success": true, + "data": { + "topic": "universal-connectivity", + "mesh_peers": [ + "12D3KooWRogVw8icxSguqjKonoTsbVymkbRoBtyr8Zz2bFdytJh7" + ], + "count": 1 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +## File Sharing + +File sharing is powered by **Bitswap** and **MerkleDag** — the same protocol used by IPFS. + +### GET /api/v1/files/shared + +Lists all files this node has shared in the current session. + +**Request:** +```bash +curl http://localhost:8765/api/v1/files/shared +``` + +**Response:** +```json +{ + "success": true, + "data": { + "shared_files": [ + { + "cid": "a1b2c3d4e5f6...", + "filename": "document.pdf", + "filesize": 204800, + "filepath": "/home/alice/document.pdf" + } + ], + "count": 1 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/files/shared/{cid} + +Returns metadata for a specific shared file by its CID (hex string). + +**Request:** +```bash +curl http://localhost:8765/api/v1/files/shared/a1b2c3d4e5f6 +``` + +**Response:** +```json +{ + "success": true, + "data": { + "cid": "a1b2c3d4e5f6...", + "filename": "document.pdf", + "filesize": 204800, + "filepath": "/home/alice/document.pdf" + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### POST /api/v1/files/share + +Shares a file that already exists on the node's local disk. The file is added to Bitswap and its CID is announced via the subscribed topic. + +**Request:** +```bash +curl -X POST http://localhost:8765/api/v1/files/share \ + -H "Content-Type: application/json" \ + -d '{"file_path": "/home/alice/photo.jpg", "topic": "universal-connectivity"}' +``` + +**Body:** +| Field | Type | Required | Description | +|---|---|---|---| +| `file_path` | string | ✅ | Absolute path to the file on disk | +| `topic` | string | ✅ | Topic to announce the file on | + +**Response `202`:** +```json +{ + "success": true, + "data": { + "message": "File share request queued", + "filename": "photo.jpg", + "topic": "universal-connectivity" + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### POST /api/v1/files/download + +Downloads a file by its CID hex from a remote peer via Bitswap. + +**Request:** +```bash +curl -X POST http://localhost:8765/api/v1/files/download \ + -H "Content-Type: application/json" \ + -d '{"file_cid": "a1b2c3d4e5f6...", "file_name": "photo.jpg"}' +``` + +**Body:** +| Field | Type | Required | Description | +|---|---|---|---| +| `file_cid` | string | ✅ | CID hex string of the file to download | +| `file_name` | string | — | Expected filename (used for saving) | + +**Response `202`:** +```json +{ + "success": true, + "data": { + "message": "Download request queued", + "file_cid": "a1b2c3d4e5f6...", + "file_name": "photo.jpg" + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +> **Note:** When the download completes, a `file_downloaded` event is pushed via `WS /ws/messages`. + +--- + +### POST /api/v1/files/upload + +Accepts a file uploaded via `multipart/form-data`, saves it to the node's download directory, and shares it to a topic. + +**Request:** +```bash +curl -X POST http://localhost:8765/api/v1/files/upload \ + -F "file=@/home/alice/photo.jpg" \ + -F "topic=universal-connectivity" +``` + +**Form fields:** +| Field | Type | Required | Description | +|---|---|---|---| +| `file` | file | ✅ | The file to upload | +| `topic` | string | ✅ | Topic to share the file on | + +**Response `202`:** +```json +{ + "success": true, + "data": { + "message": "File uploaded and share request queued", + "filename": "photo.jpg", + "size": 204800, + "topic": "universal-connectivity", + "saved_path": "/Users/alice/Downloads/photo.jpg" + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +## DHT + +### GET /api/v1/dht/status + +Returns the current Kademlia DHT mode and routing table summary. + +**Request:** +```bash +curl http://localhost:8765/api/v1/dht/status +``` + +**Response:** +```json +{ + "success": true, + "data": { + "mode": "SERVER", + "random_walk_enabled": true, + "routing_table_size": 9 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +| Field | Values | Description | +|---|---|---| +| `mode` | `"SERVER"`, `"CLIENT"` | DHT operating mode | +| `random_walk_enabled` | bool | Whether random walk peer discovery is active | +| `routing_table_size` | integer | Number of peers in the Kademlia routing table | + +--- + +### GET /api/v1/dht/peers + +Returns the list of peer IDs currently in the DHT routing table. + +**Request:** +```bash +curl http://localhost:8765/api/v1/dht/peers +``` + +**Response:** +```json +{ + "success": true, + "data": { + "peers": [ + "QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt", + "QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa", + "12D3KooWRogVw8icxSguqjKonoTsbVymkbRoBtyr8Zz2bFdytJh7" + ], + "count": 9 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/dht/routing-table + +Returns the full DHT routing table as a flat list. + +**Request:** +```bash +curl http://localhost:8765/api/v1/dht/routing-table +``` + +**Response:** +```json +{ + "success": true, + "data": { + "routing_table": ["QmcZf59...", "QmQCU2Ec...", "12D3KooW..."], + "total_peers": 9 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +## PubSub + +### GET /api/v1/pubsub/peers + +Returns all peers currently connected via the GossipSub/PubSub protocol. + +**Request:** +```bash +curl http://localhost:8765/api/v1/pubsub/peers +``` + +**Response:** +```json +{ + "success": true, + "data": { + "peers": ["12D3KooWRogVw8icxSguqjKonoTsbVymkbRoBtyr8Zz2bFdytJh7"], + "count": 1 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/pubsub/mesh + +Returns the current GossipSub mesh — which peers are in the mesh for each subscribed topic. + +**Request:** +```bash +curl http://localhost:8765/api/v1/pubsub/mesh +``` + +**Response:** +```json +{ + "success": true, + "data": { + "mesh": { + "universal-connectivity": ["12D3KooWRogVw8...", "12D3KooWMtHTK4..."], + "universal-connectivity-browser-peer-discovery": ["12D3KooWRogVw8..."] + }, + "topic_count": 2, + "total_mesh_peers": 3 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/pubsub/fanout + +Returns the GossipSub fanout peers — peers that receive messages on topics they haven't subscribed to yet. + +**Request:** +```bash +curl http://localhost:8765/api/v1/pubsub/fanout +``` + +**Response:** +```json +{ + "success": true, + "data": { + "fanout": { + "universal-connectivity": ["12D3KooWRogVw8..."] + } + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/pubsub/config + +Returns the GossipSub router configuration values set at startup. + +**Request:** +```bash +curl http://localhost:8765/api/v1/pubsub/config +``` + +**Response:** +```json +{ + "success": true, + "data": { + "degree": 3, + "degree_low": 2, + "degree_high": 4, + "gossip_window": null, + "gossip_history": null, + "heartbeat_interval": 5, + "heartbeat_initial_delay": 2.0, + "protocols": ["/meshsub/1.0.0", "/meshsub/1.1.0", "/meshsub/1.2.0"] + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/pubsub/subscriptions + +Returns the names of all currently active pubsub topic subscriptions. + +**Request:** +```bash +curl http://localhost:8765/api/v1/pubsub/subscriptions +``` + +**Response:** +```json +{ + "success": true, + "data": { + "subscriptions": [ + "universal-connectivity", + "universal-connectivity-browser-peer-discovery" + ], + "count": 2 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +## Identity + +The identify protocol is used by libp2p nodes to exchange metadata (agent version, supported protocols, listen addresses). Data is cached when a peer connects. + +### GET /api/v1/identity/cache + +Returns all currently cached identify entries. + +**Request:** +```bash +curl http://localhost:8765/api/v1/identity/cache +``` + +**Response:** +```json +{ + "success": true, + "data": { + "cache": { + "12D3KooWRogVw8icxSguqjKonoTsbVymkbRoBtyr8Zz2bFdytJh7": { + "protocol_version": "ipfs/0.1.0", + "agent_version": "go-libp2p/0.31.0", + "listen_addrs": ["/ip4/10.0.0.1/tcp/4001"], + "protocols": ["/ipfs/id/1.0.0", "/meshsub/1.1.0"], + "cached_at": 1772532100.0 + } + }, + "count": 1 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/identity/{peer_id} + +Returns cached identify data for a specific peer. + +**Request:** +```bash +curl http://localhost:8765/api/v1/identity/12D3KooWRogVw8icxSguqjKonoTsbVymkbRoBtyr8Zz2bFdytJh7 +``` + +**Response:** +```json +{ + "success": true, + "data": { + "peer_id": "12D3KooWRogVw8icxSguqjKonoTsbVymkbRoBtyr8Zz2bFdytJh7", + "protocol_version": "ipfs/0.1.0", + "agent_version": "go-libp2p/0.31.0", + "listen_addrs": ["/ip4/10.0.0.1/tcp/4001"], + "protocols": ["/ipfs/id/1.0.0", "/meshsub/1.1.0"], + "cached_at": 1772532100.0 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +**Error `404`** — if the peer has not connected yet. + +--- + +### GET /api/v1/identity/{peer_id}/pubkey + +Returns the public key (hex) for a peer from the identity cache. + +**Request:** +```bash +curl http://localhost:8765/api/v1/identity/12D3KooWRogVw8.../pubkey +``` + +**Response:** +```json +{ + "success": true, + "data": { + "peer_id": "12D3KooWRogVw8...", + "public_key_hex": "0802122102a1b2c3d4..." + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### DELETE /api/v1/identity/{peer_id}/cache + +Removes a specific peer's cached identify data so it will be re-fetched on next connection. + +**Request:** +```bash +curl -X DELETE http://localhost:8765/api/v1/identity/12D3KooWRogVw8.../cache +``` + +**Response:** +```json +{ + "success": true, + "data": { + "message": "Cache entry for '12D3KooWRogVw8...' deleted." + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +## Service + +### GET /api/v1/service/status + +Returns the overall health of the running service. + +**Request:** +```bash +curl http://localhost:8765/api/v1/service/status +``` + +**Response:** +```json +{ + "success": true, + "data": { + "ready": true, + "running": true, + "uptime_seconds": 507.2, + "peer_count": 3 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### GET /api/v1/service/config + +Returns the configuration the service was started with. + +**Request:** +```bash +curl http://localhost:8765/api/v1/service/config +``` + +**Response:** +```json +{ + "success": true, + "data": { + "nickname": "alice", + "port": 54770, + "topic": null, + "strict_signing": false, + "download_dir": "/Users/alice/Downloads", + "connect_addrs": [] + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### POST /api/v1/service/stop + +Sends a graceful shutdown signal to the `HeadlessService`. The libp2p node will stop accepting connections and close. + +**Request:** +```bash +curl -X POST http://localhost:8765/api/v1/service/stop +``` + +**Response:** +```json +{ + "success": true, + "data": { "message": "Stop signal sent to HeadlessService." }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +### POST /api/v1/service/bootstrap + +Re-queues connections to all default bootstrap peers. Useful if the node lost connectivity. + +**Request:** +```bash +curl -X POST http://localhost:8765/api/v1/service/bootstrap +``` + +**Response `202`:** +```json +{ + "success": true, + "data": { + "message": "Queued connections to 9 bootstrap peers.", + "bootstrap_peers_count": 9 + }, + "error": null, + "timestamp": 1772532115.19 +} +``` + +--- + +## RAG Assistant + +### POST /api/v1/ask + +Asks the py-libp2p assistant a grounded question using the local vector store + Groq model backend. + +> **Response shape note:** this endpoint does **not** return the standard `BaseHandler` envelope. + +**Request:** +```bash +curl -X POST http://localhost:8765/api/v1/ask \ + -H "Content-Type: application/json" \ + -d '{"question": "How does GossipSub mesh maintenance work?"}' +``` + +**Body:** +| Field | Type | Required | Description | +|---|---|---|---| +| `question` | string | ✅ | Natural-language question about py-libp2p/libp2p specs | + +**Success `200`:** +```json +{ + "success": true, + "answer": "...grounded answer...", + "sources": [ + "py-libp2p/libp2p/pubsub/gossipsub.py", + "specs/pubsub/gossipsub/README.md" + ] +} +``` + +**No relevant chunks found (`200`):** +```json +{ + "success": true, + "answer": "No relevant context found in the knowledge base.", + "sources": [] +} +``` + +**Error `400` (bad/missing `question`):** +```json +{ + "success": false, + "error": "'question' cannot be empty." +} +``` + +**Error `503` (vector store unavailable):** +```json +{ + "success": false, + "error": "RAG assistant is not available. Vector store not loaded." +} +``` + +**Error `500` (vector search failure):** +```json +{ + "success": false, + "error": "Vector search failed." +} +``` + +**Error `502` (LLM backend failure):** +```json +{ + "success": false, + "error": "LLM backend error: ..." +} +``` + +--- + +## WebSocket APIs + +WebSocket endpoints provide **real-time, push-based** data delivery. Connect once and receive a continuous stream of events — no polling needed. + +> **Important:** WebSocket endpoints are **Tornado-level** feeds of internal data. They are completely separate from the py-libp2p WebSocket transport that peers use to talk to each other. + +--- + +### WS /ws/messages + +Streams all incoming chat messages and file events in real-time. + +**Connect:** +```bash +# Using wscat (npm install -g wscat) +wscat -c ws://localhost:8765/ws/messages +``` + +**Received frame — chat message:** +```json +{ + "event": "chat_message", + "data": { + "type": "chat_message", + "message": "Hello!", + "sender_nick": "bob", + "sender_id": "QmBob...", + "topic": "universal-connectivity", + "timestamp": 1772532200.0, + "read": false + } +} +``` + +**Received frame — file announced:** +```json +{ + "event": "file_message", + "data": { + "type": "file_message", + "file_cid": "a1b2c3...", + "file_name": "photo.jpg", + "file_size": 204800, + "sender_nick": "carol", + "topic": "universal-connectivity", + "timestamp": 1772532210.0 + } +} +``` + +**Received frame — file download complete:** +```json +{ + "event": "file_downloaded", + "data": { + "type": "file_downloaded", + "file_cid": "a1b2c3...", + "file_name": "photo.jpg", + "file_size": 204800, + "save_path": "/Users/alice/Downloads/photo.jpg", + "timestamp": 1772532220.0 + } +} +``` + +**Client → server commands:** + +Optionally send JSON to filter messages by topic: + +```json +{ "action": "filter_topic", "topic": "my-channel" } +``` +```json +{ "action": "unfilter" } +``` + +Server acknowledgements for these commands: + +```json +{ "event": "filter_set", "topic": "my-channel" } +``` + +```json +{ "event": "filter_cleared" } +``` + +--- + +### WS /ws/system + +Streams service system events and notifications. + +**Connect:** +```bash +wscat -c ws://localhost:8765/ws/system +``` + +**Received frame:** +```json +{ + "event": "system_message", + "data": { + "type": "system_message", + "message": "Connected to peer: 12D3KooWRog...", + "timestamp": 1772532115.19 + } +} +``` + +Common system messages include: connection established, connection failed, subscription confirmed, file share started. + +--- + +### WS /ws/peers + +Pushes the current connected peer list whenever it changes (checked every 3 seconds). + +**Connect:** +```bash +wscat -c ws://localhost:8765/ws/peers +``` + +**Received frame:** +```json +{ + "event": "peer_update", + "data": { + "connected_peers": [ + "12D3KooWRogVw8icxSguqjKonoTsbVymkbRoBtyr8Zz2bFdytJh7", + "12D3KooWMtHTK4bKh8gkaq65JuUNEo1Q8kJUpUfy98RC7sog2SXT" + ], + "peer_count": 2, + "timestamp": 1772532115.19 + } +} +``` + +Only pushes when the peer list actually changes. + +--- + +### WS /ws/pubsub/mesh + +Pushes GossipSub mesh topology updates every 5 seconds. + +**Connect:** +```bash +wscat -c ws://localhost:8765/ws/pubsub/mesh +``` + +**Received frame:** +```json +{ + "event": "mesh_update", + "data": { + "mesh": { + "universal-connectivity": [ + "12D3KooWRogVw8icxSguqjKonoTsbVymkbRoBtyr8Zz2bFdytJh7" + ], + "universal-connectivity-browser-peer-discovery": [] + }, + "topic_count": 2, + "total_mesh_peers": 1, + "timestamp": 1772532115.19 + } +} +``` + +--- + +## Common Workflows + +### Start a node and check it's ready + +```bash +python main.py --nick alice --api --api-port 8765 --no-strict-signing + +curl http://localhost:8765/api/v1/service/status +``` + +### Connect to a specific peer + +```bash +curl -X POST http://localhost:8765/api/v1/peers/connect \ + -H "Content-Type: application/json" \ + -d '{"multiaddr": "/ip4/192.168.1.10/tcp/9095/p2p/QmBob..."}' +``` + +### Subscribe to a custom topic and send a message + +```bash +curl -X POST http://localhost:8765/api/v1/topics \ + -H "Content-Type: application/json" \ + -d '{"topic": "my-team-chat"}' + +# Wait a moment for subscription to be confirmed via WS /ws/system, then: +curl -X POST http://localhost:8765/api/v1/messages/my-team-chat \ + -H "Content-Type: application/json" \ + -d '{"message": "Hello from the API!"}' +``` + +### Read and clear new messages + +```bash +# Get unread count +curl http://localhost:8765/api/v1/messages/universal-connectivity/unread + +# Fetch messages (paginated) +curl "http://localhost:8765/api/v1/messages/universal-connectivity?limit=20&offset=0" + +# Mark all as read +curl -X PUT http://localhost:8765/api/v1/messages/universal-connectivity/read +``` + +### Share a file + +```bash +# Share a local file +curl -X POST http://localhost:8765/api/v1/files/share \ + -H "Content-Type: application/json" \ + -d '{"file_path": "/home/alice/doc.pdf", "topic": "universal-connectivity"}' + +# Or upload directly +curl -X POST http://localhost:8765/api/v1/files/upload \ + -F "file=@/home/alice/doc.pdf" \ + -F "topic=universal-connectivity" +``` + +### Download a file announced by another peer + +```bash +# First, listen for file_message events on WS /ws/messages to get the CID. +# Then: +curl -X POST http://localhost:8765/api/v1/files/download \ + -H "Content-Type: application/json" \ + -d '{"file_cid": "a1b2c3d4e5f6...", "file_name": "doc.pdf"}' +``` + +### Monitor in real-time (three terminals) + +```bash +# Terminal 1 — all messages +wscat -c ws://localhost:8765/ws/messages + +# Terminal 2 — peer updates +wscat -c ws://localhost:8765/ws/peers + +# Terminal 3 — system events +wscat -c ws://localhost:8765/ws/system +``` + +--- + +## File Structure + +``` +py-peer/ +├── main.py ← --api, --api-port, --api-routes flags added here +├── tornado_server.py ← TornadoServer class, URL routing, _print_routes() +└── api/ + ├── __init__.py + ├── base.py ← BaseHandler (CORS, JSON envelope, 503 guard) + ├── node.py ← /api/v1/node/* + ├── peers.py ← /api/v1/peers/* + ├── messages.py ← /api/v1/messages/* + ├── topics.py ← /api/v1/topics/* + ├── files.py ← /api/v1/files/* + ├── dht.py ← /api/v1/dht/* + ├── pubsub.py ← /api/v1/pubsub/* + ├── identity.py ← /api/v1/identity/* + ├── service.py ← /api/v1/service/* + └── websocket.py ← /ws/* WebSocket handlers +``` diff --git a/py-peer/README.md b/py-peer/README.md index 010f8532..5ea727d6 100644 --- a/py-peer/README.md +++ b/py-peer/README.md @@ -1,6 +1,746 @@ -# Python Peer (py-peer) of Universal Connectivity +# py-peer 🌐 -This is the Python implementation of the [Universal Connectivity][UNIV_CONN] app showcasing the [Gossipsub][GOSSIPSUB], and eventually [QUIC][QUIC], features of the core libp2p protocol as found in the [py-libp2p][PYLIBP2P] Python libp2p implementation. +A Python implementation of the Universal Connectivity peer-to-peer chat application using libp2p networking with multiple UI options, a Tornado API layer, and a React web frontend. + +This is the Python implementation of the [Universal Connectivity][UNIV_CONN] app showcasing the [Gossipsub][GOSSIPSUB] features of the core libp2p protocol as found in the [py-libp2p][PYLIBP2P] Python libp2p implementation. The implementation currently uses TCP. + +## 📋 Table of Contents + +- [Overview](#overview) +- [Features](#features) +- [Screenshots](#screenshots) +- [Architecture](#architecture) +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Usage](#usage) +- [User Interfaces](#user-interfaces) +- [Configuration](#configuration) +- [Development](#development) +- [Troubleshooting](#troubleshooting) +- [Contributing](#contributing) + +## 🚀 Overview + +py-peer is a decentralized chat application that enables peer-to-peer communication without requiring central servers. Built on libp2p, it provides secure, direct communication between participants using modern networking protocols. It offers multiple interaction modes: a mobile-friendly Kivy interface, a terminal-based Textual TUI, a simple CLI mode, and a React frontend powered by a Tornado REST + WebSocket API. + +Kivy, Textual, and CLI modes run with direct in-process access to the HeadlessService. The React frontend is decoupled and communicates with HeadlessService through Tornado REST and WebSocket endpoints. + +## 📸 Screenshots + +### Kivy Mobile-Friendly Interface (NEW!) +![py-peer Kivy UI](assets/images/py-peer-kivy.png) + +*The modern Kivy/KivyMD interface featuring WhatsApp-style navigation, topic-based conversations, and one-tap copy functionality for peer IDs and multiaddresses.* + +### Textual TUI Interface +![py-peer Textual UI](assets/images/py-peer-textual-ui.png) + +*The Textual Terminal User Interface showing a live chat session with multiple connected peers. The interface features a main chat area, connected peers panel, system messages, and input field.* + +## 🎯 Key Technologies + +- **[libp2p](https://libp2p.io/)** - Modular peer-to-peer networking stack +- **[Trio](https://trio.readthedocs.io/)** - Async/await framework for Python +- **[KivyMD](https://kivymd.readthedocs.io/)** - Material Design mobile UI framework +- **[Textual](https://textual.textualize.io/)** - Modern Terminal User Interface framework +- **[Tornado](https://www.tornadoweb.org/)** - REST and WebSocket API server +- **[React + Vite](https://vitejs.dev/)** - Modern web frontend for py-peer +- **[GossipSub](https://docs.libp2p.io/concepts/pubsub/overview/)** - Pub/sub messaging protocol + +## ✨ Features + +### Core Features +- **Peer-to-Peer Chat** - Direct communication without central servers +- **Multiple UI Modes** - Kivy mobile UI, Textual TUI, or simple CLI mode +- **Real-time Messaging** - Instant message delivery through GossipSub +- **Peer Discovery** - Automatic discovery of other peers in the network +- **Cross-Platform** - Works on Linux, macOS, Windows, and mobile platforms +- **Secure Communication** - Built-in encryption and peer authentication +- **REST + WebSocket API** - Full Tornado API for programmatic control and real-time streams +- **Web Frontend** - React dashboard for chat, topic management, and peer operations + +### Advanced Features (NEW!) +- **Topic-Based Conversations** - Subscribe to and chat in multiple topics simultaneously +- **WhatsApp-Style Interface** - Intuitive topic selector with unread message counts +- **Dynamic Topic Management** - Add new topics on the fly without restarting +- **Per-Topic Message Storage** - Messages organized by topic with read/unread tracking +- **Dynamic Peer Connection** - Connect to peers using their multiaddress through the UI +- **One-Tap Copy** - Easy sharing of Peer ID and Multiaddr with clickable copy +- **Message Filtering** - View messages only for the selected topic +- **Custom Topics** - Use custom topic names via command line or UI +- **Persistent Connections** - Automatic peer connection maintenance +- **File Sharing API** - Share/download files via Bitswap/MerkleDag API endpoints +- **RAG Assistant** - Ask py-libp2p questions through `/api/v1/ask` powered by vector search + LLM + +### UI-Specific Features + +#### Kivy UI Features +- Topic list with unread counts +- Per-topic conversation views +- Tap-to-copy for Peer ID and Multiaddr +- Dynamic peer connection dialog +- Connection info display +- Material Design aesthetic + +#### Textual TUI Features +- Live peer count display +- System message logging +- Interactive command support +- Real-time chat updates + +#### CLI Features +- Simple text-based interface +- Command history +- Minimal resource usage + +#### React Frontend Features +- Browser-based chat UI with live updates +- Connection panel for peer connect + topic subscribe +- Topic sidebar with unread counts +- WebSocket-backed real-time messages and peer updates +- Built-in py-libp2p assistant widget (RAG) + +## 🏗️ Architecture + +**Access model:** +- Kivy UI, Textual TUI, and CLI mode access HeadlessService directly in-process +- React frontend accesses HeadlessService indirectly via Tornado API (`/api/v1/*`) and WebSockets (`/ws/*`) + +``` +┌────────────────────────────────────┐ ┌──────────────────────────────┐ +│ Local UI Path (Choose One) │ │ Web Client Path │ +├────────────────────────────────────┤ ├──────────────────────────────┤ +│ Kivy UI / Textual TUI / CLI │ │ React Frontend (Vite + TS) │ +│ │ │ • /api/v1 calls │ +│ • In-process access │ │ • /ws streams │ +│ • No HTTP hop required │ │ • RAG assistant UI │ +└───────────────────┬────────────────┘ └──────────────┬───────────────┘ + │ │ + │ direct in-process │ HTTP + WS + │ │ + │ ┌────────▼────────┐ + │ │ Tornado API │ + │ │ • REST endpoints │ + │ │ • WebSockets │ + │ │ • CORS + JSON │ + │ └────────┬─────────┘ + │ │ + └───────────────────────┬───────────────┘ + │ + ┌─────────▼─────────┐ + │ Headless Service │ + │ • Message Queue │ + │ • Topic Storage │ + │ • Event Loop │ + │ • State Mgmt │ + └─────────┬─────────┘ + │ + ┌─────────▼─────────┐ + │ Chat Room │ + │ • libp2p Host │ + │ • PubSub/GossipSub│ + │ • DHT │ + │ • Topic Handlers │ + └─────────┬─────────┘ + │ + ┌─────────▼─────────┐ + │ P2P Network │ + │ • Peer Discovery │ + │ • Message Relay │ + │ • Topic Routing │ + └───────────────────┘ +``` + +### Components + +- **main.py** - Application entry point and argument parsing +- **kivy_ui.py** - Modern mobile-friendly Kivy interface (NEW!) +- **ui.py** - Textual-based terminal user interface +- **headless.py** - Background service managing libp2p operations +- **chatroom.py** - Chat room logic, topic management, and message handling +- **tornado_server.py** - Tornado REST + WebSocket server bootstrap and route wiring +- **api/** - Versioned API handlers (node, peers, topics, messages, files, dht, pubsub, identity, service, websocket) +- **rag_handler.py** - Retrieval-augmented QA endpoint (`/api/v1/ask`) +- **py-peer-frontend/** - React + Vite web UI consuming the API + +### Message Flow + +``` +User Input → UI Thread → Queue → Async Thread (Trio) + ↓ + HeadlessService + ↓ + ChatRoom + ↓ + GossipSub/PubSub + ↓ + P2P Network + ↓ + Other Peers +``` + +## 📋 Prerequisites + +- **Python 3.12+** +- **uv** (recommended) or pip package manager +- Network connectivity for peer-to-peer communication +- For Kivy UI: Additional system dependencies (see Installation) +- For React frontend: **Node.js 18+** and npm +- For RAG endpoint (`/api/v1/ask`): Groq API key + vector store build dependencies + +## 🛠️ Installation + +### Option 1: Using uv (Recommended) + +```bash +# Clone the repository +git clone https://github.com/sumanjeet0012/universal-connectivity.git +cd universal-connectivity + +# Switch to the py-peer development branch +git checkout py-peer-development +cd py-peer + +# Create virtual environment +uv venv + +# Activate virtual environment +source .venv/bin/activate # Linux/macOS +# or +.venv\Scripts\activate # Windows + +# Install the package +uv pip install -e . +``` + +### Option 2: Using pip + +```bash +# Clone the repository +git clone https://github.com/sumanjeet0012/universal-connectivity.git +cd universal-connectivity + +# Switch to the py-peer development branch +git checkout py-peer-development +cd py-peer + +# Create virtual environment +python -m venv .venv + +# Activate virtual environment +source .venv/bin/activate # Linux/macOS +# or +.venv\Scripts\activate # Windows + +# Install the package +pip install -e . +``` + +### Kivy UI Dependencies + +For the Kivy mobile interface, you may need additional system dependencies: + +**Linux (Ubuntu/Debian):** +```bash +sudo apt-get install -y \ + python3-dev \ + build-essential \ + git \ + ffmpeg \ + libsdl2-dev \ + libsdl2-image-dev \ + libsdl2-mixer-dev \ + libsdl2-ttf-dev \ + libportmidi-dev \ + libswscale-dev \ + libavformat-dev \ + libavcodec-dev \ + zlib1g-dev +``` + +**macOS:** +```bash +brew install sdl2 sdl2_image sdl2_ttf sdl2_mixer +``` + +## 🎯 Usage + +### Quick Start + +```bash +# Kivy UI (Mobile-friendly interface) +python main.py --kivy --nick Alice + +# Textual TUI (Terminal interface) +python main.py --ui --nick Bob + +# Simple CLI mode +python main.py --nick Charlie + +# Headless mode (no UI) +python main.py --headless --nick Dave + +# Tornado API mode (REST + WebSocket) +python main.py --api --nick Eve --api-port 8765 + +# Print all API routes and exit +python main.py --api-routes +``` + +### Command Line Options + +| Option | Description | Example | +|--------|-------------|---------| +| `--nick NAME` | Set your nickname (required) | `--nick Alice` | +| `--kivy` | Use Kivy mobile-friendly UI | `--kivy` | +| `--ui` | Use Textual TUI interface | `--ui` | +| `--headless` | Run without UI (logs only) | `--headless` | +| `-c, --connect ADDR` | Connect to specific peer | `-c /ip4/...` | +| `-p, --port PORT` | Set listening port | `-p 8080` | +| `-t, --topic TOPIC` | Set custom default topic | `-t my-topic` | +| `-s, --seed SEED` | Seed for deterministic peer ID | `-s 12345` | +| `-v, --verbose` | Enable debug logging | `-v` | +| `--no-strict-signing` | Allow unsigned messages | `--no-strict-signing` | +| `--api` | Start Tornado REST + WebSocket API server | `--api` | +| `--api-port PORT` | Set Tornado API port | `--api-port 8765` | +| `--api-routes` | Print all API routes and exit | `--api-routes` | + +### Connecting Peers + +**Peer 1:** +```bash +python main.py --kivy --nick Alice --port 9095 +# Get your multiaddr from the info dialog (tap ℹ️ icon) +``` + +**Peer 2:** +```bash +# Use the multiaddr from Peer 1 +python main.py --kivy --nick Bob --connect /ip4/192.168.1.100/tcp/9095/p2p/QmXXX... +``` + +## 🖥️ User Interfaces + +Kivy, Textual, and CLI run against the local in-process HeadlessService. React frontend clients connect over HTTP/WebSocket through the Tornado API layer. + +### 1. Kivy UI (Recommended for Mobile/Desktop) + +**Features:** +- WhatsApp-style topic selector +- Per-topic conversations +- Unread message counts +- Tap-to-copy for sharing +- Dynamic peer connection +- Material Design + +**Navigation:** +``` +Topics Screen (Main) + ├─ Tap topic → Open conversation + ├─ + button → Subscribe to new topic + ├─ 🔗 button → Connect to peer + └─ ℹ️ button → View connection info + +Chat Screen + ├─ Back → Return to topics + ├─ ℹ️ button → Show status + └─ Type & send messages +``` + +**Copying Peer Info:** +1. Tap ℹ️ (info) icon +2. Tap on Peer ID or Multiaddr card +3. Value copied to clipboard automatically + +### 2. Textual TUI + +**Features:** +- Terminal-based interface +- Live peer count +- System messages panel +- Command support + +**Commands:** +- `/quit` - Exit the chat +- `/peers` - Show connected peers +- `/status` - Display connection status +- `/multiaddr` - Show your multiaddress + +**Note:** Copy multiaddr from `system_messages.txt` file for sharing. + +### 3. Simple CLI Mode + +**Features:** +- Minimal interface +- Direct input/output +- Command history +- Low resource usage + +**Usage:** +Just type messages and press Enter. Use `/quit` to exit. + +### 4. React Web Frontend + +The React frontend is located in `py-peer/py-peer-frontend` and uses the Tornado API. + +**Start backend API:** +```bash +python main.py --api --nick Alice --api-port 8765 +``` + +**Start frontend (new terminal):** +```bash +cd py-peer/py-peer-frontend +npm install +npm run dev +``` + +Open `http://localhost:3000`. + +**Production/remote backend:** +- Set `VITE_API_URL` in `py-peer/py-peer-frontend/.env.local` +- Example: `VITE_API_URL=https://your-backend.example.com` + +## ⚙️ Configuration + +### Custom Topics + +**Via Command Line:** +```bash +python main.py --kivy --nick Alice --topic my-custom-topic +``` + +**Via UI (Kivy):** +1. Tap + button in Topics screen +2. Enter topic name +3. Tap SUBSCRIBE + +### Dynamic Peer Connection (Kivy UI) + +1. Get peer's multiaddr: + - Tap ℹ️ icon in Topics screen + - Tap on Multiaddr card to copy + +2. Connect to peer: + - Tap 🔗 icon in Topics screen + - Paste multiaddr + - Tap CONNECT + +### Message Storage + +Messages are stored per-topic with read/unread tracking: +- Unread messages appear in topic list +- Opening a topic marks messages as read +- Messages persist during session only + +### Tornado API Configuration + +The API runs in headless + server mode and exposes REST + WebSocket endpoints. + +```bash +python main.py --api --nick MyNode --api-port 8765 +``` + +- Base URL: `http://localhost:8765/api/v1` +- Route listing: `python main.py --api-routes` +- Full endpoint docs: see **`API_REFERENCE.md`** + +### RAG Assistant Configuration + +The `/api/v1/ask` endpoint is available when vector store + LLM dependencies are configured. + +1. Install additional packages: +```bash +pip install chromadb langchain langchain-community langchain-huggingface sentence-transformers langchain-chroma +``` +2. Build vector store: +```bash +cd llm/codes +python build_vectorstore.py +``` +3. Set your Groq API key in environment: +```bash +export GROQ_API_KEY=your_key_here +``` + +### Log Files + +- **`system_messages.txt`** - System events and connection logs + - Format: `[HH:MM:SS] message` + - Contains: startup, peer connections, errors, multiaddr + +## 🔧 Development + +### Project Structure + +``` +py-peer/ +├── main.py # Entry point & argument parsing +├── kivy_ui.py # Kivy mobile UI (NEW!) +├── ui.py # Textual TUI implementation +├── headless.py # Background service & state management +├── chatroom.py # Chat room logic & topic handling +├── tornado_server.py # REST + WebSocket server +├── rag_handler.py # RAG assistant endpoint +├── API_REFERENCE.md # Full API docs +├── api/ # Tornado API handlers (v1) +├── py-peer-frontend/ # React + Vite frontend +├── pyproject.toml # Project configuration & dependencies +├── uv.lock # Dependency lock file +├── system_messages.txt # System logs +└── README.md # This file +``` + +### Key Classes + +**kivy_ui.py:** +- `ChatScreen` - Per-topic conversation view +- `TopicsScreen` - Main topic selector (WhatsApp-style) +- `PeersScreen` - Connected peers list +- `ChatApp` - Main Kivy application + +**headless.py:** +- `HeadlessService` - Background libp2p service +- Manages queues, message storage, peer connections +- Per-topic message tracking with read/unread status + +**chatroom.py:** +- `ChatRoom` - Core libp2p chat functionality +- Dynamic topic subscription +- Unified message handler for all topics +- Message validation & signing + +**tornado_server.py + api/** +- REST endpoints for node, peers, topics, messages, files, DHT, pubsub, identity, service +- WebSocket streams for messages, system events, peer updates, mesh updates +- Shared JSON response envelope + readiness guards + +**rag_handler.py:** +- `POST /api/v1/ask` retrieval-augmented question answering +- Loads Chroma vector store at startup +- Uses Groq LLM backend for grounded answers with source attribution + +### Running from Source + +```bash +# Development mode with debug logging +python main.py --kivy --nick TestUser --verbose + +# Test with multiple topics +python main.py --kivy --nick Alice --topic dev-chat + +# Run with specific port +python main.py --kivy --nick Bob --port 8080 + +# Run REST + WebSocket API +python main.py --api --nick ApiNode --api-port 8765 + +# Run React frontend +cd py-peer/py-peer-frontend && npm run dev +``` + +### Code Style + +The project follows Python best practices: +- Type hints where applicable +- Async/await patterns with Trio +- Modular architecture +- Thread-safe queue-based communication +- Comprehensive logging +- Material Design UI principles (Kivy) + +## 🐛 Troubleshooting + +### Common Issues + +**1. Port Already in Use** +```bash +# Solution: Specify a different port +python main.py --kivy --nick YourName --port 8081 +``` + +**2. No Peers Found** +- Ensure other peers are running on the same network +- Check firewall settings +- Use `--connect` or UI connect button to manually connect +- Verify multiaddr format is correct + +**3. Kivy UI Not Starting** +```bash +# Install system dependencies (Linux) +sudo apt-get install libsdl2-dev + +# Reinstall Kivy +pip install --force-reinstall kivy kivymd +``` + +**4. Messages Not Appearing** +- Check if you're subscribed to the correct topic +- Verify peer connection in info dialog +- Look for errors in system_messages.txt + +**5. Clipboard Copy Not Working (Kivy)** +- Ensure clipboard permissions (mobile) +- Try restarting the app +- Check system logs for errors + +**6. Tornado API Not Reachable** +- Ensure backend was started with `--api` +- Check API port (default `8765`) +- Run `python main.py --api-routes` to verify route setup + +**7. React Frontend Shows "Cannot reach py-peer API"** +- Confirm backend is running: `python main.py --api --nick YourName` +- For local dev, keep `VITE_API_URL` empty or set to `http://localhost:8765` +- Restart Vite after `.env.local` changes + +**8. RAG Assistant Unavailable (`/api/v1/ask` returns 503)** +- Build vector store via `llm/codes/build_vectorstore.py` +- Install RAG dependencies (Chroma/LangChain/SentenceTransformers) +- Set `GROQ_API_KEY` environment variable + +### Debug Mode + +Enable verbose logging to diagnose issues: +```bash +python main.py --kivy --nick DebugUser --verbose +``` + +Check logs: +```bash +tail -f system_messages.txt +``` + +### Network Testing + +Test peer connectivity: +```bash +# Terminal 1 +python main.py --kivy --nick Peer1 --port 8080 + +# Get multiaddr from Peer1's info dialog + +# Terminal 2 (connect to Peer1) +python main.py --kivy --nick Peer2 --connect /ip4/127.0.0.1/tcp/8080/p2p/PEER_ID +``` + +### Topic Issues + +**Can't receive messages on new topic:** +- Ensure both peers are subscribed to the same topic name +- Topic names are case-sensitive +- Check spelling in both peers + +**Unread counts not updating:** +- Open the chat screen for that topic +- Messages are marked read when you view the topic + +## 📚 Advanced Usage + +### Multiple Topics + +Subscribe to multiple topics and switch between conversations: +```bash +# Start with default topic +python main.py --kivy --nick Alice + +# In UI: +# 1. Tap + to add "project-updates" +# 2. Tap + to add "random-chat" +# 3. Switch between topics by tapping on them +``` + +### Custom Topic Names + +```bash +# Use your own topic namespace +python main.py --kivy --nick Alice --topic my-company-chat +``` + +### API + Frontend Pairing + +Run backend and frontend together: + +```bash +# Terminal 1 (backend) +python main.py --api --nick BackendNode --api-port 8765 + +# Terminal 2 (frontend) +cd py-peer/py-peer-frontend +npm run dev +``` + +Use REST directly: + +```bash +curl http://localhost:8765/api/v1/node/info +curl http://localhost:8765/api/v1/topics +``` + +### Peer Groups + +Create private peer groups by using unique topic names: +```bash +# All peers use the same custom topic +python main.py --kivy --nick Alice --topic secret-group-2024 +python main.py --kivy --nick Bob --topic secret-group-2024 +``` + +## 🤝 Contributing + +We welcome contributions! Here's how: + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Make your changes +4. Test thoroughly (all three UI modes) +5. Commit your changes (`git commit -m 'Add amazing feature'`) +6. Push to the branch (`git push origin feature/amazing-feature`) +7. Open a Pull Request + +### Development Guidelines + +- Follow existing code style +- Add type hints +- Update README for new features +- Test on multiple platforms if possible +- Ensure backward compatibility + +## 🙏 Acknowledgments + +- [libp2p](https://libp2p.io/) - Peer-to-peer networking framework +- [Trio](https://trio.readthedocs.io/) - Async framework +- [Kivy](https://kivy.org/) & [KivyMD](https://kivymd.readthedocs.io/) - Mobile UI framework +- [Textual](https://textual.textualize.io/) - Terminal UI framework +- Universal Connectivity project contributors + +## 📄 License + +This project is part of the Universal Connectivity project. See LICENSE files for details. + +--- + +## 📞 Support + +For support and questions: +- Create an issue in the [GitHub repository](https://github.com/sumanjeet0012/universal-connectivity) +- Check the troubleshooting section above +- Review the system logs in `system_messages.txt` +- Join our community discussions + +## 🗺️ Roadmap + +- [ ] Message persistence across sessions +- [x] File sharing support (REST endpoints: share/download/upload) +- [ ] Voice/video chat +- [ ] End-to-end encryption +- [ ] Mobile app packaging (Android/iOS) +- [ ] Bootstrap node configuration persistence +- [ ] Message search functionality +- [ ] Notification system +- [ ] Custom themes + +**Happy chatting! 🎉** [GOSSIPSUB]: https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/README.md [PYLIBP2P]: https://github.com/libp2p/py-libp2p diff --git a/py-peer/api/__init__.py b/py-peer/api/__init__.py new file mode 100644 index 00000000..634f0234 --- /dev/null +++ b/py-peer/api/__init__.py @@ -0,0 +1,3 @@ +""" +Tornado REST + WebSocket API package for py-peer Universal Connectivity DApp. +""" diff --git a/py-peer/api/base.py b/py-peer/api/base.py new file mode 100644 index 00000000..d4c67ed5 --- /dev/null +++ b/py-peer/api/base.py @@ -0,0 +1,107 @@ +""" +Base handler for all Tornado API endpoints. + +Provides: +- JSON response helpers +- CORS headers +- Service readiness check +- Uniform error envelope +""" + +import json +import time +import logging +import traceback + +import tornado.web + +logger = logging.getLogger("api.base") + + +class BaseHandler(tornado.web.RequestHandler): + """Base class for all REST API handlers.""" + + def initialize(self, service): + """Inject the HeadlessService instance.""" + self.service = service + + # ------------------------------------------------------------------ # + # CORS # + # ------------------------------------------------------------------ # + def set_default_headers(self): + self.set_header("Access-Control-Allow-Origin", "*") + self.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + self.set_header("Access-Control-Allow-Headers", "Content-Type, X-API-Key") + self.set_header("Content-Type", "application/json") + + def options(self, *args, **kwargs): + """Handle CORS preflight.""" + self.set_status(204) + self.finish() + + # ------------------------------------------------------------------ # + # JSON helpers # + # ------------------------------------------------------------------ # + def send_success(self, data=None, status=200): + self.set_status(status) + self.finish(json.dumps({ + "success": True, + "data": data, + "error": None, + "timestamp": time.time(), + })) + + def send_error_response(self, message, status=400, detail=None): + self.set_status(status) + self.finish(json.dumps({ + "success": False, + "data": None, + "error": { + "code": status, + "message": message, + "detail": detail, + }, + "timestamp": time.time(), + })) + + # ------------------------------------------------------------------ # + # Request body helpers # + # ------------------------------------------------------------------ # + def get_json_body(self): + try: + return json.loads(self.request.body) + except (json.JSONDecodeError, Exception): + return {} + + # ------------------------------------------------------------------ # + # Service readiness guard # + # ------------------------------------------------------------------ # + def require_ready(self): + """Return False and send 503 if the service is not ready yet.""" + if not self.service or not self.service.ready: + self.send_error_response( + "Service not ready yet — HeadlessService is still initialising.", + status=503, + ) + return False + return True + + # ------------------------------------------------------------------ # + # Global exception handler # + # ------------------------------------------------------------------ # + def write_error(self, status_code, **kwargs): + exc_info = kwargs.get("exc_info") + detail = None + if exc_info: + detail = traceback.format_exception(*exc_info)[-1].strip() + self.set_header("Content-Type", "application/json") + self.finish(json.dumps({ + "success": False, + "data": None, + "error": { + "code": status_code, + "message": self._reason, + "detail": detail, + }, + "timestamp": time.time(), + })) diff --git a/py-peer/api/dht.py b/py-peer/api/dht.py new file mode 100644 index 00000000..d7b00da5 --- /dev/null +++ b/py-peer/api/dht.py @@ -0,0 +1,74 @@ +""" +DHT endpoints. + +GET /api/v1/dht/status - mode, routing table size, random walk +GET /api/v1/dht/peers - peer IDs in DHT routing table +GET /api/v1/dht/routing-table - concise routing table dump +""" + +from .base import BaseHandler + + +class DHTStatusHandler(BaseHandler): + """GET /api/v1/dht/status""" + + def get(self): + if not self.require_ready(): + return + dht = self.service.dht + if not dht: + self.send_error_response("DHT is not initialised.", status=503) + return + try: + rt_size = len(list(dht.routing_table.get_peer_ids())) + except Exception: + rt_size = -1 + + # DHTMode enum → string + mode_val = getattr(dht, "mode", None) + mode_str = mode_val.name if hasattr(mode_val, "name") else str(mode_val) + + self.send_success({ + "mode": mode_str, + "random_walk_enabled": getattr(dht, "enable_random_walk", False), + "routing_table_size": rt_size, + }) + + +class DHTPeersHandler(BaseHandler): + """GET /api/v1/dht/peers""" + + def get(self): + if not self.require_ready(): + return + dht = self.service.dht + if not dht: + self.send_error_response("DHT is not initialised.", status=503) + return + try: + peers = [str(p) for p in dht.routing_table.get_peer_ids()] + except Exception as e: + self.send_error_response(f"Could not read routing table: {e}", status=500) + return + self.send_success({"peers": peers, "count": len(peers)}) + + +class DHTRoutingTableHandler(BaseHandler): + """GET /api/v1/dht/routing-table""" + + def get(self): + if not self.require_ready(): + return + dht = self.service.dht + if not dht: + self.send_error_response("DHT is not initialised.", status=503) + return + try: + peers = [str(p) for p in dht.routing_table.get_peer_ids()] + except Exception as e: + self.send_error_response(f"Could not read routing table: {e}", status=500) + return + self.send_success({ + "routing_table": peers, + "total_peers": len(peers), + }) diff --git a/py-peer/api/files.py b/py-peer/api/files.py new file mode 100644 index 00000000..b708f91c --- /dev/null +++ b/py-peer/api/files.py @@ -0,0 +1,163 @@ +""" +File sharing endpoints (Bitswap / MerkleDag). + +GET /api/v1/files/shared - list files this node has shared +GET /api/v1/files/shared/{cid} - metadata for a specific shared file +POST /api/v1/files/share - share a local file to a topic +POST /api/v1/files/download - download a file by CID hex +POST /api/v1/files/upload - upload via multipart and share to a topic +""" + +import os +from .base import BaseHandler + + +class SharedFilesHandler(BaseHandler): + """GET /api/v1/files/shared""" + + def get(self): + if not self.require_ready(): + return + files = [ + {"cid": cid, **meta} + for cid, meta in self.service.shared_files.items() + ] + self.send_success({"shared_files": files, "count": len(files)}) + + +class SharedFileDetailHandler(BaseHandler): + """GET /api/v1/files/shared/{cid}""" + + def get(self, cid): + if not self.require_ready(): + return + meta = self.service.shared_files.get(cid) + if not meta: + self.send_error_response(f"No shared file with CID '{cid}'.", status=404) + return + self.send_success({"cid": cid, **meta}) + + +class ShareFileHandler(BaseHandler): + """POST /api/v1/files/share — share a file that already exists on disk""" + + def post(self): + if not self.require_ready(): + return + body = self.get_json_body() + file_path = body.get("file_path", "").strip() + topic = body.get("topic", "").strip() + + if not file_path: + self.send_error_response("'file_path' is required.") + return + if not topic: + self.send_error_response("'topic' is required.") + return + if not os.path.exists(file_path): + self.send_error_response(f"File not found: {file_path}", status=400) + return + + subscribed = self.service.get_subscribed_topics() + if topic not in subscribed: + self.send_error_response( + f"Not subscribed to topic '{topic}'. Subscribe first via POST /api/v1/topics.", + status=400, + ) + return + + queued = self.service.share_file(file_path, topic) + if queued: + filename = os.path.basename(file_path) + self.send_success( + {"message": "File share request queued", "filename": filename, "topic": topic}, + status=202, + ) + else: + self.send_error_response("Failed to queue file share — service not ready.", status=503) + + +class DownloadFileHandler(BaseHandler): + """POST /api/v1/files/download — download a file by CID hex""" + + def post(self): + if not self.require_ready(): + return + body = self.get_json_body() + cid = body.get("file_cid", "").strip() + name = body.get("file_name", "unknown").strip() + + if not cid: + self.send_error_response("'file_cid' is required.") + return + + queued = self.service.download_file(cid, name) + if queued: + self.send_success( + {"message": "Download request queued", "file_cid": cid, "file_name": name}, + status=202, + ) + else: + self.send_error_response("Failed to queue download — service not ready.", status=503) + + +class UploadAndShareHandler(BaseHandler): + """ + POST /api/v1/files/upload + Accepts multipart/form-data with fields: + - file : the file bytes + - topic : the topic to share to + Saves the file to the service's download_dir and queues a share. + """ + + def post(self): + if not self.require_ready(): + return + + topic = self.get_argument("topic", "").strip() + if not topic: + self.send_error_response("'topic' form field is required.") + return + + if "file" not in self.request.files: + self.send_error_response("'file' form-data field is required.") + return + + file_info = self.request.files["file"][0] + filename = file_info["filename"] or "upload" + file_data = file_info["body"] + + # Save to download_dir + save_path = os.path.join(self.service.download_dir, filename) + # Handle name collisions + counter = 1 + base, ext = os.path.splitext(filename) + while os.path.exists(save_path): + save_path = os.path.join(self.service.download_dir, f"{base}_{counter}{ext}") + counter += 1 + + with open(save_path, "wb") as f: + f.write(file_data) + + subscribed = self.service.get_subscribed_topics() + if topic not in subscribed: + self.send_error_response( + f"Not subscribed to topic '{topic}'. Subscribe first via POST /api/v1/topics.", + status=400, + ) + return + + queued = self.service.share_file(save_path, topic) + if queued: + self.send_success( + { + "message": "File uploaded and share request queued", + "filename": os.path.basename(save_path), + "size": len(file_data), + "topic": topic, + "saved_path": save_path, + }, + status=202, + ) + else: + self.send_error_response("Failed to queue file share — service not ready.", status=503) diff --git a/py-peer/api/identity.py b/py-peer/api/identity.py new file mode 100644 index 00000000..5a2262c4 --- /dev/null +++ b/py-peer/api/identity.py @@ -0,0 +1,88 @@ +""" +Identity / Identify-protocol endpoints. + +GET /api/v1/identity/cache - list all cached identify entries +GET /api/v1/identity/{peer_id} - cached info (returns 404 if not cached) +GET /api/v1/identity/{peer_id}/pubkey - public key bytes (hex) from cache +DELETE /api/v1/identity/{peer_id}/cache - invalidate cached entry +""" + +from .base import BaseHandler + + +class IdentityCacheListHandler(BaseHandler): + """GET /api/v1/identity/cache""" + + def get(self): + if not self.require_ready(): + return + cache = self.service.peer_info_cache + result = {} + for peer_id, info in cache.items(): + result[peer_id] = { + "protocol_version": info.get("protocol_version"), + "agent_version": info.get("agent_version"), + "listen_addrs": [str(a) for a in info.get("listen_addrs", [])], + "protocols": [str(p) for p in info.get("protocols", [])], + "cached_at": info.get("timestamp"), + } + self.send_success({"cache": result, "count": len(result)}) + + +class IdentityPeerHandler(BaseHandler): + """GET /api/v1/identity/{peer_id}""" + + def get(self, peer_id): + if not self.require_ready(): + return + info = self.service.peer_info_cache.get(peer_id) + if not info: + self.send_error_response( + f"No cached identify info for peer '{peer_id[:16]}...'. " + "Connect to the peer first so that the identify protocol is triggered automatically.", + status=404, + ) + return + self.send_success({ + "peer_id": peer_id, + "protocol_version": info.get("protocol_version"), + "agent_version": info.get("agent_version"), + "listen_addrs": [str(a) for a in info.get("listen_addrs", [])], + "protocols": [str(p) for p in info.get("protocols", [])], + "cached_at": info.get("timestamp"), + }) + + +class IdentityPublicKeyHandler(BaseHandler): + """GET /api/v1/identity/{peer_id}/pubkey""" + + def get(self, peer_id): + if not self.require_ready(): + return + pubkey = self.service.get_public_key_for_peer(peer_id) + if pubkey is None: + self.send_error_response( + f"No public key cached for peer '{peer_id[:16]}...'.", status=404 + ) + return + # public key may be bytes or an object — safely convert to hex + if isinstance(pubkey, bytes): + pubkey_hex = pubkey.hex() + else: + pubkey_hex = str(pubkey) + self.send_success({"peer_id": peer_id, "public_key_hex": pubkey_hex}) + + +class IdentityCacheDeleteHandler(BaseHandler): + """DELETE /api/v1/identity/{peer_id}/cache""" + + def delete(self, peer_id): + if not self.require_ready(): + return + if peer_id in self.service.peer_info_cache: + del self.service.peer_info_cache[peer_id] + self.send_success({"message": f"Cache entry for '{peer_id[:16]}...' deleted."}) + else: + self.send_error_response( + f"No cache entry found for peer '{peer_id[:16]}...'.", status=404 + ) diff --git a/py-peer/api/messages.py b/py-peer/api/messages.py new file mode 100644 index 00000000..c088e7cf --- /dev/null +++ b/py-peer/api/messages.py @@ -0,0 +1,94 @@ +""" +Messaging endpoints. + +POST /api/v1/messages - send to default chat topic +POST /api/v1/messages/{topic} - send to specific topic +GET /api/v1/messages/{topic} - retrieve stored messages (paginated) +GET /api/v1/messages/{topic}/unread - unread count +PUT /api/v1/messages/{topic}/read - mark all as read +""" + +from .base import BaseHandler + + +class SendDefaultMessageHandler(BaseHandler): + """POST /api/v1/messages — sends to the node's default chat topic""" + + def post(self): + if not self.require_ready(): + return + body = self.get_json_body() + msg = body.get("message", "").strip() + if not msg: + self.send_error_response("'message' field is required.") + return + self.service.send_message(msg) + self.send_success({"message": "Message queued for delivery"}, status=202) + + +class TopicMessagesHandler(BaseHandler): + """ + GET /api/v1/messages/{topic} — list stored messages + POST /api/v1/messages/{topic} — send a message to this topic + """ + + def get(self, topic): + if not self.require_ready(): + return + try: + limit = int(self.get_argument("limit", 100)) + offset = int(self.get_argument("offset", 0)) + except ValueError: + self.send_error_response("'limit' and 'offset' must be integers.") + return + + all_msgs = self.service.get_topic_messages(topic) + page = all_msgs[offset: offset + limit] + self.send_success({ + "topic": topic, + "messages": page, + "total": len(all_msgs), + "limit": limit, + "offset": offset, + }) + + def post(self, topic): + if not self.require_ready(): + return + body = self.get_json_body() + msg = body.get("message", "").strip() + if not msg: + self.send_error_response("'message' field is required.") + return + + # Check topic is subscribed + subscribed = self.service.get_subscribed_topics() + if topic not in subscribed: + self.send_error_response( + f"Not subscribed to topic '{topic}'. Subscribe first via POST /api/v1/topics.", + status=400, + ) + return + + self.service.send_message_to_topic(topic, msg) + self.send_success({"message": "Message queued for delivery", "topic": topic}, status=202) + + +class TopicUnreadHandler(BaseHandler): + """GET /api/v1/messages/{topic}/unread""" + + def get(self, topic): + if not self.require_ready(): + return + count = self.service.get_unread_count(topic) + self.send_success({"topic": topic, "unread_count": count}) + + +class TopicMarkReadHandler(BaseHandler): + """PUT /api/v1/messages/{topic}/read""" + + def put(self, topic): + if not self.require_ready(): + return + self.service.mark_topic_as_read(topic) + self.send_success({"topic": topic, "message": "All messages marked as read"}) diff --git a/py-peer/api/node.py b/py-peer/api/node.py new file mode 100644 index 00000000..64e8f47f --- /dev/null +++ b/py-peer/api/node.py @@ -0,0 +1,55 @@ +""" +Node info endpoints. + +GET /api/v1/node/info - peer ID, nickname, multiaddr, port, ready state +GET /api/v1/node/status - running, ready, uptime +GET /api/v1/node/multiaddr - full multiaddr string +""" + +import time +from .base import BaseHandler + +_start_time = time.time() + + +class NodeInfoHandler(BaseHandler): + """GET /api/v1/node/info""" + + def get(self): + if not self.require_ready(): + return + info = self.service.get_connection_info() + self.send_success({ + "peer_id": info.get("peer_id"), + "nickname": info.get("nickname"), + "multiaddr": info.get("multiaddr"), + "port": self.service.port, + "ready": self.service.ready, + "uptime_seconds": round(time.time() - _start_time, 1), + }) + + +class NodeStatusHandler(BaseHandler): + """GET /api/v1/node/status""" + + def get(self): + self.send_success({ + "ready": self.service.ready, + "running": self.service.running, + "uptime_seconds": round(time.time() - _start_time, 1), + "port": self.service.port, + "strict_signing": self.service.strict_signing, + "nickname": self.service.nickname, + "topic": self.service.topic, + }) + + +class NodeMultiaddrHandler(BaseHandler): + """GET /api/v1/node/multiaddr""" + + def get(self): + if not self.require_ready(): + return + self.send_success({ + "multiaddr": self.service.full_multiaddr, + }) diff --git a/py-peer/api/peers.py b/py-peer/api/peers.py new file mode 100644 index 00000000..659b9c70 --- /dev/null +++ b/py-peer/api/peers.py @@ -0,0 +1,112 @@ +""" +Peer management endpoints. + +GET /api/v1/peers - list connected peers +GET /api/v1/peers/count - count of connected peers +GET /api/v1/peers/known - peers in peerstore +POST /api/v1/peers/connect - connect via multiaddr +GET /api/v1/peers/{peer_id} - info for a specific peer +GET /api/v1/peers/{peer_id}/identify - identify protocol info (from cache) +""" + +from .base import BaseHandler + + +class PeerListHandler(BaseHandler): + """GET /api/v1/peers""" + + def get(self): + if not self.require_ready(): + return + info = self.service.get_connection_info() + peers = list(info.get("connected_peers", set())) + self.send_success({ + "peers": peers, + "count": len(peers), + }) + + +class PeerCountHandler(BaseHandler): + """GET /api/v1/peers/count""" + + def get(self): + if not self.require_ready(): + return + info = self.service.get_connection_info() + self.send_success({"count": info.get("peer_count", 0)}) + + +class KnownPeersHandler(BaseHandler): + """GET /api/v1/peers/known""" + + def get(self): + if not self.require_ready(): + return + try: + known = [str(p) for p in self.service.host.get_peerstore().peers_with_addrs()] + except Exception as e: + self.send_error_response(f"Could not fetch known peers: {e}", status=500) + return + self.send_success({"peers": known, "count": len(known)}) + + +class PeerConnectHandler(BaseHandler): + """POST /api/v1/peers/connect""" + + def post(self): + if not self.require_ready(): + return + body = self.get_json_body() + maddr = body.get("multiaddr", "").strip() + if not maddr: + self.send_error_response("'multiaddr' field is required.") + return + queued = self.service.connect_to_peer(maddr) + if queued: + self.send_success({"message": "Connection request queued", "multiaddr": maddr}, status=202) + else: + self.send_error_response("Failed to queue connection request — service not ready.", status=503) + + +class PeerDetailHandler(BaseHandler): + """GET /api/v1/peers/{peer_id}""" + + def get(self, peer_id): + if not self.require_ready(): + return + try: + from libp2p.peer.id import ID + pid = ID.from_base58(peer_id) + pinfo = self.service.host.get_peerstore().peer_info(pid) + addrs = [str(a) for a in pinfo.addrs] if pinfo.addrs else [] + except Exception as e: + self.send_error_response(f"Peer not found: {e}", status=404) + return + self.send_success({ + "peer_id": peer_id, + "addrs": addrs, + }) + + +class PeerIdentifyHandler(BaseHandler): + """GET /api/v1/peers/{peer_id}/identify — returns cached identify data""" + + def get(self, peer_id): + if not self.require_ready(): + return + cached = self.service.peer_info_cache.get(peer_id) + if not cached: + self.send_error_response( + f"No cached identify info for peer {peer_id[:12]}... — " + "connect to the peer first so identify can be triggered.", + status=404, + ) + return + self.send_success({ + "peer_id": peer_id, + "protocol_version": cached.get("protocol_version"), + "agent_version": cached.get("agent_version"), + "listen_addrs": [str(a) for a in cached.get("listen_addrs", [])], + "protocols": [str(p) for p in cached.get("protocols", [])], + "cached_at": cached.get("timestamp"), + }) diff --git a/py-peer/api/pubsub.py b/py-peer/api/pubsub.py new file mode 100644 index 00000000..2fd84a80 --- /dev/null +++ b/py-peer/api/pubsub.py @@ -0,0 +1,95 @@ +""" +PubSub / GossipSub endpoints. + +GET /api/v1/pubsub/peers - all peers connected via PubSub +GET /api/v1/pubsub/mesh - GossipSub mesh per topic +GET /api/v1/pubsub/fanout - GossipSub fanout per topic +GET /api/v1/pubsub/config - GossipSub configuration values +GET /api/v1/pubsub/subscriptions - active subscription topic names +""" + +from .base import BaseHandler + + +class PubSubPeersHandler(BaseHandler): + """GET /api/v1/pubsub/peers""" + + def get(self): + if not self.require_ready(): + return + try: + peers = [str(p) for p in self.service.pubsub.peers.keys()] + except Exception as e: + self.send_error_response(f"Could not read pubsub peers: {e}", status=500) + return + self.send_success({"peers": peers, "count": len(peers)}) + + +class PubSubMeshHandler(BaseHandler): + """GET /api/v1/pubsub/mesh""" + + def get(self): + if not self.require_ready(): + return + try: + raw_mesh = getattr(self.service.gossipsub, "mesh", {}) + mesh = { + topic: [str(p) for p in peers] + for topic, peers in raw_mesh.items() + } + total_peers = sum(len(v) for v in mesh.values()) + except Exception as e: + self.send_error_response(f"Could not read GossipSub mesh: {e}", status=500) + return + self.send_success({ + "mesh": mesh, + "topic_count": len(mesh), + "total_mesh_peers": total_peers, + }) + + +class PubSubFanoutHandler(BaseHandler): + """GET /api/v1/pubsub/fanout""" + + def get(self): + if not self.require_ready(): + return + try: + raw = getattr(self.service.gossipsub, "fanout", {}) + fanout = { + topic: [str(p) for p in peers] + for topic, peers in raw.items() + } + except Exception as e: + self.send_error_response(f"Could not read GossipSub fanout: {e}", status=500) + return + self.send_success({"fanout": fanout}) + + +class PubSubConfigHandler(BaseHandler): + """GET /api/v1/pubsub/config""" + + def get(self): + if not self.require_ready(): + return + gs = self.service.gossipsub + self.send_success({ + "degree": getattr(gs, "degree", None), + "degree_low": getattr(gs, "degree_low", None), + "degree_high": getattr(gs, "degree_high", None), + "gossip_window": getattr(gs, "gossip_window", None), + "gossip_history": getattr(gs, "gossip_history", None), + "heartbeat_interval": getattr(gs, "heartbeat_interval", None), + "heartbeat_initial_delay": getattr(gs, "heartbeat_initial_delay", None), + "protocols": [str(p) for p in getattr(gs, "protocols", [])], + }) + + +class PubSubSubscriptionsHandler(BaseHandler): + """GET /api/v1/pubsub/subscriptions""" + + def get(self): + if not self.require_ready(): + return + topics = list(self.service.get_subscribed_topics()) + self.send_success({"subscriptions": topics, "count": len(topics)}) diff --git a/py-peer/api/service.py b/py-peer/api/service.py new file mode 100644 index 00000000..8bbc37da --- /dev/null +++ b/py-peer/api/service.py @@ -0,0 +1,84 @@ +""" +Service control endpoints. + +GET /api/v1/service/status - overall health, readiness, uptime +GET /api/v1/service/config - current configuration +POST /api/v1/service/stop - gracefully stop the HeadlessService +POST /api/v1/service/bootstrap - re-trigger bootstrap connections +""" + +import time +import asyncio +from .base import BaseHandler + +_start_time = time.time() + + +class ServiceStatusHandler(BaseHandler): + """GET /api/v1/service/status""" + + def get(self): + self.send_success({ + "ready": self.service.ready, + "running": self.service.running, + "uptime_seconds": round(time.time() - _start_time, 1), + "peer_count": self.service.get_connection_info().get("peer_count", 0) if self.service.ready else 0, + }) + + +class ServiceConfigHandler(BaseHandler): + """GET /api/v1/service/config""" + + def get(self): + self.send_success({ + "nickname": self.service.nickname, + "port": self.service.port, + "topic": self.service.topic, + "strict_signing": self.service.strict_signing, + "download_dir": self.service.download_dir, + "connect_addrs": self.service.connect_addrs, + }) + + +class ServiceStopHandler(BaseHandler): + """POST /api/v1/service/stop — graceful shutdown""" + + def post(self): + if not self.service: + self.send_error_response("No service attached.", status=503) + return + # Schedule the stop coroutine without blocking the handler + asyncio.get_event_loop().call_soon(self._do_stop) + self.send_success({"message": "Stop signal sent to HeadlessService."}) + + def _do_stop(self): + """Fire-and-forget the stop signal via threading.""" + import threading + def _stop(): + import trio + # The service's stop_event is a trio.Event; set it from another + # thread via the sync API if available, otherwise notify via a flag. + try: + self.service.running = False + self.service.stop_event.set() + except Exception: + self.service.running = False + threading.Thread(target=_stop, daemon=True).start() + + +class ServiceBootstrapHandler(BaseHandler): + """POST /api/v1/service/bootstrap — re-trigger bootstrap peer connections""" + + def post(self): + if not self.require_ready(): + return + # Queue connection requests for all bootstrap peers + from headless import BOOTSTRAP_PEERS + count = 0 + for addr in BOOTSTRAP_PEERS: + if self.service.connect_to_peer(addr): + count += 1 + self.send_success({ + "message": f"Queued connections to {count} bootstrap peers.", + "bootstrap_peers_count": count, + }, status=202) diff --git a/py-peer/api/topics.py b/py-peer/api/topics.py new file mode 100644 index 00000000..8752bb95 --- /dev/null +++ b/py-peer/api/topics.py @@ -0,0 +1,79 @@ +""" +Topic / channel endpoints. + +GET /api/v1/topics - list all subscribed topics with metadata +POST /api/v1/topics - subscribe to a new topic +GET /api/v1/topics/{topic}/info - topic details (unread, total, last msg) +GET /api/v1/topics/{topic}/peers - peers in GossipSub mesh for topic +""" + +from .base import BaseHandler + + +class TopicListHandler(BaseHandler): + """GET /api/v1/topics — POST /api/v1/topics""" + + def get(self): + if not self.require_ready(): + return + topics_info = self.service.get_all_topics_with_info() + # Ensure last_message timestamps are serialisable + for v in topics_info.values(): + if v.get("last_message") and not isinstance(v["last_message"], dict): + v["last_message"] = None + self.send_success({"topics": topics_info, "count": len(topics_info)}) + + def post(self): + if not self.require_ready(): + return + body = self.get_json_body() + topic = body.get("topic", "").strip() + if not topic: + self.send_error_response("'topic' field is required.") + return + + already = self.service.get_subscribed_topics() + if topic in already: + self.send_error_response(f"Already subscribed to topic '{topic}'.", status=409) + return + + queued = self.service.subscribe_to_topic(topic) + if queued: + self.send_success({"message": f"Subscription request queued for '{topic}'", "topic": topic}, status=202) + else: + self.send_error_response("Failed to queue subscription — service not ready.", status=503) + + +class TopicInfoHandler(BaseHandler): + """GET /api/v1/topics/{topic}/info""" + + def get(self, topic): + if not self.require_ready(): + return + all_info = self.service.get_all_topics_with_info() + if topic not in all_info: + self.send_error_response(f"Topic '{topic}' not found or not subscribed.", status=404) + return + info = all_info[topic] + if info.get("last_message") and not isinstance(info["last_message"], dict): + info["last_message"] = None + self.send_success({"topic": topic, **info}) + + +class TopicMeshPeersHandler(BaseHandler): + """GET /api/v1/topics/{topic}/peers — peers in GossipSub mesh for topic""" + + def get(self, topic): + if not self.require_ready(): + return + try: + mesh = getattr(self.service.gossipsub, "mesh", {}) + peers_in_mesh = [str(p) for p in mesh.get(topic, set())] + except Exception as e: + self.send_error_response(f"Could not read mesh: {e}", status=500) + return + self.send_success({ + "topic": topic, + "mesh_peers": peers_in_mesh, + "count": len(peers_in_mesh), + }) diff --git a/py-peer/api/websocket.py b/py-peer/api/websocket.py new file mode 100644 index 00000000..a594be03 --- /dev/null +++ b/py-peer/api/websocket.py @@ -0,0 +1,213 @@ +""" +WebSocket handlers — real-time streaming for API clients. + +WS /ws/messages - stream chat + file messages from message_queue +WS /ws/system - stream system events from system_queue +WS /ws/peers - periodic peer list updates +WS /ws/pubsub/mesh - periodic GossipSub mesh topology updates +""" + +import json +import time +import logging +import asyncio + +import tornado.websocket + +logger = logging.getLogger("api.websocket") + +# How often (seconds) periodic pushes fire for peers and mesh endpoints +PEERS_PUSH_INTERVAL = 3.0 +MESH_PUSH_INTERVAL = 5.0 + + +class BaseWebSocketHandler(tornado.websocket.WebSocketHandler): + """Shared WebSocket base: CORS + injected service.""" + + def initialize(self, service): + self.service = service + self._running = False + self._task: asyncio.Task | None = None + + def check_origin(self, origin): + return True # Allow all origins (CORS) + + def open(self): + self._running = True + logger.info(f"WS opened: {self.__class__.__name__}") + self._task = asyncio.get_event_loop().create_task(self._push_loop()) + + def on_close(self): + self._running = False + if self._task: + self._task.cancel() + logger.info(f"WS closed: {self.__class__.__name__}") + + def on_message(self, message): + """Sub-classes override this to handle client commands.""" + pass + + async def _push_loop(self): + """Override in subclass.""" + pass + + def _safe_write(self, data: dict): + try: + self.write_message(json.dumps(data)) + except tornado.websocket.WebSocketClosedError: + self._running = False + + +# ────────────────────────────────────────────────────────── +# WS /ws/messages +# ────────────────────────────────────────────────────────── +class MessageStreamHandler(BaseWebSocketHandler): + """ + Streams all incoming chat and file messages in real-time. + + Client can optionally send a JSON command to filter by topic: + { "action": "filter_topic", "topic": "my-channel" } + { "action": "unfilter" } + """ + + def initialize(self, service): + super().initialize(service) + self._topic_filter: str | None = None + + def on_message(self, raw): + try: + cmd = json.loads(raw) + action = cmd.get("action") + if action == "filter_topic": + self._topic_filter = cmd.get("topic") + self._safe_write({"event": "filter_set", "topic": self._topic_filter}) + elif action == "unfilter": + self._topic_filter = None + self._safe_write({"event": "filter_cleared"}) + except Exception: + pass + + async def _push_loop(self): + # Wait until service is ready + while self._running and not (self.service and self.service.ready): + await asyncio.sleep(0.2) + + mq = self.service.get_message_queue() + if not mq: + return + + while self._running: + try: + # Drain all available messages + while True: + try: + msg = mq.sync_q.get_nowait() + # Apply optional topic filter + if self._topic_filter and msg.get("topic") != self._topic_filter: + continue + self._safe_write({"event": msg.get("type", "message"), "data": msg}) + except Exception: + break + await asyncio.sleep(0.1) + except asyncio.CancelledError: + break + except Exception as e: + logger.error(f"MessageStreamHandler error: {e}") + await asyncio.sleep(1) + + +# ────────────────────────────────────────────────────────── +# WS /ws/system +# ────────────────────────────────────────────────────────── +class SystemStreamHandler(BaseWebSocketHandler): + """Streams system events and service notifications.""" + + async def _push_loop(self): + while self._running and not (self.service and self.service.ready): + await asyncio.sleep(0.2) + + sq = self.service.get_system_queue() + if not sq: + return + + while self._running: + try: + while True: + try: + msg = sq.sync_q.get_nowait() + self._safe_write({"event": "system_message", "data": msg}) + except Exception: + break + await asyncio.sleep(0.1) + except asyncio.CancelledError: + break + except Exception as e: + logger.error(f"SystemStreamHandler error: {e}") + await asyncio.sleep(1) + + +# ────────────────────────────────────────────────────────── +# WS /ws/peers +# ────────────────────────────────────────────────────────── +class PeerUpdateHandler(BaseWebSocketHandler): + """Pushes the current connected-peer list every PEERS_PUSH_INTERVAL seconds.""" + + async def _push_loop(self): + while self._running and not (self.service and self.service.ready): + await asyncio.sleep(0.2) + + last_peers: list = [] + while self._running: + try: + info = self.service.get_connection_info() + peers = sorted(str(p) for p in info.get("connected_peers", set())) + if peers != last_peers: + last_peers = peers + self._safe_write({ + "event": "peer_update", + "data": { + "connected_peers": peers, + "peer_count": len(peers), + "timestamp": time.time(), + }, + }) + await asyncio.sleep(PEERS_PUSH_INTERVAL) + except asyncio.CancelledError: + break + except Exception as e: + logger.error(f"PeerUpdateHandler error: {e}") + await asyncio.sleep(PEERS_PUSH_INTERVAL) + + +# ────────────────────────────────────────────────────────── +# WS /ws/pubsub/mesh +# ────────────────────────────────────────────────────────── +class MeshUpdateHandler(BaseWebSocketHandler): + """Pushes GossipSub mesh topology every MESH_PUSH_INTERVAL seconds.""" + + async def _push_loop(self): + while self._running and not (self.service and self.service.ready): + await asyncio.sleep(0.2) + + while self._running: + try: + raw_mesh = getattr(self.service.gossipsub, "mesh", {}) + mesh = { + topic: sorted(str(p) for p in peers) + for topic, peers in raw_mesh.items() + } + self._safe_write({ + "event": "mesh_update", + "data": { + "mesh": mesh, + "topic_count": len(mesh), + "total_mesh_peers": sum(len(v) for v in mesh.values()), + "timestamp": time.time(), + }, + }) + await asyncio.sleep(MESH_PUSH_INTERVAL) + except asyncio.CancelledError: + break + except Exception as e: + logger.error(f"MeshUpdateHandler error: {e}") + await asyncio.sleep(MESH_PUSH_INTERVAL) diff --git a/py-peer/assets/images/py-peer-kivy.png b/py-peer/assets/images/py-peer-kivy.png new file mode 100644 index 00000000..fff85f0a Binary files /dev/null and b/py-peer/assets/images/py-peer-kivy.png differ diff --git a/py-peer/assets/images/py-peer-textual-ui.png b/py-peer/assets/images/py-peer-textual-ui.png new file mode 100644 index 00000000..08b635bf Binary files /dev/null and b/py-peer/assets/images/py-peer-textual-ui.png differ diff --git a/py-peer/chatroom.py b/py-peer/chatroom.py new file mode 100644 index 00000000..5cca1e73 --- /dev/null +++ b/py-peer/chatroom.py @@ -0,0 +1,437 @@ +""" +ChatRoom module for Universal Connectivity Python Peer + +This module handles chat room functionality including message handling, +pubsub subscriptions, and peer discovery. +""" + +import base58 +import logging +import time +import trio +from dataclasses import dataclass +from typing import Set, Optional, AsyncIterator + +from libp2p.host.basic_host import BasicHost +from libp2p.pubsub.pb.rpc_pb2 import Message +from libp2p.pubsub.pubsub import Pubsub + +logger = logging.getLogger("chatroom") + +# Create a separate logger for system messages +system_logger = logging.getLogger("system_messages") +system_handler = logging.FileHandler("system_messages.txt", mode='a') +system_handler.setFormatter(logging.Formatter("[%(asctime)s] %(message)s", datefmt="%H:%M:%S")) +system_logger.addHandler(system_handler) +system_logger.setLevel(logging.INFO) +system_logger.propagate = False # Don't send to parent loggers + +# Chat room buffer size for incoming messages +CHAT_ROOM_BUF_SIZE = 128 + +# Topics used in the chat system +PUBSUB_DISCOVERY_TOPIC = "universal-connectivity-browser-peer-discovery" +CHAT_TOPIC = "universal-connectivity" + + +@dataclass +class ChatMessage: + """Represents a chat message.""" + message: str + sender_id: str + sender_nick: str + topic: str = None # Topic the message was received on + timestamp: Optional[float] = None + + def __post_init__(self): + if self.timestamp is None: + self.timestamp = time.time() + + +class ChatRoom: + """ + Represents a subscription to PubSub topics for chat functionality. + Messages can be published to topics and received messages are handled + through callback functions. + """ + + def __init__(self, host: BasicHost, pubsub: Pubsub, nickname: str, multiaddr: str = None, headless_service=None, topic: str = None): + self.host = host + self.pubsub = pubsub + self.nickname = nickname + self.peer_id = str(host.get_id()) + self.multiaddr = multiaddr or f"unknown/{self.peer_id}" + self.headless_service = headless_service # Reference for identify protocol + + # Use custom topic if provided, otherwise use default + self.chat_topic = topic if topic else CHAT_TOPIC + + # Subscriptions - now a dictionary to track all subscriptions + self.subscriptions = {} # topic_name -> subscription object + self.chat_subscription = None + self.discovery_subscription = None + + # Message handlers + self.message_handlers = [] + self.system_message_handlers = [] + + # Topic handlers - stores (topic_name, subscription) for dynamic topics + self.topic_handlers = [] + self.active_topic_handlers = set() # Track which topics already have handlers running + + # Running state + self.running = False + self.nursery = None # Store nursery reference for spawning new handlers + + logger.info(f"ChatRoom initialized for peer {self.peer_id[:8]}... with nickname '{nickname}'") + logger.info(f"Chat topic: {self.chat_topic}") + self._log_system_message("Universal Connectivity Chat Started") + self._log_system_message(f"Nickname: {nickname}") + self._log_system_message(f"Topic: {self.chat_topic}") + self._log_system_message(f"Multiaddr: {self.multiaddr}") + self._log_system_message("Commands: /quit, /peers, /status, /multiaddr") + + def _log_system_message(self, message: str): + """Log system message to file.""" + system_logger.info(message) + + @classmethod + async def join_chat_room(cls, host: BasicHost, pubsub: Pubsub, nickname: str, multiaddr: str = None, headless_service=None, topic: str = None) -> "ChatRoom": + """Create and join a chat room.""" + chat_room = cls(host, pubsub, nickname, multiaddr, headless_service, topic) + await chat_room._subscribe_to_topics() + chat_room._log_system_message(f"Joined chat room as '{nickname}'") + return chat_room + + async def _subscribe_to_topics(self): + """Subscribe to all necessary topics.""" + try: + # Subscribe to chat topic (either custom or default) + self.chat_subscription = await self.pubsub.subscribe(self.chat_topic) + self.subscriptions[self.chat_topic] = self.chat_subscription + logger.info(f"Subscribed to chat topic: {self.chat_topic}") + + # Add chat topic to handlers list + self.topic_handlers.append((self.chat_topic, self.chat_subscription)) + + # Subscribe to discovery topic + self.discovery_subscription = await self.pubsub.subscribe(PUBSUB_DISCOVERY_TOPIC) + self.subscriptions[PUBSUB_DISCOVERY_TOPIC] = self.discovery_subscription + logger.info(f"Subscribed to discovery topic: {PUBSUB_DISCOVERY_TOPIC}") + + # Add discovery topic to handlers list + self.topic_handlers.append((PUBSUB_DISCOVERY_TOPIC, self.discovery_subscription)) + + except Exception as e: + logger.error(f"Failed to subscribe to topics: {e}") + self._log_system_message(f"ERROR: Failed to subscribe to topics: {e}") + raise + + async def publish_message(self, message: str): + """Publish a chat message in plain text format (Go-compatible).""" + try: + # Check if we have any peers connected + peer_count = len(self.pubsub.peers) + logger.info(f"📤 Publishing message to {peer_count} peers: {message}") + logger.info(f"Total pubsub peers: {list(self.pubsub.peers.keys())}") + + # Send plain text message (Go-compatible format) to the custom topic + print(f"Sending message {message}") + await self.pubsub.publish(self.chat_topic, message.encode()) + logger.info(f"✅ Message published successfully to topic '{self.chat_topic}'") + + if peer_count == 0: + print(f"⚠️ No peers connected - message sent to topic but no one will receive it") + else: + print(f"✓ Message sent to {peer_count} peer(s)") + + except Exception as e: + logger.error(f"❌ Failed to publish message: {e}") + print(f"❌ Error sending message: {e}") + self._log_system_message(f"ERROR: Failed to publish message: {e}") + + async def publish_to_topic(self, topic: str, message: str): + """Publish a message to a specific topic.""" + try: + # Check if we're subscribed to this topic + if topic not in self.subscriptions: + logger.warning(f"Not subscribed to topic: {topic}") + return False + + peer_count = len(self.pubsub.peers) + logger.info(f"📤 Publishing message to topic '{topic}' with {peer_count} peers: {message}") + + # Send plain text message + await self.pubsub.publish(topic, message.encode()) + logger.info(f"✅ Message published successfully to topic '{topic}'") + + return True + + except Exception as e: + logger.error(f"❌ Failed to publish message to topic '{topic}': {e}") + self._log_system_message(f"ERROR: Failed to publish message to topic '{topic}': {e}") + return False + + async def _validate_message_with_identify(self, message, sender_id): + """Validate message using identify protocol to get sender's public key. + + This should only be called for messages from OTHER peers that don't include + a public key in the message data. + """ + # Safety check: never try to identify ourselves + if sender_id == self.peer_id: + logger.debug(f"⏭️ Skipping identify for own peer ID {sender_id}") + return True + + if not self.headless_service: + logger.warning("No headless service available for identify protocol") + return True # Default to accepting message if no identify available + + try: + # Get peer info via identify protocol (this will cache it) + peer_info = await self.headless_service.get_cached_peer_info(sender_id) + + if peer_info and peer_info.get('public_key'): + logger.info(f"✅ Retrieved public key for {sender_id} via identify protocol") + # Here you could add actual message signature validation + # For now, we just log that we got the public key + return True + else: + logger.warning(f"⚠️ Could not get public key for {sender_id} via identify protocol") + return True # Still accept message but log the issue + + except Exception as e: + logger.error(f"❌ Error validating message with identify: {e}") + return True # Default to accepting message on error + + async def _handle_topic_messages(self, topic_name: str, subscription): + """Handle incoming messages for any subscribed topic (including chat and discovery).""" + logger.debug(f"📨 Starting message handler for topic: {topic_name}") + + try: + async for message in self._message_stream(subscription): + try: + # Handle messages in the same way as chat messages + raw_data = message.data.decode() + sender_id = base58.b58encode(message.from_id).decode() if message.from_id else "unknown" + + # Check if this is our own message + is_own_message = sender_id == self.peer_id + + # Only validate messages from other peers + if not is_own_message: + if not message.key: + logger.debug(f"🔍 Message from {sender_id} has no public key, using identify protocol") + is_valid = await self._validate_message_with_identify(message, sender_id) + if not is_valid: + logger.warning(f"⚠️ Message validation failed for {sender_id}, skipping") + continue + else: + logger.debug(f"✅ Message from {sender_id} includes public key") + else: + logger.debug(f"📝 Processing own message from {sender_id} (no validation needed)") + + # Format sender nickname + if is_own_message: + sender_nick = f"{self.nickname}" + else: + sender_nick = sender_id[-8:] if len(sender_id) > 8 else sender_id + + actual_message = raw_data + + logger.info(f"📨 Received message on topic '{topic_name}' from {sender_id} ({sender_nick}): {actual_message}") + + # Create ChatMessage object for handlers + chat_msg = ChatMessage( + message=actual_message, + sender_id=sender_id, + sender_nick=sender_nick, + topic=topic_name + ) + + # Call message handlers + for handler in self.message_handlers: + try: + await handler(chat_msg) + except Exception as e: + logger.error(f"❌ Error in message handler: {e}") + + # Default console output if no handlers + if not self.message_handlers: + print(f"[{topic_name}][{chat_msg.sender_nick}]: {chat_msg.message}") + + except Exception as e: + logger.error(f"❌ Error processing message on topic '{topic_name}': {e}") + + except Exception as e: + logger.error(f"❌ Error in message handler for topic '{topic_name}': {e}") + + async def _message_stream(self, subscription) -> AsyncIterator[Message]: + """Create an async iterator for subscription messages.""" + while self.running: + try: + message = await subscription.get() + yield message + except Exception as e: + logger.error(f"Error getting message from subscription: {e}") + await trio.sleep(1) # Avoid tight loop on error + + async def start_message_handlers(self): + """Start all message handler tasks.""" + self.running = True + + async with trio.open_nursery() as nursery: + # Store nursery reference for dynamic task spawning + self.nursery = nursery + + # Start background task to monitor for new topic subscriptions + nursery.start_soon(self._monitor_new_topics) + + async def _monitor_new_topics(self): + """Monitor for new topic subscriptions and start handlers for them.""" + while self.running: + try: + # Check if there are any new topics that need handlers + for topic_name, subscription in self.topic_handlers: + if topic_name not in self.active_topic_handlers: + logger.info(f"Starting message handler for topic: {topic_name}") + + # Use generic handler for all topics (including chat and discovery) + self.nursery.start_soon(self._handle_topic_messages, topic_name, subscription) + self.active_topic_handlers.add(topic_name) + + # Check periodically (every 0.5 seconds) + await trio.sleep(0.5) + + except Exception as e: + logger.error(f"Error in topic monitor: {e}") + await trio.sleep(1) + + def add_message_handler(self, handler): + """Add a custom message handler.""" + self.message_handlers.append(handler) + + def add_system_message_handler(self, handler): + """Add a custom system message handler.""" + self.system_message_handlers.append(handler) + + async def run_interactive(self): + """Run interactive chat mode.""" + print(f"\n=== Universal Connectivity Chat ===") + print(f"Nickname: {self.nickname}") + print(f"Peer ID: {self.peer_id}") + print(f"Type messages and press Enter to send. Type 'quit' to exit.") + print(f"Commands: /peers, /status, /multiaddr") + print() + + async with trio.open_nursery() as nursery: + # Start message handlers + nursery.start_soon(self.start_message_handlers) + + # Start input handler + nursery.start_soon(self._input_handler) + + async def _input_handler(self): + """Handle user input in interactive mode.""" + try: + while self.running: + try: + # Use trio's to_thread to avoid blocking the event loop + message = await trio.to_thread.run_sync(input) + + if message.lower() in ["quit", "exit", "q"]: + print("Goodbye!") + self.running = False + break + + # Handle special commands + elif message.strip() == "/peers": + peers = self.get_connected_peers() + if peers: + print(f"📡 Connected peers ({len(peers)}):") + for peer in peers: + print(f" - {peer[:8]}...") + else: + print("📡 No peers connected") + continue + + elif message.strip() == "/multiaddr": + print(f"\n📋 Copy this multiaddress:") + print(f"{self.multiaddr}") + print() + continue + + elif message.strip() == "/status": + peer_count = self.get_peer_count() + subscribed_topics = ", ".join(sorted(self.get_subscribed_topics())) + print(f"📊 Status:") + print(f" - Multiaddr: {self.multiaddr}") + print(f" - Nickname: {self.nickname}") + print(f" - Connected peers: {peer_count}") + print(f" - Chat topic: {self.chat_topic}") + print(f" - Subscribed topics: {subscribed_topics}") + continue + + if message.strip(): + await self.publish_message(message) + + except EOFError: + print("\nGoodbye!") + self.running = False + break + except Exception as e: + logger.error(f"Error in input handler: {e}") + await trio.sleep(0.1) + + except Exception as e: + logger.error(f"Fatal error in input handler: {e}") + self.running = False + + async def stop(self): + """Stop the chat room.""" + self.running = False + logger.info("ChatRoom stopped") + + def get_connected_peers(self) -> Set[str]: + """Get list of connected peer IDs.""" + return set(str(peer_id) for peer_id in self.pubsub.peers.keys()) + + def get_peer_count(self) -> int: + """Get number of connected peers.""" + return len(self.pubsub.peers) + + def get_subscribed_topics(self) -> Set[str]: + """Get list of all subscribed topics.""" + return set(self.subscriptions.keys()) + + async def subscribe_to_topic(self, topic_name: str) -> bool: + """ + Subscribe to a new topic dynamically. + + Args: + topic_name: The name of the topic to subscribe to + + Returns: + True if subscription was successful, False otherwise + """ + try: + if topic_name in self.subscriptions: + logger.warning(f"Already subscribed to topic: {topic_name}") + return False + + logger.info(f"Subscribing to new topic: {topic_name}") + subscription = await self.pubsub.subscribe(topic_name) + self.subscriptions[topic_name] = subscription + logger.info(f"Successfully subscribed to topic: {topic_name}") + self._log_system_message(f"Subscribed to topic: {topic_name}") + + # Add to topic_handlers list - will be started in start_message_handlers + self.topic_handlers.append((topic_name, subscription)) + logger.info(f"Added handler for topic: {topic_name}") + + return True + + except Exception as e: + logger.error(f"Failed to subscribe to topic {topic_name}: {e}") + self._log_system_message(f"ERROR: Failed to subscribe to topic {topic_name}: {e}") + return False diff --git a/py-peer/headless.py b/py-peer/headless.py new file mode 100644 index 00000000..bffb17c7 --- /dev/null +++ b/py-peer/headless.py @@ -0,0 +1,1167 @@ +""" +Headless Service for Universal Connectivity Python Peer + +This module provides a headless service that manages libp2p host, pubsub, and chat functionality +without any UI. It communicates with the UI through queues and events. +""" + +import json +import logging +import os +import socket +import time +import traceback +import multiaddr +import janus +import trio +import trio_asyncio +import hashlib +from queue import Empty +from typing import List, Dict, Any, Set +from libp2p.discovery.bootstrap import BootstrapDiscovery +from libp2p.kad_dht.kad_dht import ( + DHTMode, + KadDHT, +) +from libp2p import new_host +from libp2p.crypto.ed25519 import create_new_key_pair +from libp2p.pubsub.gossipsub import GossipSub +from libp2p.pubsub.pubsub import Pubsub +from libp2p.tools.anyio_service import background_trio_service +from libp2p.peer.peerinfo import info_from_p2p_addr +from libp2p.identity.identify.identify import identify_handler_for, parse_identify_response, ID as IDENTIFY_PROTOCOL_ID +from libp2p.utils.varint import read_length_prefixed_protobuf +from libp2p.peer.id import ID +from libp2p.custom_types import TProtocol +from libp2p.pubsub.gossipsub import PROTOCOL_ID, PROTOCOL_ID_V11, PROTOCOL_ID_V12 +from libp2p.protocol_muxer.exceptions import ( + MultiselectClientError, +) +from libp2p.host.exceptions import ( + StreamFailure, +) +from chatroom import ChatRoom, ChatMessage +from libp2p.bitswap import BitswapClient, MemoryBlockStore +from libp2p.bitswap.dag import MerkleDag +from libp2p.network.config import ConnectionConfig + +# File message prefix for pubsub +FILE_MESSAGE_PREFIX = "[FILE]" +# Default download directory +DEFAULT_DOWNLOAD_DIR = os.path.expanduser("~/Downloads") + +DEFAULT_SEED = "py-peer" # Default seed for deterministic peer ID generation + +logger = logging.getLogger("headless") + +# Constants +DISCOVERY_SERVICE_TAG = "universal-connectivity" +PROTOCOL_ID_LIST = [PROTOCOL_ID, PROTOCOL_ID_V11, PROTOCOL_ID_V12] +DEFAULT_PORT = 9095 + +# Bootstrap nodes for peer discovery +BOOTSTRAP_PEERS = [ + "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa", + "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zp7ykQCj2gRNdrFeqQ1vG13rMb4sPS", + "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt", + "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", + "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb" +] + +def filter_compatible_peer_info(peer_info) -> bool: + """Filter peer info to check if it has compatible addresses (TCP + IPv4).""" + if not hasattr(peer_info, "addrs") or not peer_info.addrs: + return False + + for addr in peer_info.addrs: + addr_str = str(addr) + if "/tcp/" in addr_str and "/ip4/" in addr_str and "/quic" not in addr_str: + return True + return False + + +class HeadlessService: + """ + Headless service that manages libp2p components and provides data to UI through queues. + """ + + def __init__(self, nickname: str, port: int = 0, connect_addrs: List[str] = None, ui_mode: bool = False, strict_signing: bool = True, seed: str = None, topic: str = None): + self.nickname = nickname + self.port = port if port != 0 else 4001 + self.connect_addrs = connect_addrs or [] + self.ui_mode = ui_mode # Flag to control logging behavior + self.strict_signing = strict_signing # Flag to control message signing + self.seed = seed if seed else DEFAULT_SEED # Seed string for deterministic peer ID (default: 'py-peer') + self.topic = topic # Custom topic to use instead of default + + # libp2p components + self.host = None + self.pubsub = None + self.gossipsub = None + self.dht = None + self.chat_room = None + + # Bitswap components for file sharing + self.bitswap_client = None + self.merkle_dag = None + self.block_store = MemoryBlockStore() + + # Service state + self.running = False + self.ready = False + self.full_multiaddr = None + + # Communication with UI + self.message_queue = None # UI receives messages from headless + self.system_queue = None # UI receives system messages from headless + self.outgoing_queue = None # UI sends messages to headless + self.topic_subscription_queue = None # UI sends topic subscription requests + self.peer_connection_queue = None # UI sends peer connection requests + self.file_share_queue = None # UI sends file sharing requests + self.file_download_queue = None # UI sends file download requests + + # Per-topic message storage + self.topic_messages = {} # {topic: [{'message': msg, 'timestamp': ts, 'read': bool}]} + self.topic_unread_counts = {} # {topic: int} + + # File sharing state + self.shared_files = {} # {cid_hex: {'filename': str, 'filesize': int, 'filepath': str}} + self.download_dir = DEFAULT_DOWNLOAD_DIR + os.makedirs(self.download_dir, exist_ok=True) + + # Peer information storage for identify protocol + self.peer_info_cache = {} # Store peer info retrieved through identify + + # Events for synchronization + self.ready_event = trio.Event() + self.stop_event = trio.Event() + + if not ui_mode: # Only log initialization if not in UI mode + logger.info(f"HeadlessService initialized - nickname: {nickname}, port: {self.port}, strict_signing: {strict_signing}, seed: {self.seed}") + + async def monitor_peers(self): + while True: + logger.info(f"Connected peers are: {len(self.host.get_connected_peers())}") + logger.info(f"Peers in peer store are: {len(self.host.get_peerstore().peers_with_addrs())}") + logger.info(f"Peers in routing table are: {len(self.dht.routing_table.get_peer_ids())}") + logger.info(f"Peers in pubsub are: {len(self.pubsub.peers.keys())}") + await trio.sleep(5) + + async def start(self): + """Start the headless service.""" + logger.info("Starting headless service...") + + try: + # Create queues for communication with UI + logger.debug("Creating message queues...") + self.message_queue = janus.Queue() # Messages from headless to UI + self.system_queue = janus.Queue() # System messages from headless to UI + self.outgoing_queue = janus.Queue() # Messages from UI to headless + self.topic_subscription_queue = janus.Queue() # Topic subscription requests from UI + self.peer_connection_queue = janus.Queue() # Peer connection requests from UI + self.file_share_queue = janus.Queue() # File sharing requests from UI + self.file_download_queue = janus.Queue() # File download requests from UI + logger.debug("Message queues created successfully") + + # Enable trio-asyncio mode + async with trio_asyncio.open_loop(): + # Send initial system message to test queue inside trio context + await self._send_system_message("Headless service starting...") + await self._run_service() + + except Exception as e: + logger.error(f"Failed to start headless service: {e}") + logger.error(f"Traceback:\n{traceback.format_exc()}") + raise + + async def _run_service(self): + """Run the main service loop.""" + secret = hashlib.sha256(self.seed.encode()).digest() + logger.info(f"Using deterministic Ed25519 key derived from seed='{self.seed}'") + key_pair = create_new_key_pair(seed=secret) + + # Create listen address + listen_addr = multiaddr.Multiaddr(f"/ip4/0.0.0.0/tcp/{self.port}") + + config = ConnectionConfig( + min_connections=10, + low_watermark=12, + high_watermark=40, + max_connections=50, + auto_connect_interval=5.0, # Check every 5 seconds + ) + + # Create libp2p host WITHOUT bootstrap nodes initially + # We'll connect to bootstrap nodes after pubsub is running + self.host = new_host( + key_pair=key_pair, + connection_config=config, + # bootstrap = BOOTSTRAP_PEERS + ) + + # Register identify protocol handler + logger.info("📋 Registering identify protocol handler (raw protobuf format for go-libp2p compatibility)") + identify_handler = identify_handler_for(self.host, use_varint_format=True) + self.host.set_stream_handler(IDENTIFY_PROTOCOL_ID, identify_handler) + logger.info(f"✅ Identify protocol handler registered for {IDENTIFY_PROTOCOL_ID} (raw format)") + + # Create DHT with random walk enabled + self.dht = KadDHT(self.host, DHTMode.SERVER, enable_random_walk=True) + logger.info("✅ DHT created with random walk enabled") + + self.full_multiaddr = f"{listen_addr}/p2p/{self.host.get_id()}" + logger.info(f"Host created with PeerID: {self.host.get_id()}") + logger.info(f"Listening on: {listen_addr}") + logger.info(f"Full multiaddr: {self.full_multiaddr}") + + # Log GossipSub protocol configuration + logger.info(f"📋 Configuring GossipSub with protocols: {PROTOCOL_ID_LIST}") + logger.info(f" Protocol 1: {PROTOCOL_ID}") + logger.info(f" Protocol 2: {PROTOCOL_ID_V11}") + + # Create GossipSub with optimized parameters (matching working pubsub.py) + self.gossipsub = GossipSub( + protocols=PROTOCOL_ID_LIST, + degree=3, + degree_low=2, + degree_high=4, + gossip_window=2, # Smaller window for faster gossip + gossip_history=5, # Keep more history + heartbeat_initial_delay=2.0, # Start heartbeats sooner + heartbeat_interval=5, # More frequent heartbeats for testing + ) + logger.info("✅ GossipSub router created successfully") + + # Create PubSub + logger.info(f"🔐 Creating PubSub with strict_signing={self.strict_signing}") + self.pubsub = Pubsub(self.host, self.gossipsub, strict_signing=self.strict_signing) + logger.info("✅ PubSub service created successfully") + + # Create Bitswap client for file sharing + self.bitswap_client = BitswapClient(self.host, self.block_store) + self.merkle_dag = MerkleDag(self.bitswap_client) + logger.info("✅ Bitswap client and MerkleDag created for file sharing") + + # Start host and pubsub services + async with self.host.run(listen_addrs=[listen_addr]): + logger.info("📡 Initializing PubSub, GossipSub, and DHT services...") + try: + async with background_trio_service(self.pubsub): + async with background_trio_service(self.gossipsub): + async with background_trio_service(self.dht): + logger.info("✅ Pubsub, GossipSub, and DHT services started.") + await self.pubsub.wait_until_ready() + logger.info("✅ Pubsub ready and operational.") + logger.info("✅ DHT service started with random walk enabled.") + bootstrap = None + if BOOTSTRAP_PEERS: + bootstrap = BootstrapDiscovery(self.host.get_network(), BOOTSTRAP_PEERS) + await bootstrap.start() + # Setup chat room BEFORE connections so topics are subscribed + # This ensures GossipSub protocol negotiation succeeds when connecting + await self._setup_chat_room() + + # Start Bitswap client + await self.bitswap_client.start() + logger.info("✅ Bitswap client started for file sharing") + + # Now setup connections after we're subscribed to topics + await self._setup_connections() + + # Setup connection event handlers for DHT + + # Mark service as ready + self.ready = True + self.ready_event.set() + logger.info("✅ Headless service is ready") + + # Start message processing and wait for stop + async with trio.open_nursery() as nursery: + # Set nursery for bitswap client + self.bitswap_client.set_nursery(nursery) + + nursery.start_soon(self._process_messages) + nursery.start_soon(self._process_outgoing_messages) + nursery.start_soon(self._process_topic_subscriptions) + nursery.start_soon(self._process_peer_connections) + nursery.start_soon(self._process_file_shares) + nursery.start_soon(self._process_file_downloads) + nursery.start_soon(self._wait_for_stop) + nursery.start_soon(self.monitor_peers) + # nursery.start_soon(maintain_connections, self.host) + + except (MultiselectClientError, StreamFailure) as e: + logger.error(f"The protocol negotiation failed: {e}") + pass + + async def _setup_connections(self): + """Setup connections to specified peers with detailed protocol logging.""" + if not self.connect_addrs: + return + + for addr_str in self.connect_addrs: + try: + logger.info(f"🔗 Attempting to connect to: {addr_str}") + maddr = multiaddr.Multiaddr(addr_str) + info = info_from_p2p_addr(maddr) + logger.info(f"🔗 Parsed peer info - ID: {info.peer_id}, Addrs: {info.addrs}") + + # Check if already connected + existing_conns = self.host.get_network().connections.get(info.peer_id) + if existing_conns: + logger.info(f"✅ Already connected to peer: {info.peer_id}, skipping connection attempt") + continue + + # Log connection attempt + logger.info(f"🔗 Initiating connection to peer: {info.peer_id}") + await self.host.connect(info) + logger.info(f"✅ TCP connection established to peer: {info.peer_id}") + + # Wait longer for protocol negotiation + await trio.sleep(3) + + # Detailed protocol inspection + logger.info(f"🔍 Starting protocol inspection for peer: {info.peer_id}") + await self._inspect_peer_protocols(info.peer_id) + + # Check connection status + try: + # In py-libp2p, we can check if peer is connected via the swarm + swarm = self.host.get_network() + if hasattr(swarm, 'connections') and info.peer_id in swarm.connections: + connections = [swarm.connections[info.peer_id]] + logger.info(f"📊 Active connections to peer {info.peer_id}: {len(connections)}") + else: + logger.info(f"📊 No direct connection info available for peer {info.peer_id}") + except Exception as conn_err: + logger.warning(f"⚠️ Could not check connection status: {conn_err}") + + # Wait for PubSub protocol negotiation + logger.info(f"⏳ Waiting for PubSub protocol negotiation...") + await trio.sleep(3) + + # Check final PubSub status + await self._check_pubsub_status(info.peer_id) + + await self._send_system_message(f"Connected to peer: {str(info.peer_id)[:8]}") + + except Exception as e: + logger.error(f"❌ Failed to connect to {addr_str}: {e}") + await self._send_system_message(f"Failed to connect to {addr_str}: {e}") + + async def _inspect_peer_protocols(self, peer_id): + """Inspect and log all protocols supported by a peer.""" + try: + logger.info(f"🔍 Checking peerstore for peer: {peer_id}") + + # Get peer's protocols from peerstore (simplified approach) + peerstore = self.host.get_peerstore() + + # Check if we can access protocols - different py-libp2p versions have different APIs + try: + if hasattr(peerstore, 'get_protocols'): + protocols = peerstore.get_protocols(peer_id) + elif hasattr(peerstore, 'protocols'): + protocols = peerstore.protocols(peer_id) + else: + # Fallback - just log that we connected successfully + logger.info(f"✅ Successfully connected to peer {peer_id}") + logger.info(f"🔍 Protocol inspection not available in this py-libp2p version") + return + + if protocols: + logger.info(f"📋 Peer {peer_id} supports {len(protocols)} protocols:") + for i, protocol in enumerate(protocols, 1): + logger.info(f" {i}: {protocol}") + if "meshsub" in str(protocol) or "gossipsub" in str(protocol): + logger.info(f" 🎯 Found PubSub protocol: {protocol}") + else: + logger.info(f"📋 No protocols found for peer {peer_id} yet (may still be negotiating)") + + except Exception as proto_err: + logger.info(f"🔍 Protocol details not accessible: {proto_err}") + logger.info(f"✅ Peer {peer_id} connected successfully") + + except Exception as e: + logger.warning(f"⚠️ Error inspecting peer protocols: {e}") + logger.info(f"✅ Peer {peer_id} connected successfully") + + async def _check_pubsub_status(self, peer_id): + """Check the PubSub connection status with a specific peer.""" + try: + logger.info(f"🔍 Checking PubSub status for peer: {peer_id}") + + # Check if peer is in pubsub.peers + pubsub_peers = list(self.pubsub.peers.keys()) + logger.info(f"📡 Total PubSub peers: {len(pubsub_peers)}") + for i, p in enumerate(pubsub_peers, 1): + logger.info(f" PubSub peer {i}: {p}") + + if peer_id in self.pubsub.peers: + logger.info(f"✅ Peer {peer_id} is in PubSub mesh") + + # Check GossipSub specific status + if hasattr(self.pubsub, 'router') and hasattr(self.pubsub.router, 'mesh'): + mesh = self.pubsub.router.mesh + logger.info(f"🕸️ GossipSub mesh status:") + logger.info(f" Mesh topics: {list(mesh.keys())}") + for topic, topic_peers in mesh.items(): + logger.info(f" Topic '{topic}': {len(topic_peers)} peers") + if peer_id in topic_peers: + logger.info(f" ✅ Peer {peer_id} is in mesh for topic '{topic}'") + else: + logger.warning(f" ❌ Peer {peer_id} is NOT in mesh for topic '{topic}'") + else: + logger.warning(f"❌ Peer {peer_id} is NOT in PubSub mesh") + logger.info("🔧 Possible reasons:") + logger.info(" 1. PubSub protocol negotiation failed") + logger.info(" 2. Peer doesn't support compatible GossipSub version") + logger.info(" 3. Network issues preventing PubSub handshake") + + except Exception as e: + logger.error(f"❌ Error checking PubSub status: {e}") + + async def _setup_chat_room(self): + """Setup the chat room.""" + logger.info("Setting up chat room...") + + self.chat_room = await ChatRoom.join_chat_room( + host=self.host, + pubsub=self.pubsub, + nickname=self.nickname, + multiaddr=self.full_multiaddr, + headless_service=self, + topic=self.topic + ) + + # Add custom message handler to forward messages to UI + self.chat_room.add_message_handler(self._handle_chat_message) + + # Start message handlers + self.running = True + + logger.info(f"Chat room setup complete for '{self.nickname}'") + await self._send_system_message(f"Joined chat room as '{self.nickname}'") + + async def _handle_chat_message(self, message: ChatMessage): + """Handle incoming chat messages and store them per-topic.""" + try: + topic = message.topic or "default" + + # Initialize topic storage if needed + if topic not in self.topic_messages: + self.topic_messages[topic] = [] + self.topic_unread_counts[topic] = 0 + + # Check if this is a file sharing message + is_file_message = message.message.startswith(FILE_MESSAGE_PREFIX) + + if is_file_message: + try: + file_meta_json = message.message[len(FILE_MESSAGE_PREFIX):] + file_meta = json.loads(file_meta_json) + + message_data = { + 'type': 'file_message', + 'message': message.message, + 'sender_nick': message.sender_nick, + 'sender_id': message.sender_id, + 'timestamp': message.timestamp, + 'topic': topic, + 'read': False, + 'file_cid': file_meta.get('cid', ''), + 'file_name': file_meta.get('filename', 'unknown'), + 'file_size': file_meta.get('filesize', 0), + } + except (json.JSONDecodeError, KeyError) as e: + logger.error(f"Failed to parse file message: {e}") + # Fall back to regular message + message_data = { + 'type': 'chat_message', + 'message': message.message, + 'sender_nick': message.sender_nick, + 'sender_id': message.sender_id, + 'timestamp': message.timestamp, + 'topic': topic, + 'read': False + } + else: + # Store message with unread flag + message_data = { + 'type': 'chat_message', + 'message': message.message, + 'sender_nick': message.sender_nick, + 'sender_id': message.sender_id, + 'timestamp': message.timestamp, + 'topic': topic, + 'read': False + } + + self.topic_messages[topic].append(message_data) + self.topic_unread_counts[topic] += 1 + + # Log in simplified format only if not in UI mode + if not self.ui_mode: + if is_file_message: + logger.info(f"[{topic}] {message.sender_nick} shared a file: {message_data.get('file_name', 'unknown')}") + else: + logger.info(f"[{topic}] {message.sender_nick}: {message.message}") + + # Still put message in queue for UI updates + await self.message_queue.async_q.put(message_data) + + except Exception as e: + logger.error(f"Error handling chat message: {e}") + logger.exception("Full traceback:") + + async def _send_system_message(self, message: str): + """Send system message to UI queue.""" + logger.debug(f"_send_system_message called with: {message}") + try: + if self.system_queue: + logger.debug(f"System queue available, sending message: {message}") + await self.system_queue.async_q.put({ + 'type': 'system_message', + 'message': message, + 'timestamp': trio.current_time() + }) + logger.debug(f"System message sent successfully: {message}") + else: + logger.warning(f"System queue not available, cannot send message: {message}") + except Exception as e: + logger.error(f"Error sending system message: {e}") + logger.exception("Full traceback:") + + async def _process_messages(self): + """Process messages from chat room.""" + try: + # Start chat room message handlers + await self.chat_room.start_message_handlers() + except Exception as e: + logger.error(f"Error in message processing: {e}") + + async def _process_outgoing_messages(self): + """Process outgoing messages from UI to chat room.""" + + while self.running: + try: + # Check for messages from UI (non-blocking) + try: + outgoing_data = self.outgoing_queue.sync_q.get_nowait() + if outgoing_data and 'message' in outgoing_data: + message = outgoing_data['message'] + topic = outgoing_data.get('topic') # Optional topic parameter + + # Send message through chat room + if self.chat_room and self.running: + if topic: + # Send to specific topic + success = await self.chat_room.publish_to_topic(topic, message) + if not self.ui_mode: + logger.info(f"{self.nickname} (you) to {topic}: {message}") + else: + # Send to default chat topic + await self.chat_room.publish_message(message) + if not self.ui_mode: + logger.info(f"{self.nickname} (you): {message}") + else: + logger.warning("Cannot send message: chat room not ready") + await self._send_system_message("Cannot send message: chat room not ready") + + except Empty: + # No message available, that's fine + await trio.sleep(0.1) # Brief pause to avoid busy loop + except Exception as e: + logger.error(f"Error processing outgoing message: {e}") + await trio.sleep(0.1) + + except Exception as e: + logger.error(f"Error in outgoing message processing: {e}") + await trio.sleep(0.1) + + async def _process_topic_subscriptions(self): + """Process topic subscription requests from UI.""" + + while self.running: + try: + # Check for subscription requests from UI (non-blocking) + try: + subscription_data = self.topic_subscription_queue.sync_q.get_nowait() + if subscription_data and 'topic' in subscription_data: + topic_name = subscription_data['topic'] + + # Subscribe to the topic through chat room + if self.chat_room and self.running: + success = await self.chat_room.subscribe_to_topic(topic_name) + if success: + logger.info(f"Successfully subscribed to topic: {topic_name}") + await self._send_system_message(f"Subscribed to topic: {topic_name}") + else: + logger.warning(f"Failed to subscribe to topic: {topic_name}") + await self._send_system_message(f"Failed to subscribe to topic: {topic_name}") + else: + logger.warning("Cannot subscribe to topic: chat room not ready") + await self._send_system_message("Cannot subscribe to topic: chat room not ready") + + except Empty: + # No request available, that's fine + await trio.sleep(0.1) # Brief pause to avoid busy loop + except Exception as e: + logger.error(f"Error processing topic subscription: {e}") + await trio.sleep(0.1) + + except Exception as e: + logger.error(f"Error in topic subscription processing: {e}") + await trio.sleep(0.1) + + async def _process_peer_connections(self): + """Process peer connection requests from UI.""" + + while self.running: + try: + # Check for connection requests from UI (non-blocking) + try: + multiaddr_str = self.peer_connection_queue.sync_q.get_nowait() + if multiaddr_str: + logger.info(f"Processing peer connection request: {multiaddr_str}") + + # Parse and connect to the peer + try: + # Parse the multiaddress + maddr = multiaddr.Multiaddr(multiaddr_str) + + # Try to get peer info from the multiaddress + peer_info = info_from_p2p_addr(maddr) + + if peer_info: + # Connect to the peer + logger.info(f"Attempting to connect to peer: {peer_info.peer_id}") + await self.host.connect(peer_info) + logger.info(f"✅ Successfully connected to peer: {peer_info.peer_id}") + await self._send_system_message(f"Connected to peer: {peer_info.peer_id}") + else: + logger.error(f"Could not extract peer info from multiaddress: {multiaddr_str}") + await self._send_system_message(f"Invalid multiaddress format") + + except Exception as e: + logger.error(f"Failed to connect to peer {multiaddr_str}: {e}") + await self._send_system_message(f"Connection failed: {str(e)}") + + except Empty: + # No request available, that's fine + await trio.sleep(0.1) # Brief pause to avoid busy loop + except Exception as e: + logger.error(f"Error processing peer connection: {e}") + await trio.sleep(0.1) + + except Exception as e: + logger.error(f"Error in peer connection processing: {e}") + await trio.sleep(0.1) + + async def _process_file_shares(self): + """Process file sharing requests from UI.""" + while self.running: + try: + try: + share_data = self.file_share_queue.sync_q.get_nowait() + if share_data: + file_path = share_data.get('file_path') + topic = share_data.get('topic') + + if not file_path or not os.path.exists(file_path): + logger.error(f"File not found: {file_path}") + await self._send_system_message(f"File not found: {file_path}") + continue + + filename = os.path.basename(file_path) + filesize = os.path.getsize(file_path) + + logger.info(f"📁 Sharing file: {filename} ({filesize} bytes)") + await self._send_system_message(f"Preparing to share: {filename}...") + + try: + # Add file to Merkle DAG (chunks + stores in bitswap) + root_cid = await self.merkle_dag.add_file( + file_path, + wrap_with_directory=True + ) + + cid_hex = root_cid.hex() + + # Track shared file locally + self.shared_files[cid_hex] = { + 'filename': filename, + 'filesize': filesize, + 'filepath': file_path, + } + + logger.info(f"✅ File added to DAG. CID: {cid_hex}") + + # Create file sharing message with metadata + file_meta = { + 'cid': cid_hex, + 'filename': filename, + 'filesize': filesize, + } + file_message = f"{FILE_MESSAGE_PREFIX}{json.dumps(file_meta)}" + + # Publish file message via pubsub to the topic + if topic and self.chat_room: + success = await self.chat_room.publish_to_topic(topic, file_message) + if success: + logger.info(f"✅ File shared to topic '{topic}': {filename}") + + # Don't store in topic_messages here - the pubsub echo + # will come back through _handle_chat_message and store it. + # Only notify the UI immediately so it shows the bubble. + await self.message_queue.async_q.put({ + 'type': 'file_shared', + 'topic': topic, + 'file_cid': cid_hex, + 'file_name': filename, + 'file_size': filesize, + 'sender_nick': 'You', + 'sender_id': 'self', + 'timestamp': time.time(), + }) + else: + await self._send_system_message(f"Failed to share file to topic") + else: + await self._send_system_message(f"No topic specified for file sharing") + + except Exception as e: + logger.error(f"Failed to share file: {e}") + logger.exception("Full traceback:") + await self._send_system_message(f"Failed to share file: {str(e)}") + + except Empty: + await trio.sleep(0.1) + except Exception as e: + logger.error(f"Error processing file share: {e}") + await trio.sleep(0.1) + except Exception as e: + logger.error(f"Error in file share processing: {e}") + await trio.sleep(0.1) + + async def _process_file_downloads(self): + """Process file download requests from UI.""" + while self.running: + try: + try: + download_data = self.file_download_queue.sync_q.get_nowait() + if download_data: + cid_hex = download_data.get('file_cid') + filename = download_data.get('file_name', 'unknown') + + if not cid_hex: + logger.error("No CID provided for download") + continue + + logger.info(f"📥 Downloading file: {filename} (CID: {cid_hex})") + await self._send_system_message(f"Downloading: {filename}...") + + try: + root_cid = bytes.fromhex(cid_hex) + + # Fetch file via bitswap + file_data, extracted_filename = await self.merkle_dag.fetch_file( + root_cid, + timeout=60.0 + ) + + # Use extracted filename if available, fallback to provided name + save_filename = extracted_filename or filename + + # Ensure download directory exists + os.makedirs(self.download_dir, exist_ok=True) + + # Handle filename conflicts + save_path = os.path.join(self.download_dir, save_filename) + if os.path.exists(save_path): + name, ext = os.path.splitext(save_filename) + counter = 1 + while os.path.exists(save_path): + save_path = os.path.join(self.download_dir, f"{name}_{counter}{ext}") + counter += 1 + + # Write file + with open(save_path, 'wb') as f: + f.write(file_data) + + logger.info(f"✅ File downloaded: {save_path} ({len(file_data)} bytes)") + + # Notify UI + await self.message_queue.async_q.put({ + 'type': 'file_downloaded', + 'file_cid': cid_hex, + 'file_name': save_filename, + 'file_size': len(file_data), + 'save_path': save_path, + 'timestamp': time.time(), + }) + + except Exception as e: + logger.error(f"Failed to download file: {e}") + logger.exception("Full traceback:") + await self.message_queue.async_q.put({ + 'type': 'file_download_failed', + 'file_cid': cid_hex, + 'file_name': filename, + 'error': str(e), + 'timestamp': time.time(), + }) + + except Empty: + await trio.sleep(0.1) + except Exception as e: + logger.error(f"Error processing file download: {e}") + await trio.sleep(0.1) + except Exception as e: + logger.error(f"Error in file download processing: {e}") + await trio.sleep(0.1) + + async def _wait_for_stop(self): + """Wait for stop signal.""" + await self.stop_event.wait() + logger.info("Stop signal received, shutting down...") + self.running = False + + def send_message(self, message: str): + """Send a message through the chat room (thread-safe).""" + if self.outgoing_queue and self.running: + try: + # Put message in outgoing queue (sync call, safe from UI thread) + self.outgoing_queue.sync_q.put({ + 'message': message, + 'timestamp': time.time() + }) + except Exception as e: + logger.error(f"Failed to queue message: {e}") + else: + logger.warning("Cannot send message: outgoing queue not ready or service not running") + + def send_message_to_topic(self, topic: str, message: str): + """Send a message to a specific topic (thread-safe).""" + if self.outgoing_queue and self.running: + try: + # Put message with topic in outgoing queue + self.outgoing_queue.sync_q.put({ + 'message': message, + 'topic': topic, + 'timestamp': time.time() + }) + except Exception as e: + logger.error(f"Failed to queue message to topic {topic}: {e}") + else: + logger.warning("Cannot send message: outgoing queue not ready or service not running") + + def get_connection_info(self) -> Dict[str, Any]: + """Get connection information for UI.""" + if not self.ready: + return {} + + return { + 'peer_id': str(self.host.get_id()), + 'nickname': self.nickname, + 'multiaddr': self.full_multiaddr, + 'connected_peers': self.chat_room.get_connected_peers() if self.chat_room else set(), + 'peer_count': self.chat_room.get_peer_count() if self.chat_room else 0 + } + + def get_subscribed_topics(self) -> Set[str]: + """Get list of all subscribed topics.""" + if not self.chat_room: + return set() + return self.chat_room.get_subscribed_topics() + + def subscribe_to_topic(self, topic_name: str) -> bool: + """ + Subscribe to a new topic (thread-safe wrapper). + + Args: + topic_name: The name of the topic to subscribe to + + Returns: + True if subscription request was queued, False otherwise + """ + if not self.chat_room or not self.running: + logger.warning("Cannot subscribe to topic: chat room not ready or service not running") + return False + + try: + # Put subscription request in queue (sync call, safe from UI thread) + self.topic_subscription_queue.sync_q.put({ + 'topic': topic_name, + 'timestamp': time.time() + }) + logger.info(f"Queued subscription request for topic: {topic_name}") + return True + + except Exception as e: + logger.error(f"Failed to queue topic subscription: {e}") + return False + + def connect_to_peer(self, multiaddr: str) -> bool: + """ + Connect to a peer using multiaddress (thread-safe wrapper). + + Args: + multiaddr: The multiaddress of the peer to connect to + + Returns: + True if connection request was queued, False otherwise + """ + if not self.host or not self.running: + logger.warning("Cannot connect to peer: host not ready or service not running") + return False + + try: + # Put connection request in queue (sync call, safe from UI thread) + self.peer_connection_queue.sync_q.put(multiaddr) + logger.info(f"Queued peer connection request: {multiaddr}") + return True + + except Exception as e: + logger.error(f"Failed to queue peer connection: {e}") + return False + + def share_file(self, file_path: str, topic: str) -> bool: + """ + Share a file to a topic via bitswap (thread-safe wrapper). + + Args: + file_path: Path to the file to share + topic: The topic to share the file in + + Returns: + True if file share request was queued, False otherwise + """ + if not self.running or not self.file_share_queue: + logger.warning("Cannot share file: service not ready") + return False + + try: + self.file_share_queue.sync_q.put({ + 'file_path': file_path, + 'topic': topic, + 'timestamp': time.time(), + }) + logger.info(f"Queued file share request: {file_path} -> {topic}") + return True + except Exception as e: + logger.error(f"Failed to queue file share: {e}") + return False + + def download_file(self, file_cid: str, file_name: str = "unknown") -> bool: + """ + Download a file by CID via bitswap (thread-safe wrapper). + + Args: + file_cid: Hex CID of the file to download + file_name: Expected filename + + Returns: + True if download request was queued, False otherwise + """ + if not self.running or not self.file_download_queue: + logger.warning("Cannot download file: service not ready") + return False + + try: + self.file_download_queue.sync_q.put({ + 'file_cid': file_cid, + 'file_name': file_name, + 'timestamp': time.time(), + }) + logger.info(f"Queued file download request: {file_name} (CID: {file_cid[:16]}...)") + return True + except Exception as e: + logger.error(f"Failed to queue file download: {e}") + return False + + def get_message_queue(self): + """Get the message queue for UI.""" + return self.message_queue + + def get_system_queue(self): + """Get the system queue for UI.""" + return self.system_queue + + def get_topic_messages(self, topic: str) -> List[Dict[str, Any]]: + """ + Get all messages for a specific topic. + + Args: + topic: The topic name + + Returns: + List of message dictionaries + """ + return self.topic_messages.get(topic, []) + + def get_all_topics_with_info(self) -> Dict[str, Dict[str, Any]]: + """ + Get all subscribed topics with their message counts and unread status. + + Returns: + Dict mapping topic names to info dicts containing: + - unread_count: Number of unread messages + - total_count: Total number of messages + - last_message: Most recent message (if any) + """ + result = {} + subscribed_topics = self.get_subscribed_topics() + + for topic in subscribed_topics: + messages = self.topic_messages.get(topic, []) + unread_count = self.topic_unread_counts.get(topic, 0) + + info = { + 'unread_count': unread_count, + 'total_count': len(messages), + 'last_message': messages[-1] if messages else None + } + result[topic] = info + + return result + + def mark_topic_as_read(self, topic: str): + """ + Mark all messages in a topic as read. + + Args: + topic: The topic name + """ + if topic in self.topic_messages: + for message in self.topic_messages[topic]: + message['read'] = True + self.topic_unread_counts[topic] = 0 + logger.debug(f"Marked all messages in topic '{topic}' as read") + + def get_unread_count(self, topic: str) -> int: + """ + Get the count of unread messages for a topic. + + Args: + topic: The topic name + + Returns: + Number of unread messages + """ + return self.topic_unread_counts.get(topic, 0) + + def get_outgoing_queue(self): + """Get the outgoing queue for UI to send messages.""" + return self.outgoing_queue + + async def get_peer_info_via_identify(self, peer_id): + """Get peer information using official identify protocol implementation.""" + try: + logger.info(f"🔍 Requesting identify info from peer: {peer_id}") + logger.info(f"peers in peer store are: {self.host.get_peerstore().peers_with_addrs()}") + logger.info(f"address of peer {peer_id} is {self.host.get_peerstore().peer_info(peer_id).addrs} ") + + # Create a stream to the peer for identify protocol - use tuple format as in example + stream = await self.host.new_stream(peer_id, (IDENTIFY_PROTOCOL_ID,)) + + try: + # Use official py-libp2p utilities to read the response + # Use raw protobuf format (use_varint_format=False) for go-libp2p compatibility + # go-libp2p uses the old/raw format, not the newer varint length-prefixed format + response_bytes = await read_length_prefixed_protobuf(stream, use_varint_format=True) + + if not response_bytes: + logger.warning(f"Empty identify response from peer: {peer_id}") + return None + + # Parse the identify response using official parser + identify_info = parse_identify_response(response_bytes) + + logger.info(f"✅ Received identify info from {peer_id}") + logger.info(f" - Protocol Version: {identify_info.protocol_version}") + logger.info(f" - Agent Version: {identify_info.agent_version}") + logger.info(f" - Public Key: {len(identify_info.public_key)} bytes") + logger.info(f" - Listen Addresses: {len(identify_info.listen_addrs)} addresses") + logger.info(f" - Protocols: {len(identify_info.protocols)} protocols") + + # Store the peer info in our cache + self.peer_info_cache[str(peer_id)] = { + 'public_key': identify_info.public_key, + 'protocol_version': identify_info.protocol_version, + 'agent_version': identify_info.agent_version, + 'listen_addrs': identify_info.listen_addrs, + 'protocols': identify_info.protocols, + 'timestamp': time.time() + } + + return identify_info + + finally: + await stream.close() + + except Exception as e: + logger.error(f"❌ Failed to get identify info from peer {peer_id}: {e}") + return None + + async def get_cached_peer_info(self, peer_id: str): + """Get cached peer info, or fetch it if not available.""" + peer_id_str = str(peer_id) + + # Check if we have cached info + if peer_id_str in self.peer_info_cache: + cached_info = self.peer_info_cache[peer_id_str] + # Check if cache is not too old (5 minutes) + if time.time() - cached_info['timestamp'] < 300: + return cached_info + else: + logger.debug(f"Cached info for {peer_id_str} is stale, refreshing") + + # Fetch fresh info + try: + peer_id_obj = ID.from_base58(peer_id_str) if isinstance(peer_id, str) else peer_id + identify_info = await self.get_peer_info_via_identify(peer_id_obj) + + if identify_info: + return self.peer_info_cache[peer_id_str] + except Exception as e: + logger.error(f"❌ Failed to get peer info for {peer_id_str}: {e}") + + return None + + def get_public_key_for_peer(self, peer_id: str): + """Get public key for a peer (synchronous access to cache).""" + peer_id_str = str(peer_id) + if peer_id_str in self.peer_info_cache: + return self.peer_info_cache[peer_id_str]['public_key'] + return None + + async def stop(self): + """Stop the headless service.""" + logger.info("Stopping headless service...") + self.stop_event.set() + + if self.chat_room: + await self.chat_room.stop() + + # Stop bitswap client + if self.bitswap_client: + await self.bitswap_client.stop() + + # Close queues + if self.message_queue: + self.message_queue.close() + if self.system_queue: + self.system_queue.close() + if self.outgoing_queue: + self.outgoing_queue.close() + if self.topic_subscription_queue: + self.topic_subscription_queue.close() + if self.peer_connection_queue: + self.peer_connection_queue.close() + if self.file_share_queue: + self.file_share_queue.close() + if self.file_download_queue: + self.file_download_queue.close() + + logger.info("Headless service stopped") diff --git a/py-peer/hello.py b/py-peer/hello.py deleted file mode 100644 index 279c6898..00000000 --- a/py-peer/hello.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from py-peer!") - - -if __name__ == "__main__": - main() diff --git a/py-peer/kivy_ui.py b/py-peer/kivy_ui.py new file mode 100644 index 00000000..5bcca6a4 --- /dev/null +++ b/py-peer/kivy_ui.py @@ -0,0 +1,1374 @@ +""" +Kivy UI module for Universal Connectivity Python Peer + +This module provides a modern mobile-friendly UI using Kivy and KivyMD. +It works with the headless service and uses queues for communication. +Design inspired by WhatsApp/Telegram for a familiar chat experience. +""" + +import os +# Disable Kivy argument parsing to avoid conflicts with our app's arguments +os.environ['KIVY_NO_ARGS'] = '1' + +import json +import logging +import time +import threading +from queue import Empty +from typing import Optional + +from kivy.app import App +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.metrics import dp +from kivy.properties import StringProperty, NumericProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.screenmanager import ScreenManager, Screen + +from kivymd.app import MDApp +from kivymd.uix.list import OneLineAvatarIconListItem, IconLeftWidget, IconRightWidget +from kivymd.uix.label import MDLabel +from kivymd.uix.textfield import MDTextField +from kivymd.uix.button import MDIconButton, MDFlatButton, MDRaisedButton +from kivymd.uix.toolbar import MDTopAppBar +from kivymd.uix.scrollview import MDScrollView +from kivymd.uix.card import MDCard +from kivymd.uix.dialog import MDDialog +from kivymd.uix.navigationdrawer import MDNavigationDrawer, MDNavigationDrawerMenu +from kivymd.uix.snackbar import Snackbar +from kivymd.uix.filemanager import MDFileManager + +logger = logging.getLogger("kivy_ui") + +# File message prefix (must match headless.py) +FILE_MESSAGE_PREFIX = "[FILE]" + + +def format_file_size(size_bytes: int) -> str: + """Format file size in human-readable form.""" + size = float(size_bytes) + for unit in ["B", "KB", "MB", "GB"]: + if size < 1024: + return f"{size:.1f} {unit}" + size /= 1024 + return f"{size:.1f} TB" + + +class MessageBubble(MDCard): + """A message bubble similar to WhatsApp/Telegram.""" + + def __init__(self, message: str, sender: str, is_self: bool = False, timestamp: str = "", **kwargs): + super().__init__(**kwargs) + + # Set bubble properties + self.orientation = 'vertical' + self.size_hint_y = None + self.height = dp(80) + self.padding = dp(10) + self.spacing = dp(5) + + # Set different colors for sent/received messages + if is_self: + self.md_bg_color = (0.85, 0.95, 0.85, 1) # Light green for sent + self.pos_hint = {'right': 0.98} + self.size_hint_x = 0.75 + else: + self.md_bg_color = (1, 1, 1, 1) # White for received + self.pos_hint = {'x': 0.02} + self.size_hint_x = 0.75 + + # Sender label (only for received messages) + if not is_self: + sender_label = MDLabel( + text=sender, + font_style='Caption', + theme_text_color='Secondary', + size_hint_y=None, + height=dp(15) + ) + self.add_widget(sender_label) + + # Message content + message_label = MDLabel( + text=message, + size_hint_y=None, + height=dp(40) + ) + self.add_widget(message_label) + + # Timestamp + time_label = MDLabel( + text=timestamp, + font_style='Caption', + theme_text_color='Hint', + size_hint_y=None, + height=dp(15), + halign='right' + ) + self.add_widget(time_label) + + +class FileBubble(MDCard): + """A file sharing bubble displayed in chat for shared/received files.""" + + def __init__(self, filename: str, filesize: int, file_cid: str, sender: str, + is_self: bool = False, timestamp: str = "", on_download=None, **kwargs): + super().__init__(**kwargs) + + self.file_cid = file_cid + self.filename = filename + self.filesize = filesize + self.on_download_callback = on_download + + # Set bubble properties + self.orientation = 'vertical' + self.size_hint_y = None + self.height = dp(120) + self.padding = dp(10) + self.spacing = dp(5) + self.radius = [dp(10)] + + # Set different colors for sent/received + if is_self: + self.md_bg_color = (0.8, 0.93, 0.8, 1) # Light green for sent + self.pos_hint = {'right': 0.98} + self.size_hint_x = 0.8 + else: + self.md_bg_color = (1, 1, 1, 1) # White for received + self.pos_hint = {'x': 0.02} + self.size_hint_x = 0.8 + + # Sender label (only for received) + if not is_self: + sender_label = MDLabel( + text=sender, + font_style='Caption', + theme_text_color='Secondary', + size_hint_y=None, + height=dp(15) + ) + self.add_widget(sender_label) + + # File info row + file_row = BoxLayout( + orientation='horizontal', + size_hint_y=None, + height=dp(50), + spacing=dp(10) + ) + + # File icon + file_icon = MDIconButton( + icon="file", + theme_icon_color="Custom", + icon_color=(0.2, 0.6, 0.2, 1), + disabled=True + ) + file_row.add_widget(file_icon) + + # File details + file_details = BoxLayout( + orientation='vertical', + spacing=dp(2) + ) + + name_label = MDLabel( + text=filename, + font_style='Body2', + bold=True, + size_hint_y=None, + height=dp(22) + ) + file_details.add_widget(name_label) + + size_label = MDLabel( + text=format_file_size(filesize), + font_style='Caption', + theme_text_color='Secondary', + size_hint_y=None, + height=dp(18) + ) + file_details.add_widget(size_label) + + file_row.add_widget(file_details) + self.add_widget(file_row) + + # Download button (only for received files) + if not is_self and on_download: + self.download_btn = MDRaisedButton( + text="Download", + size_hint=(None, None), + size=(dp(120), dp(36)), + pos_hint={'center_x': 0.5}, + on_release=lambda x: self._on_download_pressed() + ) + self.add_widget(self.download_btn) + elif is_self: + status_label = MDLabel( + text="Shared", + font_style='Caption', + theme_text_color='Custom', + text_color=(0.2, 0.7, 0.2, 1), + halign='center', + size_hint_y=None, + height=dp(20) + ) + self.add_widget(status_label) + + # Timestamp + time_label = MDLabel( + text=timestamp, + font_style='Caption', + theme_text_color='Hint', + size_hint_y=None, + height=dp(15), + halign='right' + ) + self.add_widget(time_label) + + def _on_download_pressed(self): + """Handle download button press.""" + if self.on_download_callback: + # Disable button to prevent double-click + if hasattr(self, 'download_btn'): + self.download_btn.text = "Downloading..." + self.download_btn.disabled = True + self.on_download_callback(self.file_cid, self.filename) + + def mark_downloaded(self, save_path: str = ""): + """Update bubble to show download is complete.""" + if hasattr(self, 'download_btn'): + self.download_btn.text = "Downloaded" + self.download_btn.disabled = True + self.download_btn.md_bg_color = (0.2, 0.7, 0.2, 1) + + +class ChatScreen(Screen): + """Chat screen for a specific topic conversation.""" + + def __init__(self, headless_service, **kwargs): + super().__init__(**kwargs) + self.headless_service = headless_service + self.message_queue = headless_service.get_message_queue() + self.system_queue = headless_service.get_system_queue() + self.connection_info = headless_service.get_connection_info() + self.current_topic = None # The topic this chat screen is currently showing + self.file_bubbles = {} # Track file bubbles by CID for download status updates + + # File manager for picking files + self.file_manager = MDFileManager( + exit_manager=self._exit_file_manager, + select_path=self._on_file_selected, + preview=False, + ) + self.file_manager_open = False + + # Main layout + layout = BoxLayout(orientation='vertical') + + # Top app bar + self.toolbar = MDTopAppBar( + title="Select a Topic", + left_action_items=[["arrow-left", lambda x: self.go_back()]], + right_action_items=[ + ["information", lambda x: self.show_info()] + ], + elevation=2 + ) + layout.add_widget(self.toolbar) + + # Messages container + self.messages_layout = BoxLayout( + orientation='vertical', + spacing=dp(10), + padding=dp(10), + size_hint_y=None + ) + self.messages_layout.bind(minimum_height=self.messages_layout.setter('height')) + + # Scroll view for messages + self.scroll = MDScrollView() + self.scroll.add_widget(self.messages_layout) + layout.add_widget(self.scroll) + + # Input area + input_layout = BoxLayout( + orientation='horizontal', + size_hint_y=None, + height=dp(60), + padding=dp(10), + spacing=dp(5) + ) + + # File attachment button + self.attach_btn = MDIconButton( + icon="paperclip", + on_release=self.open_file_picker, + disabled=True + ) + input_layout.add_widget(self.attach_btn) + + # Text input + self.message_input = MDTextField( + hint_text="Select a topic first...", + multiline=False, + size_hint_x=0.75, + disabled=True + ) + self.message_input.bind(on_text_validate=self.send_message) + input_layout.add_widget(self.message_input) + + # Send button + self.send_btn = MDIconButton( + icon="send", + on_release=self.send_message, + disabled=True + ) + input_layout.add_widget(self.send_btn) + + layout.add_widget(input_layout) + + self.add_widget(layout) + + # Start queue checking + Clock.schedule_interval(self.check_queues, 0.1) + + def go_back(self): + """Go back to topics list.""" + self.manager.current = 'topics' + + def set_topic(self, topic: str): + """ + Set the topic for this chat screen and load its messages. + + Args: + topic: The topic name to display + """ + self.current_topic = topic + self.toolbar.title = f"# {topic}" + self.message_input.hint_text = f"Message in {topic}..." + self.message_input.disabled = False + self.send_btn.disabled = False + self.attach_btn.disabled = False + + # Mark topic as read + self.headless_service.mark_topic_as_read(topic) + + # Clear and reload messages + self.messages_layout.clear_widgets() + self.load_topic_messages() + + def load_topic_messages(self): + """Load all messages for the current topic.""" + if not self.current_topic: + return + + messages = self.headless_service.get_topic_messages(self.current_topic) + our_peer_id = self.connection_info.get('peer_id', '') + + for msg_data in messages: + sender_id = msg_data['sender_id'] + sender_nick = msg_data['sender_nick'] + timestamp = time.strftime("%H:%M", time.localtime(msg_data['timestamp'])) + is_self = (sender_id == our_peer_id or sender_id == "self") + + msg_type = msg_data.get('type', 'chat_message') + + if msg_type == 'file_message': + # Render as file bubble + file_cid = msg_data.get('file_cid', '') + file_name = msg_data.get('file_name', 'unknown') + file_size = msg_data.get('file_size', 0) + + self.add_file_bubble( + filename=file_name, + filesize=file_size, + file_cid=file_cid, + sender=sender_nick, + is_self=is_self, + timestamp=timestamp + ) + else: + message = msg_data['message'] + self.add_message_bubble(message, sender_nick, is_self=is_self, timestamp=timestamp) + + def send_message(self, *args): + """Send a message to the current topic.""" + if not self.current_topic: + return + + message = self.message_input.text.strip() + + if not message: + return + + # Clear input + self.message_input.text = "" + + # Handle commands + if message.startswith("/"): + self.handle_command(message) + return + + # Send message through headless service + try: + self.headless_service.send_message_to_topic(self.current_topic, message) + logger.info(f"Sending message to topic {self.current_topic}: {message}") + + # Display message immediately as sent + timestamp = time.strftime("%H:%M") + self.add_message_bubble(message, "You", is_self=True, timestamp=timestamp) + + except Exception as e: + logger.error(f"Failed to send message: {e}") + self.show_system_message(f"Error: {e}") + + def handle_command(self, command: str): + """Handle special commands.""" + cmd = command.lower().strip() + + if cmd in ["/quit", "/exit"]: + MDApp.get_running_app().stop() + elif cmd == "/status": + self.show_info() + else: + self.show_system_message(f"Unknown command: {command}") + + def check_queues(self, dt): + """Check message queues for new messages for the current topic.""" + # Check message queue + try: + while True: + try: + message_data = self.message_queue.sync_q.get_nowait() + msg_type = message_data.get('type', '') + + if msg_type == 'chat_message': + # Only show messages for the current topic + msg_topic = message_data.get('topic', 'default') + if msg_topic != self.current_topic: + continue + + sender_nick = message_data['sender_nick'] + sender_id = message_data['sender_id'] + msg = message_data['message'] + + # Don't display our own messages again + our_peer_id = self.connection_info.get('peer_id', '') + if sender_id != our_peer_id and sender_id != "self": + timestamp = time.strftime("%H:%M") + self.add_message_bubble(msg, sender_nick, is_self=False, timestamp=timestamp) + + elif msg_type == 'file_message': + # Received a file sharing message from another peer + msg_topic = message_data.get('topic', 'default') + if msg_topic != self.current_topic: + continue + + sender_id = message_data.get('sender_id', '') + our_peer_id = self.connection_info.get('peer_id', '') + if sender_id != our_peer_id and sender_id != "self": + timestamp = time.strftime("%H:%M") + self.add_file_bubble( + filename=message_data.get('file_name', 'unknown'), + filesize=message_data.get('file_size', 0), + file_cid=message_data.get('file_cid', ''), + sender=message_data.get('sender_nick', 'Unknown'), + is_self=False, + timestamp=timestamp + ) + + elif msg_type == 'file_shared': + # Our own file was successfully shared + msg_topic = message_data.get('topic', 'default') + if msg_topic != self.current_topic: + continue + + timestamp = time.strftime("%H:%M") + self.add_file_bubble( + filename=message_data.get('file_name', 'unknown'), + filesize=message_data.get('file_size', 0), + file_cid=message_data.get('file_cid', ''), + sender='You', + is_self=True, + timestamp=timestamp + ) + + elif msg_type == 'file_downloaded': + # File download completed + file_cid = message_data.get('file_cid', '') + save_path = message_data.get('save_path', '') + file_name = message_data.get('file_name', 'unknown') + + # Update the file bubble if it exists + if file_cid in self.file_bubbles: + self.file_bubbles[file_cid].mark_downloaded(save_path) + + # Show download notification + self._show_download_notification(file_name, save_path) + + elif msg_type == 'file_download_failed': + # File download failed + file_cid = message_data.get('file_cid', '') + file_name = message_data.get('file_name', 'unknown') + error = message_data.get('error', 'Unknown error') + + # Re-enable download button + if file_cid in self.file_bubbles: + bubble = self.file_bubbles[file_cid] + if hasattr(bubble, 'download_btn'): + bubble.download_btn.text = "Retry" + bubble.download_btn.disabled = False + + self.show_system_message(f"Download failed: {file_name} - {error}") + + except Empty: + break + except Exception as e: + logger.error(f"Error checking message queue: {e}") + + # Check system queue + try: + while True: + try: + system_data = self.system_queue.sync_q.get_nowait() + if system_data.get('type') == 'system_message': + self.show_system_message(system_data['message']) + except Empty: + break + except Exception as e: + logger.error(f"Error checking system queue: {e}") + + def add_message_bubble(self, message: str, sender: str, is_self: bool = False, timestamp: str = ""): + """Add a message bubble to the chat.""" + bubble = MessageBubble( + message=message, + sender=sender, + is_self=is_self, + timestamp=timestamp + ) + self.messages_layout.add_widget(bubble) + + def add_file_bubble(self, filename: str, filesize: int, file_cid: str, + sender: str, is_self: bool = False, timestamp: str = ""): + """Add a file sharing bubble to the chat.""" + on_download = None if is_self else self._request_download + + bubble = FileBubble( + filename=filename, + filesize=filesize, + file_cid=file_cid, + sender=sender, + is_self=is_self, + timestamp=timestamp, + on_download=on_download + ) + self.messages_layout.add_widget(bubble) + + # Track file bubbles for download status updates + if file_cid: + self.file_bubbles[file_cid] = bubble + + def open_file_picker(self, *args): + """Open the file picker to select a file to share.""" + if not self.current_topic: + self.show_system_message("Select a topic first") + return + + if self.file_manager_open: + return + + # Open file manager at home directory + home_dir = os.path.expanduser("~") + self.file_manager.show(home_dir) + self.file_manager_open = True + + def _exit_file_manager(self, *args): + """Called when file manager is closed without selection.""" + self.file_manager.close() + self.file_manager_open = False + + def _on_file_selected(self, path: str): + """Called when a file is selected from the file picker.""" + self.file_manager.close() + self.file_manager_open = False + + if not path or not os.path.isfile(path): + self.show_system_message("Invalid file selected") + return + + if not self.current_topic: + self.show_system_message("Select a topic first") + return + + filename = os.path.basename(path) + filesize = os.path.getsize(path) + + # Show confirmation dialog + self._confirm_file_share(path, filename, filesize) + + def _confirm_file_share(self, file_path: str, filename: str, filesize: int): + """Show a confirmation dialog before sharing a file.""" + dialog = MDDialog( + title="Share File?", + text=f"File: {filename}\nSize: {format_file_size(filesize)}\nTopic: {self.current_topic}", + buttons=[ + MDFlatButton( + text="CANCEL", + on_release=lambda x: dialog.dismiss() + ), + MDFlatButton( + text="SHARE", + on_release=lambda x: (dialog.dismiss(), self._do_share_file(file_path)) + ) + ] + ) + dialog.open() + + def _do_share_file(self, file_path: str): + """Execute file sharing via headless service.""" + try: + success = self.headless_service.share_file(file_path, self.current_topic) + if success: + self.show_system_message(f"Preparing file for sharing...") + else: + self.show_system_message("Failed to queue file share request") + except Exception as e: + logger.error(f"Error sharing file: {e}") + self.show_system_message(f"Error: {str(e)}") + + def _request_download(self, file_cid: str, filename: str): + """Request download of a file.""" + try: + success = self.headless_service.download_file(file_cid, filename) + if success: + self.show_system_message(f"Downloading: {filename}...") + else: + self.show_system_message("Failed to queue download request") + # Re-enable download button + if file_cid in self.file_bubbles: + bubble = self.file_bubbles[file_cid] + if hasattr(bubble, 'download_btn'): + bubble.download_btn.text = "Download" + bubble.download_btn.disabled = False + except Exception as e: + logger.error(f"Error requesting download: {e}") + self.show_system_message(f"Error: {str(e)}") + + def _show_download_notification(self, filename: str, save_path: str): + """Show a brief notification that file has been downloaded.""" + try: + Snackbar( + text=f"Downloaded: {filename} -> {save_path}", + duration=2, + ).open() + except Exception: + # Fallback if Snackbar fails + self.show_system_message(f"Downloaded: {filename} saved to {save_path}") + + def show_system_message(self, message: str): + """Show a system message.""" + timestamp = time.strftime("%H:%M") + + # Create a centered system message + system_card = MDCard( + orientation='vertical', + size_hint=(0.8, None), + height=dp(40), + pos_hint={'center_x': 0.5}, + md_bg_color=(0.95, 0.95, 0.95, 1), + padding=dp(10) + ) + + label = MDLabel( + text=f"[{timestamp}] {message}", + font_style='Caption', + theme_text_color='Secondary', + halign='center' + ) + system_card.add_widget(label) + self.messages_layout.add_widget(system_card) + + def show_peers(self, *args): + """Show connected peers dialog.""" + info = self.headless_service.get_connection_info() + peers = info.get('connected_peers', set()) + + if peers: + peer_list = "\n".join([f"• {peer[:16]}..." for peer in sorted(peers)]) + content = f"Connected Peers ({len(peers)}):\n\n{peer_list}" + else: + content = "No peers connected yet." + + dialog = MDDialog( + title="Connected Peers", + text=content, + buttons=[ + MDFlatButton( + text="CLOSE", + on_release=lambda x: dialog.dismiss() + ) + ] + ) + dialog.open() + + def show_info(self, *args): + """Show connection info dialog with clickable text to copy.""" + info = self.headless_service.get_connection_info() + peer_id = info.get('peer_id', 'Unknown') + multiaddr = info.get('multiaddr', 'Unknown') + topics = self.headless_service.get_subscribed_topics() + topics_list = ", ".join(sorted(topics)) if topics else "None" + + # Create content layout + content = BoxLayout( + orientation='vertical', + spacing=dp(10), + padding=dp(10), + size_hint_y=None, + height=dp(320) + ) + + # Info text + info_label = MDLabel( + text=f"""Nickname: {info.get('nickname', 'Unknown')} +Connected Peers: {info.get('peer_count', 0)} +Subscribed Topics: {topics_list} +""", + size_hint_y=None, + height=dp(100) + ) + content.add_widget(info_label) + + # Peer ID section + peer_id_label = MDLabel( + text="Peer ID:", + font_style='Caption', + theme_text_color='Secondary', + size_hint_y=None, + height=dp(20) + ) + content.add_widget(peer_id_label) + + # Clickable Peer ID card + peer_id_card = MDCard( + orientation='vertical', + size_hint_y=None, + height=dp(50), + padding=dp(10), + md_bg_color=(0.9, 0.95, 1, 1), # Light blue tint + on_release=lambda x: self.copy_to_clipboard(peer_id, "Peer ID copied!") + ) + + peer_id_text = MDLabel( + text=peer_id, + font_style='Body2', + halign='left', + valign='middle', + size_hint_y=1 + ) + peer_id_card.add_widget(peer_id_text) + content.add_widget(peer_id_card) + + # Multiaddr section + multiaddr_label = MDLabel( + text="Multiaddr:", + font_style='Caption', + theme_text_color='Secondary', + size_hint_y=None, + height=dp(20) + ) + content.add_widget(multiaddr_label) + + # Clickable Multiaddr card + multiaddr_card = MDCard( + orientation='vertical', + size_hint_y=None, + height=dp(70), + padding=dp(10), + md_bg_color=(0.9, 0.95, 1, 1), # Light blue tint + on_release=lambda x: self.copy_to_clipboard(multiaddr, "Multiaddr copied!") + ) + + multiaddr_text = MDLabel( + text=multiaddr, + font_style='Body2', + halign='left', + valign='middle', + size_hint_y=1 + ) + multiaddr_card.add_widget(multiaddr_text) + content.add_widget(multiaddr_card) + + dialog = MDDialog( + title="Connection Status", + type="custom", + content_cls=content, + buttons=[ + MDFlatButton( + text="CLOSE", + on_release=lambda x: dialog.dismiss() + ) + ] + ) + dialog.open() + + def copy_to_clipboard(self, text, success_message): + """Copy text to clipboard and show confirmation.""" + try: + from kivy.core.clipboard import Clipboard + Clipboard.copy(text) + logger.info(f"Copied to clipboard: {text[:50]}...") + self.show_system_message(success_message) + except Exception as e: + logger.error(f"Failed to copy to clipboard: {e}") + self.show_system_message("Failed to copy to clipboard") + + def show_multiaddr(self, *args): + """Show multiaddr dialog with clickable text to copy.""" + info = self.headless_service.get_connection_info() + multiaddr = info.get('multiaddr', 'Unknown') + + # Create content layout + content = BoxLayout( + orientation='vertical', + spacing=dp(10), + padding=dp(10), + size_hint_y=None, + height=dp(100) + ) + + # Hint text + hint_label = MDLabel( + text="Tap multiaddr to copy:", + font_style='Caption', + theme_text_color='Secondary', + size_hint_y=None, + height=dp(20) + ) + content.add_widget(hint_label) + + # Clickable Multiaddr card + multiaddr_card = MDCard( + orientation='vertical', + size_hint_y=None, + height=dp(70), + padding=dp(10), + md_bg_color=(0.9, 0.95, 1, 1), # Light blue tint + on_release=lambda x: self.copy_to_clipboard(multiaddr, "Multiaddr copied!") + ) + + multiaddr_text = MDLabel( + text=multiaddr, + font_style='Body2', + halign='left', + valign='middle', + size_hint_y=1 + ) + multiaddr_card.add_widget(multiaddr_text) + content.add_widget(multiaddr_card) + + dialog = MDDialog( + title="My Multiaddress", + type="custom", + content_cls=content, + buttons=[ + MDFlatButton( + text="CLOSE", + on_release=lambda x: dialog.dismiss() + ) + ] + ) + dialog.open() + + +class PeersScreen(Screen): + """Screen showing list of connected peers.""" + + def __init__(self, headless_service, **kwargs): + super().__init__(**kwargs) + self.headless_service = headless_service + + layout = BoxLayout(orientation='vertical') + + # Top app bar + toolbar = MDTopAppBar( + title="Connected Peers", + left_action_items=[["arrow-left", lambda x: self.go_back()]], + elevation=2 + ) + layout.add_widget(toolbar) + + # Peers list + self.peers_layout = BoxLayout( + orientation='vertical', + spacing=dp(5), + padding=dp(10), + size_hint_y=None + ) + self.peers_layout.bind(minimum_height=self.peers_layout.setter('height')) + + scroll = MDScrollView() + scroll.add_widget(self.peers_layout) + layout.add_widget(scroll) + + self.add_widget(layout) + + # Update peers periodically + Clock.schedule_interval(self.update_peers, 1.0) + + def go_back(self): + """Go back to chat screen.""" + self.manager.current = 'chat' + + def update_peers(self, dt): + """Update the peers list.""" + self.peers_layout.clear_widgets() + + info = self.headless_service.get_connection_info() + peers = info.get('connected_peers', set()) + + if not peers: + label = MDLabel( + text="No peers connected", + halign='center', + theme_text_color='Hint' + ) + self.peers_layout.add_widget(label) + return + + for peer in sorted(peers): + peer_item = OneLineAvatarIconListItem( + text=f"{peer[:16]}...", + on_release=lambda x, p=peer: self.show_peer_info(p) + ) + peer_item.add_widget(IconLeftWidget(icon="account")) + self.peers_layout.add_widget(peer_item) + + def show_peer_info(self, peer_id): + """Show information about a specific peer.""" + dialog = MDDialog( + title="Peer Information", + text=f"Peer ID:\n{peer_id}", + buttons=[ + MDFlatButton( + text="CLOSE", + on_release=lambda x: dialog.dismiss() + ) + ] + ) + dialog.open() + + +class TopicsScreen(Screen): + """Main screen showing list of subscribed topics - WhatsApp style selector.""" + + def __init__(self, headless_service, **kwargs): + super().__init__(**kwargs) + self.headless_service = headless_service + self.new_topic_dialog = None + + layout = BoxLayout(orientation='vertical') + + # Top app bar + toolbar = MDTopAppBar( + title="Universal Chat", + right_action_items=[ + ["plus", lambda x: self.show_add_topic_dialog()], + ["connection", lambda x: self.show_connect_dialog()], + ["information", lambda x: self.show_app_info()] + ], + elevation=2 + ) + layout.add_widget(toolbar) + + # Topics list + self.topics_layout = BoxLayout( + orientation='vertical', + spacing=dp(5), + padding=dp(10), + size_hint_y=None + ) + self.topics_layout.bind(minimum_height=self.topics_layout.setter('height')) + + scroll = MDScrollView() + scroll.add_widget(self.topics_layout) + layout.add_widget(scroll) + + self.add_widget(layout) + + # Update topics periodically + Clock.schedule_interval(self.update_topics, 1.0) + + def go_back(self): + """Not used - Topics is the main screen now.""" + pass + + def update_topics(self, dt): + """Update the topics list with unread counts.""" + self.topics_layout.clear_widgets() + + # Get all topics with their info + topics_info = self.headless_service.get_all_topics_with_info() + + if not topics_info: + label = MDLabel( + text="No topics subscribed\nTap + to add a topic", + halign='center', + theme_text_color='Hint' + ) + self.topics_layout.add_widget(label) + return + + # Sort topics by unread count (most unread first), then alphabetically + sorted_topics = sorted( + topics_info.items(), + key=lambda x: (-x[1]['unread_count'], x[0]) + ) + + for topic, info in sorted_topics: + unread_count = info['unread_count'] + last_message = info.get('last_message') + + # Create topic item with two lines (topic name + last message preview) + from kivymd.uix.list import TwoLineAvatarIconListItem + + # Preview of last message + preview = "" + if last_message: + preview = last_message['message'][:50] + if len(last_message['message']) > 50: + preview += "..." + + topic_item = TwoLineAvatarIconListItem( + text=topic, + secondary_text=preview or "No messages yet", + on_release=lambda x, t=topic: self.open_topic_chat(t) + ) + topic_item.add_widget(IconLeftWidget(icon="pound")) + + # Add unread badge if there are unread messages + if unread_count > 0: + # Show unread count in the secondary text + unread_text = f" ({unread_count} unread)" + topic_item.secondary_text = (preview or "No messages yet") + unread_text + + self.topics_layout.add_widget(topic_item) + + def open_topic_chat(self, topic): + """Open the chat screen for a specific topic.""" + # Switch to chat screen + chat_screen = self.manager.get_screen('chat') + chat_screen.set_topic(topic) + self.manager.current = 'chat' + + def show_topic_info(self, topic): + """Show information about a specific topic.""" + info = self.headless_service.get_all_topics_with_info().get(topic, {}) + unread = info.get('unread_count', 0) + total = info.get('total_count', 0) + + dialog = MDDialog( + title=f"Topic: {topic}", + text=f"Total messages: {total}\nUnread messages: {unread}", + buttons=[ + MDFlatButton( + text="CLOSE", + on_release=lambda x: dialog.dismiss() + ), + MDFlatButton( + text="OPEN CHAT", + on_release=lambda x: (dialog.dismiss(), self.open_topic_chat(topic)) + ) + ] + ) + dialog.open() + + def show_add_topic_dialog(self): + """Show dialog to add a new topic.""" + # Text field for topic name + self.topic_input = MDTextField( + hint_text="Enter topic name", + size_hint_x=0.9, + pos_hint={'center_x': 0.5} + ) + + content = BoxLayout( + orientation='vertical', + spacing=dp(10), + padding=dp(20), + size_hint_y=None, + height=dp(100) + ) + content.add_widget(self.topic_input) + + self.new_topic_dialog = MDDialog( + title="Subscribe to New Topic", + type="custom", + content_cls=content, + buttons=[ + MDFlatButton( + text="CANCEL", + on_release=lambda x: self.new_topic_dialog.dismiss() + ), + MDFlatButton( + text="SUBSCRIBE", + on_release=self.add_topic + ) + ] + ) + self.new_topic_dialog.open() + + def show_app_info(self): + """Show app connection information with clickable text to copy.""" + info = self.headless_service.get_connection_info() + peer_id = info.get('peer_id', 'Unknown') + multiaddr = info.get('multiaddr', 'Unknown') + + # Create content layout + content = BoxLayout( + orientation='vertical', + spacing=dp(15), + padding=dp(10), + size_hint_y=None, + height=dp(200) + ) + + # Peer ID section + peer_id_label = MDLabel( + text="Peer ID:", + font_style='Caption', + theme_text_color='Secondary', + size_hint_y=None, + height=dp(20) + ) + content.add_widget(peer_id_label) + + # Clickable Peer ID card + peer_id_card = MDCard( + orientation='vertical', + size_hint_y=None, + height=dp(50), + padding=dp(10), + md_bg_color=(0.9, 0.95, 1, 1), # Light blue tint + on_release=lambda x: self.copy_to_clipboard(peer_id, "Peer ID copied!") + ) + + peer_id_text = MDLabel( + text=peer_id, + font_style='Body2', + halign='left', + valign='middle', + size_hint_y=1 + ) + peer_id_card.add_widget(peer_id_text) + content.add_widget(peer_id_card) + + # Multiaddr section + multiaddr_label = MDLabel( + text="Multiaddr:", + font_style='Caption', + theme_text_color='Secondary', + size_hint_y=None, + height=dp(20) + ) + content.add_widget(multiaddr_label) + + # Clickable Multiaddr card + multiaddr_card = MDCard( + orientation='vertical', + size_hint_y=None, + height=dp(70), + padding=dp(10), + md_bg_color=(0.9, 0.95, 1, 1), # Light blue tint + on_release=lambda x: self.copy_to_clipboard(multiaddr, "Multiaddr copied!") + ) + + multiaddr_text = MDLabel( + text=multiaddr, + font_style='Body2', + halign='left', + valign='middle', + size_hint_y=1 + ) + multiaddr_card.add_widget(multiaddr_text) + content.add_widget(multiaddr_card) + + dialog = MDDialog( + title="Connection Info", + type="custom", + content_cls=content, + buttons=[ + MDFlatButton( + text="CLOSE", + on_release=lambda x: dialog.dismiss() + ) + ] + ) + dialog.open() + + def copy_to_clipboard(self, text, success_message): + """Copy text to clipboard and show confirmation.""" + try: + from kivy.core.clipboard import Clipboard + Clipboard.copy(text) + logger.info(f"Copied to clipboard: {text[:50]}...") + self.show_status_dialog("Success", success_message) + except Exception as e: + logger.error(f"Failed to copy to clipboard: {e}") + self.show_status_dialog("Error", "Failed to copy to clipboard") + + def show_connect_dialog(self): + """Show dialog to connect to a peer.""" + # Text field for multiaddress + self.connect_input = MDTextField( + hint_text="Enter peer multiaddress", + helper_text="e.g., /ip4/127.0.0.1/tcp/9095/p2p/QmXXXXXXXXXX...", + helper_text_mode="persistent", + multiline=True, + size_hint_y=None, + height=dp(120) + ) + + content = BoxLayout( + orientation='vertical', + spacing=dp(10), + padding=dp(20), + size_hint_y=None, + height=dp(140) + ) + content.add_widget(self.connect_input) + + self.connect_dialog = MDDialog( + title="Connect to Peer", + type="custom", + content_cls=content, + buttons=[ + MDFlatButton( + text="CANCEL", + on_release=lambda x: self.connect_dialog.dismiss() + ), + MDFlatButton( + text="CONNECT", + on_release=self.connect_to_peer + ) + ] + ) + self.connect_dialog.open() + + def connect_to_peer(self, *args): + """Connect to a peer using the provided multiaddress.""" + multiaddr = self.connect_input.text.strip() + + if not multiaddr: + self.show_status_dialog("Error", "Please enter a multiaddress") + return + + # Close the dialog + if self.connect_dialog: + self.connect_dialog.dismiss() + + try: + # Call the headless service to connect + success = self.headless_service.connect_to_peer(multiaddr) + if success: + self.show_status_dialog("Success", f"Connection request sent!\n\n{multiaddr[:60]}...") + else: + self.show_status_dialog("Error", "Failed to queue connection request") + except Exception as e: + logger.error(f"Error connecting to peer: {e}") + self.show_status_dialog("Error", f"Connection failed: {str(e)}") + + def show_status_dialog(self, title, text): + """Show a status/error dialog.""" + dialog = MDDialog( + title=title, + text=text, + buttons=[ + MDFlatButton( + text="OK", + on_release=lambda x: dialog.dismiss() + ) + ] + ) + dialog.open() + + def add_topic(self, *args): + """Add a new topic subscription.""" + topic_name = self.topic_input.text.strip() + + if not topic_name: + return + + # Subscribe to the topic + success = self.headless_service.subscribe_to_topic(topic_name) + + if success: + logger.info(f"Successfully subscribed to topic: {topic_name}") + else: + logger.error(f"Failed to subscribe to topic: {topic_name}") + + # Close dialog + if self.new_topic_dialog: + self.new_topic_dialog.dismiss() + + # Update the topics list immediately + self.update_topics(0) + + +class ChatApp(MDApp): + """Main Kivy application for the chat.""" + + def __init__(self, headless_service, **kwargs): + super().__init__(**kwargs) + self.headless_service = headless_service + self.theme_cls.primary_palette = "Green" + self.theme_cls.theme_style = "Light" + + def build(self): + """Build the application.""" + # Screen manager + sm = ScreenManager() + + # Add screens + topics_screen = TopicsScreen(self.headless_service, name='topics') + chat_screen = ChatScreen(self.headless_service, name='chat') + peers_screen = PeersScreen(self.headless_service, name='peers') + + sm.add_widget(topics_screen) + sm.add_widget(chat_screen) + sm.add_widget(peers_screen) + + # Set initial screen to topics (main WhatsApp-style selector) + sm.current = 'topics' + + return sm + + def on_start(self): + """Called when the app starts.""" + logger.info("Kivy Chat UI started") + + def on_stop(self): + """Called when the app stops.""" + logger.info("Kivy Chat UI stopped") + # Cleanup if needed + return True + + +def run_kivy_ui(headless_service): + """ + Run the Kivy UI with the given headless service. + + Args: + headless_service: The HeadlessService instance to use for communication + """ + logger.info("Starting Kivy UI...") + + # Set window size for desktop (will be ignored on mobile) + Window.size = (400, 600) + + # Create and run app + app = ChatApp(headless_service) + app.run() + + logger.info("Kivy UI stopped") diff --git a/py-peer/llm/codes/build_vectorstore.py b/py-peer/llm/codes/build_vectorstore.py new file mode 100644 index 00000000..699f8015 --- /dev/null +++ b/py-peer/llm/codes/build_vectorstore.py @@ -0,0 +1,52 @@ +# pip install chromadb langchain langchain-community langchain-huggingface sentence-transformers + +from langchain_community.document_loaders import DirectoryLoader, TextLoader +from langchain_text_splitters import RecursiveCharacterTextSplitter +from langchain_community.vectorstores import Chroma +from langchain_huggingface import HuggingFaceEmbeddings + +# 1. Load py-libp2p source files +loader = DirectoryLoader( + "../py-libp2p", + glob="**/*.py", + loader_cls=TextLoader +) +docs = loader.load() + +# Load py-libp2p markdown docs +md_loader = DirectoryLoader( + "../py-libp2p", + glob="**/*.md", + loader_cls=TextLoader +) +docs += md_loader.load() + +# Load libp2p spec files (md + txt) +for glob_pattern in ("**/*.md", "**/*.txt"): + spec_loader = DirectoryLoader( + "../specs", # ← cloned libp2p/specs repo + glob=glob_pattern, + loader_cls=TextLoader + ) + docs += spec_loader.load() + +# 2. Chunk them +splitter = RecursiveCharacterTextSplitter( + chunk_size=300, # reduced from 500 + chunk_overlap=30, # reduced proportionally + separators=["\nclass ", "\ndef ", "\n\n", "\n", " "] +) +chunks = splitter.split_documents(docs) +print(f"Total chunks: {len(chunks)}") + +# 3. Embed + store in ChromaDB +embeddings = HuggingFaceEmbeddings( + model_name="nomic-ai/nomic-embed-text-v1.5", + model_kwargs={"trust_remote_code": True}, +) +vectorstore = Chroma.from_documents( + documents=chunks, + embedding=embeddings, + persist_directory="./libp2p_vectorstore" +) +print("Done! Vector store saved.") diff --git a/py-peer/main.py b/py-peer/main.py new file mode 100644 index 00000000..34679615 --- /dev/null +++ b/py-peer/main.py @@ -0,0 +1,477 @@ +#!/usr/bin/env python3 +""" +Universal Connectivity Python Peer - Modular Main Entry Point + +This is the main entry point for the Python implementation of the universal connectivity peer. +It handles argument parsing and coordinates between the headless service and UI components. +""" + +import argparse +import logging +import sys +import time +import traceback +import trio +import threading + +from dotenv import load_dotenv +load_dotenv() # loads .env from cwd (py-peer/) + +from headless import HeadlessService +from ui import ChatUI + +DEFAULT_SEED = "py-peer" + +# Configure logging +def setup_logging(ui_mode=False): + """Setup logging configuration based on whether UI is active.""" + handlers = [] + + # Only add console handler if not in UI mode + if not ui_mode: + handlers.append(logging.StreamHandler()) + + # If no handlers, add a null handler to prevent logging errors + if not handlers: + handlers.append(logging.NullHandler()) + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(message)s", + handlers=handlers, + force=True # Force reconfiguration + ) + +logger = logging.getLogger("main") +logging.getLogger("headless").setLevel(logging.DEBUG) # Enable debug for headless service +logging.getLogger("chatroom").setLevel(logging.DEBUG) # Enable debug for chatroom +logging.getLogger("libp2p.transport").setLevel(logging.DEBUG) +logging.getLogger("libp2p.security").setLevel(logging.DEBUG) +logging.getLogger("libp2p.mux").setLevel(logging.DEBUG) +logging.getLogger("libp2p.stream").setLevel(logging.DEBUG) +logging.getLogger("libp2p.pubsub").setLevel(logging.DEBUG) +logging.getLogger("libp2p.discovery").setLevel(logging.DEBUG) +logging.getLogger("libp2p.kad_dht").setLevel(logging.DEBUG) + +def run_headless_in_thread(headless_service, ready_event): + """Run headless service in a separate thread.""" + def run_service(): + try: + trio.run(headless_service.start) + except Exception as e: + logger.error(f"Error in headless service thread: {e}") + logger.error(f"Traceback:\n{traceback.format_exc()}") + + # Start the service in a daemon thread + thread = threading.Thread(target=run_service, daemon=True) + thread.start() + + # Wait for the service to be ready + max_wait = 30 # Maximum wait time in seconds + waited = 0 + while not headless_service.ready and waited < max_wait: + time.sleep(0.1) + waited += 0.1 + + if not headless_service.ready: + raise RuntimeError("Headless service failed to start within timeout") + + logger.info("✅ Headless service is ready in background thread") + return thread + + +async def main_async(args): + """Main async function.""" + logger.info("Starting Universal Connectivity Python Peer...") + + # Create nickname + nickname = args.nick or f"peer-{time.time():.0f}" + + # Create headless service + strict_signing = not args.no_strict_signing # Default True, False if --no-strict-signing is used + headless_service = HeadlessService( + nickname=nickname, + port=args.port, + connect_addrs=args.connect, + strict_signing=strict_signing, + seed=args.seed, + topic=args.topic + ) + + try: + if args.headless: + # Run in headless mode + logger.info("Starting headless service...") + await headless_service.start() + elif args.ui: + # Return service configuration for UI mode + return headless_service + else: + # Run with simple interactive mode + logger.info("Starting headless service in background...") + + async with trio.open_nursery() as nursery: + # Start headless service in background + nursery.start_soon(headless_service.start) + + # Wait for service to be ready + await headless_service.ready_event.wait() + logger.info("✅ Headless service is ready, starting UI...") + + # Run simple interactive mode + await run_simple_interactive(headless_service) + + except Exception as e: + logger.error(f"Application error: {e}") + logger.error(f"Traceback:\n{traceback.format_exc()}") + await headless_service.stop() + raise + + return None + + +async def run_simple_interactive(headless_service): + """Run simple interactive mode.""" + connection_info = headless_service.get_connection_info() + + print(f"\n=== Universal Connectivity Chat ===") + print(f"Nickname: {connection_info.get('nickname', 'Unknown')}") + print(f"Peer ID: {connection_info.get('peer_id', 'Unknown')}") + print(f"Multiaddr: {connection_info.get('multiaddr', 'Unknown')}") + print(f"Type messages and press Enter to send. Type 'quit' to exit.") + print(f"Commands: /peers, /status, /multiaddr") + print() + + # Start background task to monitor message queues + async with trio.open_nursery() as nursery: + nursery.start_soon(monitor_message_queues, headless_service) + nursery.start_soon(handle_user_input, headless_service) + + +async def monitor_message_queues(headless_service): + """Monitor message queues and display incoming messages.""" + logger.debug("monitor_message_queues function started") + + message_queue = headless_service.get_message_queue() + system_queue = headless_service.get_system_queue() + + logger.debug(f"Message queue: {message_queue}") + logger.debug(f"System queue: {system_queue}") + + if not message_queue or not system_queue: + logger.warning("Message queues not available") + return + + logger.info("📡 Starting message queue monitoring...") + + while True: + try: + # Check message queue + try: + message_data = message_queue.sync_q.get_nowait() + logger.info(f"📨 Got message from queue: {message_data}") + + if message_data.get('type') == 'chat_message': + sender_nick = message_data['sender_nick'] + sender_id = message_data['sender_id'] + msg = message_data['message'] + + # Display incoming message + sender_short = sender_id[:8] if len(sender_id) > 8 else sender_id + print(f"[{sender_nick}({sender_short})]: {msg}") + + except: + pass # Empty queue is normal, no need to log + + # Check system queue + try: + system_data = system_queue.sync_q.get_nowait() + logger.info(f"📡 Got system message from queue: {system_data}") + + if system_data.get('type') == 'system_message': + print(f"📡 {system_data['message']}") + + except: + pass # Empty queue is normal, no need to log + + await trio.sleep(0.1) # Small delay to prevent busy waiting + + except Exception as e: + logger.error(f"Error monitoring message queues: {e}") + await trio.sleep(1) + + +async def handle_user_input(headless_service): + """Handle user input in interactive mode.""" + try: + while True: + message = await trio.to_thread.run_sync(input) + + if message.lower() in ["quit", "exit", "q"]: + print("Goodbye!") + break + + # Handle special commands + elif message.strip() == "/peers": + info = headless_service.get_connection_info() + peers = info.get('connected_peers', set()) + if peers: + print(f"📡 Connected peers ({len(peers)}):") + for peer in peers: + print(f" - {peer[:8]}...") + else: + print("📡 No peers connected") + continue + + elif message.strip() == "/multiaddr": + info = headless_service.get_connection_info() + print(f"\n📋 Copy this multiaddress:") + print(f"{info.get('multiaddr', 'Unknown')}") + print() + continue + + elif message.strip() == "/status": + info = headless_service.get_connection_info() + print(f"📊 Status:") + print(f" - Multiaddr: {info.get('multiaddr', 'Unknown')}") + print(f" - Nickname: {info.get('nickname', 'Unknown')}") + print(f" - Connected peers: {info.get('peer_count', 0)}") + print(f" - Subscribed topics: chat, discovery") + continue + + if message.strip(): + # Send message through headless service + headless_service.send_message(message) + + except (EOFError, KeyboardInterrupt): + print("\nGoodbye!") + + await headless_service.stop() + + +def main(): + """Main entry point.""" + parser = argparse.ArgumentParser(description="Universal Connectivity Python Peer") + + parser.add_argument( + "--nick", + type=str, + help="Nickname to use for the chat" + ) + + parser.add_argument( + "--headless", + action="store_true", + help="Run without chat UI" + ) + + parser.add_argument( + "--ui", + action="store_true", + help="Use Textual TUI instead of simple interactive mode" + ) + + parser.add_argument( + "--kivy", + action="store_true", + help="Use Kivy UI (mobile-friendly interface)" + ) + + parser.add_argument( + "-c", "--connect", + action="append", + help="Address to connect to (can be used multiple times)", + default=[] + ) + + parser.add_argument( + "-p", "--port", + type=int, + help="Port to listen on", + default=0 + ) + + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Enable debug logging" + ) + + parser.add_argument( + "--no-strict-signing", + action="store_true", + help="Disable strict message signing (allows unsigned messages)" + ) + parser.add_argument( + "-s", + "--seed", + type=str, + default=DEFAULT_SEED, + help="seed for deterministic peer ID generation", + ) + parser.add_argument( + "-t", + "--topic", + type=str, + help="Custom topic to subscribe.", + ) + parser.add_argument( + "--api", + action="store_true", + help="Start Tornado REST + WebSocket API server (headless + API, no UI)" + ) + parser.add_argument( + "--api-port", + type=int, + default=8765, + dest="api_port", + help="Port for the Tornado API server (default: 8765)" + ) + parser.add_argument( + "--api-routes", + action="store_true", + help="Print all available API routes and exit" + ) + + args = parser.parse_args() + + # Default logging setup (will be reconfigured based on mode) + setup_logging(ui_mode=False) + + # Set debug level if verbose flag is provided + if args.verbose: + logger.setLevel(logging.DEBUG) + logging.getLogger("libp2p").setLevel(logging.DEBUG) + logging.getLogger("headless").setLevel(logging.DEBUG) + logger.debug("Debug logging enabled") + + # Print routes and exit early if requested + if getattr(args, 'api_routes', False): + from tornado_server import _print_routes + _print_routes(getattr(args, 'api_port', 8765)) + sys.exit(0) + + try: + if args.api: + # ── Tornado API mode ───────────────────────────────────────────── + setup_logging(ui_mode=False) + logger.info("Starting in API mode (HeadlessService + Tornado REST server)...") + + nickname = args.nick or f"peer-{time.time():.0f}" + strict_signing = not args.no_strict_signing + headless_service = HeadlessService( + nickname=nickname, + port=args.port, + connect_addrs=args.connect, + strict_signing=strict_signing, + seed=args.seed, + topic=args.topic + ) + + # Start HeadlessService in a background trio thread + logger.info("Starting HeadlessService in background thread...") + ready_event = threading.Event() + headless_thread = run_headless_in_thread(headless_service, ready_event) + logger.info(f"✅ HeadlessService ready — launching Tornado on port {args.api_port}") + + # Start Tornado in the main thread (blocks here) + from tornado_server import TornadoServer, _print_routes + _print_routes(args.api_port) + server = TornadoServer(headless_service, port=args.api_port) + server.start() # blocks until Ctrl-C + + elif args.kivy: + # Configure logging for Kivy mode (no console output) + setup_logging(ui_mode=True) + + # Special handling for Kivy mode + logger.info("Starting in Kivy mode...") + + # Create nickname + nickname = args.nick or f"peer-{time.time():.0f}" + + # Create headless service + strict_signing = not args.no_strict_signing # Default True, False if --no-strict-signing is used + headless_service = HeadlessService( + nickname=nickname, + port=args.port, + connect_addrs=args.connect, + strict_signing=strict_signing, + seed=args.seed, + topic=args.topic + ) + + # Start headless service in background thread + logger.info("Starting headless service in background thread...") + ready_event = threading.Event() + headless_thread = run_headless_in_thread(headless_service, ready_event) + + logger.info("Starting Kivy UI in main thread...") + + # Import kivy_ui here to avoid issues if kivy is not installed + try: + from kivy_ui import run_kivy_ui + except ImportError as e: + logger.error("Failed to import kivy_ui. Make sure Kivy and KivyMD are installed.") + logger.error(f"Error: {e}") + logger.error("Install with: pip install kivy kivymd") + sys.exit(1) + + # Run Kivy UI - this will block until UI exits + run_kivy_ui(headless_service) + + elif args.ui: + # Configure logging for UI mode (no console output) + setup_logging(ui_mode=True) + + # Special handling for UI mode + logger.info("Starting in UI mode...") + + # Create nickname + nickname = args.nick or f"peer-{time.time():.0f}" + + # Create headless service + strict_signing = not args.no_strict_signing # Default True, False if --no-strict-signing is used + headless_service = HeadlessService( + nickname=nickname, + port=args.port, + connect_addrs=args.connect, + strict_signing=strict_signing, + seed=args.seed, + topic=args.topic + ) + + # Start headless service in background thread + logger.info("Starting headless service in background thread...") + ready_event = threading.Event() + headless_thread = run_headless_in_thread(headless_service, ready_event) + + logger.info("Starting Textual UI in main thread...") + + # Create and run UI in main thread + ui = ChatUI( + headless_service=headless_service, + message_queue=headless_service.get_message_queue(), + system_queue=headless_service.get_system_queue() + ) + + # Run UI - this will block until UI exits + ui.run() + + else: + # Configure logging for non-UI mode (console output enabled) + setup_logging(ui_mode=False) + + # Run the main async function for other modes + trio.run(main_async, args) + + except KeyboardInterrupt: + logger.info("Application terminated by user") + except Exception as e: + logger.error(f"Application error: {e}") + logger.error(f"Traceback:\n{traceback.format_exc()}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/py-peer/py-peer-frontend/.env.example b/py-peer/py-peer-frontend/.env.example new file mode 100644 index 00000000..04f7603f --- /dev/null +++ b/py-peer/py-peer-frontend/.env.example @@ -0,0 +1,9 @@ +# Copy this file to .env.local for local development, or set these variables +# in your Vercel / Netlify dashboard for production deployments. + +# Full origin (scheme + host + optional port) of the py-peer Tornado backend. +# Leave empty in local dev — the Vite proxy handles /api/* and /ws/* for you. +# Examples: +# http://123.45.67.89:8765 (bare IP, HTTP) +# https://py-peer.example.com (custom domain, HTTPS) +VITE_API_URL=xxxxxxxxx diff --git a/py-peer/py-peer-frontend/.gitignore b/py-peer/py-peer-frontend/.gitignore new file mode 100644 index 00000000..f4650bbd --- /dev/null +++ b/py-peer/py-peer-frontend/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.env.local +.env.*.local diff --git a/py-peer/py-peer-frontend/index.html b/py-peer/py-peer-frontend/index.html new file mode 100644 index 00000000..61c36c8d --- /dev/null +++ b/py-peer/py-peer-frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Universal Connectivity – py-peer + + +
+ + + diff --git a/py-peer/py-peer-frontend/package-lock.json b/py-peer/py-peer-frontend/package-lock.json new file mode 100644 index 00000000..934ba94e --- /dev/null +++ b/py-peer/py-peer-frontend/package-lock.json @@ -0,0 +1,2923 @@ +{ + "name": "py-peer-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "py-peer-frontend", + "version": "0.1.0", + "dependencies": { + "@headlessui/react": "^2.0.3", + "@heroicons/react": "^2.1.3", + "react": "^18.3.1", + "react-18-blockies": "^1.0.3", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3", + "typescript": "^5.4.5", + "vite": "^5.2.11" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@headlessui/react": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz", + "integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.9", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@react-aria/focus": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.4.tgz", + "integrity": "sha512-6gz+j9ip0/vFRTKJMl3R30MHopn4i19HqqLfSQfElxJD+r9hBnYG1Q6Wd/kl/WRR1+CALn2F+rn06jUnf5sT8Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.27.0", + "@react-aria/utils": "^3.33.0", + "@react-types/shared": "^3.33.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.27.0.tgz", + "integrity": "sha512-D27pOy+0jIfHK60BB26AgqjjRFOYdvVSkwC31b2LicIzRCSPOSP06V4gMHuGmkhNTF4+YWDi1HHYjxIvMeiSlA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.0", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.33.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.33.0.tgz", + "integrity": "sha512-yvz7CMH8d2VjwbSa5nGXqjU031tYhD8ddax95VzJsHSPyqHDEGfxul8RkhGV6oO7bVqZxVs6xY66NIgae+FHjw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.33.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.11.0.tgz", + "integrity": "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.33.0.tgz", + "integrity": "sha512-xuUpP6MyuPmJtzNOqF5pzFUIHH2YogyOQfUQHag54PRmWB7AbjuGWBUv0l1UDmz6+AbzAYGmDVAzcRDOu2PFpw==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.19", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.19.tgz", + "integrity": "sha512-KzwmU1IbE0IvCZSm6OXkS+kRdrgW2c2P3Ho3NC+zZXWK6oObv/L+lcV/2VuJ+snVESRlMJ+w/fg4WXI/JzoNGQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.19", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.19.tgz", + "integrity": "sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001776", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz", + "integrity": "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-18-blockies": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/react-18-blockies/-/react-18-blockies-1.0.6.tgz", + "integrity": "sha512-MPdEdfYm8wO2XEdTDEpwIJJBE0/zdkmZqGbEcypK6EV+nKeqE4qGs6XfBAs6VcrSkZ0ssu8jZNd0qXrplruVqg==", + "license": "MIT", + "peerDependencies": { + "react": ">=18.2.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "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": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "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" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/py-peer/py-peer-frontend/package.json b/py-peer/py-peer-frontend/package.json new file mode 100644 index 00000000..ec158f36 --- /dev/null +++ b/py-peer/py-peer-frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "py-peer-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint src --ext ts,tsx" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-18-blockies": "^1.0.3", + "@heroicons/react": "^2.1.3", + "@headlessui/react": "^2.0.3" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3", + "typescript": "^5.4.5", + "vite": "^5.2.11" + } +} diff --git a/py-peer/py-peer-frontend/postcss.config.js b/py-peer/py-peer-frontend/postcss.config.js new file mode 100644 index 00000000..2e7af2b7 --- /dev/null +++ b/py-peer/py-peer-frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/py-peer/py-peer-frontend/public/favicon.ico b/py-peer/py-peer-frontend/public/favicon.ico new file mode 100644 index 00000000..a23747b4 Binary files /dev/null and b/py-peer/py-peer-frontend/public/favicon.ico differ diff --git a/py-peer/py-peer-frontend/public/libp2p-hero.svg b/py-peer/py-peer-frontend/public/libp2p-hero.svg new file mode 100644 index 00000000..b9ab373f --- /dev/null +++ b/py-peer/py-peer-frontend/public/libp2p-hero.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/py-peer/py-peer-frontend/public/libp2p-logo.svg b/py-peer/py-peer-frontend/public/libp2p-logo.svg new file mode 100644 index 00000000..9c3b9360 --- /dev/null +++ b/py-peer/py-peer-frontend/public/libp2p-logo.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/py-peer/py-peer-frontend/src/App.tsx b/py-peer/py-peer-frontend/src/App.tsx new file mode 100644 index 00000000..0e901c3f --- /dev/null +++ b/py-peer/py-peer-frontend/src/App.tsx @@ -0,0 +1,40 @@ +import { useState } from 'react' +import { PyPeerProvider, usePyPeer } from './context/PyPeerContext' +import Nav from './components/Nav' +import Chat from './components/Chat' +import ConnectionPanel from './components/ConnectionPanel' +import Booting from './components/Booting' +import LibP2PAssistant from './components/LibP2PAssistant' + +function AppInner() { + const { loading, error } = usePyPeer() + const [panelOpen, setPanelOpen] = useState(false) + + if (loading || error) { + return + } + + return ( +
+
+ ) +} + +export default function App() { + return ( + + + + ) +} diff --git a/py-peer/py-peer-frontend/src/api/client.ts b/py-peer/py-peer-frontend/src/api/client.ts new file mode 100644 index 00000000..7825be68 --- /dev/null +++ b/py-peer/py-peer-frontend/src/api/client.ts @@ -0,0 +1,149 @@ +// ─── Types ──────────────────────────────────────────────────────────────────── + +export interface ApiResponse { + success: boolean + data: T + error: { code: number; message: string; detail: string | null } | null + timestamp: number +} + +export interface NodeInfo { + peer_id: string + nickname: string + multiaddr: string + port: number + ready: boolean + uptime_seconds: number +} + +export interface ServiceStatus { + ready: boolean + running: boolean + uptime_seconds: number + peer_count: number +} + +export interface ServiceConfig { + nickname: string + port: number + topic: string | null + strict_signing: boolean + download_dir: string + connect_addrs: string[] +} + +export interface TopicInfo { + unread_count: number + total_count: number + last_message: ChatMessage | null +} + +export interface ChatMessage { + type: 'chat_message' | 'file_message' | 'file_shared' | 'file_downloaded' + message?: string + sender_nick: string + sender_id: string + timestamp: number + topic: string + read: boolean + file_cid?: string + file_name?: string + file_size?: number +} + +export interface PubSubConfig { + degree: number + degree_low: number + degree_high: number + heartbeat_interval: number + protocols: string[] +} + +export interface DHTStatus { + mode: string + random_walk_enabled: boolean + routing_table_size: number +} + +// ─── Base URL ───────────────────────────────────────────────────────────────── +// In development the Vite proxy forwards /api/* → localhost:8765, so VITE_API_URL +// can be left empty. In production (Vercel / Netlify) set it to your backend's +// full origin, e.g. VITE_API_URL=https://your-backend.example.com + +const API_ORIGIN: string = import.meta.env.VITE_API_URL ?? '' +export const BASE = `${API_ORIGIN}/api/v1` + +async function get(path: string): Promise { + const res = await fetch(`${BASE}${path}`) + const json: ApiResponse = await res.json() + if (!json.success) throw new Error(json.error?.message ?? 'API error') + return json.data +} + +async function post(path: string, body?: unknown): Promise { + const res = await fetch(`${BASE}${path}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: body ? JSON.stringify(body) : undefined, + }) + const json: ApiResponse = await res.json() + if (!json.success) throw new Error(json.error?.message ?? 'API error') + return json.data +} + +async function put(path: string): Promise { + const res = await fetch(`${BASE}${path}`, { method: 'PUT' }) + const json: ApiResponse = await res.json() + if (!json.success) throw new Error(json.error?.message ?? 'API error') + return json.data +} + +// ─── Node ───────────────────────────────────────────────────────────────────── + +export const getNodeInfo = () => get('/node/info') +export const getServiceStatus = () => get('/service/status') +export const getServiceConfig = () => get('/service/config') + +// ─── Peers ──────────────────────────────────────────────────────────────────── + +export const getPeers = () => get<{ peers: string[]; count: number }>('/peers') +export const getKnownPeers = () => get<{ peers: string[]; count: number }>('/peers/known') +export const connectToPeer = (multiaddr: string) => + post<{ message: string; multiaddr: string }>('/peers/connect', { multiaddr }) + +// ─── Topics ─────────────────────────────────────────────────────────────────── + +export const getTopics = () => + get<{ topics: Record; count: number }>('/topics') +export const subscribeTopic = (topic: string) => + post<{ message: string; topic: string }>('/topics', { topic }) + +// ─── Messages ───────────────────────────────────────────────────────────────── + +export const getMessages = (topic: string, limit = 100, offset = 0) => + get<{ messages: ChatMessage[]; total: number; limit: number; offset: number }>( + `/messages/${encodeURIComponent(topic)}?limit=${limit}&offset=${offset}`, + ) +export const sendMessage = (topic: string, message: string) => + post<{ message: string; topic: string }>(`/messages/${encodeURIComponent(topic)}`, { message }) +export const getUnread = (topic: string) => + get<{ unread_count: number }>(`/messages/${encodeURIComponent(topic)}/unread`) +export const markRead = (topic: string) => + put<{ message: string }>(`/messages/${encodeURIComponent(topic)}/read`) + +// ─── PubSub / DHT ───────────────────────────────────────────────────────────── + +export const getPubSubConfig = () => get('/pubsub/config') +export const getDHTStatus = () => get('/dht/status') +export const getPubSubMesh = () => + get<{ mesh: Record; total_mesh_peers: number }>('/pubsub/mesh') + +// ─── WebSocket helpers ──────────────────────────────────────────────────────── + +export const WS_BASE: string = API_ORIGIN + ? API_ORIGIN.replace(/^http/, 'ws') + : `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}` + +export const wsMessages = () => new WebSocket(`${WS_BASE}/ws/messages`) +export const wsPeers = () => new WebSocket(`${WS_BASE}/ws/peers`) +export const wsSystem = () => new WebSocket(`${WS_BASE}/ws/system`) diff --git a/py-peer/py-peer-frontend/src/components/Booting.tsx b/py-peer/py-peer-frontend/src/components/Booting.tsx new file mode 100644 index 00000000..39e82893 --- /dev/null +++ b/py-peer/py-peer-frontend/src/components/Booting.tsx @@ -0,0 +1,20 @@ +import Spinner from './Spinner' + +export default function Booting({ error }: { error?: string }) { + return ( +
+ libp2p +

Universal Connectivity – py-peer

+ {error ? ( +
+ {error} +
+ ) : ( +
+ + Connecting to py-peer API on localhost:8765… +
+ )} +
+ ) +} diff --git a/py-peer/py-peer-frontend/src/components/Chat.tsx b/py-peer/py-peer-frontend/src/components/Chat.tsx new file mode 100644 index 00000000..e845c3af --- /dev/null +++ b/py-peer/py-peer-frontend/src/components/Chat.tsx @@ -0,0 +1,187 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { PaperAirplaneIcon } from '@heroicons/react/24/solid' +import { UsersIcon } from '@heroicons/react/24/outline' +import { usePyPeer } from '../context/PyPeerContext' +import MessageItem from './MessageItem' +import Spinner from './Spinner' + +export default function Chat() { + const { + nodeInfo, + topics, + messages, + activeTopic, + setActiveTopic, + sendMessage, + markRead, + connectedPeers, + } = usePyPeer() + + const [input, setInput] = useState('') + const [sending, setSending] = useState(false) + const [showMobilePeers, setShowMobilePeers] = useState(false) + const bottomRef = useRef(null) + + // Scroll to bottom on new messages + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [messages, activeTopic]) + + // Mark as read when topic becomes active + useEffect(() => { + if (activeTopic) markRead(activeTopic) + }, [activeTopic, markRead]) + + const handleSend = useCallback( + async (e?: React.FormEvent) => { + e?.preventDefault() + const text = input.trim() + if (!text || !activeTopic) return + setSending(true) + try { + await sendMessage(activeTopic, text) + setInput('') + } catch { /* ignore */ } + finally { + setSending(false) + } + }, + [input, activeTopic, sendMessage], + ) + + const handleKey = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + + const activeMessages = messages[activeTopic] ?? [] + const myPeerId = nodeInfo?.peer_id ?? '' + + const topicList = Object.entries(topics) + + return ( +
+ {/* ── Topic sidebar ─────────────────────────────────────────────────── */} + + + {/* ── Chat area ─────────────────────────────────────────────────────── */} +
+ {/* Chat header */} +
+
+ + {activeTopic ? `# ${activeTopic}` : 'Select a topic'} + +
+ + +
+ + {/* Mobile peer list overlay */} + {showMobilePeers && ( +
+

Connected Peers

+ {connectedPeers.length === 0 ? ( +

None

+ ) : ( + connectedPeers.map((p) => ( +

{p}

+ )) + )} +
+ )} + + {/* Messages */} +
+ {activeMessages.length === 0 ? ( +
+ 💬 + No messages yet in # {activeTopic} +
+ ) : ( + activeMessages.map((msg, i) => ( + + )) + )} +
+
+ + {/* Input */} +
+