diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2c30fb0d21c2b..c00f94a5e8382 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -280,44 +280,6 @@ jobs:
GOOS: android
GOARCH: arm64
- wasm: # builds tsconnect, which is the only wasm build we support
- runs-on: ubuntu-22.04
- steps:
- - name: checkout
- uses: actions/checkout@v3
- - uses: actions/setup-go@v5
- with:
- go-version-file: go.mod
- cache: false
- - name: Restore Cache
- uses: actions/cache@v3
- with:
- # Note: unlike the other setups, this is only grabbing the mod download
- # cache, rather than the whole mod directory, as the download cache
- # contains zips that can be unpacked in parallel faster than they can be
- # fetched and extracted by tar
- path: |
- ~/.cache/go-build
- ~/go/pkg/mod/cache
- ~\AppData\Local\go-build
- # The -2- here should be incremented when the scheme of data to be
- # cached changes (e.g. path above changes).
- key: ${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
- restore-keys: |
- ${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}
- ${{ github.job }}-${{ runner.os }}-go-2-
- - name: build tsconnect client
- run: ./tool/go build ./cmd/tsconnect/wasm ./cmd/tailscale/cli
- env:
- GOOS: js
- GOARCH: wasm
- - name: build tsconnect server
- # Note, no GOOS/GOARCH in env on this build step, we're running a build
- # tool that handles the build itself.
- run: |
- ./tool/go run ./cmd/tsconnect --fast-compression build
- ./tool/go run ./cmd/tsconnect --fast-compression build-pkg
-
fuzz:
# This target periodically breaks (see TS_FUZZ_CURRENTLY_BROKEN at the top
# of the file), so it's more complex than usual: the 'build fuzzers' step
@@ -467,7 +429,6 @@ jobs:
- vm
- cross
- ios
- - wasm
- fuzz
- depaware
- go_generate
@@ -511,7 +472,6 @@ jobs:
- vm
- cross
- ios
- - wasm
- fuzz
- depaware
- go_generate
diff --git a/Makefile b/Makefile
index 4a9acb0b45d7b..8adef020fb40e 100644
--- a/Makefile
+++ b/Makefile
@@ -33,16 +33,13 @@ build386: ## Build tailscale CLI for linux/386
buildlinuxarm: ## Build tailscale CLI for linux/arm
GOOS=linux GOARCH=arm ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
-buildwasm: ## Build tailscale CLI for js/wasm
- GOOS=js GOARCH=wasm ./tool/go install ./cmd/tsconnect/wasm ./cmd/tailscale/cli
-
buildlinuxloong64: ## Build tailscale CLI for linux/loong64
GOOS=linux GOARCH=loong64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
buildmultiarchimage: ## Build (and optionally push) multiarch docker image
./build_docker.sh
-check: staticcheck vet depaware buildwindows build386 buildlinuxarm buildwasm ## Perform basic checks and compilation tests
+check: staticcheck vet depaware buildwindows build386 buildlinuxarm ## Perform basic checks and compilation tests
staticcheck: ## Run staticcheck.io checks
./tool/go run honnef.co/go/tools/cmd/staticcheck -- $$(./tool/go list ./... | grep -v tempfork)
diff --git a/cmd/tsconnect/.gitignore b/cmd/tsconnect/.gitignore
deleted file mode 100644
index 13615d1213d63..0000000000000
--- a/cmd/tsconnect/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-node_modules/
-/dist
-/pkg
diff --git a/cmd/tsconnect/README.md b/cmd/tsconnect/README.md
deleted file mode 100644
index 536cd7bbf562c..0000000000000
--- a/cmd/tsconnect/README.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# tsconnect
-
-The tsconnect command builds and serves the static site that is generated for
-the Tailscale Connect JS/WASM client.
-
-## Development
-
-To start the development server:
-
-```
-./tool/go run ./cmd/tsconnect dev
-```
-
-The site is served at http://localhost:9090/. JavaScript, CSS and Go `wasm` package changes can be picked up with a browser reload. Server-side Go changes require the server to be stopped and restarted. In development mode the state the Tailscale client state is stored in `sessionStorage` and will thus survive page reloads (but not the tab being closed).
-
-## Deployment
-
-To build the static assets necessary for serving, run:
-
-```
-./tool/go run ./cmd/tsconnect build
-```
-
-To serve them, run:
-
-```
-./tool/go run ./cmd/tsconnect serve
-```
-
-By default the build output is placed in the `dist/` directory and embedded in the binary, but this can be controlled by the `-distdir` flag. The `-addr` flag controls the interface and port that the serve listens on.
-
-# Library / NPM Package
-
-The client is also available as [an NPM package](https://www.npmjs.com/package/@tailscale/connect). To build it, run:
-
-```
-./tool/go run ./cmd/tsconnect build-pkg
-```
-
-That places the output in the `pkg/` directory, which may then be uploaded to a package registry (or installed from the file path directly).
-
-To do two-sided development (on both the NPM package and code that uses it), run:
-
-```
-./tool/go run ./cmd/tsconnect dev-pkg
-
-```
-
-This serves the module at http://localhost:9090/pkg/pkg.js and the generated wasm file at http://localhost:9090/pkg/main.wasm. The two files can be used as drop-in replacements for normal imports of the NPM module.
diff --git a/cmd/tsconnect/README.pkg.md b/cmd/tsconnect/README.pkg.md
deleted file mode 100644
index df8d66789894d..0000000000000
--- a/cmd/tsconnect/README.pkg.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# @tailscale/connect
-
-NPM package that contains a WebAssembly-based Tailscale client, see [the `cmd/tsconnect` directory in the tailscale repo](https://github.com/tailscale/tailscale/tree/main/cmd/tsconnect#library--npm-package) for more details.
diff --git a/cmd/tsconnect/build-pkg.go b/cmd/tsconnect/build-pkg.go
deleted file mode 100644
index 1b93e5ba06741..0000000000000
--- a/cmd/tsconnect/build-pkg.go
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package main
-
-import (
- "encoding/json"
- "fmt"
- "log"
- "os"
- "path"
-
- "github.com/tailscale/hujson"
- "tailscale.com/util/precompress"
- "tailscale.com/version"
-)
-
-func runBuildPkg() {
- buildOptions, err := commonPkgSetup(prodMode)
- if err != nil {
- log.Fatalf("Cannot setup: %v", err)
- }
-
- log.Printf("Linting...\n")
- if err := runYarn("lint"); err != nil {
- log.Fatalf("Linting failed: %v", err)
- }
-
- if err := cleanDir(*pkgDir); err != nil {
- log.Fatalf("Cannot clean %s: %v", *pkgDir, err)
- }
-
- buildOptions.Write = true
- buildOptions.MinifyWhitespace = true
- buildOptions.MinifyIdentifiers = true
- buildOptions.MinifySyntax = true
-
- runEsbuild(*buildOptions)
-
- if err := precompressWasm(); err != nil {
- log.Fatalf("Could not pre-recompress wasm: %v", err)
- }
-
- log.Printf("Generating types...\n")
- if err := runYarn("pkg-types"); err != nil {
- log.Fatalf("Type generation failed: %v", err)
- }
-
- if err := updateVersion(); err != nil {
- log.Fatalf("Cannot update version: %v", err)
- }
-
- if err := copyReadme(); err != nil {
- log.Fatalf("Cannot copy readme: %v", err)
- }
-
- log.Printf("Built package version %s", version.Long())
-}
-
-func precompressWasm() error {
- log.Printf("Pre-compressing main.wasm...\n")
- return precompress.Precompress(path.Join(*pkgDir, "main.wasm"), precompress.Options{
- FastCompression: *fastCompression,
- })
-}
-
-func updateVersion() error {
- packageJSONBytes, err := os.ReadFile("package.json.tmpl")
- if err != nil {
- return fmt.Errorf("Could not read package.json: %w", err)
- }
-
- var packageJSON map[string]any
- packageJSONBytes, err = hujson.Standardize(packageJSONBytes)
- if err != nil {
- return fmt.Errorf("Could not standardize template package.json: %w", err)
- }
- if err := json.Unmarshal(packageJSONBytes, &packageJSON); err != nil {
- return fmt.Errorf("Could not unmarshal package.json: %w", err)
- }
- packageJSON["version"] = version.Long()
-
- packageJSONBytes, err = json.MarshalIndent(packageJSON, "", " ")
- if err != nil {
- return fmt.Errorf("Could not marshal package.json: %w", err)
- }
-
- return os.WriteFile(path.Join(*pkgDir, "package.json"), packageJSONBytes, 0644)
-}
-
-func copyReadme() error {
- readmeBytes, err := os.ReadFile("README.pkg.md")
- if err != nil {
- return fmt.Errorf("Could not read README.pkg.md: %w", err)
- }
- return os.WriteFile(path.Join(*pkgDir, "README.md"), readmeBytes, 0644)
-}
diff --git a/cmd/tsconnect/build.go b/cmd/tsconnect/build.go
deleted file mode 100644
index 6178ddb9b9ea2..0000000000000
--- a/cmd/tsconnect/build.go
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package main
-
-import (
- "encoding/json"
- "fmt"
- "log"
- "os"
- "path"
- "path/filepath"
-
- "tailscale.com/util/precompress"
-)
-
-func runBuild() {
- buildOptions, err := commonSetup(prodMode)
- if err != nil {
- log.Fatalf("Cannot setup: %v", err)
- }
-
- log.Printf("Linting...\n")
- if err := runYarn("lint"); err != nil {
- log.Fatalf("Linting failed: %v", err)
- }
-
- if err := cleanDir(*distDir, "placeholder"); err != nil {
- log.Fatalf("Cannot clean %s: %v", *distDir, err)
- }
-
- buildOptions.Write = true
- buildOptions.MinifyWhitespace = true
- buildOptions.MinifyIdentifiers = true
- buildOptions.MinifySyntax = true
-
- buildOptions.EntryNames = "[dir]/[name]-[hash]"
- buildOptions.AssetNames = "[name]-[hash]"
- buildOptions.Metafile = true
-
- result := runEsbuild(*buildOptions)
-
- // Preserve build metadata so we can extract hashed file names for serving.
- metadataBytes, err := fixEsbuildMetadataPaths(result.Metafile)
- if err != nil {
- log.Fatalf("Cannot fix esbuild metadata paths: %v", err)
- }
- if err := os.WriteFile(path.Join(*distDir, "/esbuild-metadata.json"), metadataBytes, 0666); err != nil {
- log.Fatalf("Cannot write metadata: %v", err)
- }
-
- if er := precompressDist(*fastCompression); err != nil {
- log.Fatalf("Cannot precompress resources: %v", er)
- }
-}
-
-// fixEsbuildMetadataPaths re-keys the esbuild metadata file to use paths
-// relative to the dist directory (it normally uses paths relative to the cwd,
-// which are awkward if we're running with a different cwd at serving time).
-func fixEsbuildMetadataPaths(metadataStr string) ([]byte, error) {
- var metadata EsbuildMetadata
- if err := json.Unmarshal([]byte(metadataStr), &metadata); err != nil {
- return nil, fmt.Errorf("Cannot parse metadata: %w", err)
- }
- distAbsPath, err := filepath.Abs(*distDir)
- if err != nil {
- return nil, fmt.Errorf("Cannot get absolute path from %s: %w", *distDir, err)
- }
- for outputPath, output := range metadata.Outputs {
- outputAbsPath, err := filepath.Abs(outputPath)
- if err != nil {
- return nil, fmt.Errorf("Cannot get absolute path from %s: %w", outputPath, err)
- }
- outputRelPath, err := filepath.Rel(distAbsPath, outputAbsPath)
- if err != nil {
- return nil, fmt.Errorf("Cannot get relative path from %s: %w", outputRelPath, err)
- }
- delete(metadata.Outputs, outputPath)
- metadata.Outputs[outputRelPath] = output
- }
- return json.Marshal(metadata)
-}
-
-func cleanDist() error {
- log.Printf("Cleaning %s...\n", *distDir)
- files, err := os.ReadDir(*distDir)
- if err != nil {
- if os.IsNotExist(err) {
- return os.MkdirAll(*distDir, 0755)
- }
- return err
- }
-
- for _, file := range files {
- if file.Name() != "placeholder" {
- if err := os.Remove(filepath.Join(*distDir, file.Name())); err != nil {
- return err
- }
- }
- }
- return nil
-}
-
-func precompressDist(fastCompression bool) error {
- log.Printf("Pre-compressing files in %s/...\n", *distDir)
- return precompress.PrecompressDir(*distDir, precompress.Options{
- FastCompression: fastCompression,
- ProgressFn: func(path string) {
- log.Printf("Pre-compressing %v\n", path)
- },
- })
-}
diff --git a/cmd/tsconnect/common.go b/cmd/tsconnect/common.go
deleted file mode 100644
index 437393b1d63e9..0000000000000
--- a/cmd/tsconnect/common.go
+++ /dev/null
@@ -1,325 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package main
-
-import (
- "fmt"
- "log"
- "net"
- "os"
- "os/exec"
- "path"
- "path/filepath"
- "runtime"
- "strconv"
- "time"
-
- esbuild "github.com/evanw/esbuild/pkg/api"
- "golang.org/x/exp/slices"
-)
-
-const (
- devMode = true
- prodMode = false
-)
-
-// commonSetup performs setup that is common to both dev and build modes.
-func commonSetup(dev bool) (*esbuild.BuildOptions, error) {
- // Change cwd to to where this file lives -- that's where all inputs for
- // esbuild and other build steps live.
- root, err := findRepoRoot()
- if err != nil {
- return nil, err
- }
- tsConnectDir := filepath.Join(root, "cmd", "tsconnect")
- if err := os.Chdir(tsConnectDir); err != nil {
- return nil, fmt.Errorf("Cannot change cwd: %w", err)
- }
- if err := installJSDeps(); err != nil {
- return nil, fmt.Errorf("Cannot install JS deps: %w", err)
- }
-
- return &esbuild.BuildOptions{
- EntryPoints: []string{"src/app/index.ts", "src/app/index.css"},
- Outdir: *distDir,
- Bundle: true,
- Sourcemap: esbuild.SourceMapLinked,
- LogLevel: esbuild.LogLevelInfo,
- Define: map[string]string{"DEBUG": strconv.FormatBool(dev)},
- Target: esbuild.ES2017,
- Plugins: []esbuild.Plugin{
- {
- Name: "tailscale-tailwind",
- Setup: func(build esbuild.PluginBuild) {
- setupEsbuildTailwind(build, dev)
- },
- },
- {
- Name: "tailscale-go-wasm-exec-js",
- Setup: setupEsbuildWasmExecJS,
- },
- {
- Name: "tailscale-wasm",
- Setup: func(build esbuild.PluginBuild) {
- setupEsbuildWasm(build, dev)
- },
- },
- },
- JSXMode: esbuild.JSXModeAutomatic,
- }, nil
-}
-
-func findRepoRoot() (string, error) {
- if *rootDir != "" {
- return *rootDir, nil
- }
- cwd, err := os.Getwd()
- if err != nil {
- return "", err
- }
- for {
- if _, err := os.Stat(path.Join(cwd, "go.mod")); err == nil {
- return cwd, nil
- }
- if cwd == "/" {
- return "", fmt.Errorf("Cannot find repo root")
- }
- cwd = path.Dir(cwd)
- }
-}
-
-func commonPkgSetup(dev bool) (*esbuild.BuildOptions, error) {
- buildOptions, err := commonSetup(dev)
- if err != nil {
- return nil, err
- }
- buildOptions.EntryPoints = []string{"src/pkg/pkg.ts", "src/pkg/pkg.css"}
- buildOptions.Outdir = *pkgDir
- buildOptions.Format = esbuild.FormatESModule
- buildOptions.AssetNames = "[name]"
- return buildOptions, nil
-}
-
-// cleanDir removes files from dirPath, except the ones specified by
-// preserveFiles.
-func cleanDir(dirPath string, preserveFiles ...string) error {
- log.Printf("Cleaning %s...\n", dirPath)
- files, err := os.ReadDir(dirPath)
- if err != nil {
- if os.IsNotExist(err) {
- return os.MkdirAll(dirPath, 0755)
- }
- return err
- }
-
- for _, file := range files {
- if !slices.Contains(preserveFiles, file.Name()) {
- if err := os.Remove(filepath.Join(dirPath, file.Name())); err != nil {
- return err
- }
- }
- }
- return nil
-}
-
-func runEsbuildServe(buildOptions esbuild.BuildOptions) {
- host, portStr, err := net.SplitHostPort(*addr)
- if err != nil {
- log.Fatalf("Cannot parse addr: %v", err)
- }
- port, err := strconv.ParseUint(portStr, 10, 16)
- if err != nil {
- log.Fatalf("Cannot parse port: %v", err)
- }
- result, err := esbuild.Serve(esbuild.ServeOptions{
- Port: uint16(port),
- Host: host,
- Servedir: "./",
- }, buildOptions)
- if err != nil {
- log.Fatalf("Cannot start esbuild server: %v", err)
- }
- log.Printf("Listening on http://%s:%d\n", result.Host, result.Port)
- result.Wait()
-}
-
-func runEsbuild(buildOptions esbuild.BuildOptions) esbuild.BuildResult {
- log.Printf("Running esbuild...\n")
- result := esbuild.Build(buildOptions)
- if len(result.Errors) > 0 {
- log.Printf("ESBuild Error:\n")
- for _, e := range result.Errors {
- log.Printf("%v", e)
- }
- log.Fatal("Build failed")
- }
- if len(result.Warnings) > 0 {
- log.Printf("ESBuild Warnings:\n")
- for _, w := range result.Warnings {
- log.Printf("%v", w)
- }
- }
- return result
-}
-
-// setupEsbuildWasmExecJS generates an esbuild plugin that serves the current
-// wasm_exec.js runtime helper library from the Go toolchain.
-func setupEsbuildWasmExecJS(build esbuild.PluginBuild) {
- wasmExecSrcPath := filepath.Join(runtime.GOROOT(), "misc", "wasm", "wasm_exec.js")
- build.OnResolve(esbuild.OnResolveOptions{
- Filter: "./wasm_exec$",
- }, func(args esbuild.OnResolveArgs) (esbuild.OnResolveResult, error) {
- return esbuild.OnResolveResult{Path: wasmExecSrcPath}, nil
- })
-}
-
-// setupEsbuildWasm generates an esbuild plugin that builds the Tailscale wasm
-// binary and serves it as a file that the JS can load.
-func setupEsbuildWasm(build esbuild.PluginBuild, dev bool) {
- // Add a resolve hook to convince esbuild that the path exists.
- build.OnResolve(esbuild.OnResolveOptions{
- Filter: "./main.wasm$",
- }, func(args esbuild.OnResolveArgs) (esbuild.OnResolveResult, error) {
- return esbuild.OnResolveResult{
- Path: "./src/main.wasm",
- Namespace: "generated",
- }, nil
- })
- build.OnLoad(esbuild.OnLoadOptions{
- Filter: "./src/main.wasm$",
- }, func(args esbuild.OnLoadArgs) (esbuild.OnLoadResult, error) {
- contents, err := buildWasm(dev)
- if err != nil {
- return esbuild.OnLoadResult{}, fmt.Errorf("Cannot build main.wasm: %w", err)
- }
- contentsStr := string(contents)
- return esbuild.OnLoadResult{
- Contents: &contentsStr,
- Loader: esbuild.LoaderFile,
- }, nil
- })
-}
-
-func buildWasm(dev bool) ([]byte, error) {
- start := time.Now()
- outputFile, err := os.CreateTemp("", "main.*.wasm")
- if err != nil {
- return nil, fmt.Errorf("Cannot create main.wasm output file: %w", err)
- }
- outputPath := outputFile.Name()
-
- defer os.Remove(outputPath)
- // Running defer (*os.File).Close() in defer order before os.Remove
- // because on some systems like Windows, it is possible for os.Remove
- // to fail for unclosed files.
- defer outputFile.Close()
-
- args := []string{"build", "-tags", "tailscale_go,osusergo,netgo,nethttpomithttp2,omitidna,omitpemdecrypt"}
- if !dev {
- if *devControl != "" {
- return nil, fmt.Errorf("Development control URL can only be used in dev mode.")
- }
- // Omit long paths and debug symbols in release builds, to reduce the
- // generated WASM binary size.
- args = append(args, "-trimpath", "-ldflags", "-s -w")
- } else if *devControl != "" {
- args = append(args, "-ldflags", fmt.Sprintf("-X 'main.ControlURL=%v'", *devControl))
- }
-
- args = append(args, "-o", outputPath, "./wasm")
- cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), args...)
- cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
- cmd.Stdin = os.Stdin
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- err = cmd.Run()
- if err != nil {
- return nil, fmt.Errorf("Cannot build main.wasm: %w", err)
- }
- log.Printf("Built wasm in %v\n", time.Since(start).Round(time.Millisecond))
-
- if !dev {
- err := runWasmOpt(outputPath)
- if err != nil {
- return nil, fmt.Errorf("Cannot run wasm-opt: %w", err)
- }
- }
-
- return os.ReadFile(outputPath)
-}
-
-func runWasmOpt(path string) error {
- start := time.Now()
- stat, err := os.Stat(path)
- if err != nil {
- return fmt.Errorf("Cannot stat %v: %w", path, err)
- }
- startSize := stat.Size()
- cmd := exec.Command("../../tool/wasm-opt", "--enable-bulk-memory", "-Oz", path, "-o", path)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- err = cmd.Run()
- if err != nil {
- return fmt.Errorf("Cannot run wasm-opt: %w", err)
- }
- stat, err = os.Stat(path)
- if err != nil {
- return fmt.Errorf("Cannot stat %v: %w", path, err)
- }
- endSize := stat.Size()
- log.Printf("Ran wasm-opt in %v, size dropped by %dK\n", time.Since(start).Round(time.Millisecond), (startSize-endSize)/1024)
- return nil
-}
-
-// installJSDeps installs the JavaScript dependencies specified by package.json
-func installJSDeps() error {
- log.Printf("Installing JS deps...\n")
- return runYarn()
-}
-
-func runYarn(args ...string) error {
- cmd := exec.Command(*yarnPath, args...)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- return cmd.Run()
-}
-
-// EsbuildMetadata is the subset of metadata struct (described by
-// https://esbuild.github.io/api/#metafile) that we care about for mapping
-// from entry points to hashed file names.
-type EsbuildMetadata struct {
- Outputs map[string]struct {
- Inputs map[string]struct {
- BytesInOutput int64 `json:"bytesInOutput"`
- } `json:"inputs,omitempty"`
- EntryPoint string `json:"entryPoint,omitempty"`
- } `json:"outputs,omitempty"`
-}
-
-func setupEsbuildTailwind(build esbuild.PluginBuild, dev bool) {
- build.OnLoad(esbuild.OnLoadOptions{
- Filter: "./src/.*\\.css$",
- }, func(args esbuild.OnLoadArgs) (esbuild.OnLoadResult, error) {
- start := time.Now()
- yarnArgs := []string{"--silent", "tailwind", "-i", args.Path}
- if !dev {
- yarnArgs = append(yarnArgs, "--minify")
- }
- cmd := exec.Command(*yarnPath, yarnArgs...)
- tailwindOutput, err := cmd.Output()
- log.Printf("Ran tailwind in %v\n", time.Since(start).Round(time.Millisecond))
- if err != nil {
- if exitErr, ok := err.(*exec.ExitError); ok {
- log.Printf("Tailwind stderr: %s", exitErr.Stderr)
- }
- return esbuild.OnLoadResult{}, fmt.Errorf("Cannot run tailwind: %w", err)
- }
- tailwindOutputStr := string(tailwindOutput)
- return esbuild.OnLoadResult{
- Contents: &tailwindOutputStr,
- Loader: esbuild.LoaderCSS,
- }, nil
-
- })
-}
diff --git a/cmd/tsconnect/dev-pkg.go b/cmd/tsconnect/dev-pkg.go
deleted file mode 100644
index b23e323e433ac..0000000000000
--- a/cmd/tsconnect/dev-pkg.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package main
-
-import (
- "log"
-)
-
-func runDevPkg() {
- buildOptions, err := commonPkgSetup(devMode)
- if err != nil {
- log.Fatalf("Cannot setup: %v", err)
- }
- runEsbuildServe(*buildOptions)
-}
diff --git a/cmd/tsconnect/dev.go b/cmd/tsconnect/dev.go
deleted file mode 100644
index 60e747270d418..0000000000000
--- a/cmd/tsconnect/dev.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package main
-
-import (
- "log"
-)
-
-func runDev() {
- buildOptions, err := commonSetup(devMode)
- if err != nil {
- log.Fatalf("Cannot setup: %v", err)
- }
- runEsbuildServe(*buildOptions)
-}
diff --git a/cmd/tsconnect/dist/placeholder b/cmd/tsconnect/dist/placeholder
deleted file mode 100644
index 4af99d997207f..0000000000000
--- a/cmd/tsconnect/dist/placeholder
+++ /dev/null
@@ -1,2 +0,0 @@
-This is here to make sure the dist/ directory exists for the go:embed command
-in serve.go.
diff --git a/cmd/tsconnect/index.html b/cmd/tsconnect/index.html
deleted file mode 100644
index 3db45fdef2bca..0000000000000
--- a/cmd/tsconnect/index.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
- Tailscale Connect
-
-
-
-
-
-
-
- Tailscale Connect
- Loading…
-
-
-
-
diff --git a/cmd/tsconnect/package.json b/cmd/tsconnect/package.json
deleted file mode 100644
index bf4eb7c099aac..0000000000000
--- a/cmd/tsconnect/package.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "name": "tsconnect",
- "version": "0.0.1",
- "license": "BSD-3-Clause",
- "devDependencies": {
- "@types/golang-wasm-exec": "^1.15.0",
- "@types/qrcode": "^1.4.2",
- "dts-bundle-generator": "^6.12.0",
- "preact": "^10.10.0",
- "qrcode": "^1.5.0",
- "tailwindcss": "^3.1.6",
- "typescript": "^4.7.4",
- "xterm": "^5.1.0",
- "xterm-addon-fit": "^0.7.0",
- "xterm-addon-web-links": "^0.8.0"
- },
- "scripts": {
- "lint": "tsc --noEmit",
- "pkg-types": "dts-bundle-generator --inline-declare-global=true --no-banner -o pkg/pkg.d.ts src/pkg/pkg.ts"
- },
- "prettier": {
- "semi": false,
- "printWidth": 80
- }
-}
diff --git a/cmd/tsconnect/package.json.tmpl b/cmd/tsconnect/package.json.tmpl
deleted file mode 100644
index 404b896eaf89e..0000000000000
--- a/cmd/tsconnect/package.json.tmpl
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Template for the package.json that is generated by the build-pkg command.
-// The version number will be replaced by the current Tailscale client version
-// number.
-{
- "author": "Tailscale Inc.",
- "description": "Tailscale Connect SDK",
- "license": "BSD-3-Clause",
- "name": "@tailscale/connect",
- "type": "module",
- "main": "./pkg.js",
- "types": "./pkg.d.ts",
- "version": "AUTO_GENERATED"
-}
diff --git a/cmd/tsconnect/serve.go b/cmd/tsconnect/serve.go
deleted file mode 100644
index 49e7d3135267b..0000000000000
--- a/cmd/tsconnect/serve.go
+++ /dev/null
@@ -1,142 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package main
-
-import (
- "bytes"
- "embed"
- "encoding/json"
- "fmt"
- "io"
- "io/fs"
- "log"
- "net/http"
- "os"
- "path"
- "time"
-
- "tailscale.com/tsweb"
- "tailscale.com/util/precompress"
-)
-
-//go:embed index.html
-var embeddedFS embed.FS
-
-//go:embed dist/*
-var embeddedDistFS embed.FS
-
-var serveStartTime = time.Now()
-
-func runServe() {
- mux := http.NewServeMux()
-
- var distFS fs.FS
- if *distDir == "./dist" {
- var err error
- distFS, err = fs.Sub(embeddedDistFS, "dist")
- if err != nil {
- log.Fatalf("Could not drop dist/ prefix from embedded FS: %v", err)
- }
- } else {
- distFS = os.DirFS(*distDir)
- }
-
- indexBytes, err := generateServeIndex(distFS)
- if err != nil {
- log.Fatalf("Could not generate index.html: %v", err)
- }
- mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- http.ServeContent(w, r, "index.html", serveStartTime, bytes.NewReader(indexBytes))
- }))
- mux.Handle("/dist/", http.StripPrefix("/dist/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- handleServeDist(w, r, distFS)
- })))
- tsweb.Debugger(mux)
-
- log.Printf("Listening on %s", *addr)
- err = http.ListenAndServe(*addr, mux)
- if err != nil {
- log.Fatal(err)
- }
-}
-
-func generateServeIndex(distFS fs.FS) ([]byte, error) {
- log.Printf("Generating index.html...\n")
- rawIndexBytes, err := embeddedFS.ReadFile("index.html")
- if err != nil {
- return nil, fmt.Errorf("Could not read index.html: %w", err)
- }
-
- esbuildMetadataFile, err := distFS.Open("esbuild-metadata.json")
- if err != nil {
- return nil, fmt.Errorf("Could not open esbuild-metadata.json: %w", err)
- }
- defer esbuildMetadataFile.Close()
- esbuildMetadataBytes, err := io.ReadAll(esbuildMetadataFile)
- if err != nil {
- return nil, fmt.Errorf("Could not read esbuild-metadata.json: %w", err)
- }
- var esbuildMetadata EsbuildMetadata
- if err := json.Unmarshal(esbuildMetadataBytes, &esbuildMetadata); err != nil {
- return nil, fmt.Errorf("Could not parse esbuild-metadata.json: %w", err)
- }
- entryPointsToHashedDistPaths := make(map[string]string)
- mainWasmPath := ""
- for outputPath, output := range esbuildMetadata.Outputs {
- if output.EntryPoint != "" {
- entryPointsToHashedDistPaths[output.EntryPoint] = path.Join("dist", outputPath)
- }
- if path.Ext(outputPath) == ".wasm" {
- for input := range output.Inputs {
- if input == "src/main.wasm" {
- mainWasmPath = path.Join("dist", outputPath)
- break
- }
- }
- }
- }
-
- indexBytes := rawIndexBytes
- for entryPointPath, defaultDistPath := range entryPointsToDefaultDistPaths {
- hashedDistPath := entryPointsToHashedDistPaths[entryPointPath]
- if hashedDistPath != "" {
- indexBytes = bytes.ReplaceAll(indexBytes, []byte(defaultDistPath), []byte(hashedDistPath))
- }
- }
- if mainWasmPath != "" {
- mainWasmPrefetch := fmt.Sprintf("\n", mainWasmPath)
- indexBytes = bytes.ReplaceAll(indexBytes, []byte(""), []byte(mainWasmPrefetch))
- }
-
- return indexBytes, nil
-}
-
-var entryPointsToDefaultDistPaths = map[string]string{
- "src/app/index.css": "dist/index.css",
- "src/app/index.ts": "dist/index.js",
-}
-
-func handleServeDist(w http.ResponseWriter, r *http.Request, distFS fs.FS) {
- path := r.URL.Path
- f, err := precompress.OpenPrecompressedFile(w, r, path, distFS)
- if err != nil {
- http.Error(w, err.Error(), http.StatusNotFound)
- return
- }
- defer f.Close()
-
- // fs.File does not claim to implement Seeker, but in practice it does.
- fSeeker, ok := f.(io.ReadSeeker)
- if !ok {
- http.Error(w, "Not seekable", http.StatusInternalServerError)
- return
- }
-
- // Aggressively cache static assets, since we cache-bust our assets with
- // hashed filenames.
- w.Header().Set("Cache-Control", "public, max-age=31535996")
- w.Header().Set("Vary", "Accept-Encoding")
-
- http.ServeContent(w, r, path, serveStartTime, fSeeker)
-}
diff --git a/cmd/tsconnect/src/app/app.tsx b/cmd/tsconnect/src/app/app.tsx
deleted file mode 100644
index ee538eaeac506..0000000000000
--- a/cmd/tsconnect/src/app/app.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-import { render, Component } from "preact"
-import { URLDisplay } from "./url-display"
-import { Header } from "./header"
-import { GoPanicDisplay } from "./go-panic-display"
-import { SSH } from "./ssh"
-
-type AppState = {
- ipn?: IPN
- ipnState: IPNState
- netMap?: IPNNetMap
- browseToURL?: string
- goPanicError?: string
-}
-
-class App extends Component<{}, AppState> {
- state: AppState = { ipnState: "NoState" }
- #goPanicTimeout?: number
-
- render() {
- const { ipn, ipnState, goPanicError, netMap, browseToURL } = this.state
-
- let goPanicDisplay
- if (goPanicError) {
- goPanicDisplay = (
-
- )
- }
-
- let urlDisplay
- if (browseToURL) {
- urlDisplay =
- }
-
- let machineAuthInstructions
- if (ipnState === "NeedsMachineAuth") {
- machineAuthInstructions = (
-
- An administrator needs to approve this device.
-
- )
- }
-
- const lockedOut = netMap?.lockedOut
- let lockedOutInstructions
- if (lockedOut) {
- lockedOutInstructions = (
-
-
This instance of Tailscale Connect needs to be signed, due to
- {" "}tailnet lock{" "}
- being enabled on this domain.
-
-
-
- Run the following command on a device with a trusted tailnet lock key:
-
tailscale lock sign {netMap.self.nodeKey}
-
-
- )
- }
-
- let ssh
- if (ipn && ipnState === "Running" && netMap && !lockedOut) {
- ssh =
- }
-
- return (
- <>
-
- {goPanicDisplay}
-
- {urlDisplay}
- {machineAuthInstructions}
- {lockedOutInstructions}
- {ssh}
-
- >
- )
- }
-
- runWithIPN(ipn: IPN) {
- this.setState({ ipn }, () => {
- ipn.run({
- notifyState: this.handleIPNState,
- notifyNetMap: this.handleNetMap,
- notifyBrowseToURL: this.handleBrowseToURL,
- notifyPanicRecover: this.handleGoPanic,
- })
- })
- }
-
- handleIPNState = (state: IPNState) => {
- const { ipn } = this.state
- this.setState({ ipnState: state })
- if (state === "NeedsLogin") {
- ipn?.login()
- } else if (["Running", "NeedsMachineAuth"].includes(state)) {
- this.setState({ browseToURL: undefined })
- }
- }
-
- handleNetMap = (netMapStr: string) => {
- const netMap = JSON.parse(netMapStr) as IPNNetMap
- if (DEBUG) {
- console.log("Received net map: " + JSON.stringify(netMap, null, 2))
- }
- this.setState({ netMap })
- }
-
- handleBrowseToURL = (url: string) => {
- if (this.state.ipnState === "Running") {
- // Ignore URL requests if we're already running -- it's most likely an
- // SSH check mode trigger and we already linkify the displayed URL
- // in the terminal.
- return
- }
- this.setState({ browseToURL: url })
- }
-
- handleGoPanic = (error: string) => {
- if (DEBUG) {
- console.error("Go panic", error)
- }
- this.setState({ goPanicError: error })
- if (this.#goPanicTimeout) {
- window.clearTimeout(this.#goPanicTimeout)
- }
- this.#goPanicTimeout = window.setTimeout(this.clearGoPanic, 10000)
- }
-
- clearGoPanic = () => {
- window.clearTimeout(this.#goPanicTimeout)
- this.#goPanicTimeout = undefined
- this.setState({ goPanicError: undefined })
- }
-}
-
-export function renderApp(): Promise {
- return new Promise((resolve) => {
- render(
- (app ? resolve(app) : undefined)} />,
- document.body
- )
- })
-}
diff --git a/cmd/tsconnect/src/app/go-panic-display.tsx b/cmd/tsconnect/src/app/go-panic-display.tsx
deleted file mode 100644
index 5dd7095a27c7d..0000000000000
--- a/cmd/tsconnect/src/app/go-panic-display.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-export function GoPanicDisplay({
- error,
- dismiss,
-}: {
- error: string
- dismiss: () => void
-}) {
- return (
-
- Tailscale has encountered an error.
-
Click to reload
-
- )
-}
diff --git a/cmd/tsconnect/src/app/header.tsx b/cmd/tsconnect/src/app/header.tsx
deleted file mode 100644
index 099ff2f8c2f7d..0000000000000
--- a/cmd/tsconnect/src/app/header.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-export function Header({ state, ipn }: { state: IPNState; ipn?: IPN }) {
- const stateText = STATE_LABELS[state]
-
- let logoutButton
- if (state === "Running") {
- logoutButton = (
-
- )
- }
- return (
-
-
- Tailscale Connect
- {stateText}
- {logoutButton}
-
-
- )
-}
-
-const STATE_LABELS = {
- NoState: "Initializing…",
- InUseOtherUser: "In-use by another user",
- NeedsLogin: "Needs login",
- NeedsMachineAuth: "Needs approval",
- Stopped: "Stopped",
- Starting: "Starting…",
- Running: "Running",
-} as const
diff --git a/cmd/tsconnect/src/app/index.css b/cmd/tsconnect/src/app/index.css
deleted file mode 100644
index 751b313d9f362..0000000000000
--- a/cmd/tsconnect/src/app/index.css
+++ /dev/null
@@ -1,74 +0,0 @@
-/* Copyright (c) Tailscale Inc & AUTHORS */
-/* SPDX-License-Identifier: BSD-3-Clause */
-
-@import "xterm/css/xterm.css";
-
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-.link {
- @apply text-blue-600;
-}
-
-.link:hover {
- @apply underline;
-}
-
-.button {
- @apply font-medium py-1 px-2 rounded-md border border-transparent text-center cursor-pointer;
- transition-property: background-color, border-color, color, box-shadow;
- transition-duration: 120ms;
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
- min-width: 80px;
-}
-.button:focus {
- @apply outline-none ring;
-}
-.button:disabled {
- @apply pointer-events-none select-none;
-}
-
-.input {
- @apply appearance-none leading-tight rounded-md bg-white border border-gray-300 hover:border-gray-400 transition-colors px-3;
- height: 2.375rem;
-}
-
-.input::placeholder {
- @apply text-gray-400;
-}
-
-.input:disabled {
- @apply border-gray-200;
- @apply bg-gray-50;
- @apply cursor-not-allowed;
-}
-
-.input:focus {
- @apply outline-none ring border-transparent;
-}
-
-.select {
- @apply appearance-none py-2 px-3 leading-tight rounded-md bg-white border border-gray-300;
-}
-
-.select-with-arrow {
- @apply relative;
-}
-
-.select-with-arrow .select {
- width: 100%;
-}
-
-.select-with-arrow::after {
- @apply absolute;
- content: "";
- top: 50%;
- right: 0.5rem;
- transform: translate(-0.3em, -0.15em);
- width: 0.6em;
- height: 0.4em;
- opacity: 0.6;
- background-color: currentColor;
- clip-path: polygon(100% 0%, 0 0%, 50% 100%);
-}
diff --git a/cmd/tsconnect/src/app/index.ts b/cmd/tsconnect/src/app/index.ts
deleted file mode 100644
index 24ca4543921ae..0000000000000
--- a/cmd/tsconnect/src/app/index.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-import "../wasm_exec"
-import wasmUrl from "./main.wasm"
-import { sessionStateStorage } from "../lib/js-state-store"
-import { renderApp } from "./app"
-
-async function main() {
- const app = await renderApp()
- const go = new Go()
- const wasmInstance = await WebAssembly.instantiateStreaming(
- fetch(`./dist/${wasmUrl}`),
- go.importObject
- )
- // The Go process should never exit, if it does then it's an unhandled panic.
- go.run(wasmInstance.instance).then(() =>
- app.handleGoPanic("Unexpected shutdown")
- )
-
- const params = new URLSearchParams(window.location.search)
- const authKey = params.get("authkey") ?? undefined
-
- const ipn = newIPN({
- // Persist IPN state in sessionStorage in development, so that we don't need
- // to re-authorize every time we reload the page.
- stateStorage: DEBUG ? sessionStateStorage : undefined,
- // authKey allows for an auth key to be
- // specified as a url param which automatically
- // authorizes the client for use.
- authKey: DEBUG ? authKey : undefined,
- })
- app.runWithIPN(ipn)
-}
-
-main()
diff --git a/cmd/tsconnect/src/app/ssh.tsx b/cmd/tsconnect/src/app/ssh.tsx
deleted file mode 100644
index df81745bd3fd7..0000000000000
--- a/cmd/tsconnect/src/app/ssh.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-import { useState, useCallback, useMemo, useEffect, useRef } from "preact/hooks"
-import { createPortal } from "preact/compat"
-import type { VNode } from "preact"
-import { runSSHSession, SSHSessionDef } from "../lib/ssh"
-
-export function SSH({ netMap, ipn }: { netMap: IPNNetMap; ipn: IPN }) {
- const [sshSessionDef, setSSHSessionDef] = useState(
- null
- )
- const clearSSHSessionDef = useCallback(() => setSSHSessionDef(null), [])
- if (sshSessionDef) {
- const sshSession = (
-
- )
- if (sshSessionDef.newWindow) {
- return {sshSession}
- }
- return sshSession
- }
- const sshPeers = netMap.peers.filter(
- (p) => p.tailscaleSSHEnabled && p.online !== false
- )
-
- if (sshPeers.length == 0) {
- return
- }
-
- return
-}
-
-type SSHFormSessionDef = SSHSessionDef & { newWindow?: boolean }
-
-function SSHSession({
- def,
- ipn,
- onDone,
-}: {
- def: SSHSessionDef
- ipn: IPN
- onDone: () => void
-}) {
- const ref = useRef(null)
- useEffect(() => {
- if (ref.current) {
- runSSHSession(ref.current, def, ipn, {
- onConnectionProgress: (p) => console.log("Connection progress", p),
- onConnected() {},
- onError: (err) => console.error(err),
- onDone,
- })
- }
- }, [ref])
-
- return
-}
-
-function NoSSHPeers() {
- return (
-
- None of your machines have{" "}
-
- Tailscale SSH
-
- {" "}enabled. Give it a try!
-
- )
-}
-
-function SSHForm({
- sshPeers,
- onSubmit,
-}: {
- sshPeers: IPNNetMapPeerNode[]
- onSubmit: (def: SSHFormSessionDef) => void
-}) {
- sshPeers = sshPeers.slice().sort((a, b) => a.name.localeCompare(b.name))
- const [username, setUsername] = useState("")
- const [hostname, setHostname] = useState(sshPeers[0].name)
- return (
-
- )
-}
-
-const NewWindow = ({
- children,
- close,
-}: {
- children: VNode
- close: () => void
-}) => {
- const newWindow = useMemo(() => {
- const newWindow = window.open(undefined, undefined, "width=600,height=400")
- if (newWindow) {
- const containerNode = newWindow.document.createElement("div")
- containerNode.className = "h-screen flex flex-col overflow-hidden"
- newWindow.document.body.appendChild(containerNode)
-
- for (const linkNode of document.querySelectorAll(
- "head link[rel=stylesheet]"
- )) {
- const newLink = document.createElement("link")
- newLink.rel = "stylesheet"
- newLink.href = (linkNode as HTMLLinkElement).href
- newWindow.document.head.appendChild(newLink)
- }
- }
- return newWindow
- }, [])
- if (!newWindow) {
- console.error("Could not open window")
- return null
- }
- newWindow.onbeforeunload = () => {
- close()
- }
-
- useEffect(() => () => newWindow.close(), [])
- return createPortal(children, newWindow.document.body.lastChild as Element)
-}
diff --git a/cmd/tsconnect/src/app/url-display.tsx b/cmd/tsconnect/src/app/url-display.tsx
deleted file mode 100644
index fc82c7fb91b3c..0000000000000
--- a/cmd/tsconnect/src/app/url-display.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-import { useState } from "preact/hooks"
-import * as qrcode from "qrcode"
-
-export function URLDisplay({ url }: { url: string }) {
- const [dataURL, setDataURL] = useState("")
- qrcode.toDataURL(url, { width: 512 }, (err, dataURL) => {
- if (err) {
- console.error("Error generating QR code", err)
- } else {
- setDataURL(dataURL)
- }
- })
-
- return (
-
- )
-}
diff --git a/cmd/tsconnect/src/lib/js-state-store.ts b/cmd/tsconnect/src/lib/js-state-store.ts
deleted file mode 100644
index e57dfd98efabd..0000000000000
--- a/cmd/tsconnect/src/lib/js-state-store.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-/** @fileoverview Callbacks used by jsStateStore to persist IPN state. */
-
-export const sessionStateStorage: IPNStateStorage = {
- setState(id, value) {
- window.sessionStorage[`ipn-state-${id}`] = value
- },
- getState(id) {
- return window.sessionStorage[`ipn-state-${id}`] || ""
- },
-}
diff --git a/cmd/tsconnect/src/lib/ssh.ts b/cmd/tsconnect/src/lib/ssh.ts
deleted file mode 100644
index afbcdb895a3ba..0000000000000
--- a/cmd/tsconnect/src/lib/ssh.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { Terminal, ITerminalOptions } from "xterm"
-import { FitAddon } from "xterm-addon-fit"
-import { WebLinksAddon } from "xterm-addon-web-links"
-
-export type SSHSessionDef = {
- username: string
- hostname: string
- /** Defaults to 5 seconds */
- timeoutSeconds?: number
-}
-
-export type SSHSessionCallbacks = {
- onConnectionProgress: (messsage: string) => void
- onConnected: () => void
- onDone: () => void
- onError?: (err: string) => void
-}
-
-export function runSSHSession(
- termContainerNode: HTMLDivElement,
- def: SSHSessionDef,
- ipn: IPN,
- callbacks: SSHSessionCallbacks,
- terminalOptions?: ITerminalOptions
-) {
- const parentWindow = termContainerNode.ownerDocument.defaultView ?? window
- const term = new Terminal({
- cursorBlink: true,
- allowProposedApi: true,
- ...terminalOptions,
- })
-
- const fitAddon = new FitAddon()
- term.loadAddon(fitAddon)
- term.open(termContainerNode)
- fitAddon.fit()
-
- const webLinksAddon = new WebLinksAddon((event, uri) =>
- event.view?.open(uri, "_blank", "noopener")
- )
- term.loadAddon(webLinksAddon)
-
- let onDataHook: ((data: string) => void) | undefined
- term.onData((e) => {
- onDataHook?.(e)
- })
-
- term.focus()
-
- let resizeObserver: ResizeObserver | undefined
- let handleUnload: ((e: Event) => void) | undefined
-
- const sshSession = ipn.ssh(def.hostname, def.username, {
- writeFn(input) {
- term.write(input)
- },
- writeErrorFn(err) {
- callbacks.onError?.(err)
- term.write(err)
- },
- setReadFn(hook) {
- onDataHook = hook
- },
- rows: term.rows,
- cols: term.cols,
- onConnectionProgress: callbacks.onConnectionProgress,
- onConnected: callbacks.onConnected,
- onDone() {
- resizeObserver?.disconnect()
- term.dispose()
- if (handleUnload) {
- parentWindow.removeEventListener("unload", handleUnload)
- }
- callbacks.onDone()
- },
- timeoutSeconds: def.timeoutSeconds,
- })
-
- // Make terminal and SSH session track the size of the containing DOM node.
- resizeObserver = new parentWindow.ResizeObserver(() => fitAddon.fit())
- resizeObserver.observe(termContainerNode)
- term.onResize(({ rows, cols }) => sshSession.resize(rows, cols))
-
- // Close the session if the user closes the window without an explicit
- // exit.
- handleUnload = () => sshSession.close()
- parentWindow.addEventListener("unload", handleUnload)
-}
diff --git a/cmd/tsconnect/src/pkg/pkg.css b/cmd/tsconnect/src/pkg/pkg.css
deleted file mode 100644
index 76ea21f5b53b2..0000000000000
--- a/cmd/tsconnect/src/pkg/pkg.css
+++ /dev/null
@@ -1,8 +0,0 @@
-/* Copyright (c) Tailscale Inc & AUTHORS */
-/* SPDX-License-Identifier: BSD-3-Clause */
-
-@import "xterm/css/xterm.css";
-
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
diff --git a/cmd/tsconnect/src/pkg/pkg.ts b/cmd/tsconnect/src/pkg/pkg.ts
deleted file mode 100644
index 4d535cb404015..0000000000000
--- a/cmd/tsconnect/src/pkg/pkg.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Type definitions need to be manually imported for dts-bundle-generator to
-// discover them.
-///
-///
-
-import "../wasm_exec"
-import wasmURL from "./main.wasm"
-
-/**
- * Superset of the IPNConfig type, with additional configuration that is
- * needed for the package to function.
- */
-type IPNPackageConfig = IPNConfig & {
- // Auth key used to initialize the Tailscale client (required)
- authKey: string
- // URL of the main.wasm file that is included in the page, if it is not
- // accessible via a relative URL.
- wasmURL?: string
- // Function invoked if the Go process panics or unexpectedly exits.
- panicHandler: (err: string) => void
-}
-
-export async function createIPN(config: IPNPackageConfig): Promise {
- const go = new Go()
- const wasmInstance = await WebAssembly.instantiateStreaming(
- fetch(config.wasmURL ?? wasmURL),
- go.importObject
- )
- // The Go process should never exit, if it does then it's an unhandled panic.
- go.run(wasmInstance.instance).then(() =>
- config.panicHandler("Unexpected shutdown")
- )
-
- return newIPN(config)
-}
-
-export { runSSHSession } from "../lib/ssh"
diff --git a/cmd/tsconnect/src/types/esbuild.d.ts b/cmd/tsconnect/src/types/esbuild.d.ts
deleted file mode 100644
index ef28f7b1cf556..0000000000000
--- a/cmd/tsconnect/src/types/esbuild.d.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-/**
- * @fileoverview Type definitions for types generated by the esbuild build
- * process.
- */
-
-declare module "*.wasm" {
- const path: string
- export default path
-}
-
-declare const DEBUG: boolean
diff --git a/cmd/tsconnect/src/types/wasm_js.d.ts b/cmd/tsconnect/src/types/wasm_js.d.ts
deleted file mode 100644
index 492197ccb1a9b..0000000000000
--- a/cmd/tsconnect/src/types/wasm_js.d.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-/**
- * @fileoverview Type definitions for types exported by the wasm_js.go Go
- * module.
- */
-
-declare global {
- function newIPN(config: IPNConfig): IPN
-
- interface IPN {
- run(callbacks: IPNCallbacks): void
- login(): void
- logout(): void
- ssh(
- host: string,
- username: string,
- termConfig: {
- writeFn: (data: string) => void
- writeErrorFn: (err: string) => void
- setReadFn: (readFn: (data: string) => void) => void
- rows: number
- cols: number
- /** Defaults to 5 seconds */
- timeoutSeconds?: number
- onConnectionProgress: (message: string) => void
- onConnected: () => void
- onDone: () => void
- }
- ): IPNSSHSession
- fetch(url: string): Promise<{
- status: number
- statusText: string
- text: () => Promise
- }>
- }
-
- interface IPNSSHSession {
- resize(rows: number, cols: number): boolean
- close(): boolean
- }
-
- interface IPNStateStorage {
- setState(id: string, value: string): void
- getState(id: string): string
- }
-
- type IPNConfig = {
- stateStorage?: IPNStateStorage
- authKey?: string
- controlURL?: string
- hostname?: string
- }
-
- type IPNCallbacks = {
- notifyState: (state: IPNState) => void
- notifyNetMap: (netMapStr: string) => void
- notifyBrowseToURL: (url: string) => void
- notifyPanicRecover: (err: string) => void
- }
-
- type IPNNetMap = {
- self: IPNNetMapSelfNode
- peers: IPNNetMapPeerNode[]
- lockedOut: boolean
- }
-
- type IPNNetMapNode = {
- name: string
- addresses: string[]
- machineKey: string
- nodeKey: string
- }
-
- type IPNNetMapSelfNode = IPNNetMapNode & {
- machineStatus: IPNMachineStatus
- }
-
- type IPNNetMapPeerNode = IPNNetMapNode & {
- online?: boolean
- tailscaleSSHEnabled: boolean
- }
-
- /** Mirrors values from ipn/backend.go */
- type IPNState =
- | "NoState"
- | "InUseOtherUser"
- | "NeedsLogin"
- | "NeedsMachineAuth"
- | "Stopped"
- | "Starting"
- | "Running"
-
- /** Mirrors values from MachineStatus in tailcfg.go */
- type IPNMachineStatus =
- | "MachineUnknown"
- | "MachineUnauthorized"
- | "MachineAuthorized"
- | "MachineInvalid"
-}
-
-export {}
diff --git a/cmd/tsconnect/tailwind.config.js b/cmd/tsconnect/tailwind.config.js
deleted file mode 100644
index 31823000b6139..0000000000000
--- a/cmd/tsconnect/tailwind.config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: ["./index.html", "./src/**/*.ts", "./src/**/*.tsx"],
- theme: {
- extend: {},
- },
- plugins: [],
-}
diff --git a/cmd/tsconnect/tsconfig.json b/cmd/tsconnect/tsconfig.json
deleted file mode 100644
index 52c25c7271f7c..0000000000000
--- a/cmd/tsconnect/tsconfig.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2017",
- "module": "ES2020",
- "moduleResolution": "node",
- "isolatedModules": true,
- "strict": true,
- "forceConsistentCasingInFileNames": true,
- "sourceMap": true,
- "jsx": "react-jsx",
- "jsxImportSource": "preact"
- },
- "include": ["src/**/*"],
- "exclude": ["node_modules"]
-}
diff --git a/cmd/tsconnect/tsconnect.go b/cmd/tsconnect/tsconnect.go
deleted file mode 100644
index 1ab13b3551a4c..0000000000000
--- a/cmd/tsconnect/tsconnect.go
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// The tsconnect command builds and serves the static site that is generated for
-// the Tailscale Connect JS/WASM client. Can be run in 3 modes:
-// - dev: builds the site and serves it. JS and CSS changes can be picked up
-// with a reload.
-// - build: builds the site and writes it to dist/
-// - serve: serves the site from dist/ (embedded in the binary)
-package main // import "tailscale.com/cmd/tsconnect"
-
-import (
- "flag"
- "fmt"
- "log"
- "os"
-)
-
-var (
- addr = flag.String("addr", ":9090", "address to listen on")
- distDir = flag.String("distdir", "./dist", "path of directory to place build output in")
- pkgDir = flag.String("pkgdir", "./pkg", "path of directory to place NPM package build output in")
- yarnPath = flag.String("yarnpath", "../../tool/yarn", "path yarn executable used to install JavaScript dependencies")
- fastCompression = flag.Bool("fast-compression", false, "Use faster compression when building, to speed up build time. Meant to iterative/debugging use only.")
- devControl = flag.String("dev-control", "", "URL of a development control server to be used with dev. If provided without specifying dev, an error will be returned.")
- rootDir = flag.String("rootdir", "", "Root directory of repo. If not specified, will be inferred from the cwd.")
-)
-
-func main() {
- flag.Usage = usage
- flag.Parse()
- if len(flag.Args()) != 1 {
- flag.Usage()
- }
-
- switch flag.Arg(0) {
- case "dev":
- runDev()
- case "dev-pkg":
- runDevPkg()
- case "build":
- runBuild()
- case "build-pkg":
- runBuildPkg()
- case "serve":
- runServe()
- default:
- log.Printf("Unknown command: %s", flag.Arg(0))
- flag.Usage()
- }
-}
-
-func usage() {
- fmt.Fprintf(os.Stderr, `
-usage: tsconnect {dev|build|serve}
-`[1:])
-
- flag.PrintDefaults()
- fmt.Fprintf(os.Stderr, `
-
-tsconnect implements development/build/serving workflows for Tailscale Connect.
-It can be invoked with one of three subcommands:
-
-- dev: Run in development mode, allowing JS and CSS changes to be picked up without a rebuilt or restart.
-- build: Run in production build mode (generating static assets)
-- serve: Run in production serve mode (serving static assets)
-`[1:])
- os.Exit(2)
-}
diff --git a/cmd/tsconnect/wasm/wasm_js.go b/cmd/tsconnect/wasm/wasm_js.go
deleted file mode 100644
index e8d6b665c40eb..0000000000000
--- a/cmd/tsconnect/wasm/wasm_js.go
+++ /dev/null
@@ -1,659 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// The wasm package builds a WebAssembly module that provides a subset of
-// Tailscale APIs to JavaScript.
-//
-// When run in the browser, a newIPN(config) function is added to the global JS
-// namespace. When called it returns an ipn object with the methods
-// run(callbacks), login(), logout(), and ssh(...).
-package main
-
-import (
- "bytes"
- "context"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "log"
- "math/rand"
- "net"
- "net/http"
- "net/netip"
- "strings"
- "syscall/js"
- "time"
-
- "golang.org/x/crypto/ssh"
- "tailscale.com/control/controlclient"
- "tailscale.com/ipn"
- "tailscale.com/ipn/ipnlocal"
- "tailscale.com/ipn/ipnserver"
- "tailscale.com/ipn/store/mem"
- "tailscale.com/logpolicy"
- "tailscale.com/logtail"
- "tailscale.com/net/netns"
- "tailscale.com/net/tsdial"
- "tailscale.com/safesocket"
- "tailscale.com/smallzstd"
- "tailscale.com/tailcfg"
- "tailscale.com/tsd"
- "tailscale.com/wgengine"
- "tailscale.com/wgengine/netstack"
- "tailscale.com/words"
-)
-
-// ControlURL defines the URL to be used for connection to Control.
-var ControlURL = ipn.DefaultControlURL
-
-func main() {
- js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) any {
- if len(args) != 1 {
- log.Fatal("Usage: newIPN(config)")
- return nil
- }
- return newIPN(args[0])
- }))
- // Keep Go runtime alive, otherwise it will be shut down before newIPN gets
- // called.
- <-make(chan bool)
-}
-
-func newIPN(jsConfig js.Value) map[string]any {
- netns.SetEnabled(false)
-
- var store ipn.StateStore
- if jsStateStorage := jsConfig.Get("stateStorage"); !jsStateStorage.IsUndefined() {
- store = &jsStateStore{jsStateStorage}
- } else {
- store = new(mem.Store)
- }
-
- controlURL := ControlURL
- if jsControlURL := jsConfig.Get("controlURL"); jsControlURL.Type() == js.TypeString {
- controlURL = jsControlURL.String()
- }
-
- var authKey string
- if jsAuthKey := jsConfig.Get("authKey"); jsAuthKey.Type() == js.TypeString {
- authKey = jsAuthKey.String()
- }
-
- var hostname string
- if jsHostname := jsConfig.Get("hostname"); jsHostname.Type() == js.TypeString {
- hostname = jsHostname.String()
- } else {
- hostname = generateHostname()
- }
-
- lpc := getOrCreateLogPolicyConfig(store)
- c := logtail.Config{
- Collection: lpc.Collection,
- PrivateID: lpc.PrivateID,
- // NewZstdEncoder is intentionally not passed in, compressed requests
- // set HTTP headers that are not supported by the no-cors fetching mode.
- HTTPC: &http.Client{Transport: &noCORSTransport{http.DefaultTransport}},
- }
- logtail := logtail.NewLogger(c, log.Printf)
- logf := logtail.Logf
-
- sys := new(tsd.System)
- sys.Set(store)
- dialer := &tsdial.Dialer{Logf: logf}
- eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
- Dialer: dialer,
- SetSubsystem: sys.Set,
- })
- if err != nil {
- log.Fatal(err)
- }
- sys.Set(eng)
-
- ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get())
- if err != nil {
- log.Fatalf("netstack.Create: %v", err)
- }
- ns.ProcessLocalIPs = true
- ns.ProcessSubnets = true
-
- dialer.UseNetstackForIP = func(ip netip.Addr) bool {
- return true
- }
- dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
- return ns.DialContextTCP(ctx, dst)
- }
- sys.NetstackRouter.Set(true)
-
- logid := lpc.PublicID
- srv := ipnserver.New(logf, logid, nil /* no netMon */)
- lb, err := ipnlocal.NewLocalBackend(logf, logid, sys, controlclient.LoginEphemeral)
- if err != nil {
- log.Fatalf("ipnlocal.NewLocalBackend: %v", err)
- }
- if err := ns.Start(lb); err != nil {
- log.Fatalf("failed to start netstack: %v", err)
- }
- lb.SetDecompressor(func() (controlclient.Decompressor, error) {
- return smallzstd.NewDecoder(nil)
- })
- srv.SetLocalBackend(lb)
-
- jsIPN := &jsIPN{
- dialer: dialer,
- srv: srv,
- lb: lb,
- controlURL: controlURL,
- authKey: authKey,
- hostname: hostname,
- }
-
- return map[string]any{
- "run": js.FuncOf(func(this js.Value, args []js.Value) any {
- if len(args) != 1 {
- log.Fatal(`Usage: run({
- notifyState(state: int): void,
- notifyNetMap(netMap: object): void,
- notifyBrowseToURL(url: string): void,
- notifyPanicRecover(err: string): void,
- })`)
- return nil
- }
- jsIPN.run(args[0])
- return nil
- }),
- "login": js.FuncOf(func(this js.Value, args []js.Value) any {
- if len(args) != 0 {
- log.Printf("Usage: login()")
- return nil
- }
- jsIPN.login()
- return nil
- }),
- "logout": js.FuncOf(func(this js.Value, args []js.Value) any {
- if len(args) != 0 {
- log.Printf("Usage: logout()")
- return nil
- }
- jsIPN.logout()
- return nil
- }),
- "ssh": js.FuncOf(func(this js.Value, args []js.Value) any {
- if len(args) != 3 {
- log.Printf("Usage: ssh(hostname, userName, termConfig)")
- return nil
- }
- return jsIPN.ssh(
- args[0].String(),
- args[1].String(),
- args[2])
- }),
- "fetch": js.FuncOf(func(this js.Value, args []js.Value) any {
- if len(args) != 1 {
- log.Printf("Usage: fetch(url)")
- return nil
- }
-
- url := args[0].String()
- return jsIPN.fetch(url)
- }),
- }
-}
-
-type jsIPN struct {
- dialer *tsdial.Dialer
- srv *ipnserver.Server
- lb *ipnlocal.LocalBackend
- controlURL string
- authKey string
- hostname string
-}
-
-var jsIPNState = map[ipn.State]string{
- ipn.NoState: "NoState",
- ipn.InUseOtherUser: "InUseOtherUser",
- ipn.NeedsLogin: "NeedsLogin",
- ipn.NeedsMachineAuth: "NeedsMachineAuth",
- ipn.Stopped: "Stopped",
- ipn.Starting: "Starting",
- ipn.Running: "Running",
-}
-
-var jsMachineStatus = map[tailcfg.MachineStatus]string{
- tailcfg.MachineUnknown: "MachineUnknown",
- tailcfg.MachineUnauthorized: "MachineUnauthorized",
- tailcfg.MachineAuthorized: "MachineAuthorized",
- tailcfg.MachineInvalid: "MachineInvalid",
-}
-
-func (i *jsIPN) run(jsCallbacks js.Value) {
- notifyState := func(state ipn.State) {
- jsCallbacks.Call("notifyState", jsIPNState[state])
- }
- notifyState(ipn.NoState)
-
- i.lb.SetNotifyCallback(func(n ipn.Notify) {
- // Panics in the notify callback are likely due to be due to bugs in
- // this bridging module (as opposed to actual bugs in Tailscale) and
- // thus may be recoverable. Let the UI know, and allow the user to
- // choose if they want to reload the page.
- defer func() {
- if r := recover(); r != nil {
- fmt.Println("Panic recovered:", r)
- jsCallbacks.Call("notifyPanicRecover", fmt.Sprint(r))
- }
- }()
- log.Printf("NOTIFY: %+v", n)
- if n.State != nil {
- notifyState(*n.State)
- }
- if nm := n.NetMap; nm != nil {
- jsNetMap := jsNetMap{
- Self: jsNetMapSelfNode{
- jsNetMapNode: jsNetMapNode{
- Name: nm.Name,
- Addresses: mapSlice(nm.Addresses, func(a netip.Prefix) string { return a.Addr().String() }),
- NodeKey: nm.NodeKey.String(),
- MachineKey: nm.MachineKey.String(),
- },
- MachineStatus: jsMachineStatus[nm.MachineStatus],
- },
- Peers: mapSlice(nm.Peers, func(p *tailcfg.Node) jsNetMapPeerNode {
- name := p.Name
- if name == "" {
- // In practice this should only happen for Hello.
- name = p.Hostinfo.Hostname()
- }
- return jsNetMapPeerNode{
- jsNetMapNode: jsNetMapNode{
- Name: name,
- Addresses: mapSlice(p.Addresses, func(a netip.Prefix) string { return a.Addr().String() }),
- MachineKey: p.Machine.String(),
- NodeKey: p.Key.String(),
- },
- Online: p.Online,
- TailscaleSSHEnabled: p.Hostinfo.TailscaleSSHEnabled(),
- }
- }),
- LockedOut: nm.TKAEnabled && len(nm.SelfNode.KeySignature) == 0,
- }
- if jsonNetMap, err := json.Marshal(jsNetMap); err == nil {
- jsCallbacks.Call("notifyNetMap", string(jsonNetMap))
- } else {
- log.Printf("Could not generate JSON netmap: %v", err)
- }
- }
- if n.BrowseToURL != nil {
- jsCallbacks.Call("notifyBrowseToURL", *n.BrowseToURL)
- }
- })
-
- go func() {
- err := i.lb.Start(ipn.Options{
- UpdatePrefs: &ipn.Prefs{
- ControlURL: i.controlURL,
- RouteAll: false,
- AllowSingleHosts: true,
- WantRunning: true,
- Hostname: i.hostname,
- },
- AuthKey: i.authKey,
- })
- if err != nil {
- log.Printf("Start error: %v", err)
- }
- }()
-
- go func() {
- ln, err := safesocket.Listen("")
- if err != nil {
- log.Fatalf("safesocket.Listen: %v", err)
- }
-
- err = i.srv.Run(context.Background(), ln)
- log.Fatalf("ipnserver.Run exited: %v", err)
- }()
-}
-
-func (i *jsIPN) login() {
- go i.lb.StartLoginInteractive()
-}
-
-func (i *jsIPN) logout() {
- if i.lb.State() == ipn.NoState {
- log.Printf("Backend not running")
- }
- go i.lb.Logout()
-}
-
-func (i *jsIPN) ssh(host, username string, termConfig js.Value) map[string]any {
- jsSSHSession := &jsSSHSession{
- jsIPN: i,
- host: host,
- username: username,
- termConfig: termConfig,
- }
-
- go jsSSHSession.Run()
-
- return map[string]any{
- "close": js.FuncOf(func(this js.Value, args []js.Value) any {
- return jsSSHSession.Close() != nil
- }),
- "resize": js.FuncOf(func(this js.Value, args []js.Value) any {
- rows := args[0].Int()
- cols := args[1].Int()
- return jsSSHSession.Resize(rows, cols) != nil
- }),
- }
-}
-
-type jsSSHSession struct {
- jsIPN *jsIPN
- host string
- username string
- termConfig js.Value
- session *ssh.Session
-
- pendingResizeRows int
- pendingResizeCols int
-}
-
-func (s *jsSSHSession) Run() {
- writeFn := s.termConfig.Get("writeFn")
- writeErrorFn := s.termConfig.Get("writeErrorFn")
- setReadFn := s.termConfig.Get("setReadFn")
- rows := s.termConfig.Get("rows").Int()
- cols := s.termConfig.Get("cols").Int()
- timeoutSeconds := 5.0
- if jsTimeoutSeconds := s.termConfig.Get("timeoutSeconds"); jsTimeoutSeconds.Type() == js.TypeNumber {
- timeoutSeconds = jsTimeoutSeconds.Float()
- }
- onConnectionProgress := s.termConfig.Get("onConnectionProgress")
- onConnected := s.termConfig.Get("onConnected")
- onDone := s.termConfig.Get("onDone")
- defer onDone.Invoke()
-
- writeError := func(label string, err error) {
- writeErrorFn.Invoke(fmt.Sprintf("%s Error: %v\r\n", label, err))
- }
- reportProgress := func(message string) {
- onConnectionProgress.Invoke(message)
- }
-
- ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds*float64(time.Second)))
- defer cancel()
- reportProgress(fmt.Sprintf("Connecting to %s…", strings.Split(s.host, ".")[0]))
- c, err := s.jsIPN.dialer.UserDial(ctx, "tcp", net.JoinHostPort(s.host, "22"))
- if err != nil {
- writeError("Dial", err)
- return
- }
- defer c.Close()
-
- config := &ssh.ClientConfig{
- HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
- // Host keys are not used with Tailscale SSH, but we can use this
- // callback to know that the connection has been established.
- reportProgress("SSH connection established…")
- return nil
- },
- User: s.username,
- }
-
- reportProgress("Starting SSH client…")
- sshConn, _, _, err := ssh.NewClientConn(c, s.host, config)
- if err != nil {
- writeError("SSH Connection", err)
- return
- }
- defer sshConn.Close()
-
- sshClient := ssh.NewClient(sshConn, nil, nil)
- defer sshClient.Close()
-
- session, err := sshClient.NewSession()
- if err != nil {
- writeError("SSH Session", err)
- return
- }
- s.session = session
- defer session.Close()
-
- stdin, err := session.StdinPipe()
- if err != nil {
- writeError("SSH Stdin", err)
- return
- }
-
- session.Stdout = termWriter{writeFn}
- session.Stderr = termWriter{writeFn}
-
- setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) any {
- input := args[0].String()
- _, err := stdin.Write([]byte(input))
- if err != nil {
- writeError("Write Input", err)
- }
- return nil
- }))
-
- // We might have gotten a resize notification since we started opening the
- // session, pick up the latest size.
- if s.pendingResizeRows != 0 {
- rows = s.pendingResizeRows
- }
- if s.pendingResizeCols != 0 {
- cols = s.pendingResizeCols
- }
- err = session.RequestPty("xterm", rows, cols, ssh.TerminalModes{})
-
- if err != nil {
- writeError("Pseudo Terminal", err)
- return
- }
-
- err = session.Shell()
- if err != nil {
- writeError("Shell", err)
- return
- }
-
- onConnected.Invoke()
- err = session.Wait()
- if err != nil {
- writeError("Wait", err)
- return
- }
-}
-
-func (s *jsSSHSession) Close() error {
- if s.session == nil {
- // We never had a chance to open the session, ignore the close request.
- return nil
- }
- return s.session.Close()
-}
-
-func (s *jsSSHSession) Resize(rows, cols int) error {
- if s.session == nil {
- s.pendingResizeRows = rows
- s.pendingResizeCols = cols
- return nil
- }
- return s.session.WindowChange(rows, cols)
-}
-
-func (i *jsIPN) fetch(url string) js.Value {
- return makePromise(func() (any, error) {
- c := &http.Client{
- Transport: &http.Transport{
- DialContext: i.dialer.UserDial,
- },
- }
- res, err := c.Get(url)
- if err != nil {
- return nil, err
- }
-
- return map[string]any{
- "status": res.StatusCode,
- "statusText": res.Status,
- "text": js.FuncOf(func(this js.Value, args []js.Value) any {
- return makePromise(func() (any, error) {
- defer res.Body.Close()
- buf := new(bytes.Buffer)
- if _, err := buf.ReadFrom(res.Body); err != nil {
- return nil, err
- }
- return buf.String(), nil
- })
- }),
- // TODO: populate a more complete JS Response object
- }, nil
- })
-}
-
-type termWriter struct {
- f js.Value
-}
-
-func (w termWriter) Write(p []byte) (n int, err error) {
- r := bytes.Replace(p, []byte("\n"), []byte("\n\r"), -1)
- w.f.Invoke(string(r))
- return len(p), nil
-}
-
-type jsNetMap struct {
- Self jsNetMapSelfNode `json:"self"`
- Peers []jsNetMapPeerNode `json:"peers"`
- LockedOut bool `json:"lockedOut"`
-}
-
-type jsNetMapNode struct {
- Name string `json:"name"`
- Addresses []string `json:"addresses"`
- MachineKey string `json:"machineKey"`
- NodeKey string `json:"nodeKey"`
-}
-
-type jsNetMapSelfNode struct {
- jsNetMapNode
- MachineStatus string `json:"machineStatus"`
-}
-
-type jsNetMapPeerNode struct {
- jsNetMapNode
- Online *bool `json:"online,omitempty"`
- TailscaleSSHEnabled bool `json:"tailscaleSSHEnabled"`
-}
-
-type jsStateStore struct {
- jsStateStorage js.Value
-}
-
-func (s *jsStateStore) ReadState(id ipn.StateKey) ([]byte, error) {
- jsValue := s.jsStateStorage.Call("getState", string(id))
- if jsValue.String() == "" {
- return nil, ipn.ErrStateNotExist
- }
- return hex.DecodeString(jsValue.String())
-}
-
-func (s *jsStateStore) WriteState(id ipn.StateKey, bs []byte) error {
- s.jsStateStorage.Call("setState", string(id), hex.EncodeToString(bs))
- return nil
-}
-
-func mapSlice[T any, M any](a []T, f func(T) M) []M {
- n := make([]M, len(a))
- for i, e := range a {
- n[i] = f(e)
- }
- return n
-}
-
-func filterSlice[T any](a []T, f func(T) bool) []T {
- n := make([]T, 0, len(a))
- for _, e := range a {
- if f(e) {
- n = append(n, e)
- }
- }
- return n
-}
-
-func generateHostname() string {
- tails := words.Tails()
- scales := words.Scales()
- if rand.Int()%2 == 0 {
- // JavaScript
- tails = filterSlice(tails, func(s string) bool { return strings.HasPrefix(s, "j") })
- scales = filterSlice(scales, func(s string) bool { return strings.HasPrefix(s, "s") })
- } else {
- // WebAssembly
- tails = filterSlice(tails, func(s string) bool { return strings.HasPrefix(s, "w") })
- scales = filterSlice(scales, func(s string) bool { return strings.HasPrefix(s, "a") })
- }
-
- tail := tails[rand.Intn(len(tails))]
- scale := scales[rand.Intn(len(scales))]
- return fmt.Sprintf("%s-%s", tail, scale)
-}
-
-// makePromise handles the boilerplate of wrapping goroutines with JS promises.
-// f is run on a goroutine and its return value is used to resolve the promise
-// (or reject it if an error is returned).
-func makePromise(f func() (any, error)) js.Value {
- handler := js.FuncOf(func(this js.Value, args []js.Value) any {
- resolve := args[0]
- reject := args[1]
- go func() {
- if res, err := f(); err == nil {
- resolve.Invoke(res)
- } else {
- reject.Invoke(err.Error())
- }
- }()
- return nil
- })
-
- promiseConstructor := js.Global().Get("Promise")
- return promiseConstructor.New(handler)
-}
-
-const logPolicyStateKey = "log-policy"
-
-func getOrCreateLogPolicyConfig(state ipn.StateStore) *logpolicy.Config {
- if configBytes, err := state.ReadState(logPolicyStateKey); err == nil {
- if config, err := logpolicy.ConfigFromBytes(configBytes); err == nil {
- return config
- } else {
- log.Printf("Could not parse log policy config: %v", err)
- }
- } else if err != ipn.ErrStateNotExist {
- log.Printf("Could not get log policy config from state store: %v", err)
- }
- config := logpolicy.NewConfig(logtail.CollectionNode)
- if err := state.WriteState(logPolicyStateKey, config.ToBytes()); err != nil {
- log.Printf("Could not save log policy config to state store: %v", err)
- }
- return config
-}
-
-// noCORSTransport wraps a RoundTripper and forces the no-cors mode on requests,
-// so that we can use it with non-CORS-aware servers.
-type noCORSTransport struct {
- http.RoundTripper
-}
-
-func (t *noCORSTransport) RoundTrip(req *http.Request) (*http.Response, error) {
- req.Header.Set("js.fetch:mode", "no-cors")
- resp, err := t.RoundTripper.RoundTrip(req)
- if err == nil {
- // In no-cors mode no response properties are returned. Populate just
- // the status so that callers do not think this was an error.
- resp.StatusCode = http.StatusOK
- resp.Status = http.StatusText(http.StatusOK)
- }
- return resp, err
-}
diff --git a/cmd/tsconnect/yarn.lock b/cmd/tsconnect/yarn.lock
deleted file mode 100644
index 663a1244ebf69..0000000000000
--- a/cmd/tsconnect/yarn.lock
+++ /dev/null
@@ -1,713 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@nodelib/fs.scandir@2.1.5":
- version "2.1.5"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
- integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
- dependencies:
- "@nodelib/fs.stat" "2.0.5"
- run-parallel "^1.1.9"
-
-"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
- integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
-
-"@nodelib/fs.walk@^1.2.3":
- version "1.2.8"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
- integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
- dependencies:
- "@nodelib/fs.scandir" "2.1.5"
- fastq "^1.6.0"
-
-"@types/golang-wasm-exec@^1.15.0":
- version "1.15.0"
- resolved "https://registry.yarnpkg.com/@types/golang-wasm-exec/-/golang-wasm-exec-1.15.0.tgz#d0aafbb2b0dc07eaf45dfb83bfb6cdd5b2b3c55c"
- integrity sha512-FrL97mp7WW8LqNinVkzTVKOIQKuYjQqgucnh41+1vRQ+bf1LT8uh++KRf9otZPXsa6H1p8ruIGz1BmCGttOL6Q==
-
-"@types/node@*":
- version "18.6.1"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.1.tgz#828e4785ccca13f44e2fb6852ae0ef11e3e20ba5"
- integrity sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg==
-
-"@types/qrcode@^1.4.2":
- version "1.4.2"
- resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.4.2.tgz#7d7142d6fa9921f195db342ed08b539181546c74"
- integrity sha512-7uNT9L4WQTNJejHTSTdaJhfBSCN73xtXaHFyBJ8TSwiLhe4PRuTue7Iph0s2nG9R/ifUaSnGhLUOZavlBEqDWQ==
- dependencies:
- "@types/node" "*"
-
-acorn-node@^1.8.2:
- version "1.8.2"
- resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
- integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==
- dependencies:
- acorn "^7.0.0"
- acorn-walk "^7.0.0"
- xtend "^4.0.2"
-
-acorn-walk@^7.0.0:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
- integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
-
-acorn@^7.0.0:
- version "7.4.1"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
- integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
-
-ansi-regex@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
- integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
-
-ansi-styles@^4.0.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
- integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
- dependencies:
- color-convert "^2.0.1"
-
-anymatch@~3.1.2:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
- integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
- dependencies:
- normalize-path "^3.0.0"
- picomatch "^2.0.4"
-
-arg@^5.0.2:
- version "5.0.2"
- resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
- integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
-
-binary-extensions@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
- integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
-
-braces@^3.0.2, braces@~3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
- integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
- dependencies:
- fill-range "^7.0.1"
-
-camelcase-css@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
- integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
-
-camelcase@^5.0.0:
- version "5.3.1"
- resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
- integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
-
-chokidar@^3.5.3:
- version "3.5.3"
- resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
- integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
- dependencies:
- anymatch "~3.1.2"
- braces "~3.0.2"
- glob-parent "~5.1.2"
- is-binary-path "~2.1.0"
- is-glob "~4.0.1"
- normalize-path "~3.0.0"
- readdirp "~3.6.0"
- optionalDependencies:
- fsevents "~2.3.2"
-
-cliui@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
- integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
- dependencies:
- string-width "^4.2.0"
- strip-ansi "^6.0.0"
- wrap-ansi "^6.2.0"
-
-cliui@^7.0.2:
- version "7.0.4"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
- integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
- dependencies:
- string-width "^4.2.0"
- strip-ansi "^6.0.0"
- wrap-ansi "^7.0.0"
-
-color-convert@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
- integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
- dependencies:
- color-name "~1.1.4"
-
-color-name@^1.1.4, color-name@~1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
- integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-
-cssesc@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
- integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
-
-decamelize@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
- integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
-
-defined@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
- integrity sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==
-
-detective@^5.2.1:
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034"
- integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==
- dependencies:
- acorn-node "^1.8.2"
- defined "^1.0.0"
- minimist "^1.2.6"
-
-didyoumean@^1.2.2:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
- integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
-
-dijkstrajs@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.2.tgz#2e48c0d3b825462afe75ab4ad5e829c8ece36257"
- integrity sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==
-
-dlv@^1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
- integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
-
-dts-bundle-generator@^6.12.0:
- version "6.12.0"
- resolved "https://registry.yarnpkg.com/dts-bundle-generator/-/dts-bundle-generator-6.12.0.tgz#0a221bdce5fdd309a56c8556e645f16ed87ab07d"
- integrity sha512-k/QAvuVaLIdyWRUHduDrWBe4j8PcE6TDt06+f32KHbW7/SmUPbX1O23fFtQgKwUyTBkbIjJFOFtNrF97tJcKug==
- dependencies:
- typescript ">=3.0.1"
- yargs "^17.2.1"
-
-emoji-regex@^8.0.0:
- version "8.0.0"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
- integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
-
-encode-utf8@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
- integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
-
-escalade@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
- integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
-
-fast-glob@^3.2.11:
- version "3.2.11"
- resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
- integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
- dependencies:
- "@nodelib/fs.stat" "^2.0.2"
- "@nodelib/fs.walk" "^1.2.3"
- glob-parent "^5.1.2"
- merge2 "^1.3.0"
- micromatch "^4.0.4"
-
-fastq@^1.6.0:
- version "1.13.0"
- resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
- integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
- dependencies:
- reusify "^1.0.4"
-
-fill-range@^7.0.1:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
- integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
- dependencies:
- to-regex-range "^5.0.1"
-
-find-up@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
- integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
- dependencies:
- locate-path "^5.0.0"
- path-exists "^4.0.0"
-
-fsevents@~2.3.2:
- version "2.3.2"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
- integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
-
-function-bind@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
- integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
-
-get-caller-file@^2.0.1, get-caller-file@^2.0.5:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
- integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-
-glob-parent@^5.1.2, glob-parent@~5.1.2:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
- integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
- dependencies:
- is-glob "^4.0.1"
-
-glob-parent@^6.0.2:
- version "6.0.2"
- resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
- integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
- dependencies:
- is-glob "^4.0.3"
-
-has@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
- integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
- dependencies:
- function-bind "^1.1.1"
-
-is-binary-path@~2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
- integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
- dependencies:
- binary-extensions "^2.0.0"
-
-is-core-module@^2.9.0:
- version "2.9.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
- integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
- dependencies:
- has "^1.0.3"
-
-is-extglob@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
- integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
-
-is-fullwidth-code-point@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
- integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
-
-is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
- integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
- dependencies:
- is-extglob "^2.1.1"
-
-is-number@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
- integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
-
-lilconfig@^2.0.5:
- version "2.0.6"
- resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4"
- integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==
-
-locate-path@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
- integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
- dependencies:
- p-locate "^4.1.0"
-
-merge2@^1.3.0:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
- integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
-
-micromatch@^4.0.4:
- version "4.0.5"
- resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
- integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
- dependencies:
- braces "^3.0.2"
- picomatch "^2.3.1"
-
-minimist@^1.2.6:
- version "1.2.6"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
- integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
-
-nanoid@^3.3.4:
- version "3.3.4"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
- integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
-
-normalize-path@^3.0.0, normalize-path@~3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
- integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
-
-object-hash@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
- integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
-
-p-limit@^2.2.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
- integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
- dependencies:
- p-try "^2.0.0"
-
-p-locate@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
- integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
- dependencies:
- p-limit "^2.2.0"
-
-p-try@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
- integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
-
-path-exists@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
- integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
-
-path-parse@^1.0.7:
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
- integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
-
-picocolors@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
- integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
-
-picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
- integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
-
-pify@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
- integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
-
-pngjs@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
- integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
-
-postcss-import@^14.1.0:
- version "14.1.0"
- resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0"
- integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==
- dependencies:
- postcss-value-parser "^4.0.0"
- read-cache "^1.0.0"
- resolve "^1.1.7"
-
-postcss-js@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00"
- integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==
- dependencies:
- camelcase-css "^2.0.1"
-
-postcss-load-config@^3.1.4:
- version "3.1.4"
- resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855"
- integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
- dependencies:
- lilconfig "^2.0.5"
- yaml "^1.10.2"
-
-postcss-nested@5.0.6:
- version "5.0.6"
- resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.6.tgz#466343f7fc8d3d46af3e7dba3fcd47d052a945bc"
- integrity sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==
- dependencies:
- postcss-selector-parser "^6.0.6"
-
-postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.6:
- version "6.0.10"
- resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
- integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
- dependencies:
- cssesc "^3.0.0"
- util-deprecate "^1.0.2"
-
-postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
- integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
-
-postcss@^8.4.14:
- version "8.4.14"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
- integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
- dependencies:
- nanoid "^3.3.4"
- picocolors "^1.0.0"
- source-map-js "^1.0.2"
-
-preact@^10.10.0:
- version "10.10.0"
- resolved "https://registry.yarnpkg.com/preact/-/preact-10.10.0.tgz#7434750a24b59dae1957d95dc0aa47a4a8e9a180"
- integrity sha512-fszkg1iJJjq68I4lI8ZsmBiaoQiQHbxf1lNq+72EmC/mZOsFF5zn3k1yv9QGoFgIXzgsdSKtYymLJsrJPoamjQ==
-
-qrcode@^1.5.0:
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.0.tgz#95abb8a91fdafd86f8190f2836abbfc500c72d1b"
- integrity sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==
- dependencies:
- dijkstrajs "^1.0.1"
- encode-utf8 "^1.0.3"
- pngjs "^5.0.0"
- yargs "^15.3.1"
-
-queue-microtask@^1.2.2:
- version "1.2.3"
- resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
- integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
-
-quick-lru@^5.1.1:
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
- integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
-
-read-cache@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
- integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==
- dependencies:
- pify "^2.3.0"
-
-readdirp@~3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
- integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
- dependencies:
- picomatch "^2.2.1"
-
-require-directory@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
- integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
-
-require-main-filename@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
- integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
-
-resolve@^1.1.7, resolve@^1.22.1:
- version "1.22.1"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
- integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
- dependencies:
- is-core-module "^2.9.0"
- path-parse "^1.0.7"
- supports-preserve-symlinks-flag "^1.0.0"
-
-reusify@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
- integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
-
-run-parallel@^1.1.9:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
- integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
- dependencies:
- queue-microtask "^1.2.2"
-
-set-blocking@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
- integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
-
-source-map-js@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
- integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
-
-string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
-supports-preserve-symlinks-flag@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
- integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
-
-tailwindcss@^3.1.6:
- version "3.1.6"
- resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.1.6.tgz#bcb719357776c39e6376a8d84e9834b2b19a49f1"
- integrity sha512-7skAOY56erZAFQssT1xkpk+kWt2NrO45kORlxFPXUt3CiGsVPhH1smuH5XoDH6sGPXLyBv+zgCKA2HWBsgCytg==
- dependencies:
- arg "^5.0.2"
- chokidar "^3.5.3"
- color-name "^1.1.4"
- detective "^5.2.1"
- didyoumean "^1.2.2"
- dlv "^1.1.3"
- fast-glob "^3.2.11"
- glob-parent "^6.0.2"
- is-glob "^4.0.3"
- lilconfig "^2.0.5"
- normalize-path "^3.0.0"
- object-hash "^3.0.0"
- picocolors "^1.0.0"
- postcss "^8.4.14"
- postcss-import "^14.1.0"
- postcss-js "^4.0.0"
- postcss-load-config "^3.1.4"
- postcss-nested "5.0.6"
- postcss-selector-parser "^6.0.10"
- postcss-value-parser "^4.2.0"
- quick-lru "^5.1.1"
- resolve "^1.22.1"
-
-to-regex-range@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
- integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
- dependencies:
- is-number "^7.0.0"
-
-typescript@>=3.0.1, typescript@^4.7.4:
- version "4.7.4"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
- integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
-
-util-deprecate@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
- integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
-
-which-module@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
- integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
-
-wrap-ansi@^6.2.0:
- version "6.2.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
- integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-xtend@^4.0.2:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
- integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
-
-xterm-addon-fit@^0.7.0:
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz#b8ade6d96e63b47443862088f6670b49fb752c6a"
- integrity sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==
-
-xterm-addon-web-links@^0.8.0:
- version "0.8.0"
- resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.8.0.tgz#2cb1d57129271022569208578b0bf4774e7e6ea9"
- integrity sha512-J4tKngmIu20ytX9SEJjAP3UGksah7iALqBtfTwT9ZnmFHVplCumYQsUJfKuS+JwMhjsjH61YXfndenLNvjRrEw==
-
-xterm@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.1.0.tgz#3e160d60e6801c864b55adf19171c49d2ff2b4fc"
- integrity sha512-LovENH4WDzpwynj+OTkLyZgJPeDom9Gra4DMlGAgz6pZhIDCQ+YuO7yfwanY+gVbn/mmZIStNOnVRU/ikQuAEQ==
-
-y18n@^4.0.0:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
- integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
-
-y18n@^5.0.5:
- version "5.0.8"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
- integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
-
-yaml@^1.10.2:
- version "1.10.2"
- resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
- integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
-
-yargs-parser@^18.1.2:
- version "18.1.3"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
- integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
- dependencies:
- camelcase "^5.0.0"
- decamelize "^1.2.0"
-
-yargs-parser@^21.0.0:
- version "21.1.1"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
- integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
-
-yargs@^15.3.1:
- version "15.4.1"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
- integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
- dependencies:
- cliui "^6.0.0"
- decamelize "^1.2.0"
- find-up "^4.1.0"
- get-caller-file "^2.0.1"
- require-directory "^2.1.1"
- require-main-filename "^2.0.0"
- set-blocking "^2.0.0"
- string-width "^4.2.0"
- which-module "^2.0.0"
- y18n "^4.0.0"
- yargs-parser "^18.1.2"
-
-yargs@^17.2.1:
- version "17.5.1"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e"
- integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==
- dependencies:
- cliui "^7.0.2"
- escalade "^3.1.1"
- get-caller-file "^2.0.5"
- require-directory "^2.1.1"
- string-width "^4.2.3"
- y18n "^5.0.5"
- yargs-parser "^21.0.0"