├── .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 | [](https://circleci.com/gh/mtlynch/logpaste)
4 | [](https://hub.docker.com/r/mtlynch/logpaste/)
5 | [](LICENSE)
6 |
7 | A minimalist web service for uploading and sharing log files.
8 |
9 | [](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 | 
25 |
26 |
27 |
28 | Choose a Nano server with 1x scale.
29 |
30 |
31 |
32 | 
33 |
34 |
35 |
36 | Click "Set up deployment" and then choose "Specify a custom deployment."
37 |
38 |
39 |
40 | 
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 | 
65 |
66 |
67 |
68 | Under "Public Endpoint," select the container you created above:
69 |
70 |
71 |
72 | 
73 |
74 |
75 |
76 | Choose a name for your service (it can be the same as your container name):
77 |
78 |
79 |
80 | 
81 |
82 |
83 |
84 | Finally, click "Create container service."
85 |
86 |
87 |
88 | 
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 | 
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 | 
107 |
108 |
109 |
110 | When your container is running, you can access it through the "Public domain" URL.
111 |
112 |
113 |
114 | 
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 | 
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+""+i.tag+">"},!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 |
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 |
--------------------------------------------------------------------------------