├── 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 |
9 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
NameStatus
Service workerunknown
Signalinguninitialized
WebRTCuninitialized
40 |
41 | 42 |
43 | 44 | Root 45 | 46 |
47 | 48 |
49 |

Requests

50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
IDMethodURLHeadersBody?
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 | ![Diagram](diagram.png) 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 | --------------------------------------------------------------------------------