├── .circleci └── config.yml ├── .dockerignore ├── .env.example ├── .envrc ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .readme-assets └── demo.gif ├── .vscode ├── extensions.json └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── app.json ├── cmd └── logpaste │ └── main.go ├── dev-scripts ├── build-backend ├── check-bash ├── check-go-formatting ├── check-trailing-newline ├── check-trailing-whitespace ├── download-prod-db ├── enable-git-hooks ├── git-hooks │ └── pre-commit ├── make-fly-config ├── reset-db ├── run-e2e-tests ├── run-go-tests ├── serve └── upload-prod-db ├── docker-entrypoint ├── docs └── deployment │ ├── cloud-run.md │ ├── fly.io.md │ ├── heroku.md │ ├── lightsail-images │ ├── container-config.png │ ├── container-pending.png │ ├── container-running.png │ ├── create-container.png │ ├── create-service.png │ ├── identify-service.png │ ├── nano-1x.png │ ├── public-domain-url.png │ ├── public-endpoint.png │ ├── set-up-deployment.png │ └── view-logs.png │ └── lightsail.md ├── e2e ├── .gitignore ├── cypress.json ├── cypress │ └── integration │ │ └── paste_spec.js ├── docker-compose.yml ├── sftp.json └── wait-for-sftp.bash ├── flake.lock ├── flake.nix ├── go.mod ├── go.sum ├── handlers ├── index.go ├── paste.go ├── paste_test.go ├── routes.go ├── server.go ├── static.go └── static │ ├── css │ ├── dark.css │ ├── light.css │ └── style.css │ ├── js │ ├── app.js │ └── logpaste.js │ └── third-party │ └── prism │ ├── dark.css │ ├── light.css │ └── prism.js ├── limit └── limit.go ├── litestream.yml ├── modd.conf ├── package-lock.json ├── package.json ├── random └── string.go ├── store ├── sqlite │ ├── migrations.go │ ├── migrations │ │ └── 001-create-entries-table.sql │ └── sqlite.go └── store.go └── views └── index.html /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | executors: 3 | base: 4 | docker: 5 | - image: cimg/base:2024.02 6 | go: 7 | docker: 8 | - image: cimg/go:1.21.1 9 | node: 10 | docker: 11 | - image: cimg/node:20.6.1 12 | jobs: 13 | check_whitespace: 14 | executor: base 15 | resource_class: small 16 | steps: 17 | - checkout 18 | - run: 19 | name: Check for trailing whitespace 20 | command: ./dev-scripts/check-trailing-whitespace 21 | - run: 22 | name: Check that all text files end in a trailing newline 23 | command: ./dev-scripts/check-trailing-newline 24 | check_bash: 25 | docker: 26 | - image: koalaman/shellcheck-alpine:v0.9.0 27 | steps: 28 | - run: 29 | name: Install dependencies needed to check out repo 30 | command: apk add bash git openssh-client grep 31 | - checkout 32 | - run: 33 | name: Run static analysis on bash scripts 34 | command: ./dev-scripts/check-bash 35 | lint_sql: 36 | docker: 37 | - image: sqlfluff/sqlfluff:1.2.1 38 | user: root 39 | steps: 40 | - checkout 41 | - run: 42 | name: Lint SQL files 43 | command: | 44 | sqlfluff lint \ 45 | --disable_progress_bar \ 46 | --dialect sqlite \ 47 | store/sqlite/migrations 48 | test: 49 | executor: go 50 | steps: 51 | - checkout 52 | - attach_workspace: 53 | at: ./ 54 | - run: 55 | name: Test go packages 56 | command: dev-scripts/run-go-tests --full 57 | - store_artifacts: 58 | path: .coverage.html 59 | - run: 60 | name: Verify golang formatting 61 | command: dev-scripts/check-go-formatting 62 | build_backend: 63 | executor: go 64 | steps: 65 | - checkout 66 | - attach_workspace: 67 | at: ./ 68 | - run: 69 | name: Compile production backend 70 | command: dev-scripts/build-backend 71 | - run: 72 | name: Compile dev backend 73 | command: dev-scripts/build-backend dev 74 | check_frontend: 75 | executor: node 76 | steps: 77 | - checkout 78 | - run: 79 | name: Download npm packages 80 | command: npm install 81 | - run: 82 | name: Check formatting 83 | command: npm run check-format 84 | - run: 85 | name: Check lint 86 | command: npm run lint 87 | e2e: 88 | machine: 89 | # Choose an explicit machine image so that we get the latest version of 90 | # docker-compose. 91 | image: ubuntu-2204:2023.10.1 92 | docker_layer_caching: true 93 | steps: 94 | - checkout 95 | - run: 96 | name: Perform e2e tests. 97 | command: dev-scripts/run-e2e-tests 98 | no_output_timeout: 1m 99 | - store_artifacts: 100 | path: e2e/cypress/videos 101 | - store_artifacts: 102 | path: e2e/cypress/screenshots 103 | deploy: 104 | executor: base 105 | resource_class: small 106 | environment: 107 | APP_NAME: logpaste-com 108 | # The flyctl changes too much to use a specific version, so use the latest for the 109 | # time being. 110 | # https://github.com/superfly/flyctl/issues/394#issuecomment-815890166 111 | FLYCTL_VERSION: "latest" 112 | steps: 113 | - checkout 114 | - run: 115 | name: Install flyctl 116 | command: curl -L https://fly.io/install.sh | sh -s "${FLYCTL_VERSION}" 117 | - run: 118 | name: Create fly.io config file 119 | command: dev-scripts/make-fly-config "${APP_NAME}" 120 | - run: 121 | name: Remove lines specifying target Docker image (force a build from source) 122 | command: sed '/^\[build\]/d' fly.toml | sed '/^ *image =/d' 123 | - run: 124 | name: Add a custom footer to the demo site. 125 | command: printf '\n[experimental]\n cmd = ["-footer", "'\''

Notice

This is a demo instance. Uploads are wiped every few hours.

'\''"]\n' >> fly.toml 126 | - run: 127 | name: Deploy logpaste to fly.io 128 | command: | 129 | "${HOME}/.fly/bin/flyctl" deploy 130 | build_docker_images: 131 | executor: base 132 | environment: 133 | BUILD_TARGETS: "linux/amd64" 134 | steps: 135 | - checkout 136 | - setup_remote_docker: 137 | version: 20.10.11 138 | - run: 139 | name: Log in to Docker Hub 140 | command: | 141 | echo "${DOCKERHUB_ACCESS_TOKEN}" | \ 142 | docker login --username "${DOCKERHUB_USERNAME}" --password-stdin 143 | - run: 144 | name: Enable multiarch builds with QEMU 145 | command: | 146 | docker run \ 147 | --rm \ 148 | --privileged \ 149 | multiarch/qemu-user-static \ 150 | --reset \ 151 | -p yes 152 | - run: 153 | name: Create multiarch build context 154 | command: docker context create builder 155 | - run: 156 | name: Create multiplatform builder 157 | command: | 158 | docker buildx create builder \ 159 | --name builder \ 160 | --driver docker-container \ 161 | --use 162 | - run: 163 | name: Ensure builder has booted 164 | command: docker buildx inspect --bootstrap 165 | - run: 166 | name: Build docker images 167 | command: | 168 | docker buildx build \ 169 | --push \ 170 | --platform "$BUILD_TARGETS" \ 171 | --tag "mtlynch/logpaste:${CIRCLE_TAG}" \ 172 | --tag mtlynch/logpaste:latest \ 173 | . 174 | workflows: 175 | version: 2 176 | test: 177 | jobs: 178 | - check_whitespace: 179 | filters: 180 | tags: 181 | only: /.*/ 182 | - check_bash: 183 | filters: 184 | tags: 185 | only: /.*/ 186 | - lint_sql: 187 | filters: 188 | tags: 189 | only: /.*/ 190 | - test: 191 | filters: 192 | tags: 193 | only: /.*/ 194 | - build_backend: 195 | filters: 196 | tags: 197 | only: /.*/ 198 | - e2e: 199 | filters: 200 | tags: 201 | only: /.*/ 202 | - check_frontend: 203 | filters: 204 | tags: 205 | only: /.*/ 206 | - build_docker_images: 207 | requires: 208 | - check_whitespace 209 | - check_bash 210 | - lint_sql 211 | - test 212 | - build_backend 213 | - e2e 214 | - check_frontend 215 | filters: 216 | tags: 217 | only: /[0-9]+(\.[0-9]+){2}/ 218 | branches: 219 | ignore: /.*/ 220 | - deploy: 221 | requires: 222 | - check_whitespace 223 | - check_bash 224 | - lint_sql 225 | - test 226 | - build_backend 227 | - e2e 228 | - check_frontend 229 | filters: 230 | branches: 231 | only: master 232 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | 3 | .dockerignore 4 | .gcloudignore 5 | 6 | .gitignore 7 | 8 | README.md 9 | LICENSE 10 | 11 | client-secret.json 12 | 13 | .*.env 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # To populate a .env file for prod, development, or devops, copy this file and 2 | # name it appropriately. For example: 3 | # 4 | # cp -n .env.example .env.prod 5 | # cp -n .env.example .env.dev 6 | 7 | LITESTREAM_REGION='' 8 | DB_REPLICA_URL='' 9 | LITESTREAM_ACCESS_KEY_ID='' 10 | LITESTREAM_SECRET_ACCESS_KEY='' 11 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use_flake 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | browser: true, 5 | es2022: true, 6 | }, 7 | extends: ["plugin:cypress/recommended", "eslint:recommended"], 8 | rules: { 9 | // This will produce an error for console.log or console.warn in production 10 | // and a warning in development console.error will not produce an error or 11 | // warning https://eslint.org/docs/rules/no-console#options 12 | "no-console": [ 13 | process.env.NODE_ENV === "production" ? "error" : "warn", 14 | { allow: ["error"] }, 15 | ], 16 | }, 17 | ignorePatterns: ["handlers/static/third-party/**/*.js"], 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | client-secret.json 2 | data/ 3 | fly.toml 4 | fly.alias 5 | node_modules 6 | .direnv/ 7 | 8 | # .env files contain secrets, so they should never be checked in to source 9 | # control. 10 | .env.* 11 | # .env.example is an exception as it only contains dummy secrets. 12 | !.env.example 13 | 14 | # Compiled binaries 15 | bin/ 16 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | handlers/static/third-party 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-go-template"], 3 | "overrides": [ 4 | { 5 | "files": ["*.html"], 6 | "options": { 7 | "parser": "go-template" 8 | } 9 | } 10 | ], 11 | "goTemplateBracketSpacing": true 12 | } 13 | -------------------------------------------------------------------------------- /.readme-assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtlynch/logpaste/20750ec29e52502af449f5d462d51ac864738104/.readme-assets/demo.gif -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "golang.go"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "[go]": { 5 | "editor.defaultFormatter": "golang.go" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21.1 as backend_builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.* ./ 6 | RUN go mod download 7 | 8 | COPY ./cmd /app/cmd 9 | COPY ./dev-scripts /app/dev-scripts 10 | COPY ./handlers /app/handlers 11 | COPY ./limit /app/limit 12 | COPY ./random /app/random 13 | COPY ./store /app/store 14 | 15 | RUN TARGETPLATFORM="${TARGETPLATFORM}" ./dev-scripts/build-backend "prod" 16 | 17 | FROM debian:stable-20211011-slim AS litestream_downloader 18 | 19 | ARG litestream_version="v0.3.9" 20 | ARG litestream_binary_tgz_filename="litestream-${litestream_version}-linux-amd64-static.tar.gz" 21 | 22 | WORKDIR /litestream 23 | 24 | RUN set -x && \ 25 | apt-get update && \ 26 | DEBIAN_FRONTEND=noninteractive apt-get install -y \ 27 | ca-certificates \ 28 | wget 29 | RUN wget "https://github.com/benbjohnson/litestream/releases/download/${litestream_version}/${litestream_binary_tgz_filename}" 30 | RUN tar -xvzf "${litestream_binary_tgz_filename}" 31 | 32 | FROM alpine:3.15 33 | 34 | RUN apk add --no-cache bash 35 | 36 | COPY --from=backend_builder /app/bin/logpaste /app/logpaste 37 | COPY --from=litestream_downloader /litestream/litestream /app/litestream 38 | COPY ./docker-entrypoint /app/docker-entrypoint 39 | COPY ./litestream.yml /etc/litestream.yml 40 | COPY ./views /app/views 41 | 42 | WORKDIR /app 43 | 44 | # Frequency that database snapshots are replicated. 45 | ENV DB_SYNC_INTERVAL="10s" 46 | 47 | ENTRYPOINT ["/app/docker-entrypoint"] 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Michael Lynch 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # logpaste 2 | 3 | [![CircleCI](https://circleci.com/gh/mtlynch/logpaste.svg?style=svg)](https://circleci.com/gh/mtlynch/logpaste) 4 | [![Docker Pulls](https://img.shields.io/docker/pulls/mtlynch/logpaste.svg?maxAge=604800)](https://hub.docker.com/r/mtlynch/logpaste/) 5 | [![License](http://img.shields.io/:license-mit-blue.svg?style=flat-square)](LICENSE) 6 | 7 | A minimalist web service for uploading and sharing log files. 8 | 9 | [![LogPaste animated demo](https://raw.githubusercontent.com/mtlynch/logpaste/master/.readme-assets/demo.gif)](https://raw.githubusercontent.com/mtlynch/logpaste/master/.readme-assets/demo.gif) 10 | 11 | ## Features 12 | 13 | - Accept text uploads from command-line, JavaScript, and web UI 14 | - Simple to deploy 15 | - Runs in a single Docker container 16 | - Fits in the free tier of Heroku 17 | - Easy database management 18 | - Syncs persistent data to any S3-compatible cloud storage provider 19 | - Customizable UI without changing source code 20 | 21 | ## Demo 22 | 23 | - 24 | 25 | ## Run LogPaste 26 | 27 | ### From source 28 | 29 | ```bash 30 | PORT=3001 go run cmd/logpaste/main.go 31 | ``` 32 | 33 | ### From Docker 34 | 35 | To run LogPaste within a Docker container, mount a volume from your local system to store the LogPaste sqlite database. 36 | 37 | ```bash 38 | docker run \ 39 | -e "PORT=3001" \ 40 | -p 3001:3001/tcp \ 41 | --volume "${PWD}/data:/app/data" \ 42 | --name logpaste \ 43 | mtlynch/logpaste 44 | ``` 45 | 46 | ### From Docker + cloud data replication 47 | 48 | If you specify settings for an S3 bucket, LogPaste will use [Litestream](https://litestream.io/) to automatically replicate your data to S3. 49 | 50 | You can kill the container and start it later, and it will restore your data from the S3 bucket and continue as if there was no interruption. 51 | 52 | ```bash 53 | LITESTREAM_ACCESS_KEY_ID=YOUR-ACCESS-ID 54 | LITESTREAM_SECRET_ACCESS_KEY=YOUR-SECRET-ACCESS-KEY 55 | LITESTREAM_REGION=YOUR-REGION 56 | DB_REPLICA_URL=s3://your-bucket-name/db 57 | 58 | docker run \ 59 | -e "PORT=3001" \ 60 | -e "LITESTREAM_ACCESS_KEY_ID=${LITESTREAM_ACCESS_KEY_ID}" \ 61 | -e "LITESTREAM_SECRET_ACCESS_KEY=${LITESTREAM_SECRET_ACCESS_KEY}" \ 62 | -e "LITESTREAM_REGION=${LITESTREAM_REGION}" \ 63 | -e "DB_REPLICA_URL=${DB_REPLICA_URL}" \ 64 | -p 3001:3001/tcp \ 65 | --name logpaste \ 66 | mtlynch/logpaste 67 | ``` 68 | 69 | Some notes: 70 | 71 | - Only run one Docker container for each S3 location 72 | - LogPaste can't sync writes across multiple instances. 73 | 74 | ### With custom site settings 75 | 76 | LogPaste offers some options to customize the text for your site. Here's an example that uses a custom title, subtitle, and footer: 77 | 78 | ```bash 79 | docker run \ 80 | -e "PORT=3001" \ 81 | -p 3001:3001/tcp \ 82 | --name logpaste \ 83 | mtlynch/logpaste \ 84 | -title 'My Cool Log Pasting Service' \ 85 | -subtitle 'Upload all your logs for FooBar here' \ 86 | -footer '

Notice

Only cool users can share logs here.

' \ 87 | -showdocs=false \ 88 | -perminutelimit 5 89 | ``` 90 | 91 | ## Parameters 92 | 93 | ### Command-line flags 94 | 95 | | Flag | Meaning | Default Value | 96 | | ----------------- | -------------------------------------------------- | ------------------------------------------------------ | 97 | | `-title` | Title to display on homepage | `"LogPaste"` | 98 | | `-subtitle` | Subtitle to display on homepage | `"A minimalist, open-source debug log upload service"` | 99 | | `-footer` | Footer to display on homepage (may include HTML) | | 100 | | `-showdocs` | Whether to display usage documentation on homepage | `true` | 101 | | `-perminutelimit` | Number of pastes to allow per IP per minute | `0` (no limit) | 102 | | `-maxsize` | Max file size users can upload | `2` (2 MiB) | 103 | 104 | ### Docker environment variables 105 | 106 | You can adjust behavior of the Docker container by passing these parameters with `docker run -e`: 107 | 108 | | Environment Variable | Meaning | 109 | | ------------------------------ | ------------------------------------------------------------------------------------------------- | 110 | | `PORT` | TCP port on which to listen for HTTP connections (defaults to 3001) | 111 | | `LP_BEHIND_PROXY` | Set to `y` if running behind an HTTP proxy to improve logging | 112 | | `DB_REPLICA_URL` | S3 URL where you want to replicate the LogPaste datastore (e.g., `s3://mybucket.mydomain.com/db`) | 113 | | `LITESTREAM_REGION` | AWS region where your S3 bucket is located | 114 | | `LITESTREAM_ACCESS_KEY_ID` | AWS access key ID for an IAM role with access to the bucket where you want to replicate data. | 115 | | `LITESTREAM_SECRET_ACCESS_KEY` | AWS secret access key for an IAM role with access to the bucket where you want to replicate data. | 116 | 117 | ### Docker build args 118 | 119 | If you rebuild the Docker image from source, you can adjust the build behavior with `docker build --build-arg`: 120 | 121 | | Build Arg | Meaning | Default Value | 122 | | -------------------- | --------------------------------------------------------------------------- | ------------- | 123 | | `litestream_version` | Version of [Litestream](https://litestream.io/) to use for data replication | `v0.3.9` | 124 | 125 | ## Deployment 126 | 127 | LogPaste is easy to deploy to cloud services. Here are some places it works well: 128 | 129 | - [fly.io](docs/deployment/fly.io.md) (recommended) 130 | - [Heroku](docs/deployment/heroku.md) 131 | - [Google Cloud Run](docs/deployment/cloud-run.md) 132 | - [Amazon LightSail](docs/deployment/lightsail.md) 133 | 134 | ## Further reading 135 | 136 | - ["How Litestream Eliminated My Database Server for $0.03/month"](https://mtlynch.io/litestream/): Explains the motivation behind LogPaste 137 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "stack": "container" 3 | } 4 | -------------------------------------------------------------------------------- /cmd/logpaste/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | 10 | gorilla "github.com/mtlynch/gorilla-handlers" 11 | 12 | "github.com/mtlynch/logpaste/handlers" 13 | ) 14 | 15 | func main() { 16 | log.Print("Starting logpaste server") 17 | 18 | title := flag.String("title", "LogPaste", "title for the site") 19 | subtitle := flag.String("subtitle", 20 | "A minimalist, open-source debug log upload service", 21 | "subtitle for the site") 22 | footer := flag.String("footer", "", "custom page footer (can contain HTML)") 23 | showDocs := flag.Bool("showdocs", 24 | true, "whether to display usage information on homepage") 25 | perMinuteLimit := flag.Int("perminutelimit", 26 | 0, "number of pastes to allow per IP per minute (set to 0 to disable rate limiting)") 27 | maxPasteMiB := flag.Int64("maxsize", 2, "max file size as MiB") 28 | 29 | flag.Parse() 30 | 31 | const charactersPerMiB = 1024 * 1024 32 | maxCharLimit := *maxPasteMiB * charactersPerMiB 33 | 34 | h := gorilla.LoggingHandler(os.Stdout, handlers.New(handlers.SiteProperties{ 35 | Title: *title, 36 | Subtitle: *subtitle, 37 | FooterHTML: *footer, 38 | ShowDocs: *showDocs, 39 | }, *perMinuteLimit, maxCharLimit).Router()) 40 | if os.Getenv("LP_BEHIND_PROXY") != "" { 41 | h = gorilla.ProxyIPHeadersHandler(h) 42 | } 43 | http.Handle("/", h) 44 | 45 | port := os.Getenv("PORT") 46 | if port == "" { 47 | port = "3001" 48 | } 49 | log.Printf("Listening on %s", port) 50 | 51 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) 52 | } 53 | -------------------------------------------------------------------------------- /dev-scripts/build-backend: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit build script on first failure. 4 | set -e 5 | 6 | # Echo commands. 7 | set -x 8 | 9 | if [[ -z $1 ]]; then 10 | MODE='prod' 11 | else 12 | MODE="$1" 13 | fi 14 | 15 | PLATFORM="${TARGETPLATFORM:-linux/amd64}" 16 | 17 | # Exit on unset variable. 18 | set -u 19 | 20 | GO_BUILD_TAGS=() 21 | BINARY='./bin/logpaste' 22 | 23 | GO_BUILD_TAGS+=('netgo') 24 | # Disable dynamically-loaded extensions, which cause a compile time warning. 25 | # https://www.arp242.net/static-go.html 26 | GO_BUILD_TAGS+=('sqlite_omit_load_extension') 27 | 28 | if [[ "${MODE}" != 'prod' ]]; then 29 | BINARY="${BINARY}-${MODE}" 30 | GO_BUILD_TAGS+=("${MODE}") 31 | fi 32 | readonly BINARY 33 | readonly GO_BUILD_TAGS 34 | 35 | readonly GOOS='linux' 36 | export GOOS 37 | if [ "${PLATFORM}" = 'linux/amd64' ]; then 38 | GOARCH='amd64' 39 | elif [ "${PLATFORM}" = 'linux/arm/v7' ]; then 40 | GOARCH='arm' 41 | elif [ "${PLATFORM}" = 'linux/arm64' ]; then 42 | GOARCH='arm64' 43 | else 44 | echo "Unsupported platform: ${PLATFORM}" 45 | exit 1 46 | fi 47 | readonly GOARCH 48 | export GOARCH 49 | 50 | # Join together build tags 51 | BUILD_TAGS_JOINED="" 52 | for tag in "${GO_BUILD_TAGS[@]}"; do 53 | BUILD_TAGS_JOINED+=" $tag" 54 | done 55 | 56 | # Trim leading space. 57 | BUILD_TAGS_JOINED="${BUILD_TAGS_JOINED# }" 58 | readonly BUILD_TAGS_JOINED 59 | 60 | # cgo is required for mattn/go-sqlite3. 61 | export CGO_ENABLED=1 62 | 63 | go build \ 64 | -tags "${BUILD_TAGS_JOINED}" \ 65 | -ldflags '-w -extldflags "-static"' \ 66 | -o "${BINARY}" \ 67 | cmd/logpaste/main.go 68 | -------------------------------------------------------------------------------- /dev-scripts/check-bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run static analysis on bash scripts. 4 | 5 | # Exit on first failing command. 6 | set -e 7 | 8 | # Exit on unset variable. 9 | set -u 10 | 11 | BASH_SCRIPTS=() 12 | 13 | while read -r filepath; do 14 | # Check shebang for bash. 15 | if head -n 1 "${filepath}" | grep --quiet --regexp 'bash'; then 16 | BASH_SCRIPTS+=("${filepath}") 17 | fi 18 | done < <(git ls-files) 19 | 20 | readonly BASH_SCRIPTS 21 | 22 | # Echo commands before executing them, by default to stderr. 23 | set -x 24 | 25 | shellcheck "${BASH_SCRIPTS[@]}" 26 | -------------------------------------------------------------------------------- /dev-scripts/check-go-formatting: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit on first failure. 4 | set -e 5 | 6 | # Exit on unset variable. 7 | set -u 8 | 9 | # Change directory to repository root. 10 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 11 | readonly SCRIPT_DIR 12 | cd "${SCRIPT_DIR}/.." 13 | 14 | test -z "$(gofmt -s -d .)" 15 | -------------------------------------------------------------------------------- /dev-scripts/check-trailing-newline: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Verify that all text files end in a trailing newline. 4 | 5 | # Exit on first failure. 6 | set -e 7 | 8 | # Exit on unset variable. 9 | set -u 10 | 11 | # Change directory to repository root. 12 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 13 | readonly SCRIPT_DIR 14 | cd "${SCRIPT_DIR}/.." 15 | 16 | success=0 17 | 18 | while read -r line; do 19 | if ! [[ -s "${line}" && -z "$(tail -c 1 "${line}")" ]]; then 20 | printf "File must end in a trailing newline: %s\n" "${line}" >&2 21 | success=255 22 | fi 23 | done < <(git ls-files \ 24 | | xargs grep ".*" \ 25 | --files-with-matches \ 26 | --binary-files=without-match \ 27 | --exclude="*third-party*") 28 | 29 | exit "${success}" 30 | -------------------------------------------------------------------------------- /dev-scripts/check-trailing-whitespace: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check for trailing whitespace 4 | 5 | # Exit on first failure. 6 | set -e 7 | 8 | # Exit on unset variable. 9 | set -u 10 | 11 | # Change directory to repository root. 12 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 13 | readonly SCRIPT_DIR 14 | cd "${SCRIPT_DIR}/.." 15 | 16 | while read -r line; do 17 | if grep \ 18 | "\s$" \ 19 | --line-number \ 20 | --with-filename \ 21 | --binary-files=without-match \ 22 | "${line}"; then 23 | echo "ERROR: Found trailing whitespace"; 24 | exit 1; 25 | fi 26 | done < <(git ls-files) 27 | -------------------------------------------------------------------------------- /dev-scripts/download-prod-db: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | # Change directory to repository root. 6 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 7 | readonly SCRIPT_DIR 8 | cd "${SCRIPT_DIR}/.." 9 | 10 | TIMESTAMP=$(date --iso-8601=minutes | sed 's/://g' | sed 's/+0000/Z/g') 11 | export DB_PATH="data/store.db" 12 | export DB_COPY_PATH="data/${TIMESTAMP}.db" 13 | 14 | ./dev-scripts/reset-db 15 | 16 | set +x 17 | # shellcheck disable=SC1091 18 | . .env.prod 19 | set -x 20 | 21 | export LITESTREAM_REGION 22 | export DB_REPLICA_URL 23 | export LITESTREAM_ACCESS_KEY_ID 24 | export LITESTREAM_SECRET_ACCESS_KEY 25 | 26 | # Export DB_PATH so that litestream uses the variable to populate 27 | # litestream.yml. 28 | export DB_PATH 29 | 30 | litestream snapshots -config litestream.yml "${DB_PATH}" 31 | 32 | # Retrieve live DB 33 | litestream restore -config litestream.yml -o "${DB_COPY_PATH}" "${DB_PATH}" 34 | cp "${DB_COPY_PATH}" "${DB_PATH}" 35 | -------------------------------------------------------------------------------- /dev-scripts/enable-git-hooks: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit on first failure. 4 | set -e 5 | 6 | # Exit on unset variable. 7 | set -u 8 | 9 | # Echo commands before executing them, by default to stderr. 10 | set -x 11 | 12 | # Change directory to repository root. 13 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 14 | readonly SCRIPT_DIR 15 | cd "${SCRIPT_DIR}/.." 16 | 17 | # If there's an existing symlink, remove it. 18 | if [[ -L .git/hooks ]] 19 | then 20 | rm .git/hooks 21 | fi 22 | 23 | # If it's a regular directory, remove all files. 24 | if [[ -d .git/hooks ]] 25 | then 26 | rm -rf .git/hooks 27 | fi 28 | 29 | ln --symbolic --force ../dev-scripts/git-hooks .git/hooks 30 | -------------------------------------------------------------------------------- /dev-scripts/git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit build script on first failure. 4 | set -e 5 | 6 | # Echo commands to stdout. 7 | set -x 8 | 9 | # Exit on unset variable. 10 | set -u 11 | 12 | ./dev-scripts/run-go-tests 13 | ./dev-scripts/check-go-formatting 14 | -------------------------------------------------------------------------------- /dev-scripts/make-fly-config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Create a fly.toml file to deploy logpaste to fly.io. 4 | 5 | # Exit on first error. 6 | set -e 7 | 8 | # Treat undefined environment variables as errors. 9 | set -u 10 | 11 | print_help() { 12 | cat << EOF 13 | Usage: ${0##*/} [-h] app_name 14 | Creates a fly.toml config file. 15 | app_name: Name of fly.io instance to use. 16 | -h Display this help and exit. 17 | EOF 18 | } 19 | 20 | # Parse command-line arguments. 21 | while getopts 'h' opt; do 22 | case "${opt}" in 23 | h) 24 | print_help 25 | exit 26 | ;; 27 | *) 28 | print_help >&2 29 | exit 1 30 | esac 31 | done 32 | 33 | # Ensure 'app_name' is given. 34 | shift $((OPTIND - 1)) 35 | if (( $# == 0 )); then 36 | echo 'Input parameter missing: app_name' >&2 37 | exit 1 38 | fi 39 | readonly APP_NAME="$1" 40 | 41 | cat > fly.toml < /dev/null && pwd )" 7 | readonly SCRIPT_DIR 8 | cd "${SCRIPT_DIR}/.." 9 | 10 | readonly DB_PATH="data/store.db" 11 | 12 | rm ${DB_PATH}* || true 13 | -------------------------------------------------------------------------------- /dev-scripts/run-e2e-tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit build script on first failure 4 | set -e 5 | 6 | # Echo commands to stdout. 7 | set -x 8 | 9 | if [ "$1" = "--skip-build" ]; then 10 | EXTRA_FLAGS="" 11 | else 12 | EXTRA_FLAGS="--build" 13 | fi 14 | readonly EXTRA_FLAGS 15 | 16 | # Exit on unset variable. 17 | set -u 18 | 19 | cd e2e 20 | COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 \ 21 | docker-compose up \ 22 | --exit-code-from cypress \ 23 | --abort-on-container-exit \ 24 | ${EXTRA_FLAGS} 25 | -------------------------------------------------------------------------------- /dev-scripts/run-go-tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Runs all unit tests and performs static code analysis. 4 | # 5 | # Options: 6 | # 7 | # --full Include slower, more exhaustive tests and capture test coverage 8 | # results (outputs to .coverage.html). 9 | 10 | # Exit on first failure. 11 | set -e 12 | 13 | # Echo commands before executing them, by default to stderr. 14 | set -x 15 | 16 | # Fail when piped commands fail. 17 | set -o pipefail 18 | 19 | full_test="" 20 | # Without netgo and osusergo, compilation fails under Nix. 21 | go_test_flags=("-tags=netgo,osusergo") 22 | readonly COVERAGE_FILE_RAW=".coverage.out" 23 | readonly COVERAGE_FILE_HTML=".coverage.html" 24 | if [[ "$1" = "--full" ]]; then 25 | full_test="1" 26 | go_test_flags+=("-v") 27 | go_test_flags+=("-race") 28 | go_test_flags+=("--coverprofile=${COVERAGE_FILE_RAW}") 29 | fi 30 | 31 | # Exit on unset variable. 32 | set -u 33 | 34 | # Change directory to repository root. 35 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 36 | readonly SCRIPT_DIR 37 | cd "${SCRIPT_DIR}/.." 38 | 39 | go test "${go_test_flags[@]}" ./... 40 | if [[ -n "$full_test" ]]; then 41 | go tool cover -html "${COVERAGE_FILE_RAW}" -o "${COVERAGE_FILE_HTML}" 42 | fi 43 | 44 | go vet ./... 45 | 46 | # Install staticcheck if it's not present. 47 | STATICCHECK_PATH="$(go env GOPATH)/bin/staticcheck" 48 | readonly STATICCHECK_PATH 49 | readonly STATICCHECK_VERSION="v0.4.6" 50 | if [[ ! -f "${STATICCHECK_PATH}" ]]; then 51 | go install \ 52 | -ldflags=-linkmode=external \ 53 | "honnef.co/go/tools/cmd/staticcheck@${STATICCHECK_VERSION}" 54 | fi 55 | "${STATICCHECK_PATH}" ./... 56 | 57 | # Install errcheck if it's not present. 58 | ERRCHECK_PATH="$(go env GOPATH)/bin/errcheck" 59 | readonly ERRCHECK_PATH 60 | readonly ERRCHECK_VERSION="v1.6.2" 61 | if [[ ! -f "${ERRCHECK_PATH}" ]]; then 62 | go install \ 63 | -ldflags=-linkmode=external \ 64 | "github.com/kisielk/errcheck@${ERRCHECK_VERSION}" 65 | fi 66 | "${ERRCHECK_PATH}" -ignoretests ./... 67 | -------------------------------------------------------------------------------- /dev-scripts/serve: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit on first failing command. 4 | set -e 5 | 6 | # Exit on unset variable. 7 | set -u 8 | 9 | # Echo commands 10 | set -x 11 | 12 | # Change directory to repository root. 13 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 14 | readonly SCRIPT_DIR 15 | cd "${SCRIPT_DIR}/.." 16 | 17 | # Install modd if it's not present. 18 | MODD_PATH="$(go env GOPATH)/bin/modd" 19 | readonly MODD_PATH 20 | if [[ ! -f "${MODD_PATH}" ]]; then 21 | go get github.com/cortesi/modd/cmd/modd 22 | go install github.com/cortesi/modd/cmd/modd 23 | fi 24 | 25 | # Run modd for hot reloading. 26 | "${MODD_PATH}" 27 | -------------------------------------------------------------------------------- /dev-scripts/upload-prod-db: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | # Change directory to repository root. 6 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 7 | readonly SCRIPT_DIR 8 | cd "${SCRIPT_DIR}/.." 9 | 10 | export DB_PATH="$1" 11 | 12 | set -u 13 | 14 | set +x 15 | # shellcheck disable=SC1091 16 | . .env.prod 17 | set -x 18 | 19 | export LITESTREAM_REGION 20 | export DB_REPLICA_URL 21 | export LITESTREAM_ACCESS_KEY_ID 22 | export LITESTREAM_SECRET_ACCESS_KEY 23 | 24 | if [[ -z "${DB_PATH}" ]]; then 25 | echo "usage: upload-prod-db [db_path]" && exit 1 26 | fi 27 | 28 | read -r -p 'Really overwrite prod database? (y/N): ' choice 29 | 30 | echo "Choice is ${choice}" 31 | 32 | if [[ $choice != "y" ]]; then 33 | echo "Upload aborted" 34 | exit 1 35 | fi 36 | 37 | flyctl scale count 0 --yes --app "${FLY_APP_NAME}" 38 | 39 | echo "Replacing prod database" 40 | 41 | litestream replicate -config litestream.yml -exec "sleep 30" 42 | 43 | flyctl scale count 1 --yes --app "${FLY_APP_NAME}" 44 | -------------------------------------------------------------------------------- /docker-entrypoint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit build script on first failure. 4 | set -e 5 | 6 | # Exit on unset variable. 7 | set -u 8 | 9 | is_litestream_enabled() { 10 | set +ux 11 | 12 | local IS_ENABLED='false' 13 | 14 | if [[ -n "${DB_REPLICA_URL}" ]]; then 15 | IS_ENABLED='true'; 16 | fi 17 | 18 | set -ux 19 | 20 | echo "${IS_ENABLED}" 21 | } 22 | 23 | IS_LITESTREAM_ENABLED="$(is_litestream_enabled)" 24 | readonly IS_LITESTREAM_ENABLED 25 | 26 | # Echo commands to stdout. 27 | set -x 28 | 29 | 30 | # Ensure that arguments stay quoted. 31 | LP_LAUNCH_CMD="/app/logpaste" 32 | for arg in "$@"; do 33 | LP_LAUNCH_CMD="${LP_LAUNCH_CMD} $(printf '%q' "$arg")" 34 | done 35 | 36 | if [[ "${IS_LITESTREAM_ENABLED}" == 'true' ]]; then 37 | /app/litestream version 38 | echo "DB_REPLICA_URL=${DB_REPLICA_URL}" 39 | 40 | readonly DB_PATH='/app/data/store.db' 41 | # We need to export DB_PATH because litestream.yml references it. 42 | export DB_PATH 43 | 44 | if [[ -f "$DB_PATH" ]]; then 45 | echo "Existing database is $(stat -c %s "${DB_PATH}") bytes" 46 | else 47 | echo "No existing database found" 48 | # Restore database from remote storage. 49 | /app/litestream restore -if-replica-exists -v "${DB_PATH}" 50 | fi 51 | 52 | # Let Litestream start LogPaste as a child process 53 | /app/litestream replicate \ 54 | -exec "${LP_LAUNCH_CMD}" 55 | else 56 | echo "Starting without litestream" 57 | 58 | # Start server. 59 | eval "exec ${LP_LAUNCH_CMD}" 60 | fi 61 | -------------------------------------------------------------------------------- /docs/deployment/cloud-run.md: -------------------------------------------------------------------------------- 1 | # Deploy LogPaste to Google Cloud Run 2 | 3 | It's possible to run LogPaste as a microservice on Google Cloud Run, Google's platform for launching Docker containers on-demand. 4 | 5 | Google Cloud Run launches your LogPaste instance in response to HTTP requests and then shuts it down during inactivity. This minimizes hosting costs, as you only pay for the time that your instance is running. 6 | 7 | Another benefit of running LogPaste on Google Cloud Run is that you don't need to configure credentials. By default, Cloud Run executes LogPaste in a context that has write access to Google Cloud Storage for persistent storage, so LogPaste can read and write data without accruing bandwidth fees or managing credentials for an external service. 8 | 9 | ## Requirements 10 | 11 | You'll need: 12 | 13 | - A Google Cloud Platform account 14 | - [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) 15 | - Docker 16 | 17 | ## Set your environment variables 18 | 19 | To begin, create environment variables for your GCP settings: 20 | 21 | ```bash 22 | GCP_PROJECT="your-gcp-project" # Replace with your GCP project ID 23 | GCS_BUCKET="bucketname" # Replace with your bucket name (you can create it later) 24 | GCP_REGION="us-east1" # Replace with your desired GCP region 25 | ``` 26 | 27 | ### Authenticate 28 | 29 | To use the Google Cloud SDK, you need to authenticate gcloud and configure docker to push to Google Container Registry: 30 | 31 | ```bash 32 | gcloud auth login && \ 33 | gcloud auth configure-docker 34 | ``` 35 | 36 | ### Specify GCP Project 37 | 38 | Next, configure gcloud to remember your GCP project: 39 | 40 | ```bash 41 | gcloud config set project "${GCP_PROJECT}" 42 | ``` 43 | 44 | ### Enable services 45 | 46 | Your GCP project will need the [Cloud Run API](https://cloud.google.com/run/docs/reference/rest) enabled, so ensure this API is activated in your project: 47 | 48 | ```bash 49 | gcloud services enable run.googleapis.com 50 | ``` 51 | 52 | ### Create a Google Cloud Storage bucket 53 | 54 | If you haven't already created the GCS bucket to provide LogPaste's persistent storage, create it with the command below: 55 | 56 | ```bash 57 | gsutil mb "gs://${GCS_BUCKET}" 58 | ``` 59 | 60 | ## Push to Google Container Registry 61 | 62 | Cloud Run can't run Docker images from external image repositories, so you'll need to copy the official LogPaste image to Google Container Registry: 63 | 64 | ```bash 65 | LOGPASTE_VERSION="mtlynch/logpaste:0.2.5" # LogPaste version on DockerHub 66 | LOGPASTE_GCR_IMAGE_NAME="logpaste" # Change to whatever name you prefer 67 | LOGPASTE_GCR_URL="gcr.io/${GCP_PROJECT}/${LOGPASTE_GCR_IMAGE_NAME}" 68 | ``` 69 | 70 | ```bash 71 | docker pull "${LOGPASTE_VERSION}" && \ 72 | docker tag "${LOGPASTE_VERSION}" "${LOGPASTE_GCR_URL}" && \ 73 | docker push "${LOGPASTE_GCR_URL}" 74 | ``` 75 | 76 | ## Deploy 77 | 78 | Finally, it's time to deploy your image to Cloud Run: 79 | 80 | ```bash 81 | # You can leave this as-is or change to a different name. 82 | GCR_SERVICE_NAME="logpaste" 83 | 84 | # Limit to a single instance, as multiple workers will generate sync 85 | # conflicts with the data store. 86 | MAX_INSTANCES="1" 87 | 88 | gcloud beta run deploy \ 89 | "${GCR_SERVICE_NAME}" \ 90 | --image "${LOGPASTE_GCR_URL}" \ 91 | --set-env-vars "DB_REPLICA_URL=gcs://${GCS_BUCKET}/db LP_BEHIND_PROXY=y" \ 92 | --allow-unauthenticated \ 93 | --region "${GCP_REGION}" \ 94 | --execution-environment gen2 \ 95 | --no-cpu-throttling \ 96 | --max-instances "${MAX_INSTANCES}" 97 | ``` 98 | 99 | If the deploy is successful, you'll see a message like the following: 100 | 101 | ```text 102 | Service [logpaste] revision [logpaste-00002-cos] has been deployed and is serving 100 percent of traffic. 103 | Service URL: https://logpaste-abc123-ue.a.run.app 104 | ``` 105 | 106 | Your LogPaste instance will serve at the URL listed after to "Service URL." 107 | 108 | If you review your GCP logs, you'll see that the LogPaste server terminates within a few minutes of each HTTP request. This is normal. LogPaste persists all of its data in Google Cloud Storage before it shuts down, and it will start up again on the next HTTP request it receives with all the same data. 109 | 110 | ## Run as service account (optional) 111 | 112 | The instructions above launch LogPaste a Cloud Run service under the Default Compute Service credentials. You can improve security by running LogPaste under a service account with a stricter set of permissions: 113 | 114 | ```bash 115 | SERVICE_ACCOUNT_NAME="logpaste" 116 | SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${GCP_PROJECT}.iam.gserviceaccount.com" 117 | 118 | # Create a new service account. 119 | gcloud iam service-accounts create \ 120 | "${GCR_SERVICE_NAME}" \ 121 | --description="LogPaste microservice, which can write to Cloud Storage" 122 | 123 | # Give service account write access to GCS. 124 | gcloud projects add-iam-policy-binding \ 125 | "${GCP_PROJECT}" \ 126 | --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \ 127 | --role="roles/storage.admin" 128 | 129 | # Update LogPaste Cloud Run service to use your service account. 130 | gcloud run services update "${GCR_SERVICE_NAME}" \ 131 | --service-account "${SERVICE_ACCOUNT_EMAIL}" \ 132 | --region "${GCP_REGION}" 133 | ``` 134 | 135 | You may receive this error after executing `gcloud run services`: 136 | 137 | ```text 138 | cannot fetch generations: googleapi: Error 403: logpaste@yourproject.iam.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket., forbidden 139 | ``` 140 | 141 | On GCP, IAM permissions sometimes take a few minutes to go into effect. Wait 2-3 minutes and try running the `gcloud run services update` command again. 142 | 143 | ## Set custom domain (optional) 144 | 145 | If you have a domain name you'd like to use (e.g., `logpaste.example.com`) instead of Cloud Run's auto-generated URL, you can assign it to your cloud service. 146 | 147 | First, you'll need to [verify to GCP that you own the domain](https://cloud.google.com/run/docs/mapping-custom-domains#command-line). Then, create a mapping from the domain to your LogPaste instance: 148 | 149 | ```bash 150 | CUSTOM_DOMAIN="logpaste.example.com" # Replace with your domain name 151 | ``` 152 | 153 | ```bash 154 | gcloud beta run domain-mappings create \ 155 | --service "${GCR_SERVICE_NAME}" \ 156 | --domain "${CUSTOM_DOMAIN}" \ 157 | --region "${GCP_REGION}" 158 | ``` 159 | 160 | The command will show you a DNS record to add to your DNS. When you add the record, wait a few minutes for records to propagate and Google Cloud Platform to generate your certificate. When it's done, you'll be able to access the Cloud Run instance from your custom domain. 161 | 162 | ## Acknowledgments 163 | 164 | Thanks to [Steren Giannini](https://github.com/steren) from the Google Cloud Run team for his help with these instructions. 165 | -------------------------------------------------------------------------------- /docs/deployment/fly.io.md: -------------------------------------------------------------------------------- 1 | # Deploy LogPaste to fly.io 2 | 3 | fly.io is the best host I've found for LogPaste. You can run up to three instances 24/7 for a month under their free tier, which includes free SSL certificates. 4 | 5 | ## Pre-requisites 6 | 7 | You'll need: 8 | 9 | - A fly.io account (with billing activated) 10 | - The `fly` CLI [already installed](https://fly.io/docs/getting-started/installing-flyctl/) and authenticated on your machine 11 | - A storage bucket and [IAM credentials](https://aws.amazon.com/iam/) on Amazon S3 or an S3-compatible storage service 12 | 13 | ## Set your environment variables 14 | 15 | To begin, create environment variables for your AWS settings: 16 | 17 | ```bash 18 | LITESTREAM_ACCESS_KEY_ID=YOUR-ACCESS-ID 19 | LITESTREAM_SECRET_ACCESS_KEY=YOUR-SECRET-ACCESS-KEY 20 | LITESTREAM_REGION=YOUR-REGION 21 | DB_REPLICA_URL=s3://your-bucket-name/db 22 | ``` 23 | 24 | ## Create your app 25 | 26 | Next, create your app on fly.io: 27 | 28 | ```bash 29 | RANDOM_SUFFIX="$(head /dev/urandom | tr -dc 'a-z0-9' | head -c 6 ; echo '')" 30 | APP_NAME="logpaste-${RANDOM_SUFFIX}" 31 | 32 | curl -s -L https://raw.githubusercontent.com/mtlynch/logpaste/master/dev-scripts/make-fly-config | \ 33 | bash /dev/stdin "${APP_NAME}" 34 | 35 | fly init "${APP_NAME}" --nowrite 36 | ``` 37 | 38 | ## Save AWS credentials 39 | 40 | Use the `fly secrets set` command to securely save your AWS credentials to your app: 41 | 42 | ```bash 43 | fly secrets set \ 44 | "LITESTREAM_ACCESS_KEY_ID=${LITESTREAM_ACCESS_KEY_ID}" \ 45 | "LITESTREAM_SECRET_ACCESS_KEY=${LITESTREAM_SECRET_ACCESS_KEY}" 46 | ``` 47 | 48 | ## Deploy 49 | 50 | Finally, it's time to deploy your app. 51 | 52 | ```bash 53 | # Change this to the latest Docker image tag 54 | LOGPASTE_IMAGE="mtlynch/logpaste:0.2.5" 55 | 56 | fly deploy \ 57 | --env "LITESTREAM_REGION=${LITESTREAM_REGION}" \ 58 | --env "DB_REPLICA_URL=${DB_REPLICA_URL}" \ 59 | --image "${LOGPASTE_IMAGE}" 60 | 61 | LOGPASTE_URL="https://${APP_NAME}.fly.dev/" 62 | echo "Your LogPaste instance is now ready at: ${LOGPASTE_URL}" 63 | ``` 64 | 65 | ## Testing your instance 66 | 67 | You can test the instance with this command: 68 | 69 | ```bash 70 | echo "hello, new fly.io instance!" | \ 71 | curl -F '_=<-' "${LOGPASTE_URL}" 72 | ``` 73 | -------------------------------------------------------------------------------- /docs/deployment/heroku.md: -------------------------------------------------------------------------------- 1 | # Deploy LogPaste to Heroku 2 | 3 | Heroku is a fantastic match for LogPaste, as you can run it in a free dyno and pay only pennies per month for data storage on S3. 4 | 5 | ## Overview 6 | 7 | You can deploy an on-demand LogPaste instance to Heroku under their free tier. It means that Heroku shuts your app down after a few hours of inactivity, but LogPaste handles this fine because it syncs all of its data to S3. The downside is that the first request to the LogPaste server after inactivity will . Heroku doesn't offer SSL certificates for dynos in its free tier, so you'll have to upgrade to a paid plan if you want a HTTPS URL for your domain. 8 | 9 | ## Pre-requisites 10 | 11 | You'll need: 12 | 13 | - A Heroku account 14 | - The [heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) installed and authenticated on your machine 15 | - Docker installed on your machine 16 | - A storage bucket and [IAM credentials](https://aws.amazon.com/iam/) on Amazon S3 or an S3-compatible storage service. 17 | 18 | ## Set your environment variables 19 | 20 | To begin, create environment variables for your AWS settings: 21 | 22 | ```bash 23 | LITESTREAM_ACCESS_KEY_ID=YOUR-ACCESS-ID 24 | LITESTREAM_SECRET_ACCESS_KEY=YOUR-SECRET-ACCESS-KEY 25 | LITESTREAM_REGION=YOUR-REGION 26 | DB_REPLICA_URL=s3://your-bucket-name/db 27 | ``` 28 | 29 | ## Configure your Heroku app 30 | 31 | First, log in to the Heroku container registry: 32 | 33 | ```bash 34 | heroku container:login 35 | ``` 36 | 37 | Next, create a container-based app: 38 | 39 | ```bash 40 | RANDOM_SUFFIX="$(head /dev/urandom | tr -dc 'a-z0-9' | head -c 6 ; echo '')" 41 | APP_NAME="logpaste-${RANDOM_SUFFIX}" 42 | heroku apps:create "${APP_NAME}" --stack container 43 | ``` 44 | 45 | Assign all the relevant environment variables to your app: 46 | 47 | ```bash 48 | heroku config:set --app "${APP_NAME}" LP_BEHIND_PROXY="y" 49 | heroku config:set --app "${APP_NAME}" LITESTREAM_ACCESS_KEY_ID="${LITESTREAM_ACCESS_KEY_ID}" 50 | heroku config:set --app "${APP_NAME}" LITESTREAM_SECRET_ACCESS_KEY="${LITESTREAM_SECRET_ACCESS_KEY}" 51 | heroku config:set --app "${APP_NAME}" LITESTREAM_REGION="${LITESTREAM_REGION}" 52 | heroku config:set --app "${APP_NAME}" DB_REPLICA_URL="${DB_REPLICA_URL}" 53 | ``` 54 | 55 | ## Deploy 56 | 57 | Finally, deploy your app: 58 | 59 | ```bash 60 | # Change this to the latest Docker image tag 61 | LOGPASTE_IMAGE="mtlynch/logpaste:0.2.5" 62 | 63 | HEROKU_PROCESS_TYPE="web" 64 | HEROKU_IMAGE_PATH="registry.heroku.com/${APP_NAME}/${HEROKU_PROCESS_TYPE}" 65 | docker pull "${LOGPASTE_IMAGE}" && \ 66 | docker tag "${LOGPASTE_IMAGE}" "${HEROKU_IMAGE_PATH}" && \ 67 | docker push "${HEROKU_IMAGE_PATH}" && \ 68 | heroku container:release "${HEROKU_PROCESS_TYPE}" --app "${APP_NAME}" && \ 69 | echo "Your app is running at http://${APP_NAME}.herokuapp.com" 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/deployment/lightsail-images/container-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtlynch/logpaste/20750ec29e52502af449f5d462d51ac864738104/docs/deployment/lightsail-images/container-config.png -------------------------------------------------------------------------------- /docs/deployment/lightsail-images/container-pending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtlynch/logpaste/20750ec29e52502af449f5d462d51ac864738104/docs/deployment/lightsail-images/container-pending.png -------------------------------------------------------------------------------- /docs/deployment/lightsail-images/container-running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtlynch/logpaste/20750ec29e52502af449f5d462d51ac864738104/docs/deployment/lightsail-images/container-running.png -------------------------------------------------------------------------------- /docs/deployment/lightsail-images/create-container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtlynch/logpaste/20750ec29e52502af449f5d462d51ac864738104/docs/deployment/lightsail-images/create-container.png -------------------------------------------------------------------------------- /docs/deployment/lightsail-images/create-service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtlynch/logpaste/20750ec29e52502af449f5d462d51ac864738104/docs/deployment/lightsail-images/create-service.png -------------------------------------------------------------------------------- /docs/deployment/lightsail-images/identify-service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtlynch/logpaste/20750ec29e52502af449f5d462d51ac864738104/docs/deployment/lightsail-images/identify-service.png -------------------------------------------------------------------------------- /docs/deployment/lightsail-images/nano-1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtlynch/logpaste/20750ec29e52502af449f5d462d51ac864738104/docs/deployment/lightsail-images/nano-1x.png -------------------------------------------------------------------------------- /docs/deployment/lightsail-images/public-domain-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtlynch/logpaste/20750ec29e52502af449f5d462d51ac864738104/docs/deployment/lightsail-images/public-domain-url.png -------------------------------------------------------------------------------- /docs/deployment/lightsail-images/public-endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtlynch/logpaste/20750ec29e52502af449f5d462d51ac864738104/docs/deployment/lightsail-images/public-endpoint.png -------------------------------------------------------------------------------- /docs/deployment/lightsail-images/set-up-deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtlynch/logpaste/20750ec29e52502af449f5d462d51ac864738104/docs/deployment/lightsail-images/set-up-deployment.png -------------------------------------------------------------------------------- /docs/deployment/lightsail-images/view-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtlynch/logpaste/20750ec29e52502af449f5d462d51ac864738104/docs/deployment/lightsail-images/view-logs.png -------------------------------------------------------------------------------- /docs/deployment/lightsail.md: -------------------------------------------------------------------------------- 1 | ## Deploy LogPaste to Amazon LightSail 2 | 3 | Amazon LightSail is an attractive option for launching LogPaste to production for two main reasons: 4 | 5 | - You can deploy entirely from Amazon's AWS dashboard, so you don't need to install any software. 6 | - You can view LogPaste's server logs directly from your AWS dashboard. 7 | - LightSail offers free TLS certificates for a custom domain. 8 | 9 | The downside is that it doesn't fit within AWS's free tier, so hosting will cost $7 per month, as of this writing. 10 | 11 | ## Pre-requisites 12 | 13 | You'll need: 14 | 15 | - An Amazon AWS account 16 | - A storage bucket and [IAM credentials](https://aws.amazon.com/iam/) on Amazon S3 or an S3-compatible storage service. 17 | 18 | ## Create a LogPaste container 19 | 20 | Visit [Amazon LightSail's container management dashboard](https://lightsail.aws.amazon.com/ls/webapp/home/containers) 21 | 22 | 23 | 24 | ![LightSail management screen](lightsail-images/create-container.png) 25 | 26 | 27 | 28 | Choose a Nano server with 1x scale. 29 | 30 | 31 | 32 | ![LightSail server capacity screen](lightsail-images/nano-1x.png) 33 | 34 | 35 | 36 | Click "Set up deployment" and then choose "Specify a custom deployment." 37 | 38 | 39 | 40 | ![Screenshot showing where to click specify custom deployment](lightsail-images/set-up-deployment.png) 41 | 42 | 43 | 44 | Enter the following information for your custom deployment: 45 | 46 | - Container name: Choose whatever name you want, such as `yourcompany-logpaste` 47 | - Image: `mtlynch/logpaste:0.2.5` (or whatever the [latest LogPaste version](https://github.com/mtlynch/logpaste/releases) is) 48 | - Launch command: _leave blank_ 49 | - Environment variables 50 | 51 | | Key | Value | 52 | | ------------------------------ | --------------------------------------------------- | 53 | | `PORT` | 3001 | 54 | | `LP_BEHIND_PROXY` | "y" | 55 | | `DB_REPLICA_URL` | The S3 URL of your S3 bucket | 56 | | `LITESTREAM_REGION` | The region of your S3 bucket | 57 | | `LITESTREAM_ACCESS_KEY_ID` | The AWS access key ID from your IAM credentials | 58 | | `LITESTREAM_SECRET_ACCESS_KEY` | The AWS secret access key from your IAM credentials | 59 | 60 | - Open ports: Choose port `3001`, protocol `HTTP` 61 | 62 | 63 | 64 | ![Screenshot showing custom values for LightSail container](lightsail-images/container-config.png) 65 | 66 | 67 | 68 | Under "Public Endpoint," select the container you created above: 69 | 70 | 71 | 72 | ![Screenshot showing "contoso-logpaste" in dropdown menu](lightsail-images/public-endpoint.png) 73 | 74 | 75 | 76 | Choose a name for your service (it can be the same as your container name): 77 | 78 | 79 | 80 | ![Screenshot showing "contoso-logpaste" as name for service](lightsail-images/identify-service.png) 81 | 82 | 83 | 84 | Finally, click "Create container service." 85 | 86 | 87 | 88 | ![Screenshot showing "Create container service" button](lightsail-images/create-service.png) 89 | 90 | 91 | 92 | ## Completing deployment 93 | 94 | As LightSail deploys your image, you'll see a status of "Pending" for the container. 95 | 96 | 97 | 98 | ![Screenshot showing LightSail in the process of deploying a container at the "Pending" stage](lightsail-images/container-pending.png) 99 | 100 | 101 | 102 | It will take LightSail about three to five minutes to deploy your instance for the first time. When deployment is complete, it will show a status of "Running." 103 | 104 | 105 | 106 | ![Screenshot showing LightSail deployment complete](lightsail-images/container-running.png) 107 | 108 | 109 | 110 | When your container is running, you can access it through the "Public domain" URL. 111 | 112 | 113 | 114 | ![Screenshot showing how to access container's public domain URL](lightsail-images/public-domain-url.png) 115 | 116 | 117 | 118 | You can view server logs for your LogPaste instance by clicking "Open log" in the box next to your container. 119 | 120 | 121 | 122 | ![Screenshot showing LightSail logs in web dashboard](lightsail-images/view-logs.png) 123 | 124 | 125 | -------------------------------------------------------------------------------- /e2e/.gitignore: -------------------------------------------------------------------------------- 1 | # Cypress artifacts 2 | cypress/screenshots/ 3 | cypress/videos/ 4 | -------------------------------------------------------------------------------- /e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": false, 3 | "supportFile": false, 4 | "defaultCommandTimeout": 10000, 5 | "videoCompression": false 6 | } 7 | -------------------------------------------------------------------------------- /e2e/cypress/integration/paste_spec.js: -------------------------------------------------------------------------------- 1 | it("pastes an entry", () => { 2 | cy.visit("/"); 3 | 4 | cy.get("#upload-textarea").type("test upload data"); 5 | cy.get("#upload").click(); 6 | cy.get("#result a").should("not.be.empty"); 7 | cy.get("#result a") 8 | .invoke("attr", "href") 9 | .then((href) => { 10 | cy.log(href); 11 | cy.request(href).its("body").should("equal", "test upload data"); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /e2e/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | services: 3 | test_sftp: 4 | image: emberstack/sftp:build-5.1.66-amd64 5 | volumes: 6 | - ./sftp.json:/app/config/sftp.json:ro 7 | logpaste: 8 | build: 9 | context: ../ 10 | entrypoint: /app/e2e/wait-for-sftp.bash 11 | command: ["test_sftp", "22", "--", "/app/docker-entrypoint"] 12 | environment: 13 | - PORT=3333 14 | - DB_REPLICA_URL="sftp://dummyuser:dummypass@test_sftp" 15 | volumes: 16 | - ./:/app/e2e 17 | depends_on: 18 | - test_sftp 19 | cypress: 20 | image: "mtlynch/cypress:8.1.0-chrome91" 21 | command: ["--browser", "chrome"] 22 | depends_on: 23 | - logpaste 24 | environment: 25 | - CYPRESS_baseUrl=http://logpaste:3333 26 | working_dir: /e2e 27 | volumes: 28 | - ./:/e2e 29 | -------------------------------------------------------------------------------- /e2e/sftp.json: -------------------------------------------------------------------------------- 1 | { 2 | "Global": { 3 | "Chroot": { 4 | "Directory": "%h", 5 | "StartPath": "sftp" 6 | }, 7 | "Directories": ["sftp"] 8 | }, 9 | "Users": [ 10 | { 11 | "Username": "dummyuser", 12 | "Password": "dummypass" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /e2e/wait-for-sftp.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | SFTP_HOST="$1" 6 | shift 7 | SFTP_PORT="$1" 8 | shift 9 | 10 | timeout 15 bash -c "until printf '' 2>>/dev/null >>/dev/tcp/${SFTP_HOST}/${SFTP_PORT}; do sleep 1; done" 11 | 12 | >&2 echo "sftp is up - executing command" 13 | 14 | exec "$@" 15 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1694529238, 9 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "flyctl-nixpkgs": { 22 | "locked": { 23 | "lastModified": 1707099344, 24 | "narHash": "sha256-TtKolIs+Fm3rleTJ3/mCv391c42KILDnZq0f8PxhSc4=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "0a254180b4cad6be45aa46dce896bdb8db5d2930", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "repo": "nixpkgs", 33 | "rev": "0a254180b4cad6be45aa46dce896bdb8db5d2930", 34 | "type": "github" 35 | } 36 | }, 37 | "go-nixpkgs": { 38 | "locked": { 39 | "lastModified": 1694343207, 40 | "narHash": "sha256-jWi7OwFxU5Owi4k2JmiL1sa/OuBCQtpaAesuj5LXC8w=", 41 | "owner": "NixOS", 42 | "repo": "nixpkgs", 43 | "rev": "78058d810644f5ed276804ce7ea9e82d92bee293", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "NixOS", 48 | "repo": "nixpkgs", 49 | "rev": "78058d810644f5ed276804ce7ea9e82d92bee293", 50 | "type": "github" 51 | } 52 | }, 53 | "litestream-nixpkgs": { 54 | "locked": { 55 | "lastModified": 1709905912, 56 | "narHash": "sha256-TofHtnlrOBCxtSZ9nnlsTybDnQXUmQrlIleXF1RQAwQ=", 57 | "owner": "NixOS", 58 | "repo": "nixpkgs", 59 | "rev": "a343533bccc62400e8a9560423486a3b6c11a23b", 60 | "type": "github" 61 | }, 62 | "original": { 63 | "owner": "NixOS", 64 | "repo": "nixpkgs", 65 | "rev": "a343533bccc62400e8a9560423486a3b6c11a23b", 66 | "type": "github" 67 | } 68 | }, 69 | "nodejs-nixpkgs": { 70 | "locked": { 71 | "lastModified": 1694343207, 72 | "narHash": "sha256-jWi7OwFxU5Owi4k2JmiL1sa/OuBCQtpaAesuj5LXC8w=", 73 | "owner": "NixOS", 74 | "repo": "nixpkgs", 75 | "rev": "78058d810644f5ed276804ce7ea9e82d92bee293", 76 | "type": "github" 77 | }, 78 | "original": { 79 | "owner": "NixOS", 80 | "repo": "nixpkgs", 81 | "rev": "78058d810644f5ed276804ce7ea9e82d92bee293", 82 | "type": "github" 83 | } 84 | }, 85 | "root": { 86 | "inputs": { 87 | "flake-utils": "flake-utils", 88 | "flyctl-nixpkgs": "flyctl-nixpkgs", 89 | "go-nixpkgs": "go-nixpkgs", 90 | "litestream-nixpkgs": "litestream-nixpkgs", 91 | "nodejs-nixpkgs": "nodejs-nixpkgs", 92 | "shellcheck-nixpkgs": "shellcheck-nixpkgs", 93 | "sqlfluff-nixpkgs": "sqlfluff-nixpkgs", 94 | "sqlite-nixpkgs": "sqlite-nixpkgs" 95 | } 96 | }, 97 | "shellcheck-nixpkgs": { 98 | "locked": { 99 | "lastModified": 1695132891, 100 | "narHash": "sha256-cJR9AFHmt816cW/C9necLJyOg/gsnkvEeFAfxgeM1hc=", 101 | "owner": "NixOS", 102 | "repo": "nixpkgs", 103 | "rev": "8b5ab8341e33322e5b66fb46ce23d724050f6606", 104 | "type": "github" 105 | }, 106 | "original": { 107 | "owner": "NixOS", 108 | "repo": "nixpkgs", 109 | "rev": "8b5ab8341e33322e5b66fb46ce23d724050f6606", 110 | "type": "github" 111 | } 112 | }, 113 | "sqlfluff-nixpkgs": { 114 | "locked": { 115 | "lastModified": 1660687964, 116 | "narHash": "sha256-8dJ8u/SUlJoHJVPFnFqoo7ugLH4RxyuyQTeuE6/EynE=", 117 | "owner": "NixOS", 118 | "repo": "nixpkgs", 119 | "rev": "7cf5ccf1cdb2ba5f08f0ac29fc3d04b0b59a07e4", 120 | "type": "github" 121 | }, 122 | "original": { 123 | "owner": "NixOS", 124 | "repo": "nixpkgs", 125 | "rev": "7cf5ccf1cdb2ba5f08f0ac29fc3d04b0b59a07e4", 126 | "type": "github" 127 | } 128 | }, 129 | "sqlite-nixpkgs": { 130 | "locked": { 131 | "lastModified": 1706683685, 132 | "narHash": "sha256-FtPPshEpxH/ewBOsdKBNhlsL2MLEFv1hEnQ19f/bFsQ=", 133 | "owner": "NixOS", 134 | "repo": "nixpkgs", 135 | "rev": "5ad9903c16126a7d949101687af0aa589b1d7d3d", 136 | "type": "github" 137 | }, 138 | "original": { 139 | "owner": "NixOS", 140 | "repo": "nixpkgs", 141 | "rev": "5ad9903c16126a7d949101687af0aa589b1d7d3d", 142 | "type": "github" 143 | } 144 | }, 145 | "systems": { 146 | "locked": { 147 | "lastModified": 1681028828, 148 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 149 | "owner": "nix-systems", 150 | "repo": "default", 151 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 152 | "type": "github" 153 | }, 154 | "original": { 155 | "owner": "nix-systems", 156 | "repo": "default", 157 | "type": "github" 158 | } 159 | } 160 | }, 161 | "root": "root", 162 | "version": 7 163 | } 164 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Dev environment for LogPaste"; 3 | 4 | inputs = { 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | 7 | # 1.21.1 release 8 | go-nixpkgs.url = "github:NixOS/nixpkgs/78058d810644f5ed276804ce7ea9e82d92bee293"; 9 | 10 | # 3.44.2 release 11 | sqlite-nixpkgs.url = "github:NixOS/nixpkgs/5ad9903c16126a7d949101687af0aa589b1d7d3d"; 12 | 13 | # 20.6.1 release 14 | nodejs-nixpkgs.url = "github:NixOS/nixpkgs/78058d810644f5ed276804ce7ea9e82d92bee293"; 15 | 16 | # 0.9.0 release 17 | shellcheck-nixpkgs.url = "github:NixOS/nixpkgs/8b5ab8341e33322e5b66fb46ce23d724050f6606"; 18 | 19 | # 1.2.1 release 20 | sqlfluff-nixpkgs.url = "github:NixOS/nixpkgs/7cf5ccf1cdb2ba5f08f0ac29fc3d04b0b59a07e4"; 21 | 22 | # 0.1.147 release 23 | flyctl-nixpkgs.url = "github:NixOS/nixpkgs/0a254180b4cad6be45aa46dce896bdb8db5d2930"; 24 | 25 | # 0.3.13 release 26 | litestream-nixpkgs.url = "github:NixOS/nixpkgs/a343533bccc62400e8a9560423486a3b6c11a23b"; 27 | }; 28 | 29 | outputs = { 30 | self, 31 | flake-utils, 32 | go-nixpkgs, 33 | sqlite-nixpkgs, 34 | nodejs-nixpkgs, 35 | shellcheck-nixpkgs, 36 | sqlfluff-nixpkgs, 37 | flyctl-nixpkgs, 38 | litestream-nixpkgs, 39 | } @ inputs: 40 | flake-utils.lib.eachDefaultSystem (system: let 41 | gopkg = go-nixpkgs.legacyPackages.${system}; 42 | go = gopkg.go_1_21; 43 | sqlite = sqlite-nixpkgs.legacyPackages.${system}.sqlite; 44 | nodejs = nodejs-nixpkgs.legacyPackages.${system}.nodejs_20; 45 | shellcheck = shellcheck-nixpkgs.legacyPackages.${system}.shellcheck; 46 | sqlfluff = sqlfluff-nixpkgs.legacyPackages.${system}.sqlfluff; 47 | flyctl = flyctl-nixpkgs.legacyPackages.${system}.flyctl; 48 | litestream = litestream-nixpkgs.legacyPackages.${system}.litestream; 49 | in { 50 | devShells.default = gopkg.mkShell.override { stdenv = gopkg.pkgsStatic.stdenv; } { 51 | packages = [ 52 | gopkg.gotools 53 | gopkg.gopls 54 | gopkg.go-outline 55 | gopkg.gocode 56 | gopkg.gopkgs 57 | gopkg.gocode-gomod 58 | gopkg.godef 59 | gopkg.golint 60 | go 61 | sqlite 62 | nodejs 63 | shellcheck 64 | sqlfluff 65 | flyctl 66 | litestream 67 | ]; 68 | 69 | shellHook = '' 70 | export GOROOT="${go}/share/go" 71 | 72 | echo "shellcheck" "$(shellcheck --version | grep '^version:')" 73 | sqlfluff --version 74 | echo "litestream" "$(litestream version)" 75 | fly version | cut -d ' ' -f 1-3 76 | echo "node" "$(node --version)" 77 | echo "npm" "$(npm --version)" 78 | echo "sqlite" "$(sqlite3 --version | cut -d ' ' -f 1-2)" 79 | go version 80 | ''; 81 | }; 82 | 83 | formatter = gopkg.alejandra; 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mtlynch/logpaste 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/gorilla/mux v1.8.0 7 | github.com/mattn/go-sqlite3 v1.14.6 8 | github.com/mtlynch/gorilla-handlers v1.5.2 9 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba 10 | ) 11 | 12 | require github.com/felixge/httpsnoop v1.0.1 // indirect 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= 2 | github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 3 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 4 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 5 | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= 6 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 7 | github.com/mtlynch/gorilla-handlers v1.5.2 h1:CnOI7baYzjgdumJMc7Dh192JFxqGsNgRewa7NTqq9Pc= 8 | github.com/mtlynch/gorilla-handlers v1.5.2/go.mod h1:qZmXSCK7pPjN71Pl9ARbQX2ec1t11FI/j8bDS+ZVbmQ= 9 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= 10 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 11 | -------------------------------------------------------------------------------- /handlers/index.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "path" 6 | "text/template" 7 | ) 8 | 9 | const indexFilename = "index.html" 10 | const viewsRootDir = "./views" 11 | 12 | func (s defaultServer) serveIndexPage() http.HandlerFunc { 13 | return func(w http.ResponseWriter, r *http.Request) { 14 | indexTemplate := template.Must(template.New(indexFilename). 15 | ParseFiles(path.Join(viewsRootDir, indexFilename))) 16 | if err := indexTemplate.ExecuteTemplate(w, indexFilename, s.siteProps); err != nil { 17 | http.Error(w, err.Error(), http.StatusInternalServerError) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /handlers/paste.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "mime/multipart" 9 | "net/http" 10 | "os" 11 | "strings" 12 | 13 | "github.com/gorilla/mux" 14 | 15 | "github.com/mtlynch/logpaste/random" 16 | "github.com/mtlynch/logpaste/store" 17 | ) 18 | 19 | type PastePutResponse struct { 20 | Id string `json:"id"` 21 | } 22 | 23 | func (s defaultServer) pasteGet() http.HandlerFunc { 24 | return func(w http.ResponseWriter, r *http.Request) { 25 | id := mux.Vars(r)["id"] 26 | w.Header().Set("Content-Type", "text/plain; charset=UTF-8") 27 | contents, err := s.store.GetEntry(id) 28 | if err != nil { 29 | if _, ok := err.(store.EntryNotFoundError); ok { 30 | http.Error(w, "entry not found", http.StatusNotFound) 31 | return 32 | } 33 | log.Printf("failed to retrieve entry %s from datastore: %v", id, err) 34 | http.Error(w, "failed to retrieve entry", http.StatusInternalServerError) 35 | return 36 | } 37 | if _, err := io.WriteString(w, contents); err != nil { 38 | log.Printf("failed to write response: %v", err) 39 | } 40 | } 41 | } 42 | 43 | func (s defaultServer) pasteOptions() http.HandlerFunc { 44 | return func(w http.ResponseWriter, r *http.Request) { 45 | w.Header().Set("Access-Control-Allow-Origin", "*") 46 | w.Header().Set("Access-Control-Allow-Methods", "GET, PUT") 47 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type") 48 | } 49 | } 50 | 51 | func (s defaultServer) pastePut() http.HandlerFunc { 52 | return func(w http.ResponseWriter, r *http.Request) { 53 | w.Header().Set("Access-Control-Allow-Origin", "*") 54 | 55 | bodyRaw, err := io.ReadAll(http.MaxBytesReader(w, r.Body, s.maxCharLimit)) 56 | if err != nil { 57 | log.Printf("Error reading body: %v", err) 58 | http.Error(w, "can't read request body", http.StatusBadRequest) 59 | return 60 | } 61 | 62 | body := string(bodyRaw) 63 | if !validatePaste(body, w, s.maxCharLimit) { 64 | return 65 | } 66 | 67 | id := generateEntryId() 68 | err = s.store.InsertEntry(id, body) 69 | if err != nil { 70 | log.Printf("failed to save entry: %v", err) 71 | http.Error(w, "can't save entry", http.StatusInternalServerError) 72 | return 73 | } 74 | log.Printf("saved entry of %d characters", len(body)) 75 | 76 | w.Header().Set("Content-Type", "application/json") 77 | resp := PastePutResponse{ 78 | Id: id, 79 | } 80 | if err := json.NewEncoder(w).Encode(resp); err != nil { 81 | panic(err) 82 | } 83 | } 84 | } 85 | 86 | func (s defaultServer) pastePost() http.HandlerFunc { 87 | return func(w http.ResponseWriter, r *http.Request) { 88 | r.Body = http.MaxBytesReader(w, r.Body, s.maxCharLimit+1024) 89 | if err := r.ParseMultipartForm(s.maxCharLimit); err != nil { 90 | log.Printf("failed to parse form: %v", err) 91 | http.Error(w, "no valid multipart/form-data found", http.StatusBadRequest) 92 | return 93 | } 94 | w.Header().Set("Access-Control-Allow-Origin", "*") 95 | 96 | body, ok := parsePasteFromMultipartForm(r.MultipartForm, w) 97 | if !ok { 98 | log.Print("form did not contain any recognizable data") 99 | http.Error(w, "form data or file is required", http.StatusBadRequest) 100 | return 101 | } 102 | 103 | if !validatePaste(body, w, s.maxCharLimit) { 104 | return 105 | } 106 | 107 | id := generateEntryId() 108 | if err := s.store.InsertEntry(id, body); err != nil { 109 | log.Printf("failed to save entry: %v", err) 110 | http.Error(w, "can't save entry", http.StatusInternalServerError) 111 | return 112 | } 113 | log.Printf("saved entry of %d characters", len(body)) 114 | 115 | w.Header().Set("Content-Type", "text/plain") 116 | resultURL := fmt.Sprintf("%s/%s\n", baseURLFromRequest(r), id) 117 | if _, err := w.Write([]byte(resultURL)); err != nil { 118 | log.Printf("failed to write response: %v", err) 119 | } 120 | } 121 | } 122 | 123 | func generateEntryId() string { 124 | return random.String(8) 125 | } 126 | 127 | func validatePaste(p string, w http.ResponseWriter, maxCharLimit int64) bool { 128 | if len(strings.TrimSpace(p)) == 0 { 129 | log.Print("Paste body was empty") 130 | http.Error(w, "empty body", http.StatusBadRequest) 131 | return false 132 | } else if int64(len(p)) > maxCharLimit { 133 | log.Printf("Paste body was too long: %d characters", len(p)) 134 | http.Error(w, "body too long", http.StatusBadRequest) 135 | return false 136 | } 137 | return true 138 | } 139 | 140 | func parsePasteFromMultipartForm(f *multipart.Form, w http.ResponseWriter) (string, bool) { 141 | if content, ok := parsePasteFromMultipartFormValue(f); ok { 142 | return content, true 143 | } 144 | if content, ok := parsePasteFromMultipartFormFile(f, w); ok { 145 | return content, true 146 | } 147 | return "", false 148 | } 149 | 150 | func parsePasteFromMultipartFormValue(f *multipart.Form) (string, bool) { 151 | return anyValueInForm(f) 152 | } 153 | 154 | func anyValueInForm(f *multipart.Form) (string, bool) { 155 | for _, values := range f.Value { 156 | if len(values) < 1 { 157 | log.Printf("form values are empty") 158 | continue 159 | } 160 | return values[0], true 161 | } 162 | return "", false 163 | } 164 | 165 | func parsePasteFromMultipartFormFile(f *multipart.Form, w http.ResponseWriter) (string, bool) { 166 | file, ok := anyFileInForm(f.File) 167 | if !ok { 168 | return "", false 169 | } 170 | 171 | body, err := io.ReadAll(file) 172 | if err != nil { 173 | log.Printf("failed to read form file: %v", err) 174 | return "", false 175 | } 176 | 177 | return string(body), true 178 | } 179 | 180 | func anyFileInForm(formFiles map[string][]*multipart.FileHeader) (multipart.File, bool) { 181 | for _, fileHeaders := range formFiles { 182 | if len(fileHeaders) < 1 { 183 | log.Printf("form files are empty") 184 | continue 185 | } 186 | file, err := fileHeaders[0].Open() 187 | if err != nil { 188 | log.Printf("failed to open form file: %v", err) 189 | return nil, false 190 | } 191 | return file, true 192 | } 193 | return nil, false 194 | } 195 | 196 | func baseURLFromRequest(r *http.Request) string { 197 | var scheme string 198 | // If we're running behind a proxy, assume that it's a TLS proxy. 199 | if r.TLS != nil || os.Getenv("LP_BEHIND_PROXY") != "" { 200 | scheme = "https" 201 | } else { 202 | scheme = "http" 203 | } 204 | return fmt.Sprintf("%s://%s", scheme, r.Host) 205 | } 206 | -------------------------------------------------------------------------------- /handlers/paste_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/gorilla/mux" 13 | "github.com/mtlynch/logpaste/store" 14 | ) 15 | 16 | const MaxPasteCharacters = 2 * 1024 * 1024 17 | 18 | type mockStore struct { 19 | entries map[string]string 20 | } 21 | 22 | func (ds mockStore) GetEntry(id string) (string, error) { 23 | if contents, ok := ds.entries[id]; ok { 24 | return contents, nil 25 | } 26 | return "", store.EntryNotFoundError{ID: id} 27 | } 28 | 29 | func (ds *mockStore) InsertEntry(id string, contents string) error { 30 | ds.entries[id] = contents 31 | return nil 32 | } 33 | 34 | func (ds *mockStore) Reset() { 35 | ds.entries = make(map[string]string) 36 | } 37 | 38 | func TestPasteGet(t *testing.T) { 39 | ds := mockStore{ 40 | entries: map[string]string{ 41 | "12345678": "dummy entry", 42 | }, 43 | } 44 | router := mux.NewRouter() 45 | s := defaultServer{ 46 | store: &ds, 47 | router: router, 48 | maxCharLimit: MaxPasteCharacters, 49 | } 50 | s.routes() 51 | 52 | for _, tt := range []struct { 53 | description string 54 | id string 55 | statusExpected int 56 | contentExpected string 57 | }{ 58 | { 59 | description: "valid entry", 60 | id: "12345678", 61 | statusExpected: http.StatusOK, 62 | contentExpected: "dummy entry", 63 | }, 64 | { 65 | description: "non-existent entry", 66 | id: "missing1", 67 | statusExpected: http.StatusNotFound, 68 | contentExpected: "", 69 | }, 70 | } { 71 | t.Run(tt.description, func(t *testing.T) { 72 | req, err := http.NewRequest("GET", "/"+tt.id, nil) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | w := httptest.NewRecorder() 78 | s.router.ServeHTTP(w, req) 79 | 80 | if got, want := w.Code, tt.statusExpected; got != want { 81 | t.Fatalf("status=%d, want=%d", got, want) 82 | } 83 | if w.Code != http.StatusOK { 84 | return 85 | } 86 | bodyBytes, err := io.ReadAll(w.Body) 87 | if err != nil { 88 | t.Fatalf("failed to read HTTP response body: %v", err) 89 | } 90 | if got, want := string(bodyBytes), tt.contentExpected; got != want { 91 | t.Errorf("body=%s, want=%s", got, want) 92 | } 93 | }) 94 | } 95 | } 96 | 97 | func TestPastePut(t *testing.T) { 98 | for _, tt := range []struct { 99 | description string 100 | body string 101 | statusExpected int 102 | }{ 103 | { 104 | description: "valid content", 105 | body: "hello, world!", 106 | statusExpected: http.StatusOK, 107 | }, 108 | { 109 | description: "just at size limit", 110 | body: strings.Repeat("A", MaxPasteCharacters), 111 | statusExpected: http.StatusOK, 112 | }, 113 | { 114 | description: "too long content", 115 | body: strings.Repeat("A", MaxPasteCharacters+1), 116 | statusExpected: http.StatusBadRequest, 117 | }, 118 | { 119 | description: "empty content", 120 | body: "", 121 | statusExpected: http.StatusBadRequest, 122 | }, 123 | { 124 | description: "just whitespace", 125 | body: " ", 126 | statusExpected: http.StatusBadRequest, 127 | }, 128 | } { 129 | t.Run(tt.description, func(t *testing.T) { 130 | ds := mockStore{ 131 | entries: make(map[string]string), 132 | } 133 | router := mux.NewRouter() 134 | s := defaultServer{ 135 | store: &ds, 136 | router: router, 137 | maxCharLimit: MaxPasteCharacters, 138 | } 139 | s.routes() 140 | 141 | ds.Reset() 142 | 143 | req, err := http.NewRequest("PUT", "/", strings.NewReader(tt.body)) 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | 148 | w := httptest.NewRecorder() 149 | s.router.ServeHTTP(w, req) 150 | 151 | if got, want := w.Code, tt.statusExpected; got != want { 152 | t.Fatalf("status=%d, want=%d", got, want) 153 | } 154 | if w.Code != http.StatusOK { 155 | return 156 | } 157 | 158 | var resp PastePutResponse 159 | if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { 160 | t.Fatalf("failed to read HTTP response body: %v", err) 161 | } 162 | storedContents, err := ds.GetEntry(resp.Id) 163 | if err != nil { 164 | t.Fatalf("no entry found with id: %s", resp.Id) 165 | } 166 | if got, want := storedContents, tt.body; got != want { 167 | t.Fatalf("stored=%v, want=%v", got, want) 168 | } 169 | }) 170 | } 171 | } 172 | 173 | func TestPastePost(t *testing.T) { 174 | ds := mockStore{ 175 | entries: make(map[string]string), 176 | } 177 | router := mux.NewRouter() 178 | s := defaultServer{ 179 | store: &ds, 180 | router: router, 181 | maxCharLimit: MaxPasteCharacters, 182 | } 183 | s.routes() 184 | for _, tt := range []struct { 185 | description string 186 | contentType string 187 | body string 188 | statusExpected int 189 | contentsExpected string 190 | }{ 191 | { 192 | description: "reject non-multipart data", 193 | contentType: "text/plain", 194 | body: "hello, world!", 195 | statusExpected: http.StatusBadRequest, 196 | }, 197 | { 198 | description: "rejects empty input", 199 | contentType: "multipart/form-data; boundary=------------------------aea33768a2527972", 200 | body: ` 201 | --------------------------aea33768a2527972 202 | Content-Disposition: form-data; name="dummyname1" 203 | 204 | 205 | 206 | --------------------------aea33768a2527972--`, 207 | statusExpected: http.StatusBadRequest, 208 | }, 209 | { 210 | description: "accepts string data", 211 | contentType: "multipart/form-data; boundary=------------------------aea33768a2527972", 212 | body: ` 213 | --------------------------aea33768a2527972 214 | Content-Disposition: form-data; name="dummyname2" 215 | 216 | some data I want to upload 217 | --------------------------aea33768a2527972--`, 218 | statusExpected: http.StatusOK, 219 | contentsExpected: "some data I want to upload", 220 | }, 221 | { 222 | description: "accepts string data at size limit", 223 | contentType: "multipart/form-data; boundary=------------------------aea33768a2527972", 224 | body: fmt.Sprintf(` 225 | --------------------------aea33768a2527972 226 | Content-Disposition: form-data; name="dummyname2" 227 | 228 | %s 229 | --------------------------aea33768a2527972--`, strings.Repeat("A", MaxPasteCharacters)), 230 | statusExpected: http.StatusOK, 231 | contentsExpected: strings.Repeat("A", MaxPasteCharacters), 232 | }, 233 | { 234 | description: "rejects string data above size limit", 235 | contentType: "multipart/form-data; boundary=------------------------aea33768a2527972", 236 | body: fmt.Sprintf(` 237 | --------------------------aea33768a2527972 238 | Content-Disposition: form-data; name="dummyname2" 239 | 240 | %s 241 | --------------------------aea33768a2527972--`, strings.Repeat("A", MaxPasteCharacters+1)), 242 | statusExpected: http.StatusBadRequest, 243 | }, 244 | { 245 | description: "accepts file data", 246 | contentType: "multipart/form-data; boundary=------------------------ff01448fc0d75457", 247 | body: ` 248 | --------------------------ff01448fc0d75457 249 | Content-Disposition: form-data; name="dummyname3"; filename="text.txt" 250 | Content-Type: text/plain 251 | 252 | some data in a file 253 | --------------------------ff01448fc0d75457--`, 254 | statusExpected: http.StatusOK, 255 | contentsExpected: "some data in a file", 256 | }, 257 | } { 258 | t.Run(tt.description, func(t *testing.T) { 259 | ds.Reset() 260 | 261 | req, err := http.NewRequest("POST", "/", 262 | strings.NewReader(strings.ReplaceAll(tt.body, "\n", "\r\n"))) 263 | if err != nil { 264 | t.Fatal(err) 265 | } 266 | req.Header.Add("Content-Type", tt.contentType) 267 | 268 | w := httptest.NewRecorder() 269 | s.router.ServeHTTP(w, req) 270 | 271 | if got, want := w.Code, tt.statusExpected; got != want { 272 | t.Fatalf("status=%d, want=%d", got, want) 273 | } 274 | if w.Code != http.StatusOK { 275 | return 276 | } 277 | 278 | for _, contents := range ds.entries { 279 | if got, want := contents, tt.contentsExpected; got != want { 280 | t.Fatalf("contents=%s, want=%s", got, want) 281 | } 282 | } 283 | }) 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /handlers/routes.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func notFound() http.HandlerFunc { 8 | return func(w http.ResponseWriter, r *http.Request) { 9 | http.Error(w, "Resource not found", http.StatusNotFound) 10 | } 11 | } 12 | 13 | func (s *defaultServer) routes() { 14 | s.router.HandleFunc("/favicon.ico", notFound()).Methods(http.MethodGet) 15 | s.router.PathPrefix("/static/").Handler(GetStaticFilesHandler()).Methods(http.MethodGet) 16 | s.router.PathPrefix("/{id}").HandlerFunc(s.pasteGet()).Methods(http.MethodGet) 17 | s.router.PathPrefix("/").HandlerFunc(s.pasteOptions()).Methods(http.MethodOptions) 18 | s.router.PathPrefix("/").Handler(s.ipRateLimiter.Limit(s.pastePut())).Methods(http.MethodPut) 19 | s.router.PathPrefix("/").Handler(s.ipRateLimiter.Limit(s.pastePost())).Methods(http.MethodPost) 20 | s.router.PathPrefix("/").HandlerFunc(s.serveIndexPage()).Methods(http.MethodGet) 21 | } 22 | -------------------------------------------------------------------------------- /handlers/server.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gorilla/mux" 5 | 6 | "github.com/mtlynch/logpaste/limit" 7 | "github.com/mtlynch/logpaste/store" 8 | "github.com/mtlynch/logpaste/store/sqlite" 9 | ) 10 | 11 | type Server interface { 12 | Router() *mux.Router 13 | } 14 | 15 | func New(sp SiteProperties, perMinuteLimit int, maxCharLimit int64) Server { 16 | s := defaultServer{ 17 | router: mux.NewRouter(), 18 | store: sqlite.New(), 19 | siteProps: sp, 20 | ipRateLimiter: limit.New(perMinuteLimit), 21 | maxCharLimit: maxCharLimit, 22 | } 23 | s.routes() 24 | return s 25 | } 26 | 27 | type SiteProperties struct { 28 | Title string 29 | Subtitle string 30 | FooterHTML string 31 | ShowDocs bool 32 | } 33 | 34 | type defaultServer struct { 35 | router *mux.Router 36 | store store.Store 37 | siteProps SiteProperties 38 | ipRateLimiter limit.IPRateLimiter 39 | maxCharLimit int64 40 | } 41 | 42 | // Router returns the underlying router interface for the server. 43 | func (s defaultServer) Router() *mux.Router { 44 | return s.router 45 | } 46 | -------------------------------------------------------------------------------- /handlers/static.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "net/http" 7 | ) 8 | 9 | //go:embed static/* 10 | var staticFiles embed.FS 11 | 12 | // CachingFileServer wraps an http.Handler to add caching headers 13 | func CachingFileServer(h http.Handler) http.Handler { 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | // Add cache control headers 16 | w.Header().Set("Cache-Control", "public, max-age=604800") // 1 week 17 | 18 | // Let the original handler serve the file 19 | // The FileServer will handle Last-Modified and If-Modified-Since automatically 20 | h.ServeHTTP(w, r) 21 | }) 22 | } 23 | 24 | // GetStaticFilesHandler returns an http.Handler that serves static files from the embedded filesystem 25 | func GetStaticFilesHandler() http.Handler { 26 | // Get the static subdirectory as a filesystem 27 | staticFS, err := fs.Sub(staticFiles, "static") 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | // Create a file server for the static files 33 | fileServer := http.FileServer(http.FS(staticFS)) 34 | 35 | // Strip the /static/ prefix from the request path before passing to the file server 36 | // This is necessary because the URL path includes /static/ but our embedded filesystem 37 | // already has the files under the "static" directory 38 | handler := http.StripPrefix("/static/", fileServer) 39 | 40 | // Wrap the handler with our caching middleware 41 | return CachingFileServer(handler) 42 | } 43 | -------------------------------------------------------------------------------- /handlers/static/css/dark.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #121212; 3 | color: #f0f0f0; 4 | } 5 | 6 | a { 7 | color: #009e1a; 8 | } 9 | 10 | textarea { 11 | background: #808080; 12 | } 13 | 14 | button { 15 | background-color: #2455a3; 16 | color: #f0f0f0; 17 | border-color: #648ed1; 18 | } 19 | 20 | button:hover { 21 | background-color: #2e66bf; 22 | } 23 | 24 | pre { 25 | background: #424242; 26 | border-color: #c7c7c7; 27 | } 28 | 29 | #upload-textarea { 30 | border-color: #cccccc; 31 | } 32 | 33 | #result { 34 | background-color: #454270; 35 | border-color: #6d68b0; 36 | } 37 | 38 | #form-upload-error { 39 | background-color: #9c2222; 40 | border-color: #cc3f3f; 41 | } 42 | -------------------------------------------------------------------------------- /handlers/static/css/light.css: -------------------------------------------------------------------------------- 1 | button { 2 | color: white; 3 | background-color: #0078e7; 4 | border-color: #082375; 5 | } 6 | 7 | button:hover { 8 | background-color: #009ee7; 9 | } 10 | 11 | #upload-textarea { 12 | border-color: #cccccc; 13 | } 14 | 15 | #result { 16 | background-color: #d6ffd9; 17 | border-color: green; 18 | } 19 | 20 | #form-upload-error { 21 | background-color: #ffa6a6; 22 | border-color: red; 23 | } 24 | -------------------------------------------------------------------------------- /handlers/static/css/style.css: -------------------------------------------------------------------------------- 1 | @import url(light.css) (prefers-color-scheme: light); 2 | @import url(../third-party/prism/light.css) (prefers-color-scheme: light); 3 | 4 | @import url(dark.css) (prefers-color-scheme: dark); 5 | @import url(../third-party/prism/dark.css) (prefers-color-scheme: dark); 6 | 7 | body { 8 | font-family: "Trebuchet MS", Tahoma, Georgia, Garamond; 9 | } 10 | 11 | pre { 12 | border: 1px solid black; 13 | padding: 0.7rem 1rem; 14 | display: inline-block; 15 | margin: 1rem auto; 16 | text-align: left; 17 | font-size: 12pt; 18 | overflow-x: auto; 19 | max-width: 90%; 20 | } 21 | 22 | .container { 23 | max-width: 90%; 24 | margin: 0 auto; 25 | } 26 | 27 | @media screen and (min-width: 768px) { 28 | .container { 29 | max-width: 600px; 30 | margin: 0 auto; 31 | } 32 | } 33 | 34 | .subtitle { 35 | font-size: 1.2em; 36 | margin-bottom: 3rem; 37 | } 38 | 39 | .web-ui-upload { 40 | display: flex; 41 | flex-direction: column; 42 | align-items: flex-start; 43 | } 44 | 45 | .upload-form { 46 | max-width: 100%; 47 | display: flex; 48 | flex-direction: column; 49 | } 50 | 51 | @media screen and (min-width: 768px) { 52 | .upload-form { 53 | width: 400px; 54 | max-width: 400px; 55 | } 56 | } 57 | 58 | button { 59 | cursor: pointer; 60 | display: inline-block; 61 | padding: 0.35em 1.2em; 62 | border-radius: 0.2em; 63 | box-sizing: border-box; 64 | text-align: center; 65 | transition: all 0.2s; 66 | font-size: 1.05em; 67 | border-width: 0.1em; 68 | border-style: solid; 69 | } 70 | 71 | .upload-form { 72 | max-width: 100%; 73 | width: 100%; 74 | } 75 | 76 | @media screen and (min-width: 600px) { 77 | .upload-form { 78 | width: 500px; 79 | } 80 | } 81 | 82 | #upload-textarea { 83 | width: 100%; 84 | max-width: 100%; 85 | height: 100px; 86 | box-sizing: border-box; 87 | -moz-box-sizing: border-box; 88 | -webkit-box-sizing: border-box; 89 | margin: 1rem 0; 90 | padding: 0.5rem; 91 | border-width: 1px; 92 | } 93 | 94 | .button-row { 95 | margin-left: auto; 96 | } 97 | 98 | #result { 99 | visibility: hidden; 100 | padding: 1.5rem 1.75rem; 101 | border-width: 1px; 102 | border-style: solid; 103 | margin: 1rem auto 0 auto; 104 | } 105 | 106 | @media screen and (min-width: 600px) { 107 | #result { 108 | margin: 2rem 0 0 0; 109 | } 110 | } 111 | 112 | #result h3 { 113 | margin: 0 auto 1rem auto; 114 | text-align: center; 115 | } 116 | 117 | #form-upload-error { 118 | visibility: hidden; 119 | padding: 1rem 0.75rem; 120 | border-width: 1px; 121 | border-style: solid; 122 | } 123 | 124 | #more-info { 125 | margin-bottom: 5rem; 126 | } 127 | 128 | .footer { 129 | margin-bottom: 5rem; 130 | } 131 | -------------------------------------------------------------------------------- /handlers/static/js/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Make ESLint happy. 4 | /* global Prism, logpaste */ 5 | 6 | const baseUrl = document.location.origin; 7 | 8 | const curlCmd = document.getElementById("curl-cmd"); 9 | if (curlCmd) { 10 | curlCmd.innerHTML = Prism.highlight( 11 | ` 12 | echo "some text I want to upload" | \\ 13 | curl -F '_=<-' ${baseUrl}`.trim(), 14 | Prism.languages.bash, 15 | "bash", 16 | ); 17 | } 18 | 19 | const curlFileCmd = document.getElementById("curl-file-cmd"); 20 | if (curlFileCmd) { 21 | curlFileCmd.innerHTML = Prism.highlight( 22 | ` 23 | curl -F "_=@/path/to/file.txt" ${baseUrl}`.trim(), 24 | Prism.languages.bash, 25 | "bash", 26 | ); 27 | } 28 | 29 | const jsExample = document.getElementById("js-example"); 30 | if (jsExample) { 31 | jsExample.innerHTML = Prism.highlight( 32 | ` 33 | 34 | 41 | `.trim(), 42 | Prism.languages.javascript, 43 | "javascript", 44 | ); 45 | } 46 | 47 | function displayResult(resultId) { 48 | clearError(); 49 | clearResult(); 50 | 51 | const resultDiv = document.getElementById("result"); 52 | 53 | const resultUrl = `${document.location}${resultId}`; 54 | 55 | const header = document.createElement("h3"); 56 | header.innerText = "Shareable link"; 57 | resultDiv.appendChild(header); 58 | 59 | const anchor = document.createElement("a"); 60 | anchor.href = `/${resultId}`; 61 | anchor.innerText = resultUrl; 62 | resultDiv.appendChild(anchor); 63 | 64 | resultDiv.style.visibility = "visible"; 65 | } 66 | 67 | function clearResult() { 68 | const resultDiv = document.getElementById("result"); 69 | while (resultDiv.firstChild) { 70 | resultDiv.removeChild(resultDiv.lastChild); 71 | } 72 | resultDiv.style.visibility = "hidden"; 73 | } 74 | 75 | function clearError() { 76 | const uploadError = document.getElementById("form-upload-error"); 77 | uploadError.innerText = " "; 78 | uploadError.style.visibility = "hidden"; 79 | } 80 | 81 | function displayError(error) { 82 | const uploadError = document.getElementById("form-upload-error"); 83 | uploadError.innerText = error; 84 | uploadError.style.visibility = "visible"; 85 | } 86 | 87 | document.getElementById("upload").addEventListener("click", () => { 88 | const textToUpload = document.getElementById("upload-textarea").value; 89 | logpaste 90 | .uploadText(textToUpload) 91 | .then((id) => { 92 | displayResult(id); 93 | }) 94 | .catch((error) => { 95 | clearResult(); 96 | displayError(error); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /handlers/static/js/logpaste.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | (function () { 4 | function uploadText(text, baseUrl = "") { 5 | return fetch(baseUrl + "/", { 6 | method: "PUT", 7 | body: text, 8 | }) 9 | .then((response) => { 10 | const contentType = response.headers.get("content-type"); 11 | const isJson = 12 | contentType && contentType.indexOf("application/json") !== -1; 13 | // Success case is an HTTP 200 response and a JSON body. 14 | if (response.status === 200 && isJson) { 15 | return Promise.resolve(response.json()); 16 | } 17 | // Treat any other response as an error. 18 | return response.text().then((text) => { 19 | if (text) { 20 | return Promise.reject(new Error(text)); 21 | } else { 22 | return Promise.reject(new Error(response.statusText)); 23 | } 24 | }); 25 | }) 26 | .then((data) => data.id); 27 | } 28 | if (!window.logpaste) { 29 | window.logpaste = { 30 | uploadText, 31 | }; 32 | } 33 | })(); 34 | -------------------------------------------------------------------------------- /handlers/static/third-party/prism/dark.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism-dark&languages=markup+css+clike+javascript */ 3 | /** 4 | * prism.js Dark theme for JavaScript, CSS and HTML 5 | * Based on the slides of the talk “/Reg(exp){2}lained/” 6 | * @author Lea Verou 7 | */ 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"] { 11 | color: white; 12 | background: none; 13 | text-shadow: 0 -.1em .2em black; 14 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 15 | font-size: 1em; 16 | text-align: left; 17 | white-space: pre; 18 | word-spacing: normal; 19 | word-break: normal; 20 | word-wrap: normal; 21 | line-height: 1.5; 22 | 23 | -moz-tab-size: 4; 24 | -o-tab-size: 4; 25 | tab-size: 4; 26 | 27 | -webkit-hyphens: none; 28 | -moz-hyphens: none; 29 | -ms-hyphens: none; 30 | hyphens: none; 31 | } 32 | 33 | @media print { 34 | code[class*="language-"], 35 | pre[class*="language-"] { 36 | text-shadow: none; 37 | } 38 | } 39 | 40 | pre[class*="language-"], 41 | :not(pre) > code[class*="language-"] { 42 | background: hsl(30, 20%, 25%); 43 | } 44 | 45 | /* Code blocks */ 46 | pre[class*="language-"] { 47 | padding: 1em; 48 | margin: .5em 0; 49 | overflow: auto; 50 | border: .3em solid hsl(30, 20%, 40%); 51 | border-radius: .5em; 52 | box-shadow: 1px 1px .5em black inset; 53 | } 54 | 55 | /* Inline code */ 56 | :not(pre) > code[class*="language-"] { 57 | padding: .15em .2em .05em; 58 | border-radius: .3em; 59 | border: .13em solid hsl(30, 20%, 40%); 60 | box-shadow: 1px 1px .3em -.1em black inset; 61 | white-space: normal; 62 | } 63 | 64 | .token.comment, 65 | .token.prolog, 66 | .token.doctype, 67 | .token.cdata { 68 | color: hsl(30, 20%, 50%); 69 | } 70 | 71 | .token.punctuation { 72 | opacity: .7; 73 | } 74 | 75 | .token.namespace { 76 | opacity: .7; 77 | } 78 | 79 | .token.property, 80 | .token.tag, 81 | .token.boolean, 82 | .token.number, 83 | .token.constant, 84 | .token.symbol { 85 | color: hsl(350, 40%, 70%); 86 | } 87 | 88 | .token.selector, 89 | .token.attr-name, 90 | .token.string, 91 | .token.char, 92 | .token.builtin, 93 | .token.inserted { 94 | color: hsl(75, 70%, 60%); 95 | } 96 | 97 | .token.operator, 98 | .token.entity, 99 | .token.url, 100 | .language-css .token.string, 101 | .style .token.string, 102 | .token.variable { 103 | color: hsl(40, 90%, 60%); 104 | } 105 | 106 | .token.atrule, 107 | .token.attr-value, 108 | .token.keyword { 109 | color: hsl(350, 40%, 70%); 110 | } 111 | 112 | .token.regex, 113 | .token.important { 114 | color: #e90; 115 | } 116 | 117 | .token.important, 118 | .token.bold { 119 | font-weight: bold; 120 | } 121 | .token.italic { 122 | font-style: italic; 123 | } 124 | 125 | .token.entity { 126 | cursor: help; 127 | } 128 | 129 | .token.deleted { 130 | color: red; 131 | } 132 | -------------------------------------------------------------------------------- /handlers/static/third-party/prism/light.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism&languages=clike+javascript+bash */ 3 | /** 4 | * prism.js default theme for JavaScript, CSS and HTML 5 | * Based on dabblet (http://dabblet.com) 6 | * @author Lea Verou 7 | */ 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"] { 11 | color: black; 12 | background: none; 13 | text-shadow: 0 1px white; 14 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 15 | font-size: 1em; 16 | text-align: left; 17 | white-space: pre; 18 | word-spacing: normal; 19 | word-break: normal; 20 | word-wrap: normal; 21 | line-height: 1.5; 22 | 23 | -moz-tab-size: 4; 24 | -o-tab-size: 4; 25 | tab-size: 4; 26 | 27 | -webkit-hyphens: none; 28 | -moz-hyphens: none; 29 | -ms-hyphens: none; 30 | hyphens: none; 31 | } 32 | 33 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 34 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 35 | text-shadow: none; 36 | background: #b3d4fc; 37 | } 38 | 39 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 40 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 41 | text-shadow: none; 42 | background: #b3d4fc; 43 | } 44 | 45 | @media print { 46 | code[class*="language-"], 47 | pre[class*="language-"] { 48 | text-shadow: none; 49 | } 50 | } 51 | 52 | /* Code blocks */ 53 | pre[class*="language-"] { 54 | padding: 1em; 55 | margin: .5em 0; 56 | overflow: auto; 57 | } 58 | 59 | :not(pre) > code[class*="language-"], 60 | pre[class*="language-"] { 61 | background: #f5f2f0; 62 | } 63 | 64 | /* Inline code */ 65 | :not(pre) > code[class*="language-"] { 66 | padding: .1em; 67 | border-radius: .3em; 68 | white-space: normal; 69 | } 70 | 71 | .token.comment, 72 | .token.prolog, 73 | .token.doctype, 74 | .token.cdata { 75 | color: slategray; 76 | } 77 | 78 | .token.punctuation { 79 | color: #999; 80 | } 81 | 82 | .token.namespace { 83 | opacity: .7; 84 | } 85 | 86 | .token.property, 87 | .token.tag, 88 | .token.boolean, 89 | .token.number, 90 | .token.constant, 91 | .token.symbol, 92 | .token.deleted { 93 | color: #905; 94 | } 95 | 96 | .token.selector, 97 | .token.attr-name, 98 | .token.string, 99 | .token.char, 100 | .token.builtin, 101 | .token.inserted { 102 | color: #690; 103 | } 104 | 105 | .token.operator, 106 | .token.entity, 107 | .token.url, 108 | .language-css .token.string, 109 | .style .token.string { 110 | color: #9a6e3a; 111 | /* This background color was intended by the author of this theme. */ 112 | background: hsla(0, 0%, 100%, .5); 113 | } 114 | 115 | .token.atrule, 116 | .token.attr-value, 117 | .token.keyword { 118 | color: #07a; 119 | } 120 | 121 | .token.function, 122 | .token.class-name { 123 | color: #DD4A68; 124 | } 125 | 126 | .token.regex, 127 | .token.important, 128 | .token.variable { 129 | color: #e90; 130 | } 131 | 132 | .token.important, 133 | .token.bold { 134 | font-weight: bold; 135 | } 136 | .token.italic { 137 | font-style: italic; 138 | } 139 | 140 | .token.entity { 141 | cursor: help; 142 | } 143 | -------------------------------------------------------------------------------- /handlers/static/third-party/prism/prism.js: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism&languages=clike+javascript+bash */ 3 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 4 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; 5 | Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; 6 | !function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",a={pattern:/(^(["']?)\w+\2)[ \t]+\S.*/,lookbehind:!0,alias:"punctuation",inside:null},n={bash:a,environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--|\+\+|\*\*=?|<<=?|>>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?:\.\w+)*(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},parameter:{pattern:/(^|\s)-{1,2}(?:\w+:[+-]?)?\w+(?:\.\w+)*(?=[=\s]|$)/,alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:n},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:a}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:n},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:n.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:n.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},a.inside=e.languages.bash;for(var s=["comment","function-name","for-or-select","assign-left","parameter","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],o=n.variable[1].inside,i=0;i=16.0.0" 36 | } 37 | }, 38 | "node_modules/@humanwhocodes/config-array": { 39 | "version": "0.10.4", 40 | "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", 41 | "integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==", 42 | "dev": true, 43 | "dependencies": { 44 | "@humanwhocodes/object-schema": "^1.2.1", 45 | "debug": "^4.1.1", 46 | "minimatch": "^3.0.4" 47 | }, 48 | "engines": { 49 | "node": ">=10.10.0" 50 | } 51 | }, 52 | "node_modules/@humanwhocodes/gitignore-to-minimatch": { 53 | "version": "1.0.2", 54 | "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", 55 | "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", 56 | "dev": true, 57 | "funding": { 58 | "type": "github", 59 | "url": "https://github.com/sponsors/nzakas" 60 | } 61 | }, 62 | "node_modules/@humanwhocodes/object-schema": { 63 | "version": "1.2.1", 64 | "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", 65 | "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", 66 | "dev": true 67 | }, 68 | "node_modules/@nodelib/fs.scandir": { 69 | "version": "2.1.5", 70 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 71 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 72 | "dev": true, 73 | "dependencies": { 74 | "@nodelib/fs.stat": "2.0.5", 75 | "run-parallel": "^1.1.9" 76 | }, 77 | "engines": { 78 | "node": ">= 8" 79 | } 80 | }, 81 | "node_modules/@nodelib/fs.stat": { 82 | "version": "2.0.5", 83 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 84 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 85 | "dev": true, 86 | "engines": { 87 | "node": ">= 8" 88 | } 89 | }, 90 | "node_modules/@nodelib/fs.walk": { 91 | "version": "1.2.8", 92 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 93 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 94 | "dev": true, 95 | "dependencies": { 96 | "@nodelib/fs.scandir": "2.1.5", 97 | "fastq": "^1.6.0" 98 | }, 99 | "engines": { 100 | "node": ">= 8" 101 | } 102 | }, 103 | "node_modules/acorn": { 104 | "version": "8.8.0", 105 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", 106 | "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", 107 | "dev": true, 108 | "bin": { 109 | "acorn": "bin/acorn" 110 | }, 111 | "engines": { 112 | "node": ">=0.4.0" 113 | } 114 | }, 115 | "node_modules/acorn-jsx": { 116 | "version": "5.3.2", 117 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 118 | "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 119 | "dev": true, 120 | "peerDependencies": { 121 | "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 122 | } 123 | }, 124 | "node_modules/ajv": { 125 | "version": "6.12.6", 126 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 127 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 128 | "dev": true, 129 | "dependencies": { 130 | "fast-deep-equal": "^3.1.1", 131 | "fast-json-stable-stringify": "^2.0.0", 132 | "json-schema-traverse": "^0.4.1", 133 | "uri-js": "^4.2.2" 134 | }, 135 | "funding": { 136 | "type": "github", 137 | "url": "https://github.com/sponsors/epoberezkin" 138 | } 139 | }, 140 | "node_modules/ansi-regex": { 141 | "version": "5.0.1", 142 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 143 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 144 | "dev": true, 145 | "engines": { 146 | "node": ">=8" 147 | } 148 | }, 149 | "node_modules/ansi-styles": { 150 | "version": "4.3.0", 151 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 152 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 153 | "dev": true, 154 | "dependencies": { 155 | "color-convert": "^2.0.1" 156 | }, 157 | "engines": { 158 | "node": ">=8" 159 | }, 160 | "funding": { 161 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 162 | } 163 | }, 164 | "node_modules/argparse": { 165 | "version": "2.0.1", 166 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 167 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 168 | "dev": true 169 | }, 170 | "node_modules/array-union": { 171 | "version": "2.1.0", 172 | "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", 173 | "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", 174 | "dev": true, 175 | "engines": { 176 | "node": ">=8" 177 | } 178 | }, 179 | "node_modules/balanced-match": { 180 | "version": "1.0.2", 181 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 182 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 183 | "dev": true 184 | }, 185 | "node_modules/brace-expansion": { 186 | "version": "1.1.11", 187 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 188 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 189 | "dev": true, 190 | "dependencies": { 191 | "balanced-match": "^1.0.0", 192 | "concat-map": "0.0.1" 193 | } 194 | }, 195 | "node_modules/braces": { 196 | "version": "3.0.2", 197 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 198 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 199 | "dev": true, 200 | "dependencies": { 201 | "fill-range": "^7.0.1" 202 | }, 203 | "engines": { 204 | "node": ">=8" 205 | } 206 | }, 207 | "node_modules/callsites": { 208 | "version": "3.1.0", 209 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 210 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 211 | "dev": true, 212 | "engines": { 213 | "node": ">=6" 214 | } 215 | }, 216 | "node_modules/chalk": { 217 | "version": "4.1.2", 218 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 219 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 220 | "dev": true, 221 | "dependencies": { 222 | "ansi-styles": "^4.1.0", 223 | "supports-color": "^7.1.0" 224 | }, 225 | "engines": { 226 | "node": ">=10" 227 | }, 228 | "funding": { 229 | "url": "https://github.com/chalk/chalk?sponsor=1" 230 | } 231 | }, 232 | "node_modules/color-convert": { 233 | "version": "2.0.1", 234 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 235 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 236 | "dev": true, 237 | "dependencies": { 238 | "color-name": "~1.1.4" 239 | }, 240 | "engines": { 241 | "node": ">=7.0.0" 242 | } 243 | }, 244 | "node_modules/color-name": { 245 | "version": "1.1.4", 246 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 247 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 248 | "dev": true 249 | }, 250 | "node_modules/concat-map": { 251 | "version": "0.0.1", 252 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 253 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 254 | "dev": true 255 | }, 256 | "node_modules/cross-spawn": { 257 | "version": "7.0.3", 258 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 259 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 260 | "dev": true, 261 | "dependencies": { 262 | "path-key": "^3.1.0", 263 | "shebang-command": "^2.0.0", 264 | "which": "^2.0.1" 265 | }, 266 | "engines": { 267 | "node": ">= 8" 268 | } 269 | }, 270 | "node_modules/debug": { 271 | "version": "4.3.4", 272 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 273 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 274 | "dev": true, 275 | "dependencies": { 276 | "ms": "2.1.2" 277 | }, 278 | "engines": { 279 | "node": ">=6.0" 280 | }, 281 | "peerDependenciesMeta": { 282 | "supports-color": { 283 | "optional": true 284 | } 285 | } 286 | }, 287 | "node_modules/deep-is": { 288 | "version": "0.1.4", 289 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 290 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 291 | "dev": true 292 | }, 293 | "node_modules/dir-glob": { 294 | "version": "3.0.1", 295 | "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", 296 | "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", 297 | "dev": true, 298 | "dependencies": { 299 | "path-type": "^4.0.0" 300 | }, 301 | "engines": { 302 | "node": ">=8" 303 | } 304 | }, 305 | "node_modules/doctrine": { 306 | "version": "3.0.0", 307 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", 308 | "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", 309 | "dev": true, 310 | "dependencies": { 311 | "esutils": "^2.0.2" 312 | }, 313 | "engines": { 314 | "node": ">=6.0.0" 315 | } 316 | }, 317 | "node_modules/escape-string-regexp": { 318 | "version": "4.0.0", 319 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 320 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 321 | "dev": true, 322 | "engines": { 323 | "node": ">=10" 324 | }, 325 | "funding": { 326 | "url": "https://github.com/sponsors/sindresorhus" 327 | } 328 | }, 329 | "node_modules/eslint": { 330 | "version": "8.22.0", 331 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.22.0.tgz", 332 | "integrity": "sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==", 333 | "dev": true, 334 | "dependencies": { 335 | "@eslint/eslintrc": "^1.3.0", 336 | "@humanwhocodes/config-array": "^0.10.4", 337 | "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", 338 | "ajv": "^6.10.0", 339 | "chalk": "^4.0.0", 340 | "cross-spawn": "^7.0.2", 341 | "debug": "^4.3.2", 342 | "doctrine": "^3.0.0", 343 | "escape-string-regexp": "^4.0.0", 344 | "eslint-scope": "^7.1.1", 345 | "eslint-utils": "^3.0.0", 346 | "eslint-visitor-keys": "^3.3.0", 347 | "espree": "^9.3.3", 348 | "esquery": "^1.4.0", 349 | "esutils": "^2.0.2", 350 | "fast-deep-equal": "^3.1.3", 351 | "file-entry-cache": "^6.0.1", 352 | "find-up": "^5.0.0", 353 | "functional-red-black-tree": "^1.0.1", 354 | "glob-parent": "^6.0.1", 355 | "globals": "^13.15.0", 356 | "globby": "^11.1.0", 357 | "grapheme-splitter": "^1.0.4", 358 | "ignore": "^5.2.0", 359 | "import-fresh": "^3.0.0", 360 | "imurmurhash": "^0.1.4", 361 | "is-glob": "^4.0.0", 362 | "js-yaml": "^4.1.0", 363 | "json-stable-stringify-without-jsonify": "^1.0.1", 364 | "levn": "^0.4.1", 365 | "lodash.merge": "^4.6.2", 366 | "minimatch": "^3.1.2", 367 | "natural-compare": "^1.4.0", 368 | "optionator": "^0.9.1", 369 | "regexpp": "^3.2.0", 370 | "strip-ansi": "^6.0.1", 371 | "strip-json-comments": "^3.1.0", 372 | "text-table": "^0.2.0", 373 | "v8-compile-cache": "^2.0.3" 374 | }, 375 | "bin": { 376 | "eslint": "bin/eslint.js" 377 | }, 378 | "engines": { 379 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 380 | }, 381 | "funding": { 382 | "url": "https://opencollective.com/eslint" 383 | } 384 | }, 385 | "node_modules/eslint-plugin-cypress": { 386 | "version": "2.12.1", 387 | "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.12.1.tgz", 388 | "integrity": "sha512-c2W/uPADl5kospNDihgiLc7n87t5XhUbFDoTl6CfVkmG+kDAb5Ux10V9PoLPu9N+r7znpc+iQlcmAqT1A/89HA==", 389 | "dev": true, 390 | "dependencies": { 391 | "globals": "^11.12.0" 392 | }, 393 | "peerDependencies": { 394 | "eslint": ">= 3.2.1" 395 | } 396 | }, 397 | "node_modules/eslint-plugin-cypress/node_modules/globals": { 398 | "version": "11.12.0", 399 | "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", 400 | "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", 401 | "dev": true, 402 | "engines": { 403 | "node": ">=4" 404 | } 405 | }, 406 | "node_modules/eslint-scope": { 407 | "version": "7.1.1", 408 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", 409 | "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", 410 | "dev": true, 411 | "dependencies": { 412 | "esrecurse": "^4.3.0", 413 | "estraverse": "^5.2.0" 414 | }, 415 | "engines": { 416 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 417 | } 418 | }, 419 | "node_modules/eslint-utils": { 420 | "version": "3.0.0", 421 | "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", 422 | "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", 423 | "dev": true, 424 | "dependencies": { 425 | "eslint-visitor-keys": "^2.0.0" 426 | }, 427 | "engines": { 428 | "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" 429 | }, 430 | "funding": { 431 | "url": "https://github.com/sponsors/mysticatea" 432 | }, 433 | "peerDependencies": { 434 | "eslint": ">=5" 435 | } 436 | }, 437 | "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { 438 | "version": "2.1.0", 439 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", 440 | "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", 441 | "dev": true, 442 | "engines": { 443 | "node": ">=10" 444 | } 445 | }, 446 | "node_modules/eslint-visitor-keys": { 447 | "version": "3.3.0", 448 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", 449 | "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", 450 | "dev": true, 451 | "engines": { 452 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 453 | } 454 | }, 455 | "node_modules/espree": { 456 | "version": "9.3.3", 457 | "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz", 458 | "integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==", 459 | "dev": true, 460 | "dependencies": { 461 | "acorn": "^8.8.0", 462 | "acorn-jsx": "^5.3.2", 463 | "eslint-visitor-keys": "^3.3.0" 464 | }, 465 | "engines": { 466 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 467 | }, 468 | "funding": { 469 | "url": "https://opencollective.com/eslint" 470 | } 471 | }, 472 | "node_modules/esquery": { 473 | "version": "1.4.0", 474 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", 475 | "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", 476 | "dev": true, 477 | "dependencies": { 478 | "estraverse": "^5.1.0" 479 | }, 480 | "engines": { 481 | "node": ">=0.10" 482 | } 483 | }, 484 | "node_modules/esrecurse": { 485 | "version": "4.3.0", 486 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 487 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 488 | "dev": true, 489 | "dependencies": { 490 | "estraverse": "^5.2.0" 491 | }, 492 | "engines": { 493 | "node": ">=4.0" 494 | } 495 | }, 496 | "node_modules/estraverse": { 497 | "version": "5.3.0", 498 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 499 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 500 | "dev": true, 501 | "engines": { 502 | "node": ">=4.0" 503 | } 504 | }, 505 | "node_modules/esutils": { 506 | "version": "2.0.3", 507 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 508 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 509 | "dev": true, 510 | "engines": { 511 | "node": ">=0.10.0" 512 | } 513 | }, 514 | "node_modules/fast-deep-equal": { 515 | "version": "3.1.3", 516 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 517 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 518 | "dev": true 519 | }, 520 | "node_modules/fast-glob": { 521 | "version": "3.2.11", 522 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", 523 | "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", 524 | "dev": true, 525 | "dependencies": { 526 | "@nodelib/fs.stat": "^2.0.2", 527 | "@nodelib/fs.walk": "^1.2.3", 528 | "glob-parent": "^5.1.2", 529 | "merge2": "^1.3.0", 530 | "micromatch": "^4.0.4" 531 | }, 532 | "engines": { 533 | "node": ">=8.6.0" 534 | } 535 | }, 536 | "node_modules/fast-glob/node_modules/glob-parent": { 537 | "version": "5.1.2", 538 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 539 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 540 | "dev": true, 541 | "dependencies": { 542 | "is-glob": "^4.0.1" 543 | }, 544 | "engines": { 545 | "node": ">= 6" 546 | } 547 | }, 548 | "node_modules/fast-json-stable-stringify": { 549 | "version": "2.1.0", 550 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 551 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 552 | "dev": true 553 | }, 554 | "node_modules/fast-levenshtein": { 555 | "version": "2.0.6", 556 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 557 | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 558 | "dev": true 559 | }, 560 | "node_modules/fastq": { 561 | "version": "1.13.0", 562 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", 563 | "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", 564 | "dev": true, 565 | "dependencies": { 566 | "reusify": "^1.0.4" 567 | } 568 | }, 569 | "node_modules/file-entry-cache": { 570 | "version": "6.0.1", 571 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", 572 | "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", 573 | "dev": true, 574 | "dependencies": { 575 | "flat-cache": "^3.0.4" 576 | }, 577 | "engines": { 578 | "node": "^10.12.0 || >=12.0.0" 579 | } 580 | }, 581 | "node_modules/fill-range": { 582 | "version": "7.0.1", 583 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 584 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 585 | "dev": true, 586 | "dependencies": { 587 | "to-regex-range": "^5.0.1" 588 | }, 589 | "engines": { 590 | "node": ">=8" 591 | } 592 | }, 593 | "node_modules/find-up": { 594 | "version": "5.0.0", 595 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 596 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 597 | "dev": true, 598 | "dependencies": { 599 | "locate-path": "^6.0.0", 600 | "path-exists": "^4.0.0" 601 | }, 602 | "engines": { 603 | "node": ">=10" 604 | }, 605 | "funding": { 606 | "url": "https://github.com/sponsors/sindresorhus" 607 | } 608 | }, 609 | "node_modules/flat-cache": { 610 | "version": "3.0.4", 611 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", 612 | "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", 613 | "dev": true, 614 | "dependencies": { 615 | "flatted": "^3.1.0", 616 | "rimraf": "^3.0.2" 617 | }, 618 | "engines": { 619 | "node": "^10.12.0 || >=12.0.0" 620 | } 621 | }, 622 | "node_modules/flatted": { 623 | "version": "3.2.7", 624 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", 625 | "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", 626 | "dev": true 627 | }, 628 | "node_modules/fs.realpath": { 629 | "version": "1.0.0", 630 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 631 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 632 | "dev": true 633 | }, 634 | "node_modules/functional-red-black-tree": { 635 | "version": "1.0.1", 636 | "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", 637 | "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", 638 | "dev": true 639 | }, 640 | "node_modules/glob": { 641 | "version": "7.2.3", 642 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 643 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 644 | "dev": true, 645 | "dependencies": { 646 | "fs.realpath": "^1.0.0", 647 | "inflight": "^1.0.4", 648 | "inherits": "2", 649 | "minimatch": "^3.1.1", 650 | "once": "^1.3.0", 651 | "path-is-absolute": "^1.0.0" 652 | }, 653 | "engines": { 654 | "node": "*" 655 | }, 656 | "funding": { 657 | "url": "https://github.com/sponsors/isaacs" 658 | } 659 | }, 660 | "node_modules/glob-parent": { 661 | "version": "6.0.2", 662 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 663 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 664 | "dev": true, 665 | "dependencies": { 666 | "is-glob": "^4.0.3" 667 | }, 668 | "engines": { 669 | "node": ">=10.13.0" 670 | } 671 | }, 672 | "node_modules/globals": { 673 | "version": "13.17.0", 674 | "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", 675 | "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", 676 | "dev": true, 677 | "dependencies": { 678 | "type-fest": "^0.20.2" 679 | }, 680 | "engines": { 681 | "node": ">=8" 682 | }, 683 | "funding": { 684 | "url": "https://github.com/sponsors/sindresorhus" 685 | } 686 | }, 687 | "node_modules/globby": { 688 | "version": "11.1.0", 689 | "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", 690 | "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", 691 | "dev": true, 692 | "dependencies": { 693 | "array-union": "^2.1.0", 694 | "dir-glob": "^3.0.1", 695 | "fast-glob": "^3.2.9", 696 | "ignore": "^5.2.0", 697 | "merge2": "^1.4.1", 698 | "slash": "^3.0.0" 699 | }, 700 | "engines": { 701 | "node": ">=10" 702 | }, 703 | "funding": { 704 | "url": "https://github.com/sponsors/sindresorhus" 705 | } 706 | }, 707 | "node_modules/grapheme-splitter": { 708 | "version": "1.0.4", 709 | "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", 710 | "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", 711 | "dev": true 712 | }, 713 | "node_modules/has-flag": { 714 | "version": "4.0.0", 715 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 716 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 717 | "dev": true, 718 | "engines": { 719 | "node": ">=8" 720 | } 721 | }, 722 | "node_modules/ignore": { 723 | "version": "5.2.0", 724 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", 725 | "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", 726 | "dev": true, 727 | "engines": { 728 | "node": ">= 4" 729 | } 730 | }, 731 | "node_modules/import-fresh": { 732 | "version": "3.3.0", 733 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", 734 | "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", 735 | "dev": true, 736 | "dependencies": { 737 | "parent-module": "^1.0.0", 738 | "resolve-from": "^4.0.0" 739 | }, 740 | "engines": { 741 | "node": ">=6" 742 | }, 743 | "funding": { 744 | "url": "https://github.com/sponsors/sindresorhus" 745 | } 746 | }, 747 | "node_modules/imurmurhash": { 748 | "version": "0.1.4", 749 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 750 | "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 751 | "dev": true, 752 | "engines": { 753 | "node": ">=0.8.19" 754 | } 755 | }, 756 | "node_modules/inflight": { 757 | "version": "1.0.6", 758 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 759 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 760 | "dev": true, 761 | "dependencies": { 762 | "once": "^1.3.0", 763 | "wrappy": "1" 764 | } 765 | }, 766 | "node_modules/inherits": { 767 | "version": "2.0.4", 768 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 769 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 770 | "dev": true 771 | }, 772 | "node_modules/is-extglob": { 773 | "version": "2.1.1", 774 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 775 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 776 | "dev": true, 777 | "engines": { 778 | "node": ">=0.10.0" 779 | } 780 | }, 781 | "node_modules/is-glob": { 782 | "version": "4.0.3", 783 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 784 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 785 | "dev": true, 786 | "dependencies": { 787 | "is-extglob": "^2.1.1" 788 | }, 789 | "engines": { 790 | "node": ">=0.10.0" 791 | } 792 | }, 793 | "node_modules/is-number": { 794 | "version": "7.0.0", 795 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 796 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 797 | "dev": true, 798 | "engines": { 799 | "node": ">=0.12.0" 800 | } 801 | }, 802 | "node_modules/isexe": { 803 | "version": "2.0.0", 804 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 805 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 806 | "dev": true 807 | }, 808 | "node_modules/js-yaml": { 809 | "version": "4.1.0", 810 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 811 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 812 | "dev": true, 813 | "dependencies": { 814 | "argparse": "^2.0.1" 815 | }, 816 | "bin": { 817 | "js-yaml": "bin/js-yaml.js" 818 | } 819 | }, 820 | "node_modules/json-schema-traverse": { 821 | "version": "0.4.1", 822 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 823 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 824 | "dev": true 825 | }, 826 | "node_modules/json-stable-stringify-without-jsonify": { 827 | "version": "1.0.1", 828 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 829 | "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 830 | "dev": true 831 | }, 832 | "node_modules/levn": { 833 | "version": "0.4.1", 834 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 835 | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 836 | "dev": true, 837 | "dependencies": { 838 | "prelude-ls": "^1.2.1", 839 | "type-check": "~0.4.0" 840 | }, 841 | "engines": { 842 | "node": ">= 0.8.0" 843 | } 844 | }, 845 | "node_modules/locate-path": { 846 | "version": "6.0.0", 847 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 848 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 849 | "dev": true, 850 | "dependencies": { 851 | "p-locate": "^5.0.0" 852 | }, 853 | "engines": { 854 | "node": ">=10" 855 | }, 856 | "funding": { 857 | "url": "https://github.com/sponsors/sindresorhus" 858 | } 859 | }, 860 | "node_modules/lodash.merge": { 861 | "version": "4.6.2", 862 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 863 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 864 | "dev": true 865 | }, 866 | "node_modules/merge2": { 867 | "version": "1.4.1", 868 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 869 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 870 | "dev": true, 871 | "engines": { 872 | "node": ">= 8" 873 | } 874 | }, 875 | "node_modules/micromatch": { 876 | "version": "4.0.5", 877 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", 878 | "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", 879 | "dev": true, 880 | "dependencies": { 881 | "braces": "^3.0.2", 882 | "picomatch": "^2.3.1" 883 | }, 884 | "engines": { 885 | "node": ">=8.6" 886 | } 887 | }, 888 | "node_modules/minimatch": { 889 | "version": "3.1.2", 890 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 891 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 892 | "dev": true, 893 | "dependencies": { 894 | "brace-expansion": "^1.1.7" 895 | }, 896 | "engines": { 897 | "node": "*" 898 | } 899 | }, 900 | "node_modules/ms": { 901 | "version": "2.1.2", 902 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 903 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 904 | "dev": true 905 | }, 906 | "node_modules/natural-compare": { 907 | "version": "1.4.0", 908 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 909 | "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 910 | "dev": true 911 | }, 912 | "node_modules/once": { 913 | "version": "1.4.0", 914 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 915 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 916 | "dev": true, 917 | "dependencies": { 918 | "wrappy": "1" 919 | } 920 | }, 921 | "node_modules/optionator": { 922 | "version": "0.9.1", 923 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", 924 | "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", 925 | "dev": true, 926 | "dependencies": { 927 | "deep-is": "^0.1.3", 928 | "fast-levenshtein": "^2.0.6", 929 | "levn": "^0.4.1", 930 | "prelude-ls": "^1.2.1", 931 | "type-check": "^0.4.0", 932 | "word-wrap": "^1.2.3" 933 | }, 934 | "engines": { 935 | "node": ">= 0.8.0" 936 | } 937 | }, 938 | "node_modules/p-limit": { 939 | "version": "3.1.0", 940 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 941 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 942 | "dev": true, 943 | "dependencies": { 944 | "yocto-queue": "^0.1.0" 945 | }, 946 | "engines": { 947 | "node": ">=10" 948 | }, 949 | "funding": { 950 | "url": "https://github.com/sponsors/sindresorhus" 951 | } 952 | }, 953 | "node_modules/p-locate": { 954 | "version": "5.0.0", 955 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 956 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 957 | "dev": true, 958 | "dependencies": { 959 | "p-limit": "^3.0.2" 960 | }, 961 | "engines": { 962 | "node": ">=10" 963 | }, 964 | "funding": { 965 | "url": "https://github.com/sponsors/sindresorhus" 966 | } 967 | }, 968 | "node_modules/parent-module": { 969 | "version": "1.0.1", 970 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 971 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 972 | "dev": true, 973 | "dependencies": { 974 | "callsites": "^3.0.0" 975 | }, 976 | "engines": { 977 | "node": ">=6" 978 | } 979 | }, 980 | "node_modules/path-exists": { 981 | "version": "4.0.0", 982 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 983 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 984 | "dev": true, 985 | "engines": { 986 | "node": ">=8" 987 | } 988 | }, 989 | "node_modules/path-is-absolute": { 990 | "version": "1.0.1", 991 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 992 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 993 | "dev": true, 994 | "engines": { 995 | "node": ">=0.10.0" 996 | } 997 | }, 998 | "node_modules/path-key": { 999 | "version": "3.1.1", 1000 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 1001 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 1002 | "dev": true, 1003 | "engines": { 1004 | "node": ">=8" 1005 | } 1006 | }, 1007 | "node_modules/path-type": { 1008 | "version": "4.0.0", 1009 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", 1010 | "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", 1011 | "dev": true, 1012 | "engines": { 1013 | "node": ">=8" 1014 | } 1015 | }, 1016 | "node_modules/picomatch": { 1017 | "version": "2.3.1", 1018 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1019 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1020 | "dev": true, 1021 | "engines": { 1022 | "node": ">=8.6" 1023 | }, 1024 | "funding": { 1025 | "url": "https://github.com/sponsors/jonschlinkert" 1026 | } 1027 | }, 1028 | "node_modules/prelude-ls": { 1029 | "version": "1.2.1", 1030 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 1031 | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 1032 | "dev": true, 1033 | "engines": { 1034 | "node": ">= 0.8.0" 1035 | } 1036 | }, 1037 | "node_modules/prettier": { 1038 | "version": "3.5.3", 1039 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", 1040 | "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", 1041 | "dev": true, 1042 | "bin": { 1043 | "prettier": "bin/prettier.cjs" 1044 | }, 1045 | "engines": { 1046 | "node": ">=14" 1047 | }, 1048 | "funding": { 1049 | "url": "https://github.com/prettier/prettier?sponsor=1" 1050 | } 1051 | }, 1052 | "node_modules/prettier-plugin-go-template": { 1053 | "version": "0.0.15", 1054 | "resolved": "https://registry.npmjs.org/prettier-plugin-go-template/-/prettier-plugin-go-template-0.0.15.tgz", 1055 | "integrity": "sha512-WqU92E1NokWYNZ9mLE6ijoRg6LtIGdLMePt2C7UBDjXeDH9okcRI3zRqtnWR4s5AloiqyvZ66jNBAa9tmRY5EQ==", 1056 | "dev": true, 1057 | "dependencies": { 1058 | "ulid": "^2.3.0" 1059 | }, 1060 | "engines": { 1061 | "node": ">=14.0.0" 1062 | }, 1063 | "peerDependencies": { 1064 | "prettier": "^3.0.0" 1065 | } 1066 | }, 1067 | "node_modules/punycode": { 1068 | "version": "2.1.1", 1069 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1070 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 1071 | "dev": true, 1072 | "engines": { 1073 | "node": ">=6" 1074 | } 1075 | }, 1076 | "node_modules/queue-microtask": { 1077 | "version": "1.2.3", 1078 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 1079 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 1080 | "dev": true, 1081 | "funding": [ 1082 | { 1083 | "type": "github", 1084 | "url": "https://github.com/sponsors/feross" 1085 | }, 1086 | { 1087 | "type": "patreon", 1088 | "url": "https://www.patreon.com/feross" 1089 | }, 1090 | { 1091 | "type": "consulting", 1092 | "url": "https://feross.org/support" 1093 | } 1094 | ] 1095 | }, 1096 | "node_modules/regexpp": { 1097 | "version": "3.2.0", 1098 | "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", 1099 | "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", 1100 | "dev": true, 1101 | "engines": { 1102 | "node": ">=8" 1103 | }, 1104 | "funding": { 1105 | "url": "https://github.com/sponsors/mysticatea" 1106 | } 1107 | }, 1108 | "node_modules/resolve-from": { 1109 | "version": "4.0.0", 1110 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 1111 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 1112 | "dev": true, 1113 | "engines": { 1114 | "node": ">=4" 1115 | } 1116 | }, 1117 | "node_modules/reusify": { 1118 | "version": "1.0.4", 1119 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 1120 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 1121 | "dev": true, 1122 | "engines": { 1123 | "iojs": ">=1.0.0", 1124 | "node": ">=0.10.0" 1125 | } 1126 | }, 1127 | "node_modules/rimraf": { 1128 | "version": "3.0.2", 1129 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 1130 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 1131 | "dev": true, 1132 | "dependencies": { 1133 | "glob": "^7.1.3" 1134 | }, 1135 | "bin": { 1136 | "rimraf": "bin.js" 1137 | }, 1138 | "funding": { 1139 | "url": "https://github.com/sponsors/isaacs" 1140 | } 1141 | }, 1142 | "node_modules/run-parallel": { 1143 | "version": "1.2.0", 1144 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 1145 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 1146 | "dev": true, 1147 | "funding": [ 1148 | { 1149 | "type": "github", 1150 | "url": "https://github.com/sponsors/feross" 1151 | }, 1152 | { 1153 | "type": "patreon", 1154 | "url": "https://www.patreon.com/feross" 1155 | }, 1156 | { 1157 | "type": "consulting", 1158 | "url": "https://feross.org/support" 1159 | } 1160 | ], 1161 | "dependencies": { 1162 | "queue-microtask": "^1.2.2" 1163 | } 1164 | }, 1165 | "node_modules/shebang-command": { 1166 | "version": "2.0.0", 1167 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1168 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1169 | "dev": true, 1170 | "dependencies": { 1171 | "shebang-regex": "^3.0.0" 1172 | }, 1173 | "engines": { 1174 | "node": ">=8" 1175 | } 1176 | }, 1177 | "node_modules/shebang-regex": { 1178 | "version": "3.0.0", 1179 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1180 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1181 | "dev": true, 1182 | "engines": { 1183 | "node": ">=8" 1184 | } 1185 | }, 1186 | "node_modules/slash": { 1187 | "version": "3.0.0", 1188 | "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", 1189 | "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", 1190 | "dev": true, 1191 | "engines": { 1192 | "node": ">=8" 1193 | } 1194 | }, 1195 | "node_modules/strip-ansi": { 1196 | "version": "6.0.1", 1197 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1198 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1199 | "dev": true, 1200 | "dependencies": { 1201 | "ansi-regex": "^5.0.1" 1202 | }, 1203 | "engines": { 1204 | "node": ">=8" 1205 | } 1206 | }, 1207 | "node_modules/strip-json-comments": { 1208 | "version": "3.1.1", 1209 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1210 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1211 | "dev": true, 1212 | "engines": { 1213 | "node": ">=8" 1214 | }, 1215 | "funding": { 1216 | "url": "https://github.com/sponsors/sindresorhus" 1217 | } 1218 | }, 1219 | "node_modules/supports-color": { 1220 | "version": "7.2.0", 1221 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1222 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1223 | "dev": true, 1224 | "dependencies": { 1225 | "has-flag": "^4.0.0" 1226 | }, 1227 | "engines": { 1228 | "node": ">=8" 1229 | } 1230 | }, 1231 | "node_modules/text-table": { 1232 | "version": "0.2.0", 1233 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 1234 | "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", 1235 | "dev": true 1236 | }, 1237 | "node_modules/to-regex-range": { 1238 | "version": "5.0.1", 1239 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1240 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1241 | "dev": true, 1242 | "dependencies": { 1243 | "is-number": "^7.0.0" 1244 | }, 1245 | "engines": { 1246 | "node": ">=8.0" 1247 | } 1248 | }, 1249 | "node_modules/type-check": { 1250 | "version": "0.4.0", 1251 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 1252 | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 1253 | "dev": true, 1254 | "dependencies": { 1255 | "prelude-ls": "^1.2.1" 1256 | }, 1257 | "engines": { 1258 | "node": ">= 0.8.0" 1259 | } 1260 | }, 1261 | "node_modules/type-fest": { 1262 | "version": "0.20.2", 1263 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", 1264 | "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", 1265 | "dev": true, 1266 | "engines": { 1267 | "node": ">=10" 1268 | }, 1269 | "funding": { 1270 | "url": "https://github.com/sponsors/sindresorhus" 1271 | } 1272 | }, 1273 | "node_modules/ulid": { 1274 | "version": "2.3.0", 1275 | "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz", 1276 | "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==", 1277 | "dev": true, 1278 | "bin": { 1279 | "ulid": "bin/cli.js" 1280 | } 1281 | }, 1282 | "node_modules/uri-js": { 1283 | "version": "4.4.1", 1284 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1285 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1286 | "dev": true, 1287 | "dependencies": { 1288 | "punycode": "^2.1.0" 1289 | } 1290 | }, 1291 | "node_modules/v8-compile-cache": { 1292 | "version": "2.3.0", 1293 | "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", 1294 | "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", 1295 | "dev": true 1296 | }, 1297 | "node_modules/which": { 1298 | "version": "2.0.2", 1299 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1300 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1301 | "dev": true, 1302 | "dependencies": { 1303 | "isexe": "^2.0.0" 1304 | }, 1305 | "bin": { 1306 | "node-which": "bin/node-which" 1307 | }, 1308 | "engines": { 1309 | "node": ">= 8" 1310 | } 1311 | }, 1312 | "node_modules/word-wrap": { 1313 | "version": "1.2.3", 1314 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", 1315 | "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", 1316 | "dev": true, 1317 | "engines": { 1318 | "node": ">=0.10.0" 1319 | } 1320 | }, 1321 | "node_modules/wrappy": { 1322 | "version": "1.0.2", 1323 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1324 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1325 | "dev": true 1326 | }, 1327 | "node_modules/yocto-queue": { 1328 | "version": "0.1.0", 1329 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1330 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1331 | "dev": true, 1332 | "engines": { 1333 | "node": ">=10" 1334 | }, 1335 | "funding": { 1336 | "url": "https://github.com/sponsors/sindresorhus" 1337 | } 1338 | } 1339 | }, 1340 | "dependencies": { 1341 | "@eslint/eslintrc": { 1342 | "version": "1.3.0", 1343 | "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", 1344 | "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", 1345 | "dev": true, 1346 | "requires": { 1347 | "ajv": "^6.12.4", 1348 | "debug": "^4.3.2", 1349 | "espree": "^9.3.2", 1350 | "globals": "^13.15.0", 1351 | "ignore": "^5.2.0", 1352 | "import-fresh": "^3.2.1", 1353 | "js-yaml": "^4.1.0", 1354 | "minimatch": "^3.1.2", 1355 | "strip-json-comments": "^3.1.1" 1356 | } 1357 | }, 1358 | "@humanwhocodes/config-array": { 1359 | "version": "0.10.4", 1360 | "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", 1361 | "integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==", 1362 | "dev": true, 1363 | "requires": { 1364 | "@humanwhocodes/object-schema": "^1.2.1", 1365 | "debug": "^4.1.1", 1366 | "minimatch": "^3.0.4" 1367 | } 1368 | }, 1369 | "@humanwhocodes/gitignore-to-minimatch": { 1370 | "version": "1.0.2", 1371 | "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", 1372 | "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", 1373 | "dev": true 1374 | }, 1375 | "@humanwhocodes/object-schema": { 1376 | "version": "1.2.1", 1377 | "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", 1378 | "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", 1379 | "dev": true 1380 | }, 1381 | "@nodelib/fs.scandir": { 1382 | "version": "2.1.5", 1383 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 1384 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 1385 | "dev": true, 1386 | "requires": { 1387 | "@nodelib/fs.stat": "2.0.5", 1388 | "run-parallel": "^1.1.9" 1389 | } 1390 | }, 1391 | "@nodelib/fs.stat": { 1392 | "version": "2.0.5", 1393 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 1394 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 1395 | "dev": true 1396 | }, 1397 | "@nodelib/fs.walk": { 1398 | "version": "1.2.8", 1399 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 1400 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 1401 | "dev": true, 1402 | "requires": { 1403 | "@nodelib/fs.scandir": "2.1.5", 1404 | "fastq": "^1.6.0" 1405 | } 1406 | }, 1407 | "acorn": { 1408 | "version": "8.8.0", 1409 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", 1410 | "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", 1411 | "dev": true 1412 | }, 1413 | "acorn-jsx": { 1414 | "version": "5.3.2", 1415 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 1416 | "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 1417 | "dev": true, 1418 | "requires": {} 1419 | }, 1420 | "ajv": { 1421 | "version": "6.12.6", 1422 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 1423 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 1424 | "dev": true, 1425 | "requires": { 1426 | "fast-deep-equal": "^3.1.1", 1427 | "fast-json-stable-stringify": "^2.0.0", 1428 | "json-schema-traverse": "^0.4.1", 1429 | "uri-js": "^4.2.2" 1430 | } 1431 | }, 1432 | "ansi-regex": { 1433 | "version": "5.0.1", 1434 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1435 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1436 | "dev": true 1437 | }, 1438 | "ansi-styles": { 1439 | "version": "4.3.0", 1440 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1441 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1442 | "dev": true, 1443 | "requires": { 1444 | "color-convert": "^2.0.1" 1445 | } 1446 | }, 1447 | "argparse": { 1448 | "version": "2.0.1", 1449 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 1450 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 1451 | "dev": true 1452 | }, 1453 | "array-union": { 1454 | "version": "2.1.0", 1455 | "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", 1456 | "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", 1457 | "dev": true 1458 | }, 1459 | "balanced-match": { 1460 | "version": "1.0.2", 1461 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 1462 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 1463 | "dev": true 1464 | }, 1465 | "brace-expansion": { 1466 | "version": "1.1.11", 1467 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 1468 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 1469 | "dev": true, 1470 | "requires": { 1471 | "balanced-match": "^1.0.0", 1472 | "concat-map": "0.0.1" 1473 | } 1474 | }, 1475 | "braces": { 1476 | "version": "3.0.2", 1477 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 1478 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 1479 | "dev": true, 1480 | "requires": { 1481 | "fill-range": "^7.0.1" 1482 | } 1483 | }, 1484 | "callsites": { 1485 | "version": "3.1.0", 1486 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 1487 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 1488 | "dev": true 1489 | }, 1490 | "chalk": { 1491 | "version": "4.1.2", 1492 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 1493 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 1494 | "dev": true, 1495 | "requires": { 1496 | "ansi-styles": "^4.1.0", 1497 | "supports-color": "^7.1.0" 1498 | } 1499 | }, 1500 | "color-convert": { 1501 | "version": "2.0.1", 1502 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1503 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1504 | "dev": true, 1505 | "requires": { 1506 | "color-name": "~1.1.4" 1507 | } 1508 | }, 1509 | "color-name": { 1510 | "version": "1.1.4", 1511 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1512 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1513 | "dev": true 1514 | }, 1515 | "concat-map": { 1516 | "version": "0.0.1", 1517 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 1518 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 1519 | "dev": true 1520 | }, 1521 | "cross-spawn": { 1522 | "version": "7.0.3", 1523 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 1524 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 1525 | "dev": true, 1526 | "requires": { 1527 | "path-key": "^3.1.0", 1528 | "shebang-command": "^2.0.0", 1529 | "which": "^2.0.1" 1530 | } 1531 | }, 1532 | "debug": { 1533 | "version": "4.3.4", 1534 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 1535 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 1536 | "dev": true, 1537 | "requires": { 1538 | "ms": "2.1.2" 1539 | } 1540 | }, 1541 | "deep-is": { 1542 | "version": "0.1.4", 1543 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 1544 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 1545 | "dev": true 1546 | }, 1547 | "dir-glob": { 1548 | "version": "3.0.1", 1549 | "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", 1550 | "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", 1551 | "dev": true, 1552 | "requires": { 1553 | "path-type": "^4.0.0" 1554 | } 1555 | }, 1556 | "doctrine": { 1557 | "version": "3.0.0", 1558 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", 1559 | "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", 1560 | "dev": true, 1561 | "requires": { 1562 | "esutils": "^2.0.2" 1563 | } 1564 | }, 1565 | "escape-string-regexp": { 1566 | "version": "4.0.0", 1567 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 1568 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 1569 | "dev": true 1570 | }, 1571 | "eslint": { 1572 | "version": "8.22.0", 1573 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.22.0.tgz", 1574 | "integrity": "sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==", 1575 | "dev": true, 1576 | "requires": { 1577 | "@eslint/eslintrc": "^1.3.0", 1578 | "@humanwhocodes/config-array": "^0.10.4", 1579 | "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", 1580 | "ajv": "^6.10.0", 1581 | "chalk": "^4.0.0", 1582 | "cross-spawn": "^7.0.2", 1583 | "debug": "^4.3.2", 1584 | "doctrine": "^3.0.0", 1585 | "escape-string-regexp": "^4.0.0", 1586 | "eslint-scope": "^7.1.1", 1587 | "eslint-utils": "^3.0.0", 1588 | "eslint-visitor-keys": "^3.3.0", 1589 | "espree": "^9.3.3", 1590 | "esquery": "^1.4.0", 1591 | "esutils": "^2.0.2", 1592 | "fast-deep-equal": "^3.1.3", 1593 | "file-entry-cache": "^6.0.1", 1594 | "find-up": "^5.0.0", 1595 | "functional-red-black-tree": "^1.0.1", 1596 | "glob-parent": "^6.0.1", 1597 | "globals": "^13.15.0", 1598 | "globby": "^11.1.0", 1599 | "grapheme-splitter": "^1.0.4", 1600 | "ignore": "^5.2.0", 1601 | "import-fresh": "^3.0.0", 1602 | "imurmurhash": "^0.1.4", 1603 | "is-glob": "^4.0.0", 1604 | "js-yaml": "^4.1.0", 1605 | "json-stable-stringify-without-jsonify": "^1.0.1", 1606 | "levn": "^0.4.1", 1607 | "lodash.merge": "^4.6.2", 1608 | "minimatch": "^3.1.2", 1609 | "natural-compare": "^1.4.0", 1610 | "optionator": "^0.9.1", 1611 | "regexpp": "^3.2.0", 1612 | "strip-ansi": "^6.0.1", 1613 | "strip-json-comments": "^3.1.0", 1614 | "text-table": "^0.2.0", 1615 | "v8-compile-cache": "^2.0.3" 1616 | } 1617 | }, 1618 | "eslint-plugin-cypress": { 1619 | "version": "2.12.1", 1620 | "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.12.1.tgz", 1621 | "integrity": "sha512-c2W/uPADl5kospNDihgiLc7n87t5XhUbFDoTl6CfVkmG+kDAb5Ux10V9PoLPu9N+r7znpc+iQlcmAqT1A/89HA==", 1622 | "dev": true, 1623 | "requires": { 1624 | "globals": "^11.12.0" 1625 | }, 1626 | "dependencies": { 1627 | "globals": { 1628 | "version": "11.12.0", 1629 | "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", 1630 | "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", 1631 | "dev": true 1632 | } 1633 | } 1634 | }, 1635 | "eslint-scope": { 1636 | "version": "7.1.1", 1637 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", 1638 | "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", 1639 | "dev": true, 1640 | "requires": { 1641 | "esrecurse": "^4.3.0", 1642 | "estraverse": "^5.2.0" 1643 | } 1644 | }, 1645 | "eslint-utils": { 1646 | "version": "3.0.0", 1647 | "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", 1648 | "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", 1649 | "dev": true, 1650 | "requires": { 1651 | "eslint-visitor-keys": "^2.0.0" 1652 | }, 1653 | "dependencies": { 1654 | "eslint-visitor-keys": { 1655 | "version": "2.1.0", 1656 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", 1657 | "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", 1658 | "dev": true 1659 | } 1660 | } 1661 | }, 1662 | "eslint-visitor-keys": { 1663 | "version": "3.3.0", 1664 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", 1665 | "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", 1666 | "dev": true 1667 | }, 1668 | "espree": { 1669 | "version": "9.3.3", 1670 | "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz", 1671 | "integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==", 1672 | "dev": true, 1673 | "requires": { 1674 | "acorn": "^8.8.0", 1675 | "acorn-jsx": "^5.3.2", 1676 | "eslint-visitor-keys": "^3.3.0" 1677 | } 1678 | }, 1679 | "esquery": { 1680 | "version": "1.4.0", 1681 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", 1682 | "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", 1683 | "dev": true, 1684 | "requires": { 1685 | "estraverse": "^5.1.0" 1686 | } 1687 | }, 1688 | "esrecurse": { 1689 | "version": "4.3.0", 1690 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 1691 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 1692 | "dev": true, 1693 | "requires": { 1694 | "estraverse": "^5.2.0" 1695 | } 1696 | }, 1697 | "estraverse": { 1698 | "version": "5.3.0", 1699 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 1700 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 1701 | "dev": true 1702 | }, 1703 | "esutils": { 1704 | "version": "2.0.3", 1705 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 1706 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 1707 | "dev": true 1708 | }, 1709 | "fast-deep-equal": { 1710 | "version": "3.1.3", 1711 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 1712 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 1713 | "dev": true 1714 | }, 1715 | "fast-glob": { 1716 | "version": "3.2.11", 1717 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", 1718 | "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", 1719 | "dev": true, 1720 | "requires": { 1721 | "@nodelib/fs.stat": "^2.0.2", 1722 | "@nodelib/fs.walk": "^1.2.3", 1723 | "glob-parent": "^5.1.2", 1724 | "merge2": "^1.3.0", 1725 | "micromatch": "^4.0.4" 1726 | }, 1727 | "dependencies": { 1728 | "glob-parent": { 1729 | "version": "5.1.2", 1730 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 1731 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 1732 | "dev": true, 1733 | "requires": { 1734 | "is-glob": "^4.0.1" 1735 | } 1736 | } 1737 | } 1738 | }, 1739 | "fast-json-stable-stringify": { 1740 | "version": "2.1.0", 1741 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 1742 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 1743 | "dev": true 1744 | }, 1745 | "fast-levenshtein": { 1746 | "version": "2.0.6", 1747 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 1748 | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 1749 | "dev": true 1750 | }, 1751 | "fastq": { 1752 | "version": "1.13.0", 1753 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", 1754 | "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", 1755 | "dev": true, 1756 | "requires": { 1757 | "reusify": "^1.0.4" 1758 | } 1759 | }, 1760 | "file-entry-cache": { 1761 | "version": "6.0.1", 1762 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", 1763 | "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", 1764 | "dev": true, 1765 | "requires": { 1766 | "flat-cache": "^3.0.4" 1767 | } 1768 | }, 1769 | "fill-range": { 1770 | "version": "7.0.1", 1771 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 1772 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 1773 | "dev": true, 1774 | "requires": { 1775 | "to-regex-range": "^5.0.1" 1776 | } 1777 | }, 1778 | "find-up": { 1779 | "version": "5.0.0", 1780 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 1781 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 1782 | "dev": true, 1783 | "requires": { 1784 | "locate-path": "^6.0.0", 1785 | "path-exists": "^4.0.0" 1786 | } 1787 | }, 1788 | "flat-cache": { 1789 | "version": "3.0.4", 1790 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", 1791 | "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", 1792 | "dev": true, 1793 | "requires": { 1794 | "flatted": "^3.1.0", 1795 | "rimraf": "^3.0.2" 1796 | } 1797 | }, 1798 | "flatted": { 1799 | "version": "3.2.7", 1800 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", 1801 | "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", 1802 | "dev": true 1803 | }, 1804 | "fs.realpath": { 1805 | "version": "1.0.0", 1806 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 1807 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 1808 | "dev": true 1809 | }, 1810 | "functional-red-black-tree": { 1811 | "version": "1.0.1", 1812 | "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", 1813 | "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", 1814 | "dev": true 1815 | }, 1816 | "glob": { 1817 | "version": "7.2.3", 1818 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 1819 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 1820 | "dev": true, 1821 | "requires": { 1822 | "fs.realpath": "^1.0.0", 1823 | "inflight": "^1.0.4", 1824 | "inherits": "2", 1825 | "minimatch": "^3.1.1", 1826 | "once": "^1.3.0", 1827 | "path-is-absolute": "^1.0.0" 1828 | } 1829 | }, 1830 | "glob-parent": { 1831 | "version": "6.0.2", 1832 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 1833 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 1834 | "dev": true, 1835 | "requires": { 1836 | "is-glob": "^4.0.3" 1837 | } 1838 | }, 1839 | "globals": { 1840 | "version": "13.17.0", 1841 | "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", 1842 | "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", 1843 | "dev": true, 1844 | "requires": { 1845 | "type-fest": "^0.20.2" 1846 | } 1847 | }, 1848 | "globby": { 1849 | "version": "11.1.0", 1850 | "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", 1851 | "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", 1852 | "dev": true, 1853 | "requires": { 1854 | "array-union": "^2.1.0", 1855 | "dir-glob": "^3.0.1", 1856 | "fast-glob": "^3.2.9", 1857 | "ignore": "^5.2.0", 1858 | "merge2": "^1.4.1", 1859 | "slash": "^3.0.0" 1860 | } 1861 | }, 1862 | "grapheme-splitter": { 1863 | "version": "1.0.4", 1864 | "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", 1865 | "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", 1866 | "dev": true 1867 | }, 1868 | "has-flag": { 1869 | "version": "4.0.0", 1870 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1871 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 1872 | "dev": true 1873 | }, 1874 | "ignore": { 1875 | "version": "5.2.0", 1876 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", 1877 | "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", 1878 | "dev": true 1879 | }, 1880 | "import-fresh": { 1881 | "version": "3.3.0", 1882 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", 1883 | "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", 1884 | "dev": true, 1885 | "requires": { 1886 | "parent-module": "^1.0.0", 1887 | "resolve-from": "^4.0.0" 1888 | } 1889 | }, 1890 | "imurmurhash": { 1891 | "version": "0.1.4", 1892 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 1893 | "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 1894 | "dev": true 1895 | }, 1896 | "inflight": { 1897 | "version": "1.0.6", 1898 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 1899 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 1900 | "dev": true, 1901 | "requires": { 1902 | "once": "^1.3.0", 1903 | "wrappy": "1" 1904 | } 1905 | }, 1906 | "inherits": { 1907 | "version": "2.0.4", 1908 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1909 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1910 | "dev": true 1911 | }, 1912 | "is-extglob": { 1913 | "version": "2.1.1", 1914 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1915 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1916 | "dev": true 1917 | }, 1918 | "is-glob": { 1919 | "version": "4.0.3", 1920 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1921 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1922 | "dev": true, 1923 | "requires": { 1924 | "is-extglob": "^2.1.1" 1925 | } 1926 | }, 1927 | "is-number": { 1928 | "version": "7.0.0", 1929 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1930 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1931 | "dev": true 1932 | }, 1933 | "isexe": { 1934 | "version": "2.0.0", 1935 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1936 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 1937 | "dev": true 1938 | }, 1939 | "js-yaml": { 1940 | "version": "4.1.0", 1941 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 1942 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 1943 | "dev": true, 1944 | "requires": { 1945 | "argparse": "^2.0.1" 1946 | } 1947 | }, 1948 | "json-schema-traverse": { 1949 | "version": "0.4.1", 1950 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 1951 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 1952 | "dev": true 1953 | }, 1954 | "json-stable-stringify-without-jsonify": { 1955 | "version": "1.0.1", 1956 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 1957 | "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 1958 | "dev": true 1959 | }, 1960 | "levn": { 1961 | "version": "0.4.1", 1962 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 1963 | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 1964 | "dev": true, 1965 | "requires": { 1966 | "prelude-ls": "^1.2.1", 1967 | "type-check": "~0.4.0" 1968 | } 1969 | }, 1970 | "locate-path": { 1971 | "version": "6.0.0", 1972 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 1973 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 1974 | "dev": true, 1975 | "requires": { 1976 | "p-locate": "^5.0.0" 1977 | } 1978 | }, 1979 | "lodash.merge": { 1980 | "version": "4.6.2", 1981 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 1982 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 1983 | "dev": true 1984 | }, 1985 | "merge2": { 1986 | "version": "1.4.1", 1987 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 1988 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 1989 | "dev": true 1990 | }, 1991 | "micromatch": { 1992 | "version": "4.0.5", 1993 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", 1994 | "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", 1995 | "dev": true, 1996 | "requires": { 1997 | "braces": "^3.0.2", 1998 | "picomatch": "^2.3.1" 1999 | } 2000 | }, 2001 | "minimatch": { 2002 | "version": "3.1.2", 2003 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 2004 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 2005 | "dev": true, 2006 | "requires": { 2007 | "brace-expansion": "^1.1.7" 2008 | } 2009 | }, 2010 | "ms": { 2011 | "version": "2.1.2", 2012 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 2013 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 2014 | "dev": true 2015 | }, 2016 | "natural-compare": { 2017 | "version": "1.4.0", 2018 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 2019 | "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 2020 | "dev": true 2021 | }, 2022 | "once": { 2023 | "version": "1.4.0", 2024 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 2025 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 2026 | "dev": true, 2027 | "requires": { 2028 | "wrappy": "1" 2029 | } 2030 | }, 2031 | "optionator": { 2032 | "version": "0.9.1", 2033 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", 2034 | "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", 2035 | "dev": true, 2036 | "requires": { 2037 | "deep-is": "^0.1.3", 2038 | "fast-levenshtein": "^2.0.6", 2039 | "levn": "^0.4.1", 2040 | "prelude-ls": "^1.2.1", 2041 | "type-check": "^0.4.0", 2042 | "word-wrap": "^1.2.3" 2043 | } 2044 | }, 2045 | "p-limit": { 2046 | "version": "3.1.0", 2047 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 2048 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 2049 | "dev": true, 2050 | "requires": { 2051 | "yocto-queue": "^0.1.0" 2052 | } 2053 | }, 2054 | "p-locate": { 2055 | "version": "5.0.0", 2056 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 2057 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 2058 | "dev": true, 2059 | "requires": { 2060 | "p-limit": "^3.0.2" 2061 | } 2062 | }, 2063 | "parent-module": { 2064 | "version": "1.0.1", 2065 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 2066 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 2067 | "dev": true, 2068 | "requires": { 2069 | "callsites": "^3.0.0" 2070 | } 2071 | }, 2072 | "path-exists": { 2073 | "version": "4.0.0", 2074 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 2075 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 2076 | "dev": true 2077 | }, 2078 | "path-is-absolute": { 2079 | "version": "1.0.1", 2080 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 2081 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 2082 | "dev": true 2083 | }, 2084 | "path-key": { 2085 | "version": "3.1.1", 2086 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 2087 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 2088 | "dev": true 2089 | }, 2090 | "path-type": { 2091 | "version": "4.0.0", 2092 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", 2093 | "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", 2094 | "dev": true 2095 | }, 2096 | "picomatch": { 2097 | "version": "2.3.1", 2098 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 2099 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 2100 | "dev": true 2101 | }, 2102 | "prelude-ls": { 2103 | "version": "1.2.1", 2104 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 2105 | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 2106 | "dev": true 2107 | }, 2108 | "prettier": { 2109 | "version": "3.5.3", 2110 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", 2111 | "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", 2112 | "dev": true 2113 | }, 2114 | "prettier-plugin-go-template": { 2115 | "version": "0.0.15", 2116 | "resolved": "https://registry.npmjs.org/prettier-plugin-go-template/-/prettier-plugin-go-template-0.0.15.tgz", 2117 | "integrity": "sha512-WqU92E1NokWYNZ9mLE6ijoRg6LtIGdLMePt2C7UBDjXeDH9okcRI3zRqtnWR4s5AloiqyvZ66jNBAa9tmRY5EQ==", 2118 | "dev": true, 2119 | "requires": { 2120 | "ulid": "^2.3.0" 2121 | } 2122 | }, 2123 | "punycode": { 2124 | "version": "2.1.1", 2125 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 2126 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 2127 | "dev": true 2128 | }, 2129 | "queue-microtask": { 2130 | "version": "1.2.3", 2131 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 2132 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 2133 | "dev": true 2134 | }, 2135 | "regexpp": { 2136 | "version": "3.2.0", 2137 | "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", 2138 | "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", 2139 | "dev": true 2140 | }, 2141 | "resolve-from": { 2142 | "version": "4.0.0", 2143 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 2144 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 2145 | "dev": true 2146 | }, 2147 | "reusify": { 2148 | "version": "1.0.4", 2149 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 2150 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 2151 | "dev": true 2152 | }, 2153 | "rimraf": { 2154 | "version": "3.0.2", 2155 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 2156 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 2157 | "dev": true, 2158 | "requires": { 2159 | "glob": "^7.1.3" 2160 | } 2161 | }, 2162 | "run-parallel": { 2163 | "version": "1.2.0", 2164 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 2165 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 2166 | "dev": true, 2167 | "requires": { 2168 | "queue-microtask": "^1.2.2" 2169 | } 2170 | }, 2171 | "shebang-command": { 2172 | "version": "2.0.0", 2173 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 2174 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 2175 | "dev": true, 2176 | "requires": { 2177 | "shebang-regex": "^3.0.0" 2178 | } 2179 | }, 2180 | "shebang-regex": { 2181 | "version": "3.0.0", 2182 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 2183 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 2184 | "dev": true 2185 | }, 2186 | "slash": { 2187 | "version": "3.0.0", 2188 | "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", 2189 | "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", 2190 | "dev": true 2191 | }, 2192 | "strip-ansi": { 2193 | "version": "6.0.1", 2194 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 2195 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 2196 | "dev": true, 2197 | "requires": { 2198 | "ansi-regex": "^5.0.1" 2199 | } 2200 | }, 2201 | "strip-json-comments": { 2202 | "version": "3.1.1", 2203 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 2204 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 2205 | "dev": true 2206 | }, 2207 | "supports-color": { 2208 | "version": "7.2.0", 2209 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 2210 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 2211 | "dev": true, 2212 | "requires": { 2213 | "has-flag": "^4.0.0" 2214 | } 2215 | }, 2216 | "text-table": { 2217 | "version": "0.2.0", 2218 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 2219 | "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", 2220 | "dev": true 2221 | }, 2222 | "to-regex-range": { 2223 | "version": "5.0.1", 2224 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 2225 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 2226 | "dev": true, 2227 | "requires": { 2228 | "is-number": "^7.0.0" 2229 | } 2230 | }, 2231 | "type-check": { 2232 | "version": "0.4.0", 2233 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 2234 | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 2235 | "dev": true, 2236 | "requires": { 2237 | "prelude-ls": "^1.2.1" 2238 | } 2239 | }, 2240 | "type-fest": { 2241 | "version": "0.20.2", 2242 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", 2243 | "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", 2244 | "dev": true 2245 | }, 2246 | "ulid": { 2247 | "version": "2.3.0", 2248 | "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz", 2249 | "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==", 2250 | "dev": true 2251 | }, 2252 | "uri-js": { 2253 | "version": "4.4.1", 2254 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 2255 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 2256 | "dev": true, 2257 | "requires": { 2258 | "punycode": "^2.1.0" 2259 | } 2260 | }, 2261 | "v8-compile-cache": { 2262 | "version": "2.3.0", 2263 | "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", 2264 | "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", 2265 | "dev": true 2266 | }, 2267 | "which": { 2268 | "version": "2.0.2", 2269 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 2270 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 2271 | "dev": true, 2272 | "requires": { 2273 | "isexe": "^2.0.0" 2274 | } 2275 | }, 2276 | "word-wrap": { 2277 | "version": "1.2.3", 2278 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", 2279 | "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", 2280 | "dev": true 2281 | }, 2282 | "wrappy": { 2283 | "version": "1.0.2", 2284 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 2285 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 2286 | "dev": true 2287 | }, 2288 | "yocto-queue": { 2289 | "version": "0.1.0", 2290 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 2291 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 2292 | "dev": true 2293 | } 2294 | } 2295 | } 2296 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logpaste-dev", 3 | "version": "1.0.0", 4 | "description": "LogPaste dev scripts", 5 | "scripts": { 6 | "check-format": "prettier --check .", 7 | "format": "prettier --write .", 8 | "lint": "eslint ./**/*.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/mtlynch/logpaste.git" 13 | }, 14 | "author": "Michael Lynch", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/mtlynch/logpaste/issues" 18 | }, 19 | "homepage": "https://logpaste.com", 20 | "devDependencies": { 21 | "eslint": "^8.22.0", 22 | "eslint-plugin-cypress": "^2.12.1", 23 | "prettier": "3.5.3", 24 | "prettier-plugin-go-template": "0.0.15" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /random/string.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | ) 7 | 8 | var characters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 9 | 10 | func String(n int) string { 11 | b := make([]rune, n) 12 | for i := range b { 13 | n, err := rand.Int(rand.Reader, big.NewInt(int64(len(characters)))) 14 | if err != nil { 15 | panic(err) 16 | } 17 | b[i] = characters[n.Int64()] 18 | } 19 | return string(b) 20 | } 21 | -------------------------------------------------------------------------------- /store/sqlite/migrations.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "embed" 7 | "fmt" 8 | "log" 9 | "path" 10 | "sort" 11 | "strconv" 12 | ) 13 | 14 | type dbMigration struct { 15 | version int 16 | query string 17 | } 18 | 19 | //go:embed migrations/*.sql 20 | var migrationsFs embed.FS 21 | 22 | func applyMigrations(ctx *sql.DB) { 23 | var version int 24 | if err := ctx.QueryRow(`PRAGMA user_version`).Scan(&version); err != nil { 25 | log.Fatalf("failed to get user_version: %v", err) 26 | } 27 | 28 | migrations, err := loadMigrations() 29 | if err != nil { 30 | log.Fatalf("error loading database migrations: %v", err) 31 | } 32 | 33 | log.Printf("Migration counter: %d/%d", version, len(migrations)) 34 | 35 | for _, migration := range migrations { 36 | if migration.version <= version { 37 | continue 38 | } 39 | tx, err := ctx.BeginTx(context.Background(), nil) 40 | if err != nil { 41 | log.Fatalf("failed to create migration transaction %d: %v", migration.version, err) 42 | } 43 | 44 | _, err = tx.Exec(migration.query) 45 | if err != nil { 46 | log.Fatalf("failed to perform DB migration %d: %v", migration.version, err) 47 | } 48 | 49 | _, err = tx.Exec(fmt.Sprintf(`pragma user_version=%d`, migration.version)) 50 | if err != nil { 51 | log.Fatalf("failed to update DB version to %d: %v", migration.version, err) 52 | } 53 | 54 | if err = tx.Commit(); err != nil { 55 | log.Fatalf("failed to commit migration %d: %v", migration.version, err) 56 | } 57 | 58 | log.Printf("Migration counter: %d/%d", migration.version, len(migrations)) 59 | } 60 | } 61 | 62 | func loadMigrations() ([]dbMigration, error) { 63 | migrations := []dbMigration{} 64 | 65 | migrationsDir := "migrations" 66 | 67 | entries, err := migrationsFs.ReadDir(migrationsDir) 68 | if err != nil { 69 | return []dbMigration{}, err 70 | } 71 | 72 | for _, entry := range entries { 73 | if entry.IsDir() { 74 | continue 75 | } 76 | 77 | version := migrationVersionFromFilename(entry.Name()) 78 | 79 | query, err := migrationsFs.ReadFile(path.Join(migrationsDir, entry.Name())) 80 | if err != nil { 81 | return []dbMigration{}, err 82 | } 83 | 84 | migrations = append(migrations, dbMigration{version, string(query)}) 85 | } 86 | sort.Slice(migrations, func(i, j int) bool { 87 | return migrations[i].version < migrations[j].version 88 | }) 89 | 90 | return migrations, nil 91 | } 92 | 93 | func migrationVersionFromFilename(filename string) int { 94 | version, err := strconv.ParseInt(filename[:3], 10, 32) 95 | if err != nil { 96 | log.Fatalf("invalid migration number in filename: %v", filename) 97 | } 98 | 99 | return int(version) 100 | } 101 | -------------------------------------------------------------------------------- /store/sqlite/migrations/001-create-entries-table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS entries ( 2 | id TEXT PRIMARY KEY, 3 | creation_time TEXT, 4 | contents TEXT 5 | ); 6 | -------------------------------------------------------------------------------- /store/sqlite/sqlite.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | _ "github.com/mattn/go-sqlite3" 10 | 11 | "github.com/mtlynch/logpaste/store" 12 | ) 13 | 14 | type db struct { 15 | ctx *sql.DB 16 | } 17 | 18 | func New() store.Store { 19 | dbDir := "data" 20 | ensureDirExists(dbDir) 21 | ctx, err := sql.Open("sqlite3", dbDir+"/store.db") 22 | if err != nil { 23 | log.Fatalln(err) 24 | } 25 | 26 | if _, err := ctx.Exec(` 27 | -- Apply Litestream recommendations: https://litestream.io/tips/ 28 | PRAGMA busy_timeout = 5000; 29 | PRAGMA synchronous = NORMAL; 30 | PRAGMA journal_mode = WAL; 31 | PRAGMA wal_autocheckpoint = 0; 32 | `); err != nil { 33 | log.Fatalf("failed to set pragmas: %v", err) 34 | } 35 | 36 | applyMigrations(ctx) 37 | 38 | return &db{ 39 | ctx: ctx, 40 | } 41 | } 42 | 43 | func (d db) GetEntry(id string) (string, error) { 44 | var contents string 45 | if err := d.ctx.QueryRow("SELECT contents FROM entries WHERE id=?", id).Scan(&contents); err != nil { 46 | if err == sql.ErrNoRows { 47 | return "", store.EntryNotFoundError{ID: id} 48 | } 49 | return "", err 50 | } 51 | return contents, nil 52 | } 53 | 54 | func (d db) InsertEntry(id string, contents string) error { 55 | _, err := d.ctx.Exec(` 56 | INSERT INTO entries( 57 | id, 58 | creation_time, 59 | contents) 60 | values(?,?,?)`, id, time.Now().Format(time.RFC3339), contents) 61 | return err 62 | } 63 | 64 | func ensureDirExists(dir string) { 65 | if _, err := os.Stat(dir); os.IsNotExist(err) { 66 | if err := os.Mkdir(dir, os.ModePerm); err != nil { 67 | panic(err) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "fmt" 4 | 5 | type Store interface { 6 | GetEntry(id string) (string, error) 7 | InsertEntry(id string, contents string) error 8 | } 9 | 10 | // EntryNotFoundError occurs when no entry exists with the given ID. 11 | type EntryNotFoundError struct { 12 | ID string 13 | } 14 | 15 | func (f EntryNotFoundError) Error() string { 16 | return fmt.Sprintf("could not find entry with ID=%v", f.ID) 17 | } 18 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ .Title }} 7 | 8 | 9 | 10 |
11 |

{{ .Title }}

12 | 13 |

{{ .Subtitle }}

14 | 15 |
16 |

Upload via web UI

17 | 18 |
19 | 20 | 21 |
22 | 23 |
24 |
25 | 26 |
27 | 28 |

 

29 |
30 | 31 | {{ if .ShowDocs }} 32 |
33 |
34 |

Upload from the command line

35 |

Upload text:

36 |
37 | 38 |

Upload a file's contents:

39 |
40 |
41 | 42 |
43 |

Upload via JavaScript

44 | 45 |
46 |
47 |
48 | {{ end }} 49 | 50 | 51 |
52 |

Source code

53 |

54 | {{ .Title }}'s source is available under the MIT License and easy to 55 | self-host. 56 |

57 | 64 |
65 | 66 | {{ if .FooterHTML }} 67 | 70 | {{ end }} 71 |
72 | 73 | {{ if .ShowDocs }} 74 | 75 | {{ end }} 76 | 77 | 78 | 79 | 80 | --------------------------------------------------------------------------------