├── web
├── .gitignore
├── src
│ ├── main.ts
│ ├── env.d.ts
│ ├── tsconfig.json
│ ├── signalingClient.ts
│ ├── webrtc.ts
│ ├── tunnel.html
│ ├── sw.ts
│ └── tunnel.ts
├── .env
├── .prettierrc
├── sw
│ ├── tsconfig.json
│ ├── sw.ts
│ └── http.ts
├── tsconfig-base.json
├── package.json
├── rollup.config.js
└── package-lock.json
├── demo.mov
├── diagram.png
├── internal
├── signaling
│ ├── message.go
│ ├── api.go
│ ├── server.go
│ ├── room.go
│ └── client.go
└── tunnel
│ ├── reverseproxy.go
│ ├── tunnel.go
│ ├── http.go
│ └── hub.go
├── cmd
├── signaling-server
│ └── main.go
└── web-p2p-tunnel
│ └── main.go
├── .github
└── workflows
│ └── pages.yaml
├── go.mod
├── README.md
└── go.sum
/web/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
3 |
--------------------------------------------------------------------------------
/web/src/main.ts:
--------------------------------------------------------------------------------
1 | window.location.href = '/tunnel';
2 |
--------------------------------------------------------------------------------
/web/.env:
--------------------------------------------------------------------------------
1 | PUBLIC_SIGNALING_SERVER_URL="http://localhost:8080"
2 |
--------------------------------------------------------------------------------
/demo.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewmthomas87/web-p2p-tunnel/HEAD/demo.mov
--------------------------------------------------------------------------------
/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewmthomas87/web-p2p-tunnel/HEAD/diagram.png
--------------------------------------------------------------------------------
/web/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "singleQuote": true,
4 | "plugins": ["prettier-plugin-organize-imports"]
5 | }
6 |
--------------------------------------------------------------------------------
/web/src/env.d.ts:
--------------------------------------------------------------------------------
1 | interface ImportMetaEnv {
2 | PUBLIC_SIGNALING_SERVER_URL: string;
3 | }
4 |
5 | interface ImportMeta {
6 | readonly env: ImportMetaEnv;
7 | }
8 |
--------------------------------------------------------------------------------
/web/sw/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig-base.json",
3 | "compilerOptions": {
4 | "lib": ["ES2020", "WebWorker"]
5 | },
6 | "include": ["."]
7 | }
8 |
--------------------------------------------------------------------------------
/web/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig-base.json",
3 | "compilerOptions": {
4 | "lib": ["ES2020", "DOM", "DOM.Iterable"]
5 | },
6 | "include": ["."]
7 | }
8 |
--------------------------------------------------------------------------------
/internal/signaling/message.go:
--------------------------------------------------------------------------------
1 | package signaling
2 |
3 | import "encoding/json"
4 |
5 | type Message struct {
6 | Type string `json:"type"`
7 | Data json.RawMessage `json:"data"`
8 | }
9 |
10 | type ServerMessage struct {
11 | Message
12 |
13 | ClientID string `json:"clientID"`
14 | }
15 |
--------------------------------------------------------------------------------
/web/tsconfig-base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ESNext",
5 | "skipLibCheck": true,
6 | "moduleResolution": "bundler",
7 | "isolatedModules": true,
8 | "noEmit": true,
9 | "strict": true,
10 | "noUnusedLocals": true,
11 | "noUnusedParameters": true,
12 | "noFallthroughCasesInSwitch": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/internal/signaling/api.go:
--------------------------------------------------------------------------------
1 | package signaling
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | "net/url"
7 | )
8 |
9 | func CreateRoom(signalingServerURL *url.URL) (string, error) {
10 | createRoomURL := signalingServerURL.JoinPath("rooms")
11 | resp, err := http.Post(createRoomURL.String(), "", nil)
12 | if err != nil {
13 | return "", err
14 | }
15 |
16 | body, err := io.ReadAll(resp.Body)
17 | resp.Body.Close()
18 | if err != nil {
19 | return "", err
20 | }
21 |
22 | return string(body), nil
23 | }
24 |
--------------------------------------------------------------------------------
/internal/tunnel/reverseproxy.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "fmt"
5 | "net/http/httputil"
6 | "net/url"
7 | )
8 |
9 | func newSingleHostReverseProxy(target *url.URL, changeHostHeader, changeOriginHeader bool) *httputil.ReverseProxy {
10 | return &httputil.ReverseProxy{
11 | Rewrite: func(r *httputil.ProxyRequest) {
12 | r.SetURL(target)
13 | r.SetXForwarded()
14 |
15 | if !changeHostHeader {
16 | r.Out.Host = r.In.Host
17 | }
18 |
19 | if changeOriginHeader {
20 | r.Out.Header.Set("Origin", fmt.Sprintf("%s://%s", target.Scheme, target.Host))
21 | }
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web-p2p-tunnel",
3 | "type": "module",
4 | "private": true,
5 | "scripts": {
6 | "build": "rollup --config",
7 | "build-watch": "rollup --config --watch",
8 | "serve": "serve dist/"
9 | },
10 | "devDependencies": {
11 | "@rollup/plugin-html": "^1.0.3",
12 | "@rollup/plugin-replace": "^5.0.5",
13 | "@rollup/plugin-terser": "^0.4.4",
14 | "@rollup/plugin-typescript": "^11.1.6",
15 | "dotenv": "^16.4.5",
16 | "prettier": "^3.2.5",
17 | "prettier-plugin-organize-imports": "^3.2.4",
18 | "rollup": "^4.14.1",
19 | "serve": "^14.2.1",
20 | "tslib": "^2.6.2",
21 | "typescript": "^5.4.4"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/cmd/signaling-server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "log"
6 | "net/http"
7 |
8 | "github.com/andrewmthomas87/web-p2p-tunnel/internal/signaling"
9 | "github.com/gorilla/websocket"
10 | )
11 |
12 | var (
13 | addr = flag.String("addr", ":8080", "http server address")
14 | upgrader = &websocket.Upgrader{
15 | CheckOrigin: func(r *http.Request) bool {
16 | return true
17 | },
18 | }
19 | )
20 |
21 | func main() {
22 | flag.Parse()
23 |
24 | s := signaling.NewServer(upgrader)
25 |
26 | http.HandleFunc("/rooms", s.CreateRoomHandler)
27 | http.HandleFunc("/ws", s.WebSocketHandler)
28 |
29 | log.Printf("signaling-server starting at %s...", *addr)
30 | log.Fatal(http.ListenAndServe(*addr, nil))
31 | }
32 |
--------------------------------------------------------------------------------
/web/src/signalingClient.ts:
--------------------------------------------------------------------------------
1 | export function connectSignalingClient(roomID: string, serverURL: string, statusEl: HTMLElement) {
2 | const wsURL = new URL('ws', serverURL);
3 | wsURL.search = new URLSearchParams({ role: 'client', 'room-id': roomID }).toString();
4 | if (wsURL.protocol === 'https:') {
5 | wsURL.protocol = 'wss:';
6 | } else {
7 | wsURL.protocol = 'ws:';
8 | }
9 |
10 | const ws = new WebSocket(wsURL);
11 |
12 | ws.addEventListener('open', () => {
13 | statusEl.innerText = readyStateToString(ws.readyState);
14 | });
15 | ws.addEventListener('close', () => {
16 | statusEl.innerText = readyStateToString(ws.readyState);
17 | });
18 | ws.addEventListener('error', () => {
19 | alert('Signaling error');
20 | });
21 |
22 | return ws;
23 | }
24 |
25 | function readyStateToString(readyState: number): string {
26 | switch (readyState) {
27 | case WebSocket.CONNECTING:
28 | return 'connecting';
29 | case WebSocket.OPEN:
30 | return 'open';
31 | case WebSocket.CLOSING:
32 | return 'closing';
33 | case WebSocket.CLOSED:
34 | return 'closed';
35 | default:
36 | return 'unknown';
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.github/workflows/pages.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy site to Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | workflow_dispatch:
9 |
10 | permissions:
11 | contents: read
12 | pages: write
13 | id-token: write
14 |
15 | concurrency:
16 | group: "pages"
17 | cancel-in-progress: false
18 |
19 | jobs:
20 | build:
21 | runs-on: ubuntu-latest
22 | defaults:
23 | run:
24 | working-directory: ./web
25 | steps:
26 | - uses: actions/checkout@v4
27 | - uses: actions/setup-node@v4
28 | with:
29 | cache: "npm"
30 | cache-dependency-path: "web/package-lock.json"
31 | - run: npm ci
32 | - run: npm run build
33 | env:
34 | PUBLIC_SIGNALING_SERVER_URL: ${{ vars.PUBLIC_SIGNALING_SERVER_URL }}
35 | - uses: actions/upload-pages-artifact@v3
36 | with:
37 | path: ./web/dist
38 |
39 | deploy:
40 | environment:
41 | name: github-pages
42 | url: ${{ steps.deployment.outputs.page_url }}
43 | runs-on: ubuntu-latest
44 | needs: build
45 | steps:
46 | - name: Deploy to GitHub Pages
47 | id: deployment
48 | uses: actions/deploy-pages@v4
49 |
--------------------------------------------------------------------------------
/web/src/webrtc.ts:
--------------------------------------------------------------------------------
1 | export async function connectWebRTC(sc: WebSocket, statusEl: HTMLElement) {
2 | const pc = new RTCPeerConnection({
3 | iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
4 | });
5 |
6 | statusEl.innerText = pc.connectionState;
7 | pc.addEventListener('connectionstatechange', () => {
8 | statusEl.innerText = pc.connectionState;
9 | });
10 |
11 | sc.addEventListener('message', (ev) => {
12 | const message = JSON.parse(ev.data);
13 | switch (message.type) {
14 | case 'answer':
15 | pc.setRemoteDescription(message.data);
16 | break;
17 |
18 | case 'icecandidate':
19 | pc.addIceCandidate(message.data);
20 | break;
21 | }
22 | });
23 |
24 | pc.addEventListener('icecandidate', (ev) => {
25 | if (ev.candidate === null) {
26 | return;
27 | }
28 |
29 | sc.send(
30 | JSON.stringify({
31 | type: 'icecandidate',
32 | data: ev.candidate,
33 | }),
34 | );
35 | });
36 |
37 | pc.createDataChannel('control');
38 |
39 | const offer = await pc.createOffer();
40 | sc.send(
41 | JSON.stringify({
42 | type: 'offer',
43 | data: offer,
44 | }),
45 | );
46 |
47 | await pc.setLocalDescription(offer);
48 |
49 | return pc;
50 | }
51 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/andrewmthomas87/web-p2p-tunnel
2 |
3 | go 1.21
4 |
5 | require (
6 | github.com/google/uuid v1.6.0
7 | github.com/gorilla/websocket v1.5.1
8 | github.com/pion/webrtc/v4 v4.0.0-beta.16
9 | golang.org/x/sync v0.1.0
10 | )
11 |
12 | require (
13 | github.com/davecgh/go-spew v1.1.1 // indirect
14 | github.com/pion/datachannel v1.5.6 // indirect
15 | github.com/pion/dtls/v2 v2.2.10 // indirect
16 | github.com/pion/ice/v3 v3.0.5 // indirect
17 | github.com/pion/interceptor v0.1.27 // indirect
18 | github.com/pion/logging v0.2.2 // indirect
19 | github.com/pion/mdns/v2 v2.0.7 // indirect
20 | github.com/pion/randutil v0.1.0 // indirect
21 | github.com/pion/rtcp v1.2.14 // indirect
22 | github.com/pion/rtp v1.8.5 // indirect
23 | github.com/pion/sctp v1.8.14 // indirect
24 | github.com/pion/sdp/v3 v3.0.9 // indirect
25 | github.com/pion/srtp/v3 v3.0.1 // indirect
26 | github.com/pion/stun/v2 v2.0.0 // indirect
27 | github.com/pion/transport/v2 v2.2.4 // indirect
28 | github.com/pion/transport/v3 v3.0.2 // indirect
29 | github.com/pion/turn/v3 v3.0.1 // indirect
30 | github.com/pmezard/go-difflib v1.0.0 // indirect
31 | github.com/stretchr/testify v1.9.0 // indirect
32 | golang.org/x/crypto v0.21.0 // indirect
33 | golang.org/x/net v0.22.0 // indirect
34 | golang.org/x/sys v0.18.0 // indirect
35 | gopkg.in/yaml.v3 v3.0.1 // indirect
36 | )
37 |
--------------------------------------------------------------------------------
/web/src/tunnel.html:
--------------------------------------------------------------------------------
1 |
web-p2p-tunnel
2 |
3 |
4 |
5 |
6 | Tunnel
7 |
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | | Name |
22 | Status |
23 |
24 |
25 |
26 |
27 | | Service worker |
28 | unknown |
29 |
30 |
31 | | Signaling |
32 | uninitialized |
33 |
34 |
35 | | WebRTC |
36 | uninitialized |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Root
45 |
46 |
47 |
48 |
49 | Requests
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | | ID |
62 | Method |
63 | URL |
64 | Headers |
65 | Body? |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
85 |
--------------------------------------------------------------------------------
/internal/signaling/server.go:
--------------------------------------------------------------------------------
1 | package signaling
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "os"
7 | "sync"
8 |
9 | "github.com/google/uuid"
10 | "github.com/gorilla/websocket"
11 | )
12 |
13 | type Server struct {
14 | log *log.Logger
15 |
16 | upgrader *websocket.Upgrader
17 |
18 | rooms map[string]*Room
19 | roomsLock sync.RWMutex
20 | }
21 |
22 | func NewServer(upgrader *websocket.Upgrader) *Server {
23 | return &Server{
24 | log: log.New(os.Stderr, "[Server] ", log.LstdFlags),
25 | upgrader: upgrader,
26 | rooms: make(map[string]*Room),
27 | }
28 | }
29 |
30 | func (s *Server) CreateRoomHandler(w http.ResponseWriter, r *http.Request) {
31 | if r.Method != "POST" {
32 | http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
33 | return
34 | }
35 |
36 | id := uuid.NewString()
37 | room := NewRoom(id)
38 |
39 | s.roomsLock.Lock()
40 | s.rooms[id] = room
41 | s.roomsLock.Unlock()
42 |
43 | s.log.Printf("Created room %s", id)
44 |
45 | if _, err := w.Write([]byte(id)); err != nil {
46 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
47 | return
48 | }
49 | }
50 |
51 | func (s *Server) WebSocketHandler(w http.ResponseWriter, r *http.Request) {
52 | role := r.URL.Query().Get("role")
53 | roomID := r.URL.Query().Get("room-id")
54 | if !((role == "client" || role == "server") && roomID != "") {
55 | http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
56 | return
57 | }
58 |
59 | s.roomsLock.RLock()
60 | room, ok := s.rooms[roomID]
61 | s.roomsLock.RUnlock()
62 | if !ok {
63 | http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
64 | return
65 | }
66 |
67 | conn, err := s.upgrader.Upgrade(w, r, nil)
68 | if err != nil {
69 | return
70 | }
71 |
72 | s.log.Printf("Adding %s ws conn to room %s...", role, room.ID[:6])
73 |
74 | if role == "client" {
75 | room.HandleClientConn(conn)
76 | } else {
77 | room.HandleServerConn(conn)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/cmd/web-p2p-tunnel/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "log"
7 | "net/url"
8 | "os"
9 | "os/signal"
10 | "time"
11 |
12 | "github.com/andrewmthomas87/web-p2p-tunnel/internal/signaling"
13 | "github.com/andrewmthomas87/web-p2p-tunnel/internal/tunnel"
14 | "github.com/pion/webrtc/v4"
15 | "golang.org/x/sync/errgroup"
16 | )
17 |
18 | var (
19 | signalingServerURLStr = flag.String("signaling-server-url", "http://localhost:8080", "signaling server url")
20 | tunnelTargetURLStr = flag.String("tunnel-target-url", "", "tunnel target url")
21 | changeHostHeader = flag.Bool(
22 | "change-host-header",
23 | false,
24 | "change the Host header to the host of the target url",
25 | )
26 | changeOriginHeader = flag.Bool(
27 | "change-origin-header",
28 | false,
29 | "change the Origin header to the origin of the target url",
30 | )
31 |
32 | defaultWebrtcConfig = webrtc.Configuration{
33 | ICEServers: []webrtc.ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}},
34 | }
35 | )
36 |
37 | func main() {
38 | flag.Parse()
39 |
40 | signalingServerURL, err := url.Parse(*signalingServerURLStr)
41 | if err != nil {
42 | log.Fatal(err)
43 | }
44 |
45 | tunnelTargetURL, err := url.Parse(*tunnelTargetURLStr)
46 | if err != nil {
47 | log.Fatal(err)
48 | }
49 |
50 | roomID, err := signaling.CreateRoom(signalingServerURL)
51 | if err != nil {
52 | log.Fatal(err)
53 | }
54 |
55 | log.Printf("Created room %s", roomID)
56 |
57 | sc := signaling.NewClient(roomID, signalingServerURL)
58 | if err := sc.Connect(); err != nil {
59 | log.Fatal(err)
60 | }
61 |
62 | th := tunnel.NewHub(tunnelTargetURL, *changeHostHeader, *changeOriginHeader, defaultWebrtcConfig)
63 |
64 | ctx, cancel := context.WithCancel(context.Background())
65 | g, ctx := errgroup.WithContext(ctx)
66 |
67 | g.Go(func() error {
68 | return sc.Run(ctx)
69 | })
70 | g.Go(func() error {
71 | return th.Run(ctx, sc)
72 | })
73 |
74 | interrupt := make(chan os.Signal, 1)
75 | signal.Notify(interrupt, os.Interrupt)
76 |
77 | select {
78 | case <-ctx.Done():
79 | case <-interrupt:
80 | log.Println("signal: interrupt")
81 |
82 | cancel()
83 | }
84 |
85 | t := time.AfterFunc(2*time.Second, func() {
86 | os.Exit(1)
87 | })
88 | defer t.Stop()
89 |
90 | if err := g.Wait(); err != nil {
91 | log.Fatal(err)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/web/src/sw.ts:
--------------------------------------------------------------------------------
1 | export type RequestData = {
2 | id: number;
3 | method: string;
4 | url: string;
5 | headersList: [string, any][];
6 | hasBody: boolean;
7 | serialized: ArrayBuffer;
8 | };
9 |
10 | export async function setupSW(
11 | tunnel: (serialized: ArrayBuffer) => Promise,
12 | statusEl: HTMLElement,
13 | requestsEl: HTMLElement,
14 | ) {
15 | if (!('serviceWorker' in navigator)) {
16 | statusEl.innerText = 'error: not supported';
17 | alert('Error: service workers are not supported');
18 | throw new Error('service workers are not supported');
19 | }
20 |
21 | let registration: ServiceWorkerRegistration;
22 | try {
23 | registration = await navigator.serviceWorker.register('/sw.js', { type: 'module' });
24 | } catch (error) {
25 | alert('Error: failed to register service worker');
26 | throw error;
27 | }
28 |
29 | const sw = registration.installing || registration.waiting || registration.active;
30 | if (sw) {
31 | statusEl.innerText = sw.state;
32 |
33 | sw.addEventListener('statechange', (ev) => {
34 | statusEl.innerText = (ev.target as ServiceWorker).state;
35 | });
36 | }
37 |
38 | navigator.serviceWorker.addEventListener('message', async (ev) => {
39 | if (ev.data.type === 'request') {
40 | const data = ev.data as RequestData;
41 | addToTable(data, requestsEl);
42 |
43 | let resp;
44 | try {
45 | resp = await tunnel(data.serialized);
46 | } catch (ex) {
47 | if (ex instanceof ArrayBuffer) {
48 | resp = ex;
49 | } else {
50 | throw ex;
51 | }
52 | }
53 |
54 | ev.source?.postMessage(
55 | {
56 | type: 'response',
57 | id: data.id,
58 | serialized: resp,
59 | },
60 | { transfer: [resp] },
61 | );
62 | }
63 | });
64 | }
65 |
66 | function addToTable(
67 | { id, method, url, headersList, hasBody }: RequestData,
68 | requestsEl: HTMLElement,
69 | ) {
70 | const headersStr = headersList.map(([key, value]) => `${key}: ${value}`).join('\n');
71 |
72 | const tr = document.createElement('tr');
73 | tr.innerHTML = `
74 | ${id} |
75 | ${method} |
76 | ${url} |
77 |
78 |
79 | ${headersList.length} header${headersList.length !== 1 ? 's' : ''}
80 | ${headersStr}
81 |
82 | |
83 | ${hasBody ? 'true' : 'false'} | `;
84 | requestsEl.querySelector('tbody')?.appendChild(tr);
85 | }
86 |
--------------------------------------------------------------------------------
/internal/tunnel/tunnel.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "net/http/cookiejar"
8 | "os"
9 |
10 | "github.com/pion/webrtc/v4"
11 | )
12 |
13 | type Tunnel struct {
14 | log *log.Logger
15 |
16 | client *http.Client
17 | pc *webrtc.PeerConnection
18 | }
19 |
20 | func NewTunnel(
21 | webrtcConfig webrtc.Configuration,
22 | transport http.RoundTripper,
23 | clientID string,
24 | onICECandidate func(*webrtc.ICECandidate),
25 | ) (*Tunnel, error) {
26 | pc, err := webrtc.NewPeerConnection(webrtcConfig)
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | jar, err := cookiejar.New(nil)
32 | if err != nil {
33 | return nil, err
34 | }
35 | client := &http.Client{
36 | Jar: jar,
37 | Transport: transport,
38 | CheckRedirect: checkRedirect,
39 | }
40 |
41 | t := &Tunnel{
42 | log: log.New(os.Stderr, fmt.Sprintf("[Tunnel %s] ", clientID[:6]), log.LstdFlags),
43 | client: client,
44 | pc: pc,
45 | }
46 |
47 | pc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) {
48 | t.log.Printf("Connection state change: %s", pcs)
49 | })
50 | pc.OnICECandidate(onICECandidate)
51 | pc.OnDataChannel(t.onDataChannel)
52 |
53 | return t, nil
54 | }
55 |
56 | func (t *Tunnel) Close() error {
57 | t.log.Println("Closing...")
58 |
59 | t.client.CloseIdleConnections()
60 | return t.pc.Close()
61 | }
62 |
63 | func (t *Tunnel) RegisterOffer(offer webrtc.SessionDescription) (webrtc.SessionDescription, error) {
64 | t.log.Println("Registering offer...")
65 |
66 | if err := t.pc.SetRemoteDescription(offer); err != nil {
67 | return webrtc.SessionDescription{}, err
68 | }
69 |
70 | t.log.Println("Creating answer...")
71 |
72 | answer, err := t.pc.CreateAnswer(nil)
73 | if err != nil {
74 | return webrtc.SessionDescription{}, err
75 | }
76 |
77 | if err := t.pc.SetLocalDescription(answer); err != nil {
78 | return webrtc.SessionDescription{}, err
79 | }
80 |
81 | return answer, nil
82 | }
83 |
84 | func (t *Tunnel) AddICECandidate(candidate webrtc.ICECandidateInit) error {
85 | t.log.Println("Adding ICE candidate...")
86 |
87 | return t.pc.AddICECandidate(candidate)
88 | }
89 |
90 | func (t *Tunnel) onDataChannel(dc *webrtc.DataChannel) {
91 | t.log.Printf("Data Channel %s, %d", dc.Label(), *dc.ID())
92 |
93 | if dc.Label() == "http" {
94 | hdc := NewHTTPDataChannel(t.client, dc)
95 | go hdc.Run()
96 | }
97 | }
98 |
99 | func checkRedirect(req *http.Request, via []*http.Request) error {
100 | firstReq := req
101 | if len(via) > 0 {
102 | firstReq = via[0]
103 | }
104 |
105 | redirect := firstReq.Header.Get("Web-P2p-Tunnel-Redirect")
106 | if redirect != "follow" {
107 | return http.ErrUseLastResponse
108 | }
109 |
110 | return nil
111 | }
112 |
--------------------------------------------------------------------------------
/web/sw/sw.ts:
--------------------------------------------------------------------------------
1 | import { deserializeResponse, serializeRequest } from './http';
2 |
3 | const sw = self as ServiceWorkerGlobalScope & typeof globalThis;
4 |
5 | sw.addEventListener('install', () => {
6 | sw.skipWaiting();
7 | });
8 |
9 | sw.addEventListener('activate', (ev) => {
10 | ev.waitUntil(sw.clients.claim());
11 | });
12 |
13 | let id = 1;
14 | const responseResolvers = new Map void>();
15 |
16 | const tunnelPattern = /^\/tunnel/;
17 |
18 | sw.addEventListener('fetch', (ev) => {
19 | const url = new URL(ev.request.url);
20 | if (url.origin !== sw.origin || tunnelPattern.test(url.pathname)) {
21 | return;
22 | }
23 |
24 | ev.respondWith(tunnelRequest(ev));
25 | });
26 |
27 | sw.addEventListener('message', (ev) => {
28 | if (ev.data.type === 'response') {
29 | const { id, serialized } = ev.data as {
30 | id: number;
31 | serialized: ArrayBuffer;
32 | };
33 | const res = deserializeResponse(serialized);
34 |
35 | if (res.status >= 300 && res.status <= 399) {
36 | const location = res.headers.get('Location');
37 | const absLocation = res.headers.get('Web-P2p-Tunnel-Abs-Location');
38 |
39 | if (location && absLocation) {
40 | res.headers.set('Location', absLocation);
41 | }
42 |
43 | res.headers.delete('Web-P2p-Tunnel-Abs-Location');
44 | }
45 |
46 | const resolve = responseResolvers.get(id);
47 | if (!resolve) {
48 | console.warn(`Received response with unknown id ${id}`);
49 | return;
50 | }
51 |
52 | resolve(res);
53 | responseResolvers.delete(id);
54 | }
55 | });
56 |
57 | async function getTunnelClient() {
58 | const clients = await sw.clients.matchAll();
59 | return clients.find((client) => {
60 | const url = new URL(client.url);
61 | return url.pathname === '/tunnel';
62 | });
63 | }
64 |
65 | async function tunnelRequest(ev: FetchEvent): Promise {
66 | const tc = await getTunnelClient();
67 | if (!tc) {
68 | return new Response(
69 | '503: Service Unavailable
' +
70 | 'The tunnel is disconnected. Tunnel page.
',
71 | { status: 503, headers: new Headers({ 'Content-Type': 'text/html' }) },
72 | );
73 | }
74 |
75 | const { method, url, headers } = ev.request;
76 | const headersList: [string, string][] = [];
77 | headers.forEach((value, key) => {
78 | headersList.push([key, value]);
79 | });
80 | const hasBody = ev.request.body !== null;
81 | const serialized = await serializeRequest(ev.request, {
82 | origin: sw.origin,
83 | userAgent: sw.navigator.userAgent,
84 | });
85 |
86 | const resPromise = new Promise((resolve) => {
87 | responseResolvers.set(id, resolve);
88 | });
89 |
90 | tc.postMessage(
91 | {
92 | type: 'request',
93 | id,
94 | method,
95 | url,
96 | headersList,
97 | hasBody,
98 | serialized,
99 | },
100 | [serialized],
101 | );
102 | id++;
103 |
104 | return await resPromise;
105 | }
106 |
--------------------------------------------------------------------------------
/internal/tunnel/http.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "io"
7 | "log"
8 | "net/http"
9 | "os"
10 |
11 | "github.com/pion/webrtc/v4"
12 | )
13 |
14 | const mtu = 16*1024 - 1
15 |
16 | type HTTPDataChannel struct {
17 | log *log.Logger
18 |
19 | client *http.Client
20 | dc *webrtc.DataChannel
21 |
22 | r *io.PipeReader
23 | w *io.PipeWriter
24 | }
25 |
26 | func NewHTTPDataChannel(client *http.Client, dc *webrtc.DataChannel) *HTTPDataChannel {
27 | r, w := io.Pipe()
28 |
29 | h := &HTTPDataChannel{
30 | log: log.New(os.Stderr, fmt.Sprintf("[HTTP Data Channel %d] ", *dc.ID()), log.LstdFlags),
31 | client: client,
32 | dc: dc,
33 | r: r,
34 | w: w,
35 | }
36 |
37 | dc.OnMessage(h.onMessage)
38 | dc.OnClose(h.onClose)
39 |
40 | return h
41 | }
42 |
43 | func (h *HTTPDataChannel) Run() {
44 | req, err := http.ReadRequest(bufio.NewReader(h.r))
45 | if err != nil {
46 | h.log.Printf("Failed to read request: %v", err)
47 |
48 | _ = h.dc.Close()
49 | return
50 | }
51 | req.RequestURI = ""
52 |
53 | h.log.Printf("%s %s", req.Method, req.URL)
54 |
55 | resp, err := h.client.Do(req)
56 | if err != nil {
57 | h.log.Printf("Proxied request failed: %v", err)
58 |
59 | resp = &http.Response{
60 | Status: http.StatusText(http.StatusBadGateway),
61 | StatusCode: http.StatusBadGateway,
62 | }
63 | }
64 |
65 | if resp.StatusCode >= 300 && resp.StatusCode <= 399 {
66 | _ = addAbsLocationHeader(resp, req)
67 | }
68 |
69 | if err := h.writeResponse(resp); err != nil {
70 | h.log.Printf("Failed to write response: %v", err)
71 |
72 | _ = h.dc.Close()
73 | return
74 | }
75 | }
76 |
77 | func (h *HTTPDataChannel) Write(p []byte) (n int, err error) {
78 | count := len(p) / mtu
79 | if len(p)%mtu > 0 {
80 | count++
81 | }
82 |
83 | for i := 0; i < count; i++ {
84 | fragment := p[i*mtu : min((i+1)*mtu, len(p))]
85 | if err := h.dc.Send(fragment); err != nil {
86 | return 0, err
87 | }
88 | }
89 |
90 | return len(p), nil
91 | }
92 |
93 | func (h *HTTPDataChannel) onMessage(msg webrtc.DataChannelMessage) {
94 | if len(msg.Data) == 0 {
95 | _ = h.w.Close()
96 | return
97 | }
98 |
99 | if _, err := h.w.Write(msg.Data); err != nil {
100 | h.log.Printf("Failed to write message data: %v", err)
101 |
102 | _ = h.dc.Close()
103 | return
104 | }
105 | }
106 |
107 | func (h *HTTPDataChannel) onClose() {
108 | _ = h.w.Close()
109 | }
110 |
111 | func (h *HTTPDataChannel) writeResponse(resp *http.Response) error {
112 | w := bufio.NewWriterSize(h, mtu)
113 | if err := resp.Write(w); err != nil {
114 | return err
115 | }
116 | if err := w.Flush(); err != nil {
117 | return err
118 | }
119 | if err := h.dc.Send(nil); err != nil {
120 | return err
121 | }
122 |
123 | return nil
124 | }
125 |
126 | func addAbsLocationHeader(resp *http.Response, req *http.Request) error {
127 | location, err := resp.Location()
128 | if err != nil {
129 | return err
130 | }
131 |
132 | resp.Header.Set("Web-P2p-Tunnel-Abs-Location", location.String())
133 |
134 | return nil
135 | }
136 |
--------------------------------------------------------------------------------
/web/rollup.config.js:
--------------------------------------------------------------------------------
1 | import html, { makeHtmlAttributes } from '@rollup/plugin-html';
2 | import replace from '@rollup/plugin-replace';
3 | import typescript from '@rollup/plugin-typescript';
4 | import 'dotenv/config';
5 | import { readFileSync } from 'fs';
6 | import { resolve } from 'path';
7 | import { defineConfig } from 'rollup';
8 |
9 | const isProduction = process.env.NODE_ENV === 'production';
10 |
11 | export default defineConfig([
12 | {
13 | input: 'src/main.ts',
14 | output: {
15 | dir: 'dist',
16 | entryFileNames: '[name]-[hash].js',
17 | },
18 | plugins: [
19 | typescript({ tsconfig: 'src/tsconfig.json' }),
20 | html({ title: 'web-p2p-tunnel' }),
21 | isProduction && (await import('@rollup/plugin-terser')).default(),
22 | ],
23 | },
24 | {
25 | input: 'src/tunnel.ts',
26 | output: {
27 | dir: 'dist',
28 | entryFileNames: '[name]-[hash].js',
29 | },
30 | plugins: [
31 | replace({
32 | 'import.meta.env.PUBLIC_SIGNALING_SERVER_URL': JSON.stringify(
33 | process.env.PUBLIC_SIGNALING_SERVER_URL,
34 | ),
35 | }),
36 | typescript({ tsconfig: 'src/tsconfig.json' }),
37 | html({
38 | fileName: 'tunnel.html',
39 | title: 'web-p2p-tunnel',
40 | template: fileTemplate('src/tunnel.html'),
41 | meta: [
42 | { charset: 'utf-8' },
43 | { name: 'viewport', content: 'width=device-width,initial-scale=1,maximum-scale=1' },
44 | ],
45 | }),
46 | isProduction && (await import('@rollup/plugin-terser')).default(),
47 | ],
48 | },
49 | {
50 | input: 'sw/sw.ts',
51 | output: { dir: 'dist' },
52 | plugins: [
53 | typescript({ tsconfig: 'sw/tsconfig.json' }),
54 | isProduction && (await import('@rollup/plugin-terser')).default(),
55 | ],
56 | },
57 | ]);
58 |
59 | function fileTemplate(relativePath) {
60 | const path = resolve(process.cwd(), relativePath);
61 | const data = readFileSync(path, 'utf8');
62 |
63 | // Adapted from https://github.com/rollup/plugins/blob/9e7f576f33e26d65e9f2221d248a2e000923e03f/packages/html/src/index.ts#L29
64 | return ({ attributes, files, meta, publicPath, title }) => {
65 | const scripts = (files.js || [])
66 | .map(({ fileName }) => {
67 | const attrs = makeHtmlAttributes(attributes.script);
68 | return ``;
69 | })
70 | .join('\n');
71 |
72 | const links = (files.css || [])
73 | .map(({ fileName }) => {
74 | const attrs = makeHtmlAttributes(attributes.link);
75 | return ``;
76 | })
77 | .join('\n');
78 |
79 | const metas = meta
80 | .map((input) => {
81 | const attrs = makeHtmlAttributes(input);
82 | return ``;
83 | })
84 | .join('\n');
85 |
86 | return `
87 |
88 |
89 |
90 | ${metas}
91 | ${title}
92 | ${links}
93 |
94 |
95 | ${data}
96 | ${scripts}
97 |
98 | `;
99 | };
100 | }
101 |
--------------------------------------------------------------------------------
/web/sw/http.ts:
--------------------------------------------------------------------------------
1 | const CRLF = '\r\n';
2 | const CRLF_ENCODED = CRLF.split('').map((s) => s.charCodeAt(0));
3 |
4 | const encoder = new TextEncoder();
5 | const decoder = new TextDecoder();
6 |
7 | type UserAgentInfo = {
8 | origin: string;
9 | userAgent: string;
10 | };
11 |
12 | export async function serializeRequest(
13 | req: Request,
14 | { origin, userAgent }: UserAgentInfo,
15 | ): Promise {
16 | const url = new URL(req.url);
17 | url.hash = '';
18 |
19 | const body = await req.arrayBuffer();
20 |
21 | const requestLine = `${req.method} ${url.toString()} HTTP/1.1`;
22 | const headerFields: string[] = [];
23 | req.headers.forEach((v, k) => {
24 | headerFields.push(`${k}: ${v}`);
25 | });
26 |
27 | const extra: [string, unknown][] = [
28 | ['Host', url.host],
29 | // TODO: set Origin to null in some cases (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin)
30 | ['Origin', origin],
31 | ['User-Agent', userAgent],
32 | ['Content-Length', body.byteLength],
33 | ['Sec-Fetch-Dest', req.destination],
34 | ['Sec-Fetch-Mode', req.mode],
35 | ['Web-P2p-Tunnel-Redirect', req.redirect],
36 | ];
37 | if (req.referrer && req.referrer !== 'about:client') {
38 | extra.push(['Referer', req.referrer]);
39 | }
40 | extra.forEach(([k, v]) => {
41 | if (!req.headers.has(k)) {
42 | headerFields.push(`${k}: ${v}`);
43 | }
44 | });
45 |
46 | const headerStr = requestLine + CRLF + headerFields.join(CRLF) + CRLF + CRLF;
47 | const header = encoder.encode(headerStr);
48 |
49 | const out = new Uint8Array(header.byteLength + body.byteLength);
50 | out.set(header, 0);
51 | out.set(new Uint8Array(body), header.byteLength);
52 |
53 | return out.buffer;
54 | }
55 |
56 | export function deserializeResponse(serialized: ArrayBuffer): Response {
57 | const arr = new Uint8Array(serialized);
58 | const headerEndIndex = findHeaderEndIndex(arr);
59 | if (headerEndIndex === -1) {
60 | return Response.error();
61 | }
62 |
63 | const header = arr.subarray(0, headerEndIndex);
64 | const body = arr.subarray(headerEndIndex + 4);
65 |
66 | const headerStr = decoder.decode(header);
67 | const { status, statusText, headersList } = parseHeader(headerStr);
68 |
69 | return new Response(body, {
70 | status,
71 | statusText,
72 | headers: new Headers(headersList),
73 | });
74 | }
75 |
76 | function findHeaderEndIndex(arr: Uint8Array): number {
77 | const [cr, lf] = CRLF_ENCODED;
78 | for (let i = 0; i < arr.length - 3; i++) {
79 | const isHeaderEnd =
80 | arr[i] === cr && arr[i + 1] === lf && arr[i + 2] === cr && arr[i + 3] === lf;
81 | if (isHeaderEnd) {
82 | return i;
83 | }
84 | }
85 |
86 | return -1;
87 | }
88 |
89 | function parseHeader(header: string) {
90 | const [requestLine, ...headerFieldLines] = header.split(CRLF);
91 |
92 | const [_, statusStr, ...statusTextParts] = requestLine.split(' ');
93 | const status = parseInt(statusStr);
94 | const statusText = statusTextParts.join(' ');
95 |
96 | const headersList = headerFieldLines.map<[string, string]>((s) => {
97 | const [name, ...valueParts] = s.split(':');
98 | return [name, valueParts.join(':').slice(1)];
99 | });
100 |
101 | return { status, statusText, headersList };
102 | }
103 |
--------------------------------------------------------------------------------
/internal/signaling/room.go:
--------------------------------------------------------------------------------
1 | package signaling
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "log"
8 | "os"
9 | "sync"
10 |
11 | "github.com/google/uuid"
12 | "github.com/gorilla/websocket"
13 | )
14 |
15 | type Room struct {
16 | log *log.Logger
17 |
18 | ID string
19 |
20 | serverConn *websocket.Conn
21 | serverConnLock sync.Mutex
22 | clientConns map[string]*websocket.Conn
23 | clientConnsLock sync.Mutex
24 | }
25 |
26 | func NewRoom(id string) *Room {
27 | return &Room{
28 | log: log.New(os.Stderr, fmt.Sprintf("[Room %s] ", id[:6]), log.LstdFlags),
29 | ID: id,
30 | clientConns: make(map[string]*websocket.Conn),
31 | }
32 | }
33 |
34 | func (r *Room) HandleServerConn(conn *websocket.Conn) {
35 | r.serverConnLock.Lock()
36 | if r.serverConn != nil {
37 | conn.WriteJSON(Message{
38 | Type: "error",
39 | Data: json.RawMessage("server conn already exists"),
40 | })
41 | conn.Close()
42 |
43 | r.serverConnLock.Unlock()
44 |
45 | r.log.Printf("Rejected server conn: %v. Server conn already exists.", conn.RemoteAddr())
46 |
47 | return
48 | }
49 |
50 | r.serverConn = conn
51 | r.serverConnLock.Unlock()
52 |
53 | r.log.Printf("Registered server conn: %v", conn.RemoteAddr())
54 |
55 | defer func() {
56 | r.serverConnLock.Lock()
57 | r.serverConn = nil
58 | r.serverConnLock.Unlock()
59 |
60 | conn.Close()
61 |
62 | r.log.Printf("Server conn closed: %v", conn.RemoteAddr())
63 | }()
64 |
65 | for {
66 | var message ServerMessage
67 | if err := conn.ReadJSON(&message); err != nil {
68 | return
69 | }
70 |
71 | if err := r.sendMessageToClient(message.ClientID, message.Message); err != nil {
72 | return
73 | }
74 | }
75 | }
76 |
77 | func (r *Room) HandleClientConn(conn *websocket.Conn) {
78 | r.clientConnsLock.Lock()
79 |
80 | id := uuid.NewString()
81 | r.clientConns[id] = conn
82 |
83 | r.clientConnsLock.Unlock()
84 |
85 | r.log.Printf("Registered client conn: %v, %s", conn.RemoteAddr(), id)
86 |
87 | defer func() {
88 | r.clientConnsLock.Lock()
89 | delete(r.clientConns, id)
90 | r.clientConnsLock.Unlock()
91 |
92 | conn.Close()
93 |
94 | r.log.Printf("Client conn closed: %v, %s", conn.RemoteAddr(), id)
95 | }()
96 |
97 | for {
98 | var message Message
99 | if err := conn.ReadJSON(&message); err != nil {
100 | return
101 | }
102 |
103 | if err := r.sendMessageToServer(id, message); err != nil {
104 | return
105 | }
106 | }
107 | }
108 |
109 | func (r *Room) sendMessageToClient(clientID string, message Message) error {
110 | r.clientConnsLock.Lock()
111 | defer r.clientConnsLock.Unlock()
112 |
113 | conn, ok := r.clientConns[clientID]
114 | if !ok {
115 | return errors.New("unknown client")
116 | }
117 |
118 | if err := conn.WriteJSON(message); err != nil {
119 | return err
120 | }
121 |
122 | return nil
123 | }
124 |
125 | func (r *Room) sendMessageToServer(clientID string, message Message) error {
126 | r.serverConnLock.Lock()
127 | defer r.serverConnLock.Unlock()
128 |
129 | if r.serverConn == nil {
130 | return errors.New("no server")
131 | }
132 |
133 | sm := ServerMessage{
134 | Message: message,
135 | ClientID: clientID,
136 | }
137 | if err := r.serverConn.WriteJSON(sm); err != nil {
138 | return err
139 | }
140 |
141 | return nil
142 | }
143 |
--------------------------------------------------------------------------------
/web/src/tunnel.ts:
--------------------------------------------------------------------------------
1 | import { connectSignalingClient } from './signalingClient';
2 | import { setupSW } from './sw';
3 | import { connectWebRTC } from './webrtc';
4 |
5 | const MTU = 16 * 1024 - 1;
6 | const TUNNEL_UNAVAILABLE_RESPONSE =
7 | 'HTTP/1.1 503 Service Unavailable\r\n' +
8 | 'Content-Type: text/html\r\n' +
9 | '\r\n' +
10 | '503: Service Unavailable
' +
11 | 'The tunnel is disconnected. Tunnel page.
';
12 | const TUNNEL_ERROR_RESPONSE = 'HTTP/1.1 502 Bad Gateway\r\n\r\n';
13 |
14 | const encoder = new TextEncoder();
15 |
16 | const tunnelConnectFormEl = document.getElementById('tunnel-connect') as HTMLFormElement;
17 | const swStatusEl = document.getElementById('sw-status')!;
18 | const signalingStatusEl = document.getElementById('signaling-status')!;
19 | const webRTCStatusEl = document.getElementById('webrtc-status')!;
20 | const requestsEl = document.getElementById('requests')!;
21 |
22 | let pc: RTCPeerConnection | null = null;
23 |
24 | await setupSW(tunnel, swStatusEl, requestsEl);
25 |
26 | async function tunnel(serialized: ArrayBuffer): Promise {
27 | return new Promise((resolve, reject) => {
28 | if (pc === null) {
29 | reject(encoder.encode(TUNNEL_UNAVAILABLE_RESPONSE).buffer);
30 | return;
31 | }
32 |
33 | const dc = pc.createDataChannel('http');
34 | dc.binaryType = 'arraybuffer';
35 |
36 | dc.addEventListener('open', () => {
37 | const arr = new Uint8Array(serialized);
38 | const count = Math.ceil(arr.length / MTU);
39 | for (let i = 0; i < count; i++) {
40 | const fragment = arr.subarray(i * MTU, Math.min((i + 1) * MTU, arr.length));
41 | dc.send(fragment);
42 | }
43 |
44 | dc.send(new ArrayBuffer(0));
45 | });
46 |
47 | dc.addEventListener('close', () => {
48 | reject(encoder.encode(TUNNEL_ERROR_RESPONSE).buffer);
49 | });
50 |
51 | const fragments: ArrayBuffer[] = [];
52 |
53 | const respond = () => {
54 | const byteLength = fragments.reduce((prev, curr) => prev + curr.byteLength, 0);
55 | const out = new ArrayBuffer(byteLength);
56 | const arr = new Uint8Array(out);
57 |
58 | let i = 0;
59 | for (const fragment of fragments) {
60 | arr.set(new Uint8Array(fragment), i);
61 | i += fragment.byteLength;
62 | }
63 |
64 | resolve(out);
65 | };
66 |
67 | dc.addEventListener('message', (ev) => {
68 | if (!(ev.data instanceof ArrayBuffer)) {
69 | reject(encoder.encode(TUNNEL_ERROR_RESPONSE).buffer);
70 | return;
71 | }
72 |
73 | if (ev.data.byteLength === 0) {
74 | respond();
75 | dc.close();
76 | return;
77 | }
78 |
79 | fragments.push(ev.data);
80 | });
81 | });
82 | }
83 |
84 | tunnelConnectFormEl.addEventListener('submit', (ev) => {
85 | ev.preventDefault();
86 |
87 | const data = new FormData(tunnelConnectFormEl);
88 | const roomID = data.get('room-id');
89 | if (!(typeof roomID === 'string' && roomID)) {
90 | alert('Invalid data');
91 | return;
92 | }
93 |
94 | const sc = connectSignalingClient(
95 | roomID,
96 | import.meta.env.PUBLIC_SIGNALING_SERVER_URL,
97 | signalingStatusEl,
98 | );
99 |
100 | sc.addEventListener('open', async () => {
101 | pc = await connectWebRTC(sc, webRTCStatusEl);
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/internal/tunnel/hub.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "log"
7 | "net/http"
8 | "net/http/httptest"
9 | "net/url"
10 | "os"
11 |
12 | "github.com/andrewmthomas87/web-p2p-tunnel/internal/signaling"
13 | "github.com/pion/webrtc/v4"
14 | )
15 |
16 | type Signaler interface {
17 | Offers() <-chan signaling.Offer
18 | Answers() chan<- signaling.Answer
19 | RemoteICECandidates() <-chan signaling.ICECandidate
20 | LocalICECandidates() chan<- signaling.ICECandidate
21 | }
22 |
23 | type Hub struct {
24 | log *log.Logger
25 |
26 | webrtcConfig webrtc.Configuration
27 |
28 | transport http.RoundTripper
29 | tunnels map[string]*Tunnel
30 | }
31 |
32 | func NewHub(target *url.URL, changeHostHeader, changeOriginHeader bool, webrtcConfig webrtc.Configuration) *Hub {
33 | return &Hub{
34 | log: log.New(os.Stderr, "[Tunnel hub] ", log.LstdFlags),
35 | webrtcConfig: webrtcConfig,
36 | transport: newHandlerTransport(newSingleHostReverseProxy(target, changeHostHeader, changeOriginHeader)),
37 | tunnels: make(map[string]*Tunnel),
38 | }
39 | }
40 |
41 | func (h *Hub) Run(ctx context.Context, signaler Signaler) error {
42 | h.log.Println("Running...")
43 |
44 | offers := signaler.Offers()
45 | answers := signaler.Answers()
46 | remoteICECandidates := signaler.RemoteICECandidates()
47 | localICECandidates := signaler.LocalICECandidates()
48 |
49 | for {
50 | select {
51 | case offer := <-offers:
52 | answer, err := h.handleOffer(offer, h.onICECandidate(offer.ClientID, localICECandidates))
53 | if err != nil {
54 | return err
55 | }
56 | answers <- answer
57 |
58 | case iceCandidate := <-remoteICECandidates:
59 | if err := h.handleRemoteICECandidate(iceCandidate); err != nil {
60 | return err
61 | }
62 |
63 | case <-ctx.Done():
64 | return h.close()
65 |
66 | }
67 | }
68 | }
69 |
70 | func (h *Hub) onICECandidate(clientID string, localICECandidates chan<- signaling.ICECandidate) func(*webrtc.ICECandidate) {
71 | return func(iceCandidate *webrtc.ICECandidate) {
72 | if iceCandidate == nil {
73 | return
74 | }
75 |
76 | localICECandidates <- signaling.ICECandidate{
77 | ClientID: clientID,
78 | Data: iceCandidate.ToJSON(),
79 | }
80 | }
81 | }
82 |
83 | func (h *Hub) handleOffer(
84 | offer signaling.Offer,
85 | onICECandidate func(*webrtc.ICECandidate),
86 | ) (signaling.Answer, error) {
87 | _, ok := h.tunnels[offer.ClientID]
88 | if ok {
89 | return signaling.Answer{}, errors.New("received offer for open tunnel")
90 | }
91 |
92 | t, err := NewTunnel(h.webrtcConfig, h.transport, offer.ClientID, onICECandidate)
93 | if err != nil {
94 | return signaling.Answer{}, err
95 | }
96 | h.tunnels[offer.ClientID] = t
97 |
98 | h.log.Printf("Created tunnel for client %s", offer.ClientID)
99 |
100 | answer, err := t.RegisterOffer(offer.Data)
101 | if err != nil {
102 | return signaling.Answer{}, err
103 | }
104 |
105 | return signaling.Answer{
106 | ClientID: offer.ClientID,
107 | Data: answer,
108 | }, nil
109 | }
110 |
111 | func (h *Hub) handleRemoteICECandidate(iceCandidate signaling.ICECandidate) error {
112 | t, ok := h.tunnels[iceCandidate.ClientID]
113 | if !ok {
114 | return errors.New("received message for unknown tunnel")
115 | }
116 |
117 | if err := t.AddICECandidate(iceCandidate.Data); err != nil {
118 | return err
119 | }
120 |
121 | return nil
122 | }
123 |
124 | func (h *Hub) close() error {
125 | h.log.Println("Closing tunnels...")
126 |
127 | for _, t := range h.tunnels {
128 | err := t.Close()
129 | if err != nil {
130 | return err
131 | }
132 | }
133 |
134 | return nil
135 | }
136 |
137 | type handlerTransport struct {
138 | handler http.Handler
139 | http.ResponseWriter
140 | }
141 |
142 | func newHandlerTransport(handler http.Handler) http.RoundTripper {
143 | return &handlerTransport{handler: handler}
144 | }
145 |
146 | func (ht *handlerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
147 | rw := httptest.NewRecorder()
148 | ht.handler.ServeHTTP(rw, req)
149 |
150 | res := rw.Result()
151 | res.Request = req
152 |
153 | return res, nil
154 | }
155 |
--------------------------------------------------------------------------------
/internal/signaling/client.go:
--------------------------------------------------------------------------------
1 | package signaling
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "log"
8 | "net/url"
9 | "os"
10 | "time"
11 |
12 | "github.com/gorilla/websocket"
13 | "github.com/pion/webrtc/v4"
14 | )
15 |
16 | type Client struct {
17 | log *log.Logger
18 |
19 | RoomID string
20 |
21 | serverURL *url.URL
22 |
23 | offers chan Offer
24 | answers chan Answer
25 | remoteICECandidates chan ICECandidate
26 | localICECandidates chan ICECandidate
27 |
28 | conn *websocket.Conn
29 | done chan struct{}
30 | }
31 |
32 | type Offer struct {
33 | ClientID string
34 | Data webrtc.SessionDescription
35 | }
36 |
37 | type Answer struct {
38 | ClientID string
39 | Data webrtc.SessionDescription
40 | }
41 |
42 | type ICECandidate struct {
43 | ClientID string
44 | Data webrtc.ICECandidateInit
45 | }
46 |
47 | func NewClient(roomID string, serverURL *url.URL) *Client {
48 | return &Client{
49 | log: log.New(os.Stderr, fmt.Sprintf("[Signaling client %s] ", roomID[:6]), log.LstdFlags),
50 | RoomID: roomID,
51 | serverURL: serverURL,
52 | offers: make(chan Offer, 16),
53 | answers: make(chan Answer, 16),
54 | remoteICECandidates: make(chan ICECandidate, 16),
55 | localICECandidates: make(chan ICECandidate, 16),
56 | done: make(chan struct{}),
57 | }
58 | }
59 |
60 | func (c *Client) Offers() <-chan Offer {
61 | return c.offers
62 | }
63 |
64 | func (c *Client) Answers() chan<- Answer {
65 | return c.answers
66 | }
67 |
68 | func (c *Client) RemoteICECandidates() <-chan ICECandidate {
69 | return c.remoteICECandidates
70 | }
71 |
72 | func (c *Client) LocalICECandidates() chan<- ICECandidate {
73 | return c.localICECandidates
74 | }
75 |
76 | func (c *Client) Connect() error {
77 | c.log.Println("Connecting...")
78 |
79 | wsURL := c.serverURL.JoinPath("ws")
80 |
81 | query := wsURL.Query()
82 | query.Set("role", "server")
83 | query.Set("room-id", c.RoomID)
84 | wsURL.RawQuery = query.Encode()
85 |
86 | if c.serverURL.Scheme == "https" {
87 | wsURL.Scheme = "wss"
88 | } else {
89 | wsURL.Scheme = "ws"
90 | }
91 |
92 | conn, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
93 | if err != nil {
94 | return err
95 | }
96 | c.conn = conn
97 |
98 | c.log.Println("Connected")
99 |
100 | return nil
101 | }
102 |
103 | func (c *Client) Run(ctx context.Context) error {
104 | c.log.Println("Running...")
105 |
106 | go c.readPump()
107 | go c.writePump()
108 |
109 | select {
110 | case <-c.done:
111 | case <-ctx.Done():
112 | c.log.Println("Closing...")
113 |
114 | err := c.conn.WriteControl(
115 | websocket.CloseMessage,
116 | websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
117 | time.Now().Add(time.Second),
118 | )
119 | if err != nil {
120 | return err
121 | }
122 |
123 | select {
124 | case <-c.done:
125 | case <-time.After(time.Second):
126 | err := c.conn.Close()
127 | if err != nil {
128 | return err
129 | }
130 | }
131 | }
132 |
133 | c.log.Println("Closed")
134 |
135 | return nil
136 | }
137 |
138 | func (c *Client) readPump() {
139 | defer close(c.done)
140 | defer c.conn.Close()
141 |
142 | for {
143 | var message ServerMessage
144 | if err := c.conn.ReadJSON(&message); err != nil {
145 | return
146 | }
147 |
148 | switch message.Type {
149 | case "offer":
150 | c.log.Println("Received offer...")
151 |
152 | var data webrtc.SessionDescription
153 | if err := json.Unmarshal(message.Data, &data); err != nil {
154 | return
155 | }
156 |
157 | c.offers <- Offer{
158 | ClientID: message.ClientID,
159 | Data: data,
160 | }
161 |
162 | case "icecandidate":
163 | c.log.Println("Received ICE candidate...")
164 |
165 | var data webrtc.ICECandidateInit
166 | if err := json.Unmarshal(message.Data, &data); err != nil {
167 | return
168 | }
169 |
170 | c.remoteICECandidates <- ICECandidate{
171 | ClientID: message.ClientID,
172 | Data: data,
173 | }
174 | }
175 | }
176 | }
177 |
178 | func (c *Client) writePump() {
179 | for {
180 | select {
181 | case answer := <-c.answers:
182 | c.log.Println("Sending answer...")
183 |
184 | if err := c.writeMessage(answer.ClientID, "answer", answer.Data); err != nil {
185 | return
186 | }
187 |
188 | case iceCandidate := <-c.localICECandidates:
189 | c.log.Println("Sending ICE candidate...")
190 |
191 | if err := c.writeMessage(iceCandidate.ClientID, "icecandidate", iceCandidate.Data); err != nil {
192 | return
193 | }
194 |
195 | }
196 | }
197 | }
198 |
199 | func (c *Client) writeMessage(clientID string, messageType string, data interface{}) error {
200 | b, err := json.Marshal(data)
201 | if err != nil {
202 | return err
203 | }
204 |
205 | message := ServerMessage{
206 | ClientID: clientID,
207 | Message: Message{
208 | Type: messageType,
209 | Data: b,
210 | },
211 | }
212 | if err := c.conn.WriteJSON(message); err != nil {
213 | return err
214 | }
215 |
216 | return nil
217 | }
218 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # web-p2p-tunnel
2 |
3 | A peer-to-peer HTTP tunnel directly to/from the browser, using WebRTC and a Service Worker. Expose a local web server
4 | directly to any device with a web browser and Internet connection - no server needed.
5 |
6 | 
7 |
8 | https://github.com/andrewmthomas87/web-p2p-tunnel/assets/7506142/d59cb10c-038b-45d5-8c07-96c6b2a8c197
9 |
10 | Consists of:
11 |
12 | - **web**: service worker that intercepts requests and web page that tunnels requests over WebRTC data channels.
13 | Deployed using Github Pages at [tunnel.andrewt.io](https://tunnel.andrewt.io).
14 | - **signaling-server**: server for WebRTC connection bootstrapping via Websockets. Deployed at
15 | [signal.andrewt.io](https://signal.andrewt.io).
16 | - **web-p2p-tunnel**: CLI program that receives requests, reverse proxies to a local web server, and tunnels responses.
17 |
18 | ## Installation
19 |
20 | The `web-p2p-tunnel` CLI program is the only component you need to install. You will need [Go](https://go.dev/) 1.21+.
21 |
22 | ```sh
23 | go install github.com/andrewmthomas87/web-p2p-tunnel/cmd/web-p2p-tunnel@latest
24 | ```
25 |
26 | Ensure the go install directory is in your `PATH` (typically `$HOME/go/bin`).
27 |
28 | If you'd like to run the signaling server or host the website yourself, read [Development](#development).
29 |
30 | ## Usage
31 |
32 | Start a local web server. Let's refer to its URL (e.g., `http://localhost:8080`) as `TARGET_URL`.
33 |
34 | On the same device, start the `web-p2p-tunnel` CLI program:
35 |
36 | ```sh
37 | web-p2p-tunnel -signaling-server-url https://signal.andrewt.io -tunnel-target-url $TARGET_URL
38 | ```
39 |
40 | The program will create and connect to a room using the signaling server. It will log the room's id, like this:
41 |
42 | ```
43 | Created room fcd549bd-eec1-4e3b-a5ce-f4b182a81f5b
44 | ```
45 |
46 | Next, on any device, open the tunnel web page at [tunnel.andrewt.io/tunnel](https://tunnel.andrewt.io/tunnel). The
47 | service worker is installed immediately. Enter the room id and click "Connect". The tunnel is active when you have
48 | these statuses:
49 |
50 | | Name | Status |
51 | | -------------- | --------- |
52 | | Service worker | activated |
53 | | Signaling | open |
54 | | WebRTC | connected |
55 |
56 | In a new tab, open the website root [tunnel.andrewt.io](https://tunnel.andrewt.io) or any path not starting with
57 | `/tunnel` (e.g., [tunnel.andrewt.io/hello/world](https://tunnel.andrewt.io/hello/world)).
58 |
59 | With the tunnel active, requests matching the origin are intercepted by the service worker and tunneled to the
60 | `web-p2p-tunnel` program, which reverse proxies to your local web server and tunnels the responses back to the service
61 | worker. Requests not matching the origin are not tunneled and execute normally.
62 |
63 | ### Options
64 |
65 | ```
66 | web-p2p-tunnel -h
67 |
68 | Usage of web-p2p-tunnel:
69 | -change-host-header
70 | change the Host header to the host of the target url
71 | -change-origin-header
72 | change the Origin header to the origin of the target url
73 | -signaling-server-url string
74 | signaling server url (default "http://localhost:8080")
75 | -tunnel-target-url string
76 | tunnel target url
77 | ```
78 |
79 | ### Cookies
80 |
81 | The browser restricts the ability to manage cookies in the service worker or tunnel web page (see
82 | [Limitations](#limitations)). So, cookies are handled by `web-p2p-tunnel`. The program manages an individual cookie jar
83 | for each tunnel, adding the proper cookies to requests and storing cookies set in responses.
84 |
85 | Manipulation of cookies with `document.cookie` is not supported.
86 |
87 | ### Redirects
88 |
89 | Redirect behavior is based on the intercepted request's
90 | [redirect mode](https://fetch.spec.whatwg.org/#concept-request-redirect-mode). I'm working out some kinks here and
91 | there likely are bugs. TODO: document exact behavior.
92 |
93 | ### Reverse proxy
94 |
95 | Go's [`httputil.ReverseProxy`](https://pkg.go.dev/net/http/httputil@go1.21#ReverseProxy) is used. The URL is rewritten
96 | based on the `tunnel-target-url` option (see
97 | [ProxyRequest.SetURL](https://pkg.go.dev/net/http/httputil@go1.21#ProxyRequest.SetURL)). The standard
98 | `X-Forwarded-For`, `X-Forwarded-Host`, and `X-Forwarded-Proto` are set (see
99 | [ProxyRequest.SetXForwarded](https://pkg.go.dev/net/http/httputil@go1.21#ProxyRequest.SetXForwarded)). If the
100 | `change-host-header` is set, the `Host` header is changed to the target url's host. If the `change-origin-header`
101 | option is set, the `Origin` header is changed to the target url's origin.
102 |
103 | ## Limitations
104 |
105 | _Single Host_. Only requests to the website's host are intercepted. Tunneled requests are reverse proxied to a single
106 | target host. Multiple hosts would be a useful feature. There's no limitation from the APIs or architecture for this
107 | feature.
108 |
109 | _No Websockets_. This is a limitation of the Service Worker API.
110 |
111 | _Different/incorrect HTTP behavior_. When it comes to HTTP requests, the browser does a lot behind the scenes. Some of
112 | this can be replicated, some cannot (e.g.,
113 | [Forbidden headers](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name)). Some of what can be
114 | replicated is intricate, unknown to me, subtly different between browsers, etc. Expect bugs. Consider opening an issue
115 | :)
116 |
117 | _No Service Workers_. That slot is taken.
118 |
119 | _Poor browser memory management_: The memory management related to tunneling in the browser is unoptimized. There is
120 | room for improvement, but there are also limitations due to the APIs and architecture.
121 |
122 | ## Development
123 |
124 | ### Go
125 |
126 | - **signaling-server**: `cmd/signaling-server`
127 | - **web-p2p-tunnel**: `cmd/web-p2p-tunnel`
128 |
129 | ### Web
130 |
131 | From the `web` directory:
132 |
133 | Build: `npm run build`
134 |
135 | Build (watch mode): `npm run build-watch`
136 |
137 | Serve: `npm run serve`
138 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
5 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
6 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
7 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
8 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
9 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
10 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
11 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
13 | github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg=
14 | github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNIVb/NfGW4=
15 | github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
16 | github.com/pion/dtls/v2 v2.2.10 h1:u2Axk+FyIR1VFTPurktB+1zoEPGIW3bmyj3LEFrXjAA=
17 | github.com/pion/dtls/v2 v2.2.10/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
18 | github.com/pion/ice/v3 v3.0.5 h1:V6tNvpGS/vNJBWh3BEzQrwiPncOmLx7jbbSJM/2PFHE=
19 | github.com/pion/ice/v3 v3.0.5/go.mod h1:GIQiugpGkBDvh18nhFLRoHgabZ9VSRJOaEPh1nHjdrs=
20 | github.com/pion/interceptor v0.1.27 h1:mZ01OiGiukwRxezmDGzYjjokCVlDOk4T6BfaL5qrtGo=
21 | github.com/pion/interceptor v0.1.27/go.mod h1:/vVaqLwDjGv4GRbgmChIKZIT5EXFDijwmj4WmIYy9bI=
22 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
23 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
24 | github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
25 | github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
26 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
27 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
28 | github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
29 | github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
30 | github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
31 | github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
32 | github.com/pion/rtp v1.8.4/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
33 | github.com/pion/rtp v1.8.5 h1:uYzINfaK+9yWs7r537z/Rc1SvT8ILjBcmDOpJcTB+OU=
34 | github.com/pion/rtp v1.8.5/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
35 | github.com/pion/sctp v1.8.13/go.mod h1:YKSgO/bO/6aOMP9LCie1DuD7m+GamiK2yIiPM6vH+GA=
36 | github.com/pion/sctp v1.8.14 h1:NzwwDrtpvbdqMMWV9Q6NYGbHE/FQmjI+GEQLyeJahu4=
37 | github.com/pion/sctp v1.8.14/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
38 | github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
39 | github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
40 | github.com/pion/srtp/v3 v3.0.1 h1:AkIQRIZ+3tAOJMQ7G301xtrD1vekQbNeRO7eY1K8ZHk=
41 | github.com/pion/srtp/v3 v3.0.1/go.mod h1:3R3a1qIOIxBkVTLGFjafKK6/fJoTdQDhcC67HOyMbJ8=
42 | github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
43 | github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
44 | github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
45 | github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
46 | github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
47 | github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
48 | github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4=
49 | github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0=
50 | github.com/pion/turn/v3 v3.0.1 h1:wLi7BTQr6/Q20R0vt/lHbjv6y4GChFtC33nkYbasoT8=
51 | github.com/pion/turn/v3 v3.0.1/go.mod h1:MrJDKgqryDyWy1/4NT9TWfXWGMC7UHT6pJIv1+gMeNE=
52 | github.com/pion/webrtc/v4 v4.0.0-beta.16 h1:FYzly1p5k0Jzi9ZDWlEaPeqXq8qqc9T5afToLVJf8YQ=
53 | github.com/pion/webrtc/v4 v4.0.0-beta.16/go.mod h1:75pRrJRrcwTuIkO/2zkzh4/pEzy3xkZVrS6+0C/0CCA=
54 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
56 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
57 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
58 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
59 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
60 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
61 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
62 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
63 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
64 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
65 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
66 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
67 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
68 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
69 | golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
70 | golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
71 | golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
72 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
73 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
74 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
75 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
76 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
77 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
78 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
79 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
80 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
81 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
82 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
83 | golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
84 | golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
85 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
86 | golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
87 | golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
88 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
89 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
90 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
91 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
92 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
93 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
94 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
95 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
96 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
97 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
98 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
99 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
100 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
101 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
102 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
103 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
104 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
105 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
106 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
107 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
108 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
109 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
110 | golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
111 | golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
112 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
113 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
114 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
115 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
116 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
117 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
118 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
119 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
120 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
121 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
122 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
123 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
124 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
125 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
126 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
127 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
128 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
129 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
130 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
131 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
132 |
--------------------------------------------------------------------------------
/web/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web-p2p-tunnel",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "name": "web-p2p-tunnel",
8 | "devDependencies": {
9 | "@rollup/plugin-html": "^1.0.3",
10 | "@rollup/plugin-replace": "^5.0.5",
11 | "@rollup/plugin-terser": "^0.4.4",
12 | "@rollup/plugin-typescript": "^11.1.6",
13 | "dotenv": "^16.4.5",
14 | "prettier": "^3.2.5",
15 | "prettier-plugin-organize-imports": "^3.2.4",
16 | "rollup": "^4.14.1",
17 | "serve": "^14.2.1",
18 | "tslib": "^2.6.2",
19 | "typescript": "^5.4.4"
20 | }
21 | },
22 | "node_modules/@jridgewell/gen-mapping": {
23 | "version": "0.3.5",
24 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
25 | "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
26 | "dev": true,
27 | "dependencies": {
28 | "@jridgewell/set-array": "^1.2.1",
29 | "@jridgewell/sourcemap-codec": "^1.4.10",
30 | "@jridgewell/trace-mapping": "^0.3.24"
31 | },
32 | "engines": {
33 | "node": ">=6.0.0"
34 | }
35 | },
36 | "node_modules/@jridgewell/resolve-uri": {
37 | "version": "3.1.2",
38 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
39 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
40 | "dev": true,
41 | "engines": {
42 | "node": ">=6.0.0"
43 | }
44 | },
45 | "node_modules/@jridgewell/set-array": {
46 | "version": "1.2.1",
47 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
48 | "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
49 | "dev": true,
50 | "engines": {
51 | "node": ">=6.0.0"
52 | }
53 | },
54 | "node_modules/@jridgewell/source-map": {
55 | "version": "0.3.6",
56 | "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
57 | "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
58 | "dev": true,
59 | "dependencies": {
60 | "@jridgewell/gen-mapping": "^0.3.5",
61 | "@jridgewell/trace-mapping": "^0.3.25"
62 | }
63 | },
64 | "node_modules/@jridgewell/sourcemap-codec": {
65 | "version": "1.4.15",
66 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
67 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
68 | "dev": true
69 | },
70 | "node_modules/@jridgewell/trace-mapping": {
71 | "version": "0.3.25",
72 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
73 | "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
74 | "dev": true,
75 | "dependencies": {
76 | "@jridgewell/resolve-uri": "^3.1.0",
77 | "@jridgewell/sourcemap-codec": "^1.4.14"
78 | }
79 | },
80 | "node_modules/@rollup/plugin-html": {
81 | "version": "1.0.3",
82 | "resolved": "https://registry.npmjs.org/@rollup/plugin-html/-/plugin-html-1.0.3.tgz",
83 | "integrity": "sha512-bbjQciNXitHX+Bgk0xsW3/0wFWih/356/r7/kvmdz4wzWhAU/a0zYBWTczihrlzz/6Qpw/kZ0yXqOJwsETgg7A==",
84 | "dev": true,
85 | "engines": {
86 | "node": ">=14.0.0"
87 | },
88 | "peerDependencies": {
89 | "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
90 | },
91 | "peerDependenciesMeta": {
92 | "rollup": {
93 | "optional": true
94 | }
95 | }
96 | },
97 | "node_modules/@rollup/plugin-replace": {
98 | "version": "5.0.5",
99 | "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz",
100 | "integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==",
101 | "dev": true,
102 | "dependencies": {
103 | "@rollup/pluginutils": "^5.0.1",
104 | "magic-string": "^0.30.3"
105 | },
106 | "engines": {
107 | "node": ">=14.0.0"
108 | },
109 | "peerDependencies": {
110 | "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
111 | },
112 | "peerDependenciesMeta": {
113 | "rollup": {
114 | "optional": true
115 | }
116 | }
117 | },
118 | "node_modules/@rollup/plugin-terser": {
119 | "version": "0.4.4",
120 | "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
121 | "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
122 | "dev": true,
123 | "dependencies": {
124 | "serialize-javascript": "^6.0.1",
125 | "smob": "^1.0.0",
126 | "terser": "^5.17.4"
127 | },
128 | "engines": {
129 | "node": ">=14.0.0"
130 | },
131 | "peerDependencies": {
132 | "rollup": "^2.0.0||^3.0.0||^4.0.0"
133 | },
134 | "peerDependenciesMeta": {
135 | "rollup": {
136 | "optional": true
137 | }
138 | }
139 | },
140 | "node_modules/@rollup/plugin-typescript": {
141 | "version": "11.1.6",
142 | "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz",
143 | "integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==",
144 | "dev": true,
145 | "dependencies": {
146 | "@rollup/pluginutils": "^5.1.0",
147 | "resolve": "^1.22.1"
148 | },
149 | "engines": {
150 | "node": ">=14.0.0"
151 | },
152 | "peerDependencies": {
153 | "rollup": "^2.14.0||^3.0.0||^4.0.0",
154 | "tslib": "*",
155 | "typescript": ">=3.7.0"
156 | },
157 | "peerDependenciesMeta": {
158 | "rollup": {
159 | "optional": true
160 | },
161 | "tslib": {
162 | "optional": true
163 | }
164 | }
165 | },
166 | "node_modules/@rollup/pluginutils": {
167 | "version": "5.1.0",
168 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
169 | "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
170 | "dev": true,
171 | "dependencies": {
172 | "@types/estree": "^1.0.0",
173 | "estree-walker": "^2.0.2",
174 | "picomatch": "^2.3.1"
175 | },
176 | "engines": {
177 | "node": ">=14.0.0"
178 | },
179 | "peerDependencies": {
180 | "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
181 | },
182 | "peerDependenciesMeta": {
183 | "rollup": {
184 | "optional": true
185 | }
186 | }
187 | },
188 | "node_modules/@rollup/rollup-android-arm-eabi": {
189 | "version": "4.14.1",
190 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.1.tgz",
191 | "integrity": "sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA==",
192 | "cpu": [
193 | "arm"
194 | ],
195 | "dev": true,
196 | "optional": true,
197 | "os": [
198 | "android"
199 | ]
200 | },
201 | "node_modules/@rollup/rollup-android-arm64": {
202 | "version": "4.14.1",
203 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.1.tgz",
204 | "integrity": "sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ==",
205 | "cpu": [
206 | "arm64"
207 | ],
208 | "dev": true,
209 | "optional": true,
210 | "os": [
211 | "android"
212 | ]
213 | },
214 | "node_modules/@rollup/rollup-darwin-arm64": {
215 | "version": "4.14.1",
216 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.1.tgz",
217 | "integrity": "sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q==",
218 | "cpu": [
219 | "arm64"
220 | ],
221 | "dev": true,
222 | "optional": true,
223 | "os": [
224 | "darwin"
225 | ]
226 | },
227 | "node_modules/@rollup/rollup-darwin-x64": {
228 | "version": "4.14.1",
229 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.1.tgz",
230 | "integrity": "sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA==",
231 | "cpu": [
232 | "x64"
233 | ],
234 | "dev": true,
235 | "optional": true,
236 | "os": [
237 | "darwin"
238 | ]
239 | },
240 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
241 | "version": "4.14.1",
242 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.1.tgz",
243 | "integrity": "sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g==",
244 | "cpu": [
245 | "arm"
246 | ],
247 | "dev": true,
248 | "optional": true,
249 | "os": [
250 | "linux"
251 | ]
252 | },
253 | "node_modules/@rollup/rollup-linux-arm64-gnu": {
254 | "version": "4.14.1",
255 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.1.tgz",
256 | "integrity": "sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w==",
257 | "cpu": [
258 | "arm64"
259 | ],
260 | "dev": true,
261 | "optional": true,
262 | "os": [
263 | "linux"
264 | ]
265 | },
266 | "node_modules/@rollup/rollup-linux-arm64-musl": {
267 | "version": "4.14.1",
268 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.1.tgz",
269 | "integrity": "sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw==",
270 | "cpu": [
271 | "arm64"
272 | ],
273 | "dev": true,
274 | "optional": true,
275 | "os": [
276 | "linux"
277 | ]
278 | },
279 | "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
280 | "version": "4.14.1",
281 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.1.tgz",
282 | "integrity": "sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw==",
283 | "cpu": [
284 | "ppc64le"
285 | ],
286 | "dev": true,
287 | "optional": true,
288 | "os": [
289 | "linux"
290 | ]
291 | },
292 | "node_modules/@rollup/rollup-linux-riscv64-gnu": {
293 | "version": "4.14.1",
294 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.1.tgz",
295 | "integrity": "sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw==",
296 | "cpu": [
297 | "riscv64"
298 | ],
299 | "dev": true,
300 | "optional": true,
301 | "os": [
302 | "linux"
303 | ]
304 | },
305 | "node_modules/@rollup/rollup-linux-s390x-gnu": {
306 | "version": "4.14.1",
307 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.1.tgz",
308 | "integrity": "sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA==",
309 | "cpu": [
310 | "s390x"
311 | ],
312 | "dev": true,
313 | "optional": true,
314 | "os": [
315 | "linux"
316 | ]
317 | },
318 | "node_modules/@rollup/rollup-linux-x64-gnu": {
319 | "version": "4.14.1",
320 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.1.tgz",
321 | "integrity": "sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA==",
322 | "cpu": [
323 | "x64"
324 | ],
325 | "dev": true,
326 | "optional": true,
327 | "os": [
328 | "linux"
329 | ]
330 | },
331 | "node_modules/@rollup/rollup-linux-x64-musl": {
332 | "version": "4.14.1",
333 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.1.tgz",
334 | "integrity": "sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g==",
335 | "cpu": [
336 | "x64"
337 | ],
338 | "dev": true,
339 | "optional": true,
340 | "os": [
341 | "linux"
342 | ]
343 | },
344 | "node_modules/@rollup/rollup-win32-arm64-msvc": {
345 | "version": "4.14.1",
346 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.1.tgz",
347 | "integrity": "sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA==",
348 | "cpu": [
349 | "arm64"
350 | ],
351 | "dev": true,
352 | "optional": true,
353 | "os": [
354 | "win32"
355 | ]
356 | },
357 | "node_modules/@rollup/rollup-win32-ia32-msvc": {
358 | "version": "4.14.1",
359 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.1.tgz",
360 | "integrity": "sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg==",
361 | "cpu": [
362 | "ia32"
363 | ],
364 | "dev": true,
365 | "optional": true,
366 | "os": [
367 | "win32"
368 | ]
369 | },
370 | "node_modules/@rollup/rollup-win32-x64-msvc": {
371 | "version": "4.14.1",
372 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.1.tgz",
373 | "integrity": "sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew==",
374 | "cpu": [
375 | "x64"
376 | ],
377 | "dev": true,
378 | "optional": true,
379 | "os": [
380 | "win32"
381 | ]
382 | },
383 | "node_modules/@types/estree": {
384 | "version": "1.0.5",
385 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
386 | "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
387 | "dev": true
388 | },
389 | "node_modules/@zeit/schemas": {
390 | "version": "2.29.0",
391 | "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.29.0.tgz",
392 | "integrity": "sha512-g5QiLIfbg3pLuYUJPlisNKY+epQJTcMDsOnVNkscrDP1oi7vmJnzOANYJI/1pZcVJ6umUkBv3aFtlg1UvUHGzA==",
393 | "dev": true
394 | },
395 | "node_modules/accepts": {
396 | "version": "1.3.8",
397 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
398 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
399 | "dev": true,
400 | "dependencies": {
401 | "mime-types": "~2.1.34",
402 | "negotiator": "0.6.3"
403 | },
404 | "engines": {
405 | "node": ">= 0.6"
406 | }
407 | },
408 | "node_modules/acorn": {
409 | "version": "8.11.3",
410 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
411 | "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
412 | "dev": true,
413 | "bin": {
414 | "acorn": "bin/acorn"
415 | },
416 | "engines": {
417 | "node": ">=0.4.0"
418 | }
419 | },
420 | "node_modules/ajv": {
421 | "version": "8.11.0",
422 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
423 | "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
424 | "dev": true,
425 | "dependencies": {
426 | "fast-deep-equal": "^3.1.1",
427 | "json-schema-traverse": "^1.0.0",
428 | "require-from-string": "^2.0.2",
429 | "uri-js": "^4.2.2"
430 | },
431 | "funding": {
432 | "type": "github",
433 | "url": "https://github.com/sponsors/epoberezkin"
434 | }
435 | },
436 | "node_modules/ansi-align": {
437 | "version": "3.0.1",
438 | "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
439 | "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
440 | "dev": true,
441 | "dependencies": {
442 | "string-width": "^4.1.0"
443 | }
444 | },
445 | "node_modules/ansi-align/node_modules/ansi-regex": {
446 | "version": "5.0.1",
447 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
448 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
449 | "dev": true,
450 | "engines": {
451 | "node": ">=8"
452 | }
453 | },
454 | "node_modules/ansi-align/node_modules/emoji-regex": {
455 | "version": "8.0.0",
456 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
457 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
458 | "dev": true
459 | },
460 | "node_modules/ansi-align/node_modules/string-width": {
461 | "version": "4.2.3",
462 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
463 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
464 | "dev": true,
465 | "dependencies": {
466 | "emoji-regex": "^8.0.0",
467 | "is-fullwidth-code-point": "^3.0.0",
468 | "strip-ansi": "^6.0.1"
469 | },
470 | "engines": {
471 | "node": ">=8"
472 | }
473 | },
474 | "node_modules/ansi-align/node_modules/strip-ansi": {
475 | "version": "6.0.1",
476 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
477 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
478 | "dev": true,
479 | "dependencies": {
480 | "ansi-regex": "^5.0.1"
481 | },
482 | "engines": {
483 | "node": ">=8"
484 | }
485 | },
486 | "node_modules/ansi-regex": {
487 | "version": "6.0.1",
488 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
489 | "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
490 | "dev": true,
491 | "engines": {
492 | "node": ">=12"
493 | },
494 | "funding": {
495 | "url": "https://github.com/chalk/ansi-regex?sponsor=1"
496 | }
497 | },
498 | "node_modules/ansi-styles": {
499 | "version": "6.2.1",
500 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
501 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
502 | "dev": true,
503 | "engines": {
504 | "node": ">=12"
505 | },
506 | "funding": {
507 | "url": "https://github.com/chalk/ansi-styles?sponsor=1"
508 | }
509 | },
510 | "node_modules/arch": {
511 | "version": "2.2.0",
512 | "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
513 | "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
514 | "dev": true,
515 | "funding": [
516 | {
517 | "type": "github",
518 | "url": "https://github.com/sponsors/feross"
519 | },
520 | {
521 | "type": "patreon",
522 | "url": "https://www.patreon.com/feross"
523 | },
524 | {
525 | "type": "consulting",
526 | "url": "https://feross.org/support"
527 | }
528 | ]
529 | },
530 | "node_modules/arg": {
531 | "version": "5.0.2",
532 | "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
533 | "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
534 | "dev": true
535 | },
536 | "node_modules/balanced-match": {
537 | "version": "1.0.2",
538 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
539 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
540 | "dev": true
541 | },
542 | "node_modules/boxen": {
543 | "version": "7.0.0",
544 | "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz",
545 | "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==",
546 | "dev": true,
547 | "dependencies": {
548 | "ansi-align": "^3.0.1",
549 | "camelcase": "^7.0.0",
550 | "chalk": "^5.0.1",
551 | "cli-boxes": "^3.0.0",
552 | "string-width": "^5.1.2",
553 | "type-fest": "^2.13.0",
554 | "widest-line": "^4.0.1",
555 | "wrap-ansi": "^8.0.1"
556 | },
557 | "engines": {
558 | "node": ">=14.16"
559 | },
560 | "funding": {
561 | "url": "https://github.com/sponsors/sindresorhus"
562 | }
563 | },
564 | "node_modules/brace-expansion": {
565 | "version": "1.1.11",
566 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
567 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
568 | "dev": true,
569 | "dependencies": {
570 | "balanced-match": "^1.0.0",
571 | "concat-map": "0.0.1"
572 | }
573 | },
574 | "node_modules/buffer-from": {
575 | "version": "1.1.2",
576 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
577 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
578 | "dev": true
579 | },
580 | "node_modules/bytes": {
581 | "version": "3.0.0",
582 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
583 | "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
584 | "dev": true,
585 | "engines": {
586 | "node": ">= 0.8"
587 | }
588 | },
589 | "node_modules/camelcase": {
590 | "version": "7.0.1",
591 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz",
592 | "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==",
593 | "dev": true,
594 | "engines": {
595 | "node": ">=14.16"
596 | },
597 | "funding": {
598 | "url": "https://github.com/sponsors/sindresorhus"
599 | }
600 | },
601 | "node_modules/chalk": {
602 | "version": "5.0.1",
603 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz",
604 | "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==",
605 | "dev": true,
606 | "engines": {
607 | "node": "^12.17.0 || ^14.13 || >=16.0.0"
608 | },
609 | "funding": {
610 | "url": "https://github.com/chalk/chalk?sponsor=1"
611 | }
612 | },
613 | "node_modules/chalk-template": {
614 | "version": "0.4.0",
615 | "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz",
616 | "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
617 | "dev": true,
618 | "dependencies": {
619 | "chalk": "^4.1.2"
620 | },
621 | "engines": {
622 | "node": ">=12"
623 | },
624 | "funding": {
625 | "url": "https://github.com/chalk/chalk-template?sponsor=1"
626 | }
627 | },
628 | "node_modules/chalk-template/node_modules/ansi-styles": {
629 | "version": "4.3.0",
630 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
631 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
632 | "dev": true,
633 | "dependencies": {
634 | "color-convert": "^2.0.1"
635 | },
636 | "engines": {
637 | "node": ">=8"
638 | },
639 | "funding": {
640 | "url": "https://github.com/chalk/ansi-styles?sponsor=1"
641 | }
642 | },
643 | "node_modules/chalk-template/node_modules/chalk": {
644 | "version": "4.1.2",
645 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
646 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
647 | "dev": true,
648 | "dependencies": {
649 | "ansi-styles": "^4.1.0",
650 | "supports-color": "^7.1.0"
651 | },
652 | "engines": {
653 | "node": ">=10"
654 | },
655 | "funding": {
656 | "url": "https://github.com/chalk/chalk?sponsor=1"
657 | }
658 | },
659 | "node_modules/cli-boxes": {
660 | "version": "3.0.0",
661 | "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
662 | "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
663 | "dev": true,
664 | "engines": {
665 | "node": ">=10"
666 | },
667 | "funding": {
668 | "url": "https://github.com/sponsors/sindresorhus"
669 | }
670 | },
671 | "node_modules/clipboardy": {
672 | "version": "3.0.0",
673 | "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz",
674 | "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==",
675 | "dev": true,
676 | "dependencies": {
677 | "arch": "^2.2.0",
678 | "execa": "^5.1.1",
679 | "is-wsl": "^2.2.0"
680 | },
681 | "engines": {
682 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
683 | },
684 | "funding": {
685 | "url": "https://github.com/sponsors/sindresorhus"
686 | }
687 | },
688 | "node_modules/color-convert": {
689 | "version": "2.0.1",
690 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
691 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
692 | "dev": true,
693 | "dependencies": {
694 | "color-name": "~1.1.4"
695 | },
696 | "engines": {
697 | "node": ">=7.0.0"
698 | }
699 | },
700 | "node_modules/color-name": {
701 | "version": "1.1.4",
702 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
703 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
704 | "dev": true
705 | },
706 | "node_modules/commander": {
707 | "version": "2.20.3",
708 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
709 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
710 | "dev": true
711 | },
712 | "node_modules/compressible": {
713 | "version": "2.0.18",
714 | "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
715 | "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
716 | "dev": true,
717 | "dependencies": {
718 | "mime-db": ">= 1.43.0 < 2"
719 | },
720 | "engines": {
721 | "node": ">= 0.6"
722 | }
723 | },
724 | "node_modules/compression": {
725 | "version": "1.7.4",
726 | "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
727 | "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
728 | "dev": true,
729 | "dependencies": {
730 | "accepts": "~1.3.5",
731 | "bytes": "3.0.0",
732 | "compressible": "~2.0.16",
733 | "debug": "2.6.9",
734 | "on-headers": "~1.0.2",
735 | "safe-buffer": "5.1.2",
736 | "vary": "~1.1.2"
737 | },
738 | "engines": {
739 | "node": ">= 0.8.0"
740 | }
741 | },
742 | "node_modules/compression/node_modules/safe-buffer": {
743 | "version": "5.1.2",
744 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
745 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
746 | "dev": true
747 | },
748 | "node_modules/concat-map": {
749 | "version": "0.0.1",
750 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
751 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
752 | "dev": true
753 | },
754 | "node_modules/content-disposition": {
755 | "version": "0.5.2",
756 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
757 | "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
758 | "dev": true,
759 | "engines": {
760 | "node": ">= 0.6"
761 | }
762 | },
763 | "node_modules/cross-spawn": {
764 | "version": "7.0.3",
765 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
766 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
767 | "dev": true,
768 | "dependencies": {
769 | "path-key": "^3.1.0",
770 | "shebang-command": "^2.0.0",
771 | "which": "^2.0.1"
772 | },
773 | "engines": {
774 | "node": ">= 8"
775 | }
776 | },
777 | "node_modules/debug": {
778 | "version": "2.6.9",
779 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
780 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
781 | "dev": true,
782 | "dependencies": {
783 | "ms": "2.0.0"
784 | }
785 | },
786 | "node_modules/deep-extend": {
787 | "version": "0.6.0",
788 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
789 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
790 | "dev": true,
791 | "engines": {
792 | "node": ">=4.0.0"
793 | }
794 | },
795 | "node_modules/dotenv": {
796 | "version": "16.4.5",
797 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
798 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
799 | "dev": true,
800 | "engines": {
801 | "node": ">=12"
802 | },
803 | "funding": {
804 | "url": "https://dotenvx.com"
805 | }
806 | },
807 | "node_modules/eastasianwidth": {
808 | "version": "0.2.0",
809 | "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
810 | "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
811 | "dev": true
812 | },
813 | "node_modules/emoji-regex": {
814 | "version": "9.2.2",
815 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
816 | "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
817 | "dev": true
818 | },
819 | "node_modules/estree-walker": {
820 | "version": "2.0.2",
821 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
822 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
823 | "dev": true
824 | },
825 | "node_modules/execa": {
826 | "version": "5.1.1",
827 | "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
828 | "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
829 | "dev": true,
830 | "dependencies": {
831 | "cross-spawn": "^7.0.3",
832 | "get-stream": "^6.0.0",
833 | "human-signals": "^2.1.0",
834 | "is-stream": "^2.0.0",
835 | "merge-stream": "^2.0.0",
836 | "npm-run-path": "^4.0.1",
837 | "onetime": "^5.1.2",
838 | "signal-exit": "^3.0.3",
839 | "strip-final-newline": "^2.0.0"
840 | },
841 | "engines": {
842 | "node": ">=10"
843 | },
844 | "funding": {
845 | "url": "https://github.com/sindresorhus/execa?sponsor=1"
846 | }
847 | },
848 | "node_modules/fast-deep-equal": {
849 | "version": "3.1.3",
850 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
851 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
852 | "dev": true
853 | },
854 | "node_modules/fast-url-parser": {
855 | "version": "1.1.3",
856 | "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz",
857 | "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==",
858 | "dev": true,
859 | "dependencies": {
860 | "punycode": "^1.3.2"
861 | }
862 | },
863 | "node_modules/fsevents": {
864 | "version": "2.3.3",
865 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
866 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
867 | "dev": true,
868 | "hasInstallScript": true,
869 | "optional": true,
870 | "os": [
871 | "darwin"
872 | ],
873 | "engines": {
874 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
875 | }
876 | },
877 | "node_modules/function-bind": {
878 | "version": "1.1.2",
879 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
880 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
881 | "dev": true,
882 | "funding": {
883 | "url": "https://github.com/sponsors/ljharb"
884 | }
885 | },
886 | "node_modules/get-stream": {
887 | "version": "6.0.1",
888 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
889 | "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
890 | "dev": true,
891 | "engines": {
892 | "node": ">=10"
893 | },
894 | "funding": {
895 | "url": "https://github.com/sponsors/sindresorhus"
896 | }
897 | },
898 | "node_modules/has-flag": {
899 | "version": "4.0.0",
900 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
901 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
902 | "dev": true,
903 | "engines": {
904 | "node": ">=8"
905 | }
906 | },
907 | "node_modules/hasown": {
908 | "version": "2.0.2",
909 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
910 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
911 | "dev": true,
912 | "dependencies": {
913 | "function-bind": "^1.1.2"
914 | },
915 | "engines": {
916 | "node": ">= 0.4"
917 | }
918 | },
919 | "node_modules/human-signals": {
920 | "version": "2.1.0",
921 | "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
922 | "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
923 | "dev": true,
924 | "engines": {
925 | "node": ">=10.17.0"
926 | }
927 | },
928 | "node_modules/ini": {
929 | "version": "1.3.8",
930 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
931 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
932 | "dev": true
933 | },
934 | "node_modules/is-core-module": {
935 | "version": "2.13.1",
936 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
937 | "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
938 | "dev": true,
939 | "dependencies": {
940 | "hasown": "^2.0.0"
941 | },
942 | "funding": {
943 | "url": "https://github.com/sponsors/ljharb"
944 | }
945 | },
946 | "node_modules/is-docker": {
947 | "version": "2.2.1",
948 | "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
949 | "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
950 | "dev": true,
951 | "bin": {
952 | "is-docker": "cli.js"
953 | },
954 | "engines": {
955 | "node": ">=8"
956 | },
957 | "funding": {
958 | "url": "https://github.com/sponsors/sindresorhus"
959 | }
960 | },
961 | "node_modules/is-fullwidth-code-point": {
962 | "version": "3.0.0",
963 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
964 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
965 | "dev": true,
966 | "engines": {
967 | "node": ">=8"
968 | }
969 | },
970 | "node_modules/is-port-reachable": {
971 | "version": "4.0.0",
972 | "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz",
973 | "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==",
974 | "dev": true,
975 | "engines": {
976 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
977 | },
978 | "funding": {
979 | "url": "https://github.com/sponsors/sindresorhus"
980 | }
981 | },
982 | "node_modules/is-stream": {
983 | "version": "2.0.1",
984 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
985 | "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
986 | "dev": true,
987 | "engines": {
988 | "node": ">=8"
989 | },
990 | "funding": {
991 | "url": "https://github.com/sponsors/sindresorhus"
992 | }
993 | },
994 | "node_modules/is-wsl": {
995 | "version": "2.2.0",
996 | "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
997 | "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
998 | "dev": true,
999 | "dependencies": {
1000 | "is-docker": "^2.0.0"
1001 | },
1002 | "engines": {
1003 | "node": ">=8"
1004 | }
1005 | },
1006 | "node_modules/isexe": {
1007 | "version": "2.0.0",
1008 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
1009 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
1010 | "dev": true
1011 | },
1012 | "node_modules/json-schema-traverse": {
1013 | "version": "1.0.0",
1014 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
1015 | "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
1016 | "dev": true
1017 | },
1018 | "node_modules/magic-string": {
1019 | "version": "0.30.9",
1020 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.9.tgz",
1021 | "integrity": "sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==",
1022 | "dev": true,
1023 | "dependencies": {
1024 | "@jridgewell/sourcemap-codec": "^1.4.15"
1025 | },
1026 | "engines": {
1027 | "node": ">=12"
1028 | }
1029 | },
1030 | "node_modules/merge-stream": {
1031 | "version": "2.0.0",
1032 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
1033 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
1034 | "dev": true
1035 | },
1036 | "node_modules/mime-db": {
1037 | "version": "1.52.0",
1038 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1039 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1040 | "dev": true,
1041 | "engines": {
1042 | "node": ">= 0.6"
1043 | }
1044 | },
1045 | "node_modules/mime-types": {
1046 | "version": "2.1.35",
1047 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1048 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1049 | "dev": true,
1050 | "dependencies": {
1051 | "mime-db": "1.52.0"
1052 | },
1053 | "engines": {
1054 | "node": ">= 0.6"
1055 | }
1056 | },
1057 | "node_modules/mimic-fn": {
1058 | "version": "2.1.0",
1059 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
1060 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
1061 | "dev": true,
1062 | "engines": {
1063 | "node": ">=6"
1064 | }
1065 | },
1066 | "node_modules/minimatch": {
1067 | "version": "3.1.2",
1068 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
1069 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
1070 | "dev": true,
1071 | "dependencies": {
1072 | "brace-expansion": "^1.1.7"
1073 | },
1074 | "engines": {
1075 | "node": "*"
1076 | }
1077 | },
1078 | "node_modules/minimist": {
1079 | "version": "1.2.8",
1080 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
1081 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
1082 | "dev": true,
1083 | "funding": {
1084 | "url": "https://github.com/sponsors/ljharb"
1085 | }
1086 | },
1087 | "node_modules/ms": {
1088 | "version": "2.0.0",
1089 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1090 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1091 | "dev": true
1092 | },
1093 | "node_modules/negotiator": {
1094 | "version": "0.6.3",
1095 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1096 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1097 | "dev": true,
1098 | "engines": {
1099 | "node": ">= 0.6"
1100 | }
1101 | },
1102 | "node_modules/npm-run-path": {
1103 | "version": "4.0.1",
1104 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
1105 | "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
1106 | "dev": true,
1107 | "dependencies": {
1108 | "path-key": "^3.0.0"
1109 | },
1110 | "engines": {
1111 | "node": ">=8"
1112 | }
1113 | },
1114 | "node_modules/on-headers": {
1115 | "version": "1.0.2",
1116 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
1117 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
1118 | "dev": true,
1119 | "engines": {
1120 | "node": ">= 0.8"
1121 | }
1122 | },
1123 | "node_modules/onetime": {
1124 | "version": "5.1.2",
1125 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
1126 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
1127 | "dev": true,
1128 | "dependencies": {
1129 | "mimic-fn": "^2.1.0"
1130 | },
1131 | "engines": {
1132 | "node": ">=6"
1133 | },
1134 | "funding": {
1135 | "url": "https://github.com/sponsors/sindresorhus"
1136 | }
1137 | },
1138 | "node_modules/path-is-inside": {
1139 | "version": "1.0.2",
1140 | "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
1141 | "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
1142 | "dev": true
1143 | },
1144 | "node_modules/path-key": {
1145 | "version": "3.1.1",
1146 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
1147 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
1148 | "dev": true,
1149 | "engines": {
1150 | "node": ">=8"
1151 | }
1152 | },
1153 | "node_modules/path-parse": {
1154 | "version": "1.0.7",
1155 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
1156 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
1157 | "dev": true
1158 | },
1159 | "node_modules/path-to-regexp": {
1160 | "version": "2.2.1",
1161 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz",
1162 | "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==",
1163 | "dev": true
1164 | },
1165 | "node_modules/picomatch": {
1166 | "version": "2.3.1",
1167 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1168 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1169 | "dev": true,
1170 | "engines": {
1171 | "node": ">=8.6"
1172 | },
1173 | "funding": {
1174 | "url": "https://github.com/sponsors/jonschlinkert"
1175 | }
1176 | },
1177 | "node_modules/prettier": {
1178 | "version": "3.2.5",
1179 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
1180 | "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
1181 | "dev": true,
1182 | "bin": {
1183 | "prettier": "bin/prettier.cjs"
1184 | },
1185 | "engines": {
1186 | "node": ">=14"
1187 | },
1188 | "funding": {
1189 | "url": "https://github.com/prettier/prettier?sponsor=1"
1190 | }
1191 | },
1192 | "node_modules/prettier-plugin-organize-imports": {
1193 | "version": "3.2.4",
1194 | "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz",
1195 | "integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==",
1196 | "dev": true,
1197 | "peerDependencies": {
1198 | "@volar/vue-language-plugin-pug": "^1.0.4",
1199 | "@volar/vue-typescript": "^1.0.4",
1200 | "prettier": ">=2.0",
1201 | "typescript": ">=2.9"
1202 | },
1203 | "peerDependenciesMeta": {
1204 | "@volar/vue-language-plugin-pug": {
1205 | "optional": true
1206 | },
1207 | "@volar/vue-typescript": {
1208 | "optional": true
1209 | }
1210 | }
1211 | },
1212 | "node_modules/punycode": {
1213 | "version": "1.4.1",
1214 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
1215 | "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
1216 | "dev": true
1217 | },
1218 | "node_modules/randombytes": {
1219 | "version": "2.1.0",
1220 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
1221 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
1222 | "dev": true,
1223 | "dependencies": {
1224 | "safe-buffer": "^5.1.0"
1225 | }
1226 | },
1227 | "node_modules/range-parser": {
1228 | "version": "1.2.0",
1229 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
1230 | "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
1231 | "dev": true,
1232 | "engines": {
1233 | "node": ">= 0.6"
1234 | }
1235 | },
1236 | "node_modules/rc": {
1237 | "version": "1.2.8",
1238 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
1239 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
1240 | "dev": true,
1241 | "dependencies": {
1242 | "deep-extend": "^0.6.0",
1243 | "ini": "~1.3.0",
1244 | "minimist": "^1.2.0",
1245 | "strip-json-comments": "~2.0.1"
1246 | },
1247 | "bin": {
1248 | "rc": "cli.js"
1249 | }
1250 | },
1251 | "node_modules/registry-auth-token": {
1252 | "version": "3.3.2",
1253 | "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz",
1254 | "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==",
1255 | "dev": true,
1256 | "dependencies": {
1257 | "rc": "^1.1.6",
1258 | "safe-buffer": "^5.0.1"
1259 | }
1260 | },
1261 | "node_modules/registry-url": {
1262 | "version": "3.1.0",
1263 | "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz",
1264 | "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==",
1265 | "dev": true,
1266 | "dependencies": {
1267 | "rc": "^1.0.1"
1268 | },
1269 | "engines": {
1270 | "node": ">=0.10.0"
1271 | }
1272 | },
1273 | "node_modules/require-from-string": {
1274 | "version": "2.0.2",
1275 | "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
1276 | "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
1277 | "dev": true,
1278 | "engines": {
1279 | "node": ">=0.10.0"
1280 | }
1281 | },
1282 | "node_modules/resolve": {
1283 | "version": "1.22.8",
1284 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
1285 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
1286 | "dev": true,
1287 | "dependencies": {
1288 | "is-core-module": "^2.13.0",
1289 | "path-parse": "^1.0.7",
1290 | "supports-preserve-symlinks-flag": "^1.0.0"
1291 | },
1292 | "bin": {
1293 | "resolve": "bin/resolve"
1294 | },
1295 | "funding": {
1296 | "url": "https://github.com/sponsors/ljharb"
1297 | }
1298 | },
1299 | "node_modules/rollup": {
1300 | "version": "4.14.1",
1301 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.1.tgz",
1302 | "integrity": "sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==",
1303 | "dev": true,
1304 | "dependencies": {
1305 | "@types/estree": "1.0.5"
1306 | },
1307 | "bin": {
1308 | "rollup": "dist/bin/rollup"
1309 | },
1310 | "engines": {
1311 | "node": ">=18.0.0",
1312 | "npm": ">=8.0.0"
1313 | },
1314 | "optionalDependencies": {
1315 | "@rollup/rollup-android-arm-eabi": "4.14.1",
1316 | "@rollup/rollup-android-arm64": "4.14.1",
1317 | "@rollup/rollup-darwin-arm64": "4.14.1",
1318 | "@rollup/rollup-darwin-x64": "4.14.1",
1319 | "@rollup/rollup-linux-arm-gnueabihf": "4.14.1",
1320 | "@rollup/rollup-linux-arm64-gnu": "4.14.1",
1321 | "@rollup/rollup-linux-arm64-musl": "4.14.1",
1322 | "@rollup/rollup-linux-powerpc64le-gnu": "4.14.1",
1323 | "@rollup/rollup-linux-riscv64-gnu": "4.14.1",
1324 | "@rollup/rollup-linux-s390x-gnu": "4.14.1",
1325 | "@rollup/rollup-linux-x64-gnu": "4.14.1",
1326 | "@rollup/rollup-linux-x64-musl": "4.14.1",
1327 | "@rollup/rollup-win32-arm64-msvc": "4.14.1",
1328 | "@rollup/rollup-win32-ia32-msvc": "4.14.1",
1329 | "@rollup/rollup-win32-x64-msvc": "4.14.1",
1330 | "fsevents": "~2.3.2"
1331 | }
1332 | },
1333 | "node_modules/safe-buffer": {
1334 | "version": "5.2.1",
1335 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1336 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1337 | "dev": true,
1338 | "funding": [
1339 | {
1340 | "type": "github",
1341 | "url": "https://github.com/sponsors/feross"
1342 | },
1343 | {
1344 | "type": "patreon",
1345 | "url": "https://www.patreon.com/feross"
1346 | },
1347 | {
1348 | "type": "consulting",
1349 | "url": "https://feross.org/support"
1350 | }
1351 | ]
1352 | },
1353 | "node_modules/serialize-javascript": {
1354 | "version": "6.0.2",
1355 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
1356 | "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
1357 | "dev": true,
1358 | "dependencies": {
1359 | "randombytes": "^2.1.0"
1360 | }
1361 | },
1362 | "node_modules/serve": {
1363 | "version": "14.2.1",
1364 | "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.1.tgz",
1365 | "integrity": "sha512-48er5fzHh7GCShLnNyPBRPEjs2I6QBozeGr02gaacROiyS/8ARADlj595j39iZXAqBbJHH/ivJJyPRWY9sQWZA==",
1366 | "dev": true,
1367 | "dependencies": {
1368 | "@zeit/schemas": "2.29.0",
1369 | "ajv": "8.11.0",
1370 | "arg": "5.0.2",
1371 | "boxen": "7.0.0",
1372 | "chalk": "5.0.1",
1373 | "chalk-template": "0.4.0",
1374 | "clipboardy": "3.0.0",
1375 | "compression": "1.7.4",
1376 | "is-port-reachable": "4.0.0",
1377 | "serve-handler": "6.1.5",
1378 | "update-check": "1.5.4"
1379 | },
1380 | "bin": {
1381 | "serve": "build/main.js"
1382 | },
1383 | "engines": {
1384 | "node": ">= 14"
1385 | }
1386 | },
1387 | "node_modules/serve-handler": {
1388 | "version": "6.1.5",
1389 | "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.5.tgz",
1390 | "integrity": "sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==",
1391 | "dev": true,
1392 | "dependencies": {
1393 | "bytes": "3.0.0",
1394 | "content-disposition": "0.5.2",
1395 | "fast-url-parser": "1.1.3",
1396 | "mime-types": "2.1.18",
1397 | "minimatch": "3.1.2",
1398 | "path-is-inside": "1.0.2",
1399 | "path-to-regexp": "2.2.1",
1400 | "range-parser": "1.2.0"
1401 | }
1402 | },
1403 | "node_modules/serve-handler/node_modules/mime-db": {
1404 | "version": "1.33.0",
1405 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
1406 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
1407 | "dev": true,
1408 | "engines": {
1409 | "node": ">= 0.6"
1410 | }
1411 | },
1412 | "node_modules/serve-handler/node_modules/mime-types": {
1413 | "version": "2.1.18",
1414 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
1415 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
1416 | "dev": true,
1417 | "dependencies": {
1418 | "mime-db": "~1.33.0"
1419 | },
1420 | "engines": {
1421 | "node": ">= 0.6"
1422 | }
1423 | },
1424 | "node_modules/shebang-command": {
1425 | "version": "2.0.0",
1426 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
1427 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
1428 | "dev": true,
1429 | "dependencies": {
1430 | "shebang-regex": "^3.0.0"
1431 | },
1432 | "engines": {
1433 | "node": ">=8"
1434 | }
1435 | },
1436 | "node_modules/shebang-regex": {
1437 | "version": "3.0.0",
1438 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
1439 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
1440 | "dev": true,
1441 | "engines": {
1442 | "node": ">=8"
1443 | }
1444 | },
1445 | "node_modules/signal-exit": {
1446 | "version": "3.0.7",
1447 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
1448 | "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
1449 | "dev": true
1450 | },
1451 | "node_modules/smob": {
1452 | "version": "1.5.0",
1453 | "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz",
1454 | "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==",
1455 | "dev": true
1456 | },
1457 | "node_modules/source-map": {
1458 | "version": "0.6.1",
1459 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
1460 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
1461 | "dev": true,
1462 | "engines": {
1463 | "node": ">=0.10.0"
1464 | }
1465 | },
1466 | "node_modules/source-map-support": {
1467 | "version": "0.5.21",
1468 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
1469 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
1470 | "dev": true,
1471 | "dependencies": {
1472 | "buffer-from": "^1.0.0",
1473 | "source-map": "^0.6.0"
1474 | }
1475 | },
1476 | "node_modules/string-width": {
1477 | "version": "5.1.2",
1478 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
1479 | "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
1480 | "dev": true,
1481 | "dependencies": {
1482 | "eastasianwidth": "^0.2.0",
1483 | "emoji-regex": "^9.2.2",
1484 | "strip-ansi": "^7.0.1"
1485 | },
1486 | "engines": {
1487 | "node": ">=12"
1488 | },
1489 | "funding": {
1490 | "url": "https://github.com/sponsors/sindresorhus"
1491 | }
1492 | },
1493 | "node_modules/strip-ansi": {
1494 | "version": "7.1.0",
1495 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
1496 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
1497 | "dev": true,
1498 | "dependencies": {
1499 | "ansi-regex": "^6.0.1"
1500 | },
1501 | "engines": {
1502 | "node": ">=12"
1503 | },
1504 | "funding": {
1505 | "url": "https://github.com/chalk/strip-ansi?sponsor=1"
1506 | }
1507 | },
1508 | "node_modules/strip-final-newline": {
1509 | "version": "2.0.0",
1510 | "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
1511 | "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
1512 | "dev": true,
1513 | "engines": {
1514 | "node": ">=6"
1515 | }
1516 | },
1517 | "node_modules/strip-json-comments": {
1518 | "version": "2.0.1",
1519 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
1520 | "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
1521 | "dev": true,
1522 | "engines": {
1523 | "node": ">=0.10.0"
1524 | }
1525 | },
1526 | "node_modules/supports-color": {
1527 | "version": "7.2.0",
1528 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
1529 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
1530 | "dev": true,
1531 | "dependencies": {
1532 | "has-flag": "^4.0.0"
1533 | },
1534 | "engines": {
1535 | "node": ">=8"
1536 | }
1537 | },
1538 | "node_modules/supports-preserve-symlinks-flag": {
1539 | "version": "1.0.0",
1540 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
1541 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
1542 | "dev": true,
1543 | "engines": {
1544 | "node": ">= 0.4"
1545 | },
1546 | "funding": {
1547 | "url": "https://github.com/sponsors/ljharb"
1548 | }
1549 | },
1550 | "node_modules/terser": {
1551 | "version": "5.30.3",
1552 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz",
1553 | "integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==",
1554 | "dev": true,
1555 | "dependencies": {
1556 | "@jridgewell/source-map": "^0.3.3",
1557 | "acorn": "^8.8.2",
1558 | "commander": "^2.20.0",
1559 | "source-map-support": "~0.5.20"
1560 | },
1561 | "bin": {
1562 | "terser": "bin/terser"
1563 | },
1564 | "engines": {
1565 | "node": ">=10"
1566 | }
1567 | },
1568 | "node_modules/tslib": {
1569 | "version": "2.6.2",
1570 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
1571 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
1572 | "dev": true
1573 | },
1574 | "node_modules/type-fest": {
1575 | "version": "2.19.0",
1576 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
1577 | "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
1578 | "dev": true,
1579 | "engines": {
1580 | "node": ">=12.20"
1581 | },
1582 | "funding": {
1583 | "url": "https://github.com/sponsors/sindresorhus"
1584 | }
1585 | },
1586 | "node_modules/typescript": {
1587 | "version": "5.4.4",
1588 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
1589 | "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
1590 | "dev": true,
1591 | "bin": {
1592 | "tsc": "bin/tsc",
1593 | "tsserver": "bin/tsserver"
1594 | },
1595 | "engines": {
1596 | "node": ">=14.17"
1597 | }
1598 | },
1599 | "node_modules/update-check": {
1600 | "version": "1.5.4",
1601 | "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz",
1602 | "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==",
1603 | "dev": true,
1604 | "dependencies": {
1605 | "registry-auth-token": "3.3.2",
1606 | "registry-url": "3.1.0"
1607 | }
1608 | },
1609 | "node_modules/uri-js": {
1610 | "version": "4.4.1",
1611 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
1612 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
1613 | "dev": true,
1614 | "dependencies": {
1615 | "punycode": "^2.1.0"
1616 | }
1617 | },
1618 | "node_modules/uri-js/node_modules/punycode": {
1619 | "version": "2.3.1",
1620 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
1621 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
1622 | "dev": true,
1623 | "engines": {
1624 | "node": ">=6"
1625 | }
1626 | },
1627 | "node_modules/vary": {
1628 | "version": "1.1.2",
1629 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1630 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1631 | "dev": true,
1632 | "engines": {
1633 | "node": ">= 0.8"
1634 | }
1635 | },
1636 | "node_modules/which": {
1637 | "version": "2.0.2",
1638 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
1639 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
1640 | "dev": true,
1641 | "dependencies": {
1642 | "isexe": "^2.0.0"
1643 | },
1644 | "bin": {
1645 | "node-which": "bin/node-which"
1646 | },
1647 | "engines": {
1648 | "node": ">= 8"
1649 | }
1650 | },
1651 | "node_modules/widest-line": {
1652 | "version": "4.0.1",
1653 | "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
1654 | "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==",
1655 | "dev": true,
1656 | "dependencies": {
1657 | "string-width": "^5.0.1"
1658 | },
1659 | "engines": {
1660 | "node": ">=12"
1661 | },
1662 | "funding": {
1663 | "url": "https://github.com/sponsors/sindresorhus"
1664 | }
1665 | },
1666 | "node_modules/wrap-ansi": {
1667 | "version": "8.1.0",
1668 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
1669 | "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
1670 | "dev": true,
1671 | "dependencies": {
1672 | "ansi-styles": "^6.1.0",
1673 | "string-width": "^5.0.1",
1674 | "strip-ansi": "^7.0.1"
1675 | },
1676 | "engines": {
1677 | "node": ">=12"
1678 | },
1679 | "funding": {
1680 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
1681 | }
1682 | }
1683 | }
1684 | }
1685 |
--------------------------------------------------------------------------------