diff --git a/.browserslistrc b/.browserslistrc deleted file mode 100644 index e94f8140cc00..000000000000 --- a/.browserslistrc +++ /dev/null @@ -1 +0,0 @@ -defaults diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000000..9145a6c10ffa --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,27 @@ +{ + "name": "Password Pusher", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "24" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "Shopify.ruby-lsp" + ], + "settings": { + "editor.formatOnSave": true + } + } + }, + "forwardPorts": [3000, 5432], + "postCreateCommand": "apt-get update && apt-get install -y --no-install-recommends build-essential libpq-dev libyaml-dev && corepack enable && bin/setup", + "remoteEnv": { + "DATABASE_URL": "postgres://postgres:postgres@db:5432/pwpush_development", + "PWP__REQUIRE_MFA": "false" + } +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 000000000000..0b9f3fe9ea70 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,28 @@ +# Dev-only compose: mounts repo into Ruby container + Postgres. +# Root docker-compose.yml is production-oriented (pre-built image); use this for Codespaces/VS Code devcontainer. + +services: + app: + image: ruby:4.0.2-bookworm + volumes: + - ..:/workspace:cached + working_dir: /workspace + command: sleep infinity + depends_on: + - db + environment: + DATABASE_URL: postgres://postgres:postgres@db:5432/pwpush_development + + db: + image: postgres:16 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: pwpush_development + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + +volumes: + pgdata: diff --git a/.do/deploy.template.yaml b/.do/deploy.template.yaml index a9457ebec943..98f9f33186f2 100644 --- a/.do/deploy.template.yaml +++ b/.do/deploy.template.yaml @@ -1,95 +1,200 @@ +# DigitalOcean App Platform deploy template (Deploy to DO button). +# App spec reference: https://docs.digitalocean.com/products/app-platform/reference/app-spec/ +# Full PWP__ options: docker-compose.yml and https://docs.pwpush.com/docs/self-hosted-configuration/ +# +# Security: Replace SECRET placeholders in the DO UI after deploy—never commit real keys. + spec: name: passwordpusher - services: - - name: pwpush - environment_slug: ruby-on-rails - git: - branch: master - repo_clone_url: https://github.com/pglombardo/PasswordPusher.git - envs: - # Private - - key: RAILS_ENV - scope: RUN_AND_BUILD_TIME - type: GENERAL - value: 'production' - - key: RACK_ENV - scope: RUN_AND_BUILD_TIME - type: GENERAL - value: 'production' - - key: BUNDLE_WITHOUT - scope: RUN_AND_BUILD_TIME - type: GENERAL - value: 'development:test' - # Application Encryption Key - - key: PWPUSH_MASTER_KEY - scope: RUN_TIME - type: SECRET - value: '569679f4d3f5739e473bbd42eadca98a0a1b219a32e7bd4b0832a862b88414c8' - # The number of processes to run. - - key: WEB_CONCURRENCY - scope: RUN_TIME - type: GENERAL - value: '3' - # Default value for expire after days - - key: PWP__EXPIRE_AFTER_DAYS_DEFAULT - scope: RUN_TIME - type: GENERAL - value: '7' - # Expire after days minimum value - - key: PWP__EXPIRE_AFTER_DAYS_MIN - scope: RUN_TIME - type: GENERAL - value: '1' - # Expire after days maximum value - - key: PWP__EXPIRE_AFTER_DAYS_MAX - scope: RUN_TIME - type: GENERAL - value: '90' - # Expire after views default value - - key: PWP__EXPIRE_AFTER_VIEWS_DEFAULT - scope: RUN_TIME - type: GENERAL - value: '5' - # Expire after views minimum value - - key: PWP__EXPIRE_AFTER_VIEWS_MIN - scope: RUN_TIME - type: GENERAL - value: '1' - # Expire after views maximum value - - key: PWP__EXPIRE_AFTER_VIEWS_MAX - scope: RUN_TIME - type: GENERAL - value: '100' - # When true, passwords will have a link to optionally delete the password being viewed. When pushing a new password, this option will also add a checkbox to conditionally enable/disable this feature on a per-password basis. - - key: PWP__ENABLE_DELETABLE_PUSHES - scope: RUN_TIME - type: GENERAL - value: 'true' - # When the option DELETABLE_PASSWORDS_ENABLED is set to true, this option does two things: 1. Sets the default check state for the \'Allow viewers to optionally delete password before expiration\' checkbox. 2. Sets the default value for newly pushed passwords if unspecified (such as with a json request) - - key: PWP__DELETABLE_PUSHES_DEFAULT - scope: RUN_TIME - type: GENERAL - value: 'true' - instance_count: 1 - instance_size_slug: basic-xxs + region: nyc + + features: + - buildpack-stack=ubuntu-22 + + alerts: + - rule: DEPLOYMENT_FAILED + - rule: DOMAIN_FAILED + databases: - - name: 'pwpush-db' - engine: PG + - name: pwpush-db + engine: PG + version: "16" + production: true + + services: + - name: pwpush + environment_slug: ruby-on-rails + github: + branch: master + deploy_on_push: true + repo: pglombardo/PasswordPusher + # Match Procfile web process; thrust runs Puma bound to PORT. + run_command: bundle exec bin/thrust bin/rails server + instance_count: 1 + instance_size_slug: apps-s-1vcpu-1gb + http_port: 8080 + envs: + # --- Build / runtime --- + - key: RAILS_ENV + scope: RUN_AND_BUILD_TIME + type: GENERAL + value: production + - key: RACK_ENV + scope: RUN_AND_BUILD_TIME + type: GENERAL + value: production + - key: BUNDLE_WITHOUT + scope: RUN_AND_BUILD_TIME + type: GENERAL + value: "development:test" + - key: RAILS_LOG_TO_STDOUT + scope: RUN_TIME + type: GENERAL + value: "true" + - key: RAILS_SERVE_STATIC_FILES + scope: RUN_TIME + type: GENERAL + value: "true" + + # --- Secrets (set in App Platform UI; placeholders avoid committing real values) --- + - key: SECRET_KEY_BASE + scope: RUN_TIME + type: SECRET + value: "REPLACE_ME_run_rails_secret" + - key: PWPUSH_MASTER_KEY + scope: RUN_TIME + type: SECRET + value: "REPLACE_ME_run_rails_runner_puts_Lockbox_generate_key" + + # --- Concurrency --- + - key: WEB_CONCURRENCY + scope: RUN_TIME + type: GENERAL + value: "0" + + # --- URL / host (set to your app hostname after first deploy) --- + - key: PWP__HOST_PROTOCOL + scope: RUN_TIME + type: GENERAL + value: https + # - key: PWP__HOST_DOMAIN + # scope: RUN_TIME + # type: GENERAL + # value: your-app.ondigitalocean.app + # - key: PWP__ALLOWED_HOSTS + # scope: RUN_TIME + # type: GENERAL + # value: your-app.ondigitalocean.app + + # --- Feature toggles (2.0 defaults) --- + - key: PWP__ALLOW_ANONYMOUS + scope: RUN_TIME + type: GENERAL + value: "true" + - key: PWP__DISABLE_SIGNUPS + scope: RUN_TIME + type: GENERAL + value: "false" + - key: PWP__DISABLE_LOGINS + scope: RUN_TIME + type: GENERAL + value: "false" + - key: PWP__REQUIRE_MFA + scope: RUN_TIME + type: GENERAL + value: "false" + - key: PWP__ENABLE_USER_ACCOUNT_EMAILS + scope: RUN_TIME + type: GENERAL + value: "false" + - key: PWP__ENABLE_FILE_PUSHES + scope: RUN_TIME + type: GENERAL + value: "true" + - key: PWP__ENABLE_URL_PUSHES + scope: RUN_TIME + type: GENERAL + value: "true" + - key: PWP__ENABLE_QR_PUSHES + scope: RUN_TIME + type: GENERAL + value: "true" + + # --- File storage: local is ephemeral on App Platform; use s3 for durable file pushes --- + - key: PWP__FILES__STORAGE + scope: RUN_TIME + type: GENERAL + value: local + + # --- Password push (pw) limits & defaults --- + - key: PWP__PW__EXPIRE_AFTER_DAYS_DEFAULT + scope: RUN_TIME + type: GENERAL + value: "7" + - key: PWP__PW__EXPIRE_AFTER_DAYS_MIN + scope: RUN_TIME + type: GENERAL + value: "1" + - key: PWP__PW__EXPIRE_AFTER_DAYS_MAX + scope: RUN_TIME + type: GENERAL + value: "90" + - key: PWP__PW__EXPIRE_AFTER_VIEWS_DEFAULT + scope: RUN_TIME + type: GENERAL + value: "5" + - key: PWP__PW__EXPIRE_AFTER_VIEWS_MIN + scope: RUN_TIME + type: GENERAL + value: "1" + - key: PWP__PW__EXPIRE_AFTER_VIEWS_MAX + scope: RUN_TIME + type: GENERAL + value: "100" + - key: PWP__PW__ENABLE_DELETABLE_PUSHES + scope: RUN_TIME + type: GENERAL + value: "true" + - key: PWP__PW__DELETABLE_PUSHES_DEFAULT + scope: RUN_TIME + type: GENERAL + value: "true" + - key: PWP__PW__ENABLE_RETRIEVAL_STEP + scope: RUN_TIME + type: GENERAL + value: "true" + - key: PWP__PW__RETRIEVAL_STEP_DEFAULT + scope: RUN_TIME + type: GENERAL + value: "true" + + - key: PWP__THEME + scope: RUN_TIME + type: GENERAL + value: default + jobs: - - environment_slug: ruby-on-rails - envs: - - key: RACK_ENV - scope: RUN_AND_BUILD_TIME - value: production - - key: RAILS_ENV - scope: RUN_AND_BUILD_TIME - value: production - github: - branch: master - deploy_on_push: true - repo: pglombardo/PasswordPusher - instance_count: 1 - instance_size_slug: basic-xxs - kind: POST_DEPLOY - name: run-migrations-after-deploys - run_command: bundle exec rails db:migrate + # Runs after each deploy: creates DB if needed and migrates. + - name: db-prepare-after-deploy + kind: POST_DEPLOY + environment_slug: ruby-on-rails + github: + branch: master + deploy_on_push: true + repo: pglombardo/PasswordPusher + run_command: bundle exec rails db:prepare + instance_count: 1 + instance_size_slug: apps-s-1vcpu-0.5gb + envs: + - key: RAILS_ENV + scope: RUN_AND_BUILD_TIME + type: GENERAL + value: production + - key: RACK_ENV + scope: RUN_AND_BUILD_TIME + type: GENERAL + value: production + - key: BUNDLE_WITHOUT + scope: RUN_AND_BUILD_TIME + type: GENERAL + value: "development:test" diff --git a/.dockerignore b/.dockerignore index d3555845485e..4fe520d601a5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,4 @@ .env* .dockerenv* tmp/ +node_modules diff --git a/.erb_lint.yml b/.erb_lint.yml new file mode 100644 index 000000000000..57498cc21658 --- /dev/null +++ b/.erb_lint.yml @@ -0,0 +1,9 @@ +--- +exclude: + - 'vendor/bundle/**/*' +EnableDefaultLinters: true +linters: + SpaceAroundErbTag: + enabled: true + ExtraNewline: + enabled: true diff --git a/.gitattributes b/.gitattributes index 0084e3205675..aa99e98b0246 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ -# reclassifies `.html` files as Ruby so the repo language is correct: *.html linguist-language=Ruby +app/assets/stylesheets/themes/* linguist-vendored=true +app/assets/javascripts/application.js linguist-vendored=true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 1e675e71d1bf..000000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -name: 🐛 Bug report -about: If something isn't working 🔧 -title: '' -labels: bug -assignees: ---- - -## 🐛 Bug Report - - - - - -## 🔬 How To Reproduce - -Steps to reproduce the behavior: - -1. ... - -### Code sample - - - -### Environment - -Where are you running/using Password Pusher? - -- [ ] pwpush.com -- [ ] Docker Image - - [ ] pwpush - - [ ] custom image -- [ ] Heroku -- [ ] Digital Ocean -- [ ] Microsoft Azure -- [ ] Google Cloud -- [ ] AWS -- [ ] Source Code -- [ ] Other (please specify) - -If applicable, what version of Password Pusher? - -### Screenshots - - - -## 📈 Expected behavior - - - -## 📎 Additional context - - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000000..047a1f3f174e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,111 @@ +name: 🐛 Bug report +description: If something isn't working 🔧 +title: "[Bug]: " +labels: + - bug + +body: + - type: markdown + attributes: + value: | + ## 🐛 Bug Report + If something isn't working for you, we definitely want to know. + Thanks for taking the time to file an issue! + + - type: textarea + id: description + attributes: + label: What happened? + description: A clear and concise description of what the bug is. + placeholder: Tell us what you expected to happen vs what actually happened. + validations: + required: true + + - type: textarea + id: reproduce + attributes: + label: 🔬 How to reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. See error + validations: + required: true + + - type: textarea + id: code_sample + attributes: + label: 💻 Code sample + description: If applicable, attach a minimal code sample or configuration that reproduces the issue. + placeholder: Paste any minimal snippet/config here (remove secrets). + render: shell + + - type: dropdown + id: edition + attributes: + label: 🌍 Which edition + description: Which edition are you running of PwPush? + options: + - pwpush.com (Premium) + - pwpush.com (Pro) + - pwpush.com (Paid Support - Self Hosted) + - oss.pwpush.com + + - type: checkboxes + id: environment + attributes: + label: 🌍 Environment + description: Where are you running/using PwPush? + options: + - label: pwpush.com (Hosted) + - label: Docker Image – pwpush + - label: Docker Image – pwpush-public-gateway + - label: Docker Image – pwpush-worker + - label: Docker Image – Custom image + - label: Digital Ocean – App Platform + - label: Digital Ocean – Kubernetes Service + - label: Heroku + - label: Microsoft Azure – App Service + - label: Microsoft Azure – Container Instances (ACI) + - label: Microsoft Azure – Container Apps (ACA) + - label: Microsoft Azure – Kubernetes Service (AKS) + - label: Google Cloud – App Engine + - label: Google Cloud – Cloud Run + - label: Google Cloud – Kubernetes Engine + - label: AWS – Elastic Container Service (ECS) + - label: AWS – Kubernetes Service + - label: Source Code + - label: Other (please specify below) + + - type: input + id: other_install + attributes: + label: Other Install location + description: If applicable, where is your PWPush hosted? + placeholder: e.g. Alternative container based cloud provider + + - type: input + id: version + attributes: + label: 📦 App version + description: If applicable, what version are you running? + placeholder: e.g. v1.40.2 + + - type: textarea + id: screenshots + attributes: + label: 📸 Screenshots + description: If applicable, add screenshots to help explain your problem. You can paste images directly here. + + - type: textarea + id: expected + attributes: + label: 📈 Expected behavior + description: A clear and concise description of what you expected to happen. + + - type: textarea + id: additional + attributes: + label: 📎 Additional context + description: Add any other context, logs, or details about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 9a03d6058906..000000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: 🚀 Feature request -about: Suggest an idea for this project 🏖 -title: '' -labels: enhancement -assignees: ---- - -## 🚀 Feature Request - - - -## 🔈 Motivation - - - -## 🛰 Alternatives - - - -## 📎 Additional context - - - - diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000000..12746e1cef31 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,60 @@ +name: 🚀 Feature request +description: Suggest an idea for this project 🏖 +title: "[Feature Request]: " +labels: + - feature + +body: + - type: markdown + attributes: + value: | + ## 🚀 Feature Request + Thanks for taking the time to suggest an improvement! + The more detail you can share, the easier it is for us to evaluate and prioritise. + + - type: textarea + id: proposal + attributes: + label: What would you like to see? + description: A clear and concise description of the feature proposal. + placeholder: Describe the feature and what problem it solves. + validations: + required: true + + - type: textarea + id: motivation + attributes: + label: 🔈 Motivation + description: Please describe the motivation for this proposal. + placeholder: Why is this important? What workflow or outcome does it improve? + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: 🛰 Alternatives considered + description: A clear and concise description of any alternative solutions or features you've considered. + placeholder: What other approaches have you tried or considered? Why are they not ideal? + validations: + required: false + + - type: textarea + id: additional_context + attributes: + label: 📎 Additional context + description: Add any other context, links, or screenshots about the request here. + placeholder: Include mockups, screenshots, or related issues/PRs if you have them. + validations: + required: false + + - type: checkboxes + id: checks + attributes: + label: ✅ Checklist + description: Help us process this request faster by confirming the following. + options: + - label: I searched existing issues to make sure this hasn’t already been requested. + required: true + - label: This request describes a single feature (not multiple unrelated items). + required: false diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index f5d73db14b2d..000000000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: ❓ Question -about: Ask a question about this project 🎓 -title: '' -labels: question -assignees: ---- - -## Checklist - - - - - -- [ ] I've searched the project's [`issues`](https://github.com/pglombardo/PasswordPusher/issues?q=is%3Aissue). -- [ ] I've searched the project's [`discussions`](https://github.com/pglombardo/PasswordPusher/discussions). - -## ❓ Question - - - -How can I [...]? - -Is it possible to [...]? - -## 📎 Additional context - - diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 000000000000..de6b7cdcb22b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,48 @@ +name: "❓ Question" +description: "Ask a question about this project 🎓" +title: "[Q]: " +labels: + - question + +body: + - type: markdown + attributes: + value: | + ## ❓ Question + Thanks for reaching out! + If something isn't clearly documented or easy to understand, please tell us — that's valuable feedback. + + - type: checkboxes + id: preflight + attributes: + label: "✅ Checklist" + description: "Please tick anything you've already tried:" + options: + - label: "I've searched the project's issues." + required: false + - label: "I've searched the project's discussions." + required: false + - label: "I've checked the documentation / README." + required: false + + - type: textarea + id: question + attributes: + label: "Your question" + description: "What would you like to know?" + placeholder: | + Examples: + - How can I [...]? + - Is it possible to [...]? + - Why does [...] happen when I [...]? + validations: + required: true + + - type: textarea + id: context + attributes: + label: "📎 Additional context" + description: "Add any context, screenshots, error messages, logs, or links that help explain the question." + placeholder: "Include screenshots, command output, configuration snippets, and what you've already tried." + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fe7b02ac8565..e2a0792085a2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,12 +10,13 @@ -- [ ] 📚 Examples / docs / tutorials / dependencies update +- [ ] 📦 Dependency & security updates - [ ] 🔧 Bug fix (non-breaking change which fixes an issue) - [ ] 🥂 Improvement (non-breaking change which improves an existing feature) - [ ] 🚀 New feature (non-breaking change which adds functionality) - [ ] 💥 Breaking change (fix or feature that would cause existing functionality to change) - [ ] 🔐 Security fix +- [ ] 📚 Examples / documentation / tutorials ## Checklist diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3de045d5ef4f..d2bd6cd08bda 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,14 +12,7 @@ updates: - dependency-type: "all" commit-message: prefix: ":arrow_up:" - open-pull-requests-limit: 50 - ignore: - - dependency-name: "rack-throttle" - versions: [">=0.7.1"] - # FIXME: Bootstrap Bug - # https://github.com/pglombardo/PasswordPusher/issues/1377 - - dependency-name: "bootstrap" - versions: [">5.2.3"] + open-pull-requests-limit: 20 - package-ecosystem: "github-actions" directory: "/" @@ -29,7 +22,7 @@ updates: - dependency-type: "all" commit-message: prefix: ":arrow_up:" - open-pull-requests-limit: 50 + open-pull-requests-limit: 20 - package-ecosystem: "docker" directory: "/containers/docker" @@ -39,4 +32,14 @@ updates: - dependency-type: "all" commit-message: prefix: ":arrow_up:" - open-pull-requests-limit: 50 + open-pull-requests-limit: 20 + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-type: "all" + commit-message: + prefix: ":arrow_up:" + open-pull-requests-limit: 20 diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 791a5fb5472b..5951ac2f0332 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -19,7 +19,7 @@ categories: labels: [dependencies] template: | - ## What’s Changed + ## :memo: What’s Changed $CHANGES @@ -27,7 +27,33 @@ template: | $CONTRIBUTORS - ## Docker Images + ## :motor_boat: Docker Images Available on Docker Hub: https://hub.docker.com/r/pglombardo/pwpush + + ## :running_man: Run This Version + + 1. Point DNS to your server (e.g. `pwpush.example.com`). + 2. Download [docker-compose.yml](https://raw.githubusercontent.com/pglombardo/PasswordPusher/refs/heads/master/docker-compose.yml) or clone the repo. + 3. In `docker-compose.yml`, uncomment and set: + - `TLS_DOMAIN: 'pwpush.example.com'` for automatic Let’s Encrypt TLS. + 4. Run: + + ```bash + docker compose up -d + ``` + + Open `https://pwpush.example.com` or alternatively `http://your-ip:5100`. + + ## :link: Useful Links + + * [Documentation](https://docs.pwpush.com) + * [Docker Hub](https://hub.docker.com/r/pglombardo/pwpush) + * [GitHub](https://github.com/pglombardo/PasswordPusher) + * [pwpush.com](https://pwpush.com) + * [X](https://twitter.com/pwpush) + * [Facebook](https://www.facebook.com/pwpush) + * [Reddit](https://reddit.com/r/pwpush) + * [Newsletter](https://buttondown.email/pwpush) + * [Apnotic](https://www.apnotic.com) diff --git a/.github/workflows/brakeman-analysis.yml b/.github/workflows/brakeman-analysis.yml deleted file mode 100644 index db8b9f37c677..000000000000 --- a/.github/workflows/brakeman-analysis.yml +++ /dev/null @@ -1,35 +0,0 @@ -# This workflow integrates Brakeman with GitHub's Code Scanning feature -# Brakeman is a static analysis security vulnerability scanner for Ruby on Rails applications - -name: Brakeman Scan - -on: - push: - branches: [ master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - schedule: - - cron: '25 2 * * 6' - -jobs: - brakeman-scan: - name: Brakeman Scan - runs-on: ubuntu-latest - steps: - # Checkout the repository to the GitHub Actions runner - - name: Checkout - uses: actions/checkout@v4 - - # Customize the ruby version depending on your needs - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.0' - - - name: Run brakeman with reviewdog - uses: reviewdog/action-brakeman@v2.7.0 - # env: - # GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - with: - reporter: github-pr-review diff --git a/.github/workflows/campfire-notify.yml b/.github/workflows/campfire-notify.yml new file mode 100644 index 000000000000..1ab5315db22b --- /dev/null +++ b/.github/workflows/campfire-notify.yml @@ -0,0 +1,30 @@ +name: Campfire Notifications + +on: + push: + branches: [ master ] + +permissions: + contents: read + +jobs: + post-commit-info: + runs-on: ubuntu-latest + steps: + - id: campfire-configured + env: + CAMPFIRE_MESSAGES_URL: ${{ secrets.CAMPFIRE_MESSAGES_URL }} + run: | + if [ -n "${CAMPFIRE_MESSAGES_URL}" ]; then + echo "configured=true" >> "$GITHUB_OUTPUT" + else + echo "configured=false" >> "$GITHUB_OUTPUT" + fi + + - name: Post commit info to Campfire + if: steps.campfire-configured.outputs.configured == 'true' + continue-on-error: true + uses: shane-lamb/campfire-notify-action@df63cf73810147306276e281bedccf71c1519caf # v1.1.6 + with: + messages_url: ${{ secrets.CAMPFIRE_MESSAGES_URL }} + template: commit_pushed diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml deleted file mode 100644 index c31eb3f599bb..000000000000 --- a/.github/workflows/dependabot-auto-merge.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Dependabot Automerge - -on: - pull_request: - branches: [ master ] - -jobs: - auto-merge: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ahmadnassri/action-dependabot-auto-merge@v2 - with: - target: minor - github-token: ${{ secrets.AUTOMERGE_TOKEN }} diff --git a/.github/workflows/docker-containers.yml b/.github/workflows/docker-containers.yml index 509a385eb952..4570ffb56b1f 100644 --- a/.github/workflows/docker-containers.yml +++ b/.github/workflows/docker-containers.yml @@ -1,10 +1,8 @@ ---- name: Docker Container Builds on: push: tags: - - "release" - "v*.*.*" workflow_dispatch: @@ -16,53 +14,251 @@ on: default: false schedule: # * is a special character in YAML so you have to quote this string - # Run every day at 5:24 UTC - build 'latest' docker containers + # Run every day at 5:24 UTC - build 'nightly' docker containers - cron: "24 17 * * *" + + pull_request: + types: + - labeled + +permissions: + contents: read + env: DOCKER_PUSH: true jobs: - build: + docker-configured: runs-on: ubuntu-latest + outputs: + enabled: ${{ steps.check.outputs.enabled }} steps: - - name: Checkout - uses: actions/checkout@v4 + - id: check + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + run: | + if [ -n "${DOCKER_USERNAME}" ]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + fi - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + pwpush-container: + needs: docker-configured + if: needs.docker-configured.outputs.enabled == 'true' && ((github.event.label && github.event.label.name == 'docker') || github.event_name != 'pull_request') + runs-on: ubuntu-latest + steps: + - id: campfire-configured + env: + CAMPFIRE_MESSAGES_URL: ${{ secrets.CAMPFIRE_MESSAGES_URL }} + run: | + if [ -n "${CAMPFIRE_MESSAGES_URL}" ]; then + echo "configured=true" >> "$GITHUB_OUTPUT" + else + echo "configured=false" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} + fetch-depth: 1 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4 - name: Populate Docker metadata id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: images: ${{ secrets.DOCKER_USERNAME }}/pwpush - flavor: | - latest=false tags: | - type=match,pattern=release - type=schedule,pattern=latest + type=ref,event=pr,format=pr-{{ref}}-docker + type=match,pattern=stable + type=schedule,pattern=nightly type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - type=semver,pattern=latest + type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }} - name: Login to DockerHub - uses: docker/login-action@v3 + if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7 with: + context: . file: ./containers/docker/Dockerfile - platforms: linux/amd64,linux/arm64 - provenance: false - push: true + platforms: linux/amd64 + push: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }} + labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.meta.outputs.tags }} + cache-from: | + type=registry,ref=${{ secrets.DOCKER_USERNAME }}/pwpush:buildcache + type=gha + cache-to: | + type=registry,ref=${{ secrets.DOCKER_USERNAME }}/pwpush:buildcache,mode=max,ignore-error=${{env.DOCKER_PUSH == 'false'}} + type=gha,mode=max + + - name: Post job failure details to Campfire + if: failure() && steps.campfire-configured.outputs.configured == 'true' && github.ref == 'refs/heads/master' + continue-on-error: true + uses: shane-lamb/campfire-notify-action@df63cf73810147306276e281bedccf71c1519caf # v1.1.6 + with: + messages_url: ${{ secrets.CAMPFIRE_MESSAGES_URL }} + template: job_failed + + + public-gateway-container: + needs: [docker-configured, pwpush-container] + if: needs.docker-configured.outputs.enabled == 'true' && ((github.event.label && github.event.label.name == 'docker') || github.event_name != 'pull_request') + runs-on: ubuntu-latest + steps: + - id: campfire-configured + env: + CAMPFIRE_MESSAGES_URL: ${{ secrets.CAMPFIRE_MESSAGES_URL }} + run: | + if [ -n "${CAMPFIRE_MESSAGES_URL}" ]; then + echo "configured=true" >> "$GITHUB_OUTPUT" + else + echo "configured=false" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} + fetch-depth: 1 + + - name: Set up QEMU + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4 + with: + image: tonistiigi/binfmt:master + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4 + + - name: Populate Docker metadata + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 + with: + images: ${{ secrets.DOCKER_USERNAME }}/pwpush-public-gateway + tags: | + type=ref,event=pr,format=pr-{{ref}}-docker + type=match,pattern=stable + type=schedule,pattern=nightly + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }} + + - name: Login to DockerHub + if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7 + with: + context: . + file: ./containers/docker/Dockerfile.public-gateway + platforms: linux/amd64 + push: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }} labels: ${{ steps.meta.outputs.labels }} tags: ${{ steps.meta.outputs.tags }} - cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/pwpush:buildcache - cache-to: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/pwpush:buildcache,mode=max,ignore-error=${{env.DOCKER_PUSH == 'false'}} + cache-from: | + type=registry,ref=${{ secrets.DOCKER_USERNAME }}/pwpush-public-gateway:buildcache + type=gha + cache-to: | + type=registry,ref=${{ secrets.DOCKER_USERNAME }}/pwpush-public-gateway:buildcache,mode=max,ignore-error=${{env.DOCKER_PUSH == 'false'}} + type=gha,mode=max + + - name: Post job failure details to Campfire + if: failure() && steps.campfire-configured.outputs.configured == 'true' && github.ref == 'refs/heads/master' + continue-on-error: true + uses: shane-lamb/campfire-notify-action@df63cf73810147306276e281bedccf71c1519caf # v1.1.6 + with: + messages_url: ${{ secrets.CAMPFIRE_MESSAGES_URL }} + template: job_failed + + worker-container: + needs: [docker-configured, pwpush-container] + if: needs.docker-configured.outputs.enabled == 'true' && ((github.event.label && github.event.label.name == 'docker') || github.event_name != 'pull_request') + runs-on: ubuntu-latest + steps: + - id: campfire-configured + env: + CAMPFIRE_MESSAGES_URL: ${{ secrets.CAMPFIRE_MESSAGES_URL }} + run: | + if [ -n "${CAMPFIRE_MESSAGES_URL}" ]; then + echo "configured=true" >> "$GITHUB_OUTPUT" + else + echo "configured=false" >> "$GITHUB_OUTPUT" + fi + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} + fetch-depth: 1 + + - name: Set up QEMU + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4 + with: + image: tonistiigi/binfmt:master + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4 + + - name: Populate Docker metadata + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 + with: + images: ${{ secrets.DOCKER_USERNAME }}/pwpush-worker + tags: | + type=ref,event=pr,format=pr-{{ref}}-docker + type=match,pattern=stable + type=schedule,pattern=nightly + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }} + + - name: Login to DockerHub + if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7 + with: + context: . + file: ./containers/docker/Dockerfile.worker + platforms: linux/amd64 + push: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }} + labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.meta.outputs.tags }} + cache-from: | + type=registry,ref=${{ secrets.DOCKER_USERNAME }}/pwpush-worker:buildcache + type=gha + cache-to: | + type=registry,ref=${{ secrets.DOCKER_USERNAME }}/pwpush-worker:buildcache,mode=max,ignore-error=${{env.DOCKER_PUSH == 'false'}} + type=gha,mode=max + + - name: Post job failure details to Campfire + if: failure() && steps.campfire-configured.outputs.configured == 'true' && github.ref == 'refs/heads/master' + continue-on-error: true + uses: shane-lamb/campfire-notify-action@df63cf73810147306276e281bedccf71c1519caf # v1.1.6 + with: + messages_url: ${{ secrets.CAMPFIRE_MESSAGES_URL }} + template: job_failed diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml deleted file mode 100644 index 7d50bfd32046..000000000000 --- a/.github/workflows/greetings.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Greetings - -on: [pull_request, issues] - -permissions: - contents: read - pull-requests: write - issues: write - -jobs: - greeting: - runs-on: ubuntu-latest - steps: - - uses: actions/first-interaction@v1.3.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - pr-message: 'Hello @${{ github.actor }}, thank you for submitting a PR! We will respond as soon as possible.' - issue-message: | - Hello @${{ github.actor }}, thanks for contributing to the Password Pusher community! We will respond as soon as possible. diff --git a/.github/workflows/hatchbox-deploy.yml b/.github/workflows/hatchbox-deploy.yml new file mode 100644 index 000000000000..5c450c3cef62 --- /dev/null +++ b/.github/workflows/hatchbox-deploy.yml @@ -0,0 +1,30 @@ +name: Hatchbox Deploy + +on: + push: + tags: + - "v*.*.*" + +permissions: + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - id: hatchbox-configured + env: + HATCHBOX_DEPLOY_KEY: ${{ secrets.HATCHBOX_DEPLOY_KEY }} + run: | + if [ -n "${HATCHBOX_DEPLOY_KEY}" ]; then + echo "configured=true" >> "$GITHUB_OUTPUT" + else + echo "configured=false" >> "$GITHUB_OUTPUT" + fi + + - name: Deploy to Hatchbox + if: steps.hatchbox-configured.outputs.configured == 'true' + uses: hatchboxio/github-hatchbox-deploy-action@3bbadafe51e89bbf29fd3932bd84534f4fc9a00c # v2 + with: + deploy_key: ${{ secrets.HATCHBOX_DEPLOY_KEY }} + sha: ${{ github.sha }} diff --git a/.github/workflows/lint-helm.yaml b/.github/workflows/lint-helm.yaml new file mode 100644 index 000000000000..b7c8592c8b05 --- /dev/null +++ b/.github/workflows/lint-helm.yaml @@ -0,0 +1,69 @@ +name: Lint and Test Helm Charts + +on: pull_request + +permissions: + contents: read + +jobs: + lint-test: + runs-on: ubuntu-latest + env: + TARGET_BRANCH: ${{ github.event.repository.default_branch }} + steps: + - id: campfire-configured + env: + CAMPFIRE_MESSAGES_URL: ${{ secrets.CAMPFIRE_MESSAGES_URL }} + run: | + if [ -n "${CAMPFIRE_MESSAGES_URL}" ]; then + echo "configured=true" >> "$GITHUB_OUTPUT" + else + echo "configured=false" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - name: Set up Helm + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5 + with: + version: v3.12.1 + + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + name: Setup Python + with: + python-version: "3.10" + check-latest: true + + - name: Set up chart-testing + uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0 + + - name: Run chart-testing (list-changed) + id: list-changed + run: | + changed=$(ct list-changed --config ./ct.yaml --target-branch "$TARGET_BRANCH") + if [[ -n "$changed" ]]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Run chart-testing (lint) + if: steps.list-changed.outputs.changed == 'true' + run: ct lint --config ./ct.yaml --target-branch "$TARGET_BRANCH" + + - name: Create kind cluster + if: steps.list-changed.outputs.changed == 'true' + uses: helm/kind-action@06c1ae10762d3b9c1644e7fe69596ae519e015a2 # v1.15.0 + + - name: Run chart-testing (install) + if: steps.list-changed.outputs.changed == 'true' + run: ct install --config ./ct.yaml --target-branch "$TARGET_BRANCH" + + - name: Post job failure details to Campfire + if: failure() && steps.campfire-configured.outputs.configured == 'true' && github.base_ref == env.TARGET_BRANCH + continue-on-error: true + uses: shane-lamb/campfire-notify-action@df63cf73810147306276e281bedccf71c1519caf # v1.1.6 + with: + messages_url: ${{ secrets.CAMPFIRE_MESSAGES_URL }} + template: job_failed diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 4152be469334..cd4c95a7e91e 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -2,15 +2,20 @@ name: Release Drafter on: push: - # branches to consider in the event; optional, defaults to all branches: - master +permissions: + contents: read + jobs: update_release_draft: + permissions: + contents: write + pull-requests: write runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into "master" - - uses: release-drafter/release-drafter@v6.0.0 + - uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7 env: - GITHUB_TOKEN: ${{ secrets.RELDRAFTER_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ruby-tests.yml b/.github/workflows/ruby-tests.yml deleted file mode 100644 index 381e43243fde..000000000000 --- a/.github/workflows/ruby-tests.yml +++ /dev/null @@ -1,70 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake -# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby - -name: Ruby Test Runs - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - test: - - runs-on: ubuntu-latest - strategy: - matrix: - ruby-version: ['3.2'] - - env: - BUNDLE_WITHOUT: 'development' - - steps: - - uses: actions/checkout@v4 - - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby-version }} - bundler-cache: true # runs 'bundle install' and caches installed gems automatically - - - name: Setup Node 14 - uses: actions/setup-node@v4 - with: - node-version: 14.x - - - name: Get yarn cache - id: yarn-cache - run: echo "::set-output name=dir::$(yarn cache dir)" - - - name: Cache yarn - uses: actions/cache@v4 - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Cache gems - uses: actions/cache@v4 - with: - path: vendor/bundle - key: ${{ runner.os }}-gem-v1-${{ hashFiles('**/Gemfile.lock') }} - restore-keys: | - ${{ runner.os }}-gem-v1- - - - name: Install dependencies - run: | - gem install bundler - bundle config path vendor/bundle - bundle install --jobs 4 --retry 3 - yarn install --frozen-lockfile - - name: Run tests - run: | - bin/rails assets:precompile RAILS_ENV=test - bin/rails test diff --git a/.gitignore b/.gitignore index 8b2fd64b0bcf..97d96375a0c3 100644 --- a/.gitignore +++ b/.gitignore @@ -619,8 +619,8 @@ paket-files/ *.xsd.cs # Local .env files -.env -.dockerenv +.env* +.dockerenv* # OpenCover UI analysis results OpenCover/ @@ -663,5 +663,14 @@ storage/ bin/*.rb /config/credentials/development.key - +/config/credentials/staging.key /config/credentials/production.key + +/containers/docker/mariadb-data/ +/containers/docker/postgres-data/ +/containers/docker/mysql-data/ + +/public/assets/ +.cursor + + diff --git a/.overcommit.yml b/.overcommit.yml new file mode 100644 index 000000000000..187f4d311498 --- /dev/null +++ b/.overcommit.yml @@ -0,0 +1,77 @@ +# Use this file to configure the Overcommit hooks you wish to use. This will +# extend the default configuration defined in: +# https://github.com/sds/overcommit/blob/main/config/default.yml +# +# At the topmost level of this YAML file is a key representing type of hook +# being run (e.g. pre-commit, commit-msg, etc.). Within each type you can +# customize each hook, such as whether to only run it on certain files (via +# `include`), whether to only display output if it fails (via `quiet`), etc. +# +# For a complete list of hooks, see: +# https://github.com/sds/overcommit/tree/main/lib/overcommit/hook +# +# For a complete list of options that you can use to customize hooks, see: +# https://github.com/sds/overcommit#configuration +# +# Uncomment the following lines to make the configuration take effect. + +verify_signatures: false + +PreCommit: + I18nTasksNormalize: + enabled: true + description: 'Run i18n-tasks normalize on locales' + required_executable: 'i18n-tasks' + include: + - 'config/locales/**/*.yml' + command: ['bundle', 'exec', 'i18n-tasks', 'normalize'] + on_warn: fail # Ensure it stops commit if there's a warning + + RuboCop: + enabled: true + required: true + command: ['bundle', 'exec', 'rubocop', '-f', 'simple'] + + AutoFixTrailingWhitespace: + enabled: true + description: 'Removes trailing whitespace in files' + command: 'sed -i -e "s/[[:space:]]\+$//"' + include: '**/*.{rb,yml}' # Fix for file pattern matching + exclude: ['vendor/**/*'] # You can add more exclusions as needed + + TrailingWhitespace: + enabled: false + exclude: + - '**/db/structure.sql' # Ignore trailing whitespace in generated files + + ErbLint: + enabled: true + required: true + command: ['bundle', 'exec', 'erblint', '--lint-all'] + +# StandardRB: +# enabled: false +# required: true +# command: ['bundle', 'exec', 'standardrb'] +# RustyWind: +# enabled: true +# required: true +# command: ['yarn', 'run', 'rustywind-fix'] + +# PrePush: +# RSpec: +# enabled: true +# required: true +# command: ['bundle', 'exec', 'rspec'] + +#PreCommit: +# RuboCop: +# enabled: true +# on_warn: fail # Treat all warnings as failures +# +#PostCheckout: +# ALL: # Special hook name that customizes all hooks of this type +# quiet: true # Change all post-checkout hooks to only display output on failure +# +# IndexTags: +# enabled: true # Generate a tags file with `ctags` each time HEAD changes diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7a5292f46ab7..dd573e5002c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v3.4.0 hooks: - id: trailing-whitespace exclude: '\.enc$' @@ -13,11 +13,20 @@ repos: - repo: local hooks: - - id: rubocop - name: Running rubocop - entry: rubocop + - id: erblint + name: ERBLint + entry: erblint language: system - pass_filenames: false - #args: ['-a'] - always_run: true - stages: [pre-commit] + types: [html] + files: \.html\.erb$ + pass_filenames: true + +- repo: local + hooks: + - id: standardrb + name: standardrb + entry: standardrb + language: system + types: [ruby] + files: \.rb$ + pass_filenames: true diff --git a/.rubocop.yml b/.rubocop.yml index a1d3536f4337..5de9d8d86a5e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,234 +1,1854 @@ -require: - - rubocop-performance - - rubocop-rails +# Omakase Ruby styling for Rails +inherit_gem: { rubocop-rails-omakase: rubocop.yml } -AllCops: - NewCops: enable - Exclude: - - 'db/migrate/*.rb' - - 'db/schema.rb' +# Overwrite or add rules to create your own house style +# +# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` +# Layout/SpaceInsideArrayLiteralBrackets: +# Enabled: false -Metrics/ClassLength: - Max: 400 +# https://github.com/standardrb/standard/blob/v1.40.0/config/base.yml +Bundler/DuplicatedGem: + Enabled: true + Include: + - '**/*.gemfile' + - '**/Gemfile' + - '**/gems.rb' -Metrics/MethodLength: - Max: 80 +Bundler/DuplicatedGroup: + Enabled: false -Metrics/CyclomaticComplexity: - Max: 15 +Bundler/GemComment: + Enabled: false -Metrics/PerceivedComplexity: - Max: 15 +Bundler/GemFilename: + Enabled: false -Metrics/AbcSize: - Max: 200 +Bundler/GemVersion: + Enabled: false -Metrics/BlockLength: - Max: 120 +Bundler/InsecureProtocolSource: + Enabled: true + Include: + - '**/*.gemfile' + - '**/Gemfile' + - '**/gems.rb' -Layout/LineLength: - Max: 130 +Bundler/OrderedGems: + Enabled: false -Style/ClassAndModuleChildren: +Gemspec/AddRuntimeDependency: Enabled: false -Style/Documentation: +Gemspec/DependencyVersion: + Enabled: false + +Gemspec/DeprecatedAttributeAssignment: + Enabled: true + +Gemspec/DevelopmentDependencies: + Enabled: false + +Gemspec/DuplicatedAssignment: + Enabled: true + Include: + - '**/*.gemspec' + +Gemspec/OrderedDependencies: + Enabled: false + +Gemspec/RequireMFA: + Enabled: false + +Gemspec/RequiredRubyVersion: + Enabled: false + +Gemspec/RubyVersionGlobalsUsage: + Enabled: false + +Layout/AccessModifierIndentation: + Enabled: true + EnforcedStyle: indent + IndentationWidth: ~ + +Layout/ArgumentAlignment: + Enabled: true + EnforcedStyle: with_fixed_indentation + +Layout/ArrayAlignment: + Enabled: true + EnforcedStyle: with_fixed_indentation + +Layout/AssignmentIndentation: + Enabled: true + IndentationWidth: ~ + +Layout/BeginEndAlignment: + Enabled: true + EnforcedStyleAlignWith: start_of_line + Severity: warning + +Layout/BlockAlignment: + Enabled: true + EnforcedStyleAlignWith: either + +Layout/BlockEndNewline: + Enabled: true + +Layout/CaseIndentation: + Enabled: true + EnforcedStyle: end + +Layout/ClassStructure: + Enabled: false + +Layout/ClosingHeredocIndentation: + Enabled: true + +Layout/ClosingParenthesisIndentation: + Enabled: true + +Layout/CommentIndentation: + Enabled: true + +Layout/ConditionPosition: + Enabled: true + +Layout/DefEndAlignment: + Enabled: true + EnforcedStyleAlignWith: start_of_line + Severity: warning + +Layout/DotPosition: + Enabled: true + EnforcedStyle: leading + +Layout/ElseAlignment: + Enabled: true + +Layout/EmptyComment: + Enabled: true + AllowBorderComment: true + AllowMarginComment: true + +Layout/EmptyLineAfterGuardClause: + Enabled: false + +Layout/EmptyLineAfterMagicComment: + Enabled: true + +Layout/EmptyLineAfterMultilineCondition: + Enabled: false + +Layout/EmptyLineBetweenDefs: + Enabled: true + AllowAdjacentOneLineDefs: false + NumberOfEmptyLines: 1 + +Layout/EmptyLines: + Enabled: true + +Layout/EmptyLinesAroundAccessModifier: + Enabled: true + +Layout/EmptyLinesAroundArguments: + Enabled: true + +Layout/EmptyLinesAroundAttributeAccessor: Enabled: false -Gemspec/DeprecatedAttributeAssignment: # new in 1.30 +Layout/EmptyLinesAroundBeginBody: Enabled: true -Gemspec/DevelopmentDependencies: # new in 1.44 + +Layout/EmptyLinesAroundBlockBody: + Enabled: true + EnforcedStyle: no_empty_lines + +Layout/EmptyLinesAroundClassBody: + Enabled: true + EnforcedStyle: no_empty_lines + +Layout/EmptyLinesAroundExceptionHandlingKeywords: + Enabled: true + +Layout/EmptyLinesAroundMethodBody: + Enabled: true + +Layout/EmptyLinesAroundModuleBody: + Enabled: true + EnforcedStyle: no_empty_lines + +Layout/EndAlignment: + Enabled: true + EnforcedStyleAlignWith: variable + Severity: warning + +Layout/EndOfLine: Enabled: true -Gemspec/RequireMFA: # new in 1.23 + EnforcedStyle: native + +Layout/ExtraSpacing: Enabled: true -Layout/LineContinuationLeadingSpace: # new in 1.31 + AllowForAlignment: false + AllowBeforeTrailingComments: true + ForceEqualSignAlignment: false + +Layout/FirstArgumentIndentation: Enabled: true -Layout/LineContinuationSpacing: # new in 1.31 + EnforcedStyle: consistent + IndentationWidth: ~ + +Layout/FirstArrayElementIndentation: Enabled: true -Layout/LineEndStringConcatenationIndentation: # new in 1.18 + EnforcedStyle: consistent + IndentationWidth: ~ + +Layout/FirstArrayElementLineBreak: + Enabled: false + +Layout/FirstHashElementIndentation: Enabled: true -Layout/SpaceBeforeBrackets: # new in 1.7 + EnforcedStyle: consistent + IndentationWidth: ~ + +Layout/FirstHashElementLineBreak: + Enabled: false + +Layout/FirstMethodArgumentLineBreak: + Enabled: false + +Layout/FirstMethodParameterLineBreak: + Enabled: false + +Layout/FirstParameterIndentation: + Enabled: false + +Layout/HashAlignment: Enabled: true -Lint/AmbiguousAssignment: # new in 1.7 + EnforcedHashRocketStyle: key + EnforcedColonStyle: key + EnforcedLastArgumentHashStyle: always_inspect + +Layout/HeredocArgumentClosingParenthesis: + Enabled: false + +Layout/HeredocIndentation: Enabled: true -Lint/AmbiguousOperatorPrecedence: # new in 1.21 + +Layout/IndentationConsistency: Enabled: true -Lint/AmbiguousRange: # new in 1.19 + EnforcedStyle: normal + +Layout/IndentationStyle: Enabled: true -Lint/ConstantOverwrittenInRescue: # new in 1.31 + IndentationWidth: ~ + +Layout/IndentationWidth: Enabled: true -Lint/DeprecatedConstants: # new in 1.8 + Width: 2 + AllowedPatterns: [] + +Layout/InitialIndentation: Enabled: true -Lint/DuplicateBranch: # new in 1.3 + +Layout/LeadingCommentSpace: Enabled: true -Lint/DuplicateMagicComment: # new in 1.37 + +Layout/LeadingEmptyLines: Enabled: true -Lint/DuplicateMatchPattern: # new in 1.50 + +Layout/LineContinuationLeadingSpace: + Enabled: false + +Layout/LineContinuationSpacing: Enabled: true -Lint/DuplicateRegexpCharacterClassElement: # new in 1.1 + +Layout/LineEndStringConcatenationIndentation: + Enabled: false + +Layout/LineLength: + Enabled: false + +Layout/MultilineArrayBraceLayout: Enabled: true -Lint/EmptyBlock: # new in 1.1 - Enabled: False -Lint/EmptyClass: # new in 1.3 + EnforcedStyle: symmetrical + +Layout/MultilineArrayLineBreaks: + Enabled: false + +Layout/MultilineAssignmentLayout: + Enabled: false + +Layout/MultilineBlockLayout: Enabled: true -Lint/EmptyInPattern: # new in 1.16 + +Layout/MultilineHashBraceLayout: Enabled: true -Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21 + EnforcedStyle: symmetrical + +Layout/MultilineHashKeyLineBreaks: + Enabled: false + +Layout/MultilineMethodArgumentLineBreaks: + Enabled: false + +Layout/MultilineMethodCallBraceLayout: Enabled: true -Lint/LambdaWithoutLiteralBlock: # new in 1.8 + EnforcedStyle: symmetrical + +Layout/MultilineMethodCallIndentation: Enabled: true -Lint/MixedCaseRange: # new in 1.53 + EnforcedStyle: indented + IndentationWidth: ~ + +Layout/MultilineMethodDefinitionBraceLayout: Enabled: true -Lint/NoReturnInBeginEndBlocks: # new in 1.2 + EnforcedStyle: symmetrical + +Layout/MultilineMethodParameterLineBreaks: + Enabled: false + +Layout/MultilineOperationIndentation: Enabled: true -Lint/NonAtomicFileOperation: # new in 1.31 + EnforcedStyle: indented + IndentationWidth: ~ + +Layout/ParameterAlignment: Enabled: true -Lint/NumberedParameterAssignment: # new in 1.9 + EnforcedStyle: with_fixed_indentation + IndentationWidth: ~ + +Layout/RedundantLineBreak: + Enabled: false + +Layout/RescueEnsureAlignment: Enabled: true -Lint/OrAssignmentToConstant: # new in 1.9 + +Layout/SingleLineBlockChain: + Enabled: false + +Layout/SpaceAfterColon: Enabled: true -Lint/RedundantDirGlobSort: # new in 1.8 + +Layout/SpaceAfterComma: Enabled: true -Lint/RedundantRegexpQuantifiers: # new in 1.53 + +Layout/SpaceAfterMethodName: Enabled: true -Lint/RefinementImportMethods: # new in 1.27 + +Layout/SpaceAfterNot: Enabled: true -Lint/RequireRangeParentheses: # new in 1.32 + +Layout/SpaceAfterSemicolon: Enabled: true -Lint/RequireRelativeSelfPath: # new in 1.22 + +Layout/SpaceAroundBlockParameters: Enabled: true -Lint/SymbolConversion: # new in 1.9 + EnforcedStyleInsidePipes: no_space + +Layout/SpaceAroundEqualsInParameterDefault: Enabled: true -Lint/ToEnumArguments: # new in 1.1 + EnforcedStyle: space + +Layout/SpaceAroundKeyword: Enabled: true -Lint/TripleQuotes: # new in 1.9 + +Layout/SpaceAroundMethodCallOperator: Enabled: true -Lint/UnexpectedBlockArity: # new in 1.5 + +Layout/SpaceAroundOperators: Enabled: true -Lint/UnmodifiedReduceAccumulator: # new in 1.1 + AllowForAlignment: true + +Layout/SpaceBeforeBlockBraces: Enabled: true -Lint/UselessRescue: # new in 1.43 + EnforcedStyle: space + EnforcedStyleForEmptyBraces: space + +Layout/SpaceBeforeBrackets: + Enabled: false + +Layout/SpaceBeforeComma: Enabled: true -Lint/UselessRuby2Keywords: # new in 1.23 + +Layout/SpaceBeforeComment: Enabled: true -Metrics/CollectionLiteralLength: # new in 1.47 + +Layout/SpaceBeforeFirstArg: Enabled: true -Naming/BlockForwarding: # new in 1.24 + AllowForAlignment: true + +Layout/SpaceBeforeSemicolon: Enabled: true -Security/CompoundHash: # new in 1.28 + +Layout/SpaceInLambdaLiteral: Enabled: true -Security/IoMethods: # new in 1.22 + EnforcedStyle: require_no_space + +Layout/SpaceInsideArrayLiteralBrackets: Enabled: true -Style/ArgumentsForwarding: # new in 1.1 + EnforcedStyle: no_space + EnforcedStyleForEmptyBrackets: no_space + +Layout/SpaceInsideArrayPercentLiteral: Enabled: true -Style/ArrayIntersect: # new in 1.40 + +Layout/SpaceInsideBlockBraces: Enabled: true -Style/CollectionCompact: # new in 1.2 + EnforcedStyle: space + EnforcedStyleForEmptyBraces: no_space + SpaceBeforeBlockParameters: true + +Layout/SpaceInsideHashLiteralBraces: Enabled: true -Style/ComparableClamp: # new in 1.44 + EnforcedStyle: no_space + EnforcedStyleForEmptyBraces: no_space + +Layout/SpaceInsideParens: Enabled: true -Style/ConcatArrayLiterals: # new in 1.41 + EnforcedStyle: no_space + +Layout/SpaceInsidePercentLiteralDelimiters: Enabled: true -Style/DataInheritance: # new in 1.49 + +Layout/SpaceInsideRangeLiteral: Enabled: true -Style/DirEmpty: # new in 1.48 + +Layout/SpaceInsideReferenceBrackets: Enabled: true -Style/DocumentDynamicEvalDefinition: # new in 1.1 + EnforcedStyle: no_space + EnforcedStyleForEmptyBrackets: no_space + +Layout/SpaceInsideStringInterpolation: Enabled: true -Style/EmptyHeredoc: # new in 1.32 + EnforcedStyle: no_space + +Layout/TrailingEmptyLines: Enabled: true -Style/EndlessMethod: # new in 1.8 + EnforcedStyle: final_newline + +Layout/TrailingWhitespace: Enabled: true -Style/EnvHome: # new in 1.29 + AllowInHeredoc: true + +Lint/AmbiguousAssignment: Enabled: true -Style/ExactRegexpMatch: # new in 1.51 + +Lint/AmbiguousBlockAssociation: + Enabled: false + +Lint/AmbiguousOperator: Enabled: true -Style/FetchEnvVar: # new in 1.28 + +Lint/AmbiguousOperatorPrecedence: + Enabled: false + +Lint/AmbiguousRange: + Enabled: false + +Lint/AmbiguousRegexpLiteral: Enabled: true -Style/FileEmpty: # new in 1.48 + +Lint/AssignmentInCondition: Enabled: true -Style/FileRead: # new in 1.24 + AllowSafeAssignment: true + # Intentionally disable autocorrect to force us to intentionally decide + # whether assignment is intended as opposed to comparison + AutoCorrect: false + +Lint/BigDecimalNew: Enabled: true -Style/FileWrite: # new in 1.24 + +Lint/BinaryOperatorWithIdenticalOperands: Enabled: true -Style/HashConversion: # new in 1.10 + +Lint/BooleanSymbol: Enabled: true -Style/HashExcept: # new in 1.7 + +Lint/CircularArgumentReference: Enabled: true -Style/IfWithBooleanLiteralBranches: # new in 1.9 + +Lint/ConstantDefinitionInBlock: Enabled: true -Style/InPatternThen: # new in 1.16 + +Lint/ConstantOverwrittenInRescue: Enabled: true -Style/MagicCommentFormat: # new in 1.35 + +Lint/ConstantResolution: + Enabled: false + +Lint/Debugger: Enabled: true -Style/MapCompactWithConditionalBlock: # new in 1.30 + +Lint/DeprecatedClassMethods: Enabled: true -Style/MapToHash: # new in 1.24 + +Lint/DeprecatedConstants: Enabled: true -Style/MapToSet: # new in 1.42 + +Lint/DeprecatedOpenSSLConstant: Enabled: true -Style/MinMaxComparison: # new in 1.42 + +Lint/DisjunctiveAssignmentInConstructor: + Enabled: false + +Lint/DuplicateBranch: + Enabled: false + +Lint/DuplicateCaseCondition: Enabled: true -Style/MultilineInPatternThen: # new in 1.16 + +Lint/DuplicateElsifCondition: Enabled: true -Style/NegatedIfElseCondition: # new in 1.2 + +Lint/DuplicateHashKey: Enabled: true -Style/NestedFileDirname: # new in 1.26 + +Lint/DuplicateMagicComment: Enabled: true -Style/NilLambda: # new in 1.3 + +Lint/DuplicateMatchPattern: + Enabled: false + +Lint/DuplicateMethods: Enabled: true -Style/NumberedParameters: # new in 1.22 + +Lint/DuplicateRegexpCharacterClassElement: Enabled: true -Style/NumberedParametersLimit: # new in 1.22 + +Lint/DuplicateRequire: Enabled: true -Style/ObjectThen: # new in 1.28 + +Lint/DuplicateRescueException: Enabled: true -Style/OpenStructUse: # new in 1.23 + +Lint/EachWithObjectArgument: Enabled: true -Style/OperatorMethodCall: # new in 1.37 + +Lint/ElseLayout: Enabled: true -Style/QuotedSymbols: # new in 1.16 + +Lint/EmptyBlock: + Enabled: false + +Lint/EmptyClass: + Enabled: false + +Lint/EmptyConditionalBody: + Enabled: false + +Lint/EmptyEnsure: Enabled: true -Style/RedundantArgument: # new in 1.4 + +Lint/EmptyExpression: Enabled: true -Style/RedundantArrayConstructor: # new in 1.52 + +Lint/EmptyFile: + Enabled: false + +Lint/EmptyInPattern: + Enabled: false + +Lint/EmptyInterpolation: Enabled: true -Style/RedundantConstantBase: # new in 1.40 + +Lint/EmptyWhen: Enabled: true -Style/RedundantCurrentDirectoryInPath: # new in 1.53 + AllowComments: true + +Lint/EnsureReturn: Enabled: true -Style/RedundantDoubleSplatHashBraces: # new in 1.41 + +Lint/ErbNewArguments: Enabled: true -Style/RedundantEach: # new in 1.38 + +Lint/FlipFlop: Enabled: true -Style/RedundantFilterChain: # new in 1.52 + +Lint/FloatComparison: Enabled: true -Style/RedundantHeredocDelimiterQuotes: # new in 1.45 + +Lint/FloatOutOfRange: Enabled: true -Style/RedundantInitialize: # new in 1.27 + +Lint/FormatParameterMismatch: Enabled: true -Style/RedundantLineContinuation: # new in 1.49 + +Lint/HashCompareByIdentity: + Enabled: false + +Lint/HeredocMethodCallPosition: + Enabled: false + +Lint/IdentityComparison: Enabled: true -Style/RedundantRegexpArgument: # new in 1.53 + +Lint/ImplicitStringConcatenation: Enabled: true -Style/RedundantRegexpConstructor: # new in 1.52 + +Lint/IncompatibleIoSelectWithFiberScheduler: + Enabled: false + +Lint/IneffectiveAccessModifier: Enabled: true -Style/RedundantSelfAssignmentBranch: # new in 1.19 + +Lint/InheritException: Enabled: true -Style/RedundantStringEscape: # new in 1.37 + EnforcedStyle: runtime_error + +Lint/InterpolationCheck: Enabled: true -Style/ReturnNilInPredicateMethodDefinition: # new in 1.53 + +Lint/ItWithoutArgumentsInBlock: Enabled: true -Style/SelectByRegexp: # new in 1.22 + +Lint/LambdaWithoutLiteralBlock: + Enabled: false + +Lint/LiteralAsCondition: Enabled: true -Style/SingleLineDoEndBlock: # new in 1.57 + +Lint/LiteralAssignmentInCondition: Enabled: true -Style/StringChars: # new in 1.12 + +Lint/LiteralInInterpolation: Enabled: true -Style/SwapValues: # new in 1.1 + +Lint/Loop: Enabled: true -Style/YAMLFileRead: # new in 1.53 + +Lint/MissingCopEnableDirective: Enabled: true + MaxRangeSize: .inf + +Lint/MissingSuper: + Enabled: false + +Lint/MixedCaseRange: + Enabled: true + +Lint/MixedRegexpCaptureTypes: + Enabled: true + +Lint/MultipleComparison: + Enabled: true + +Lint/NestedMethodDefinition: + Enabled: true + +Lint/NestedPercentLiteral: + Enabled: true + +Lint/NextWithoutAccumulator: + Enabled: true + +Lint/NoReturnInBeginEndBlocks: + Enabled: false + +Lint/NonAtomicFileOperation: + Enabled: false + +Lint/NonDeterministicRequireOrder: + Enabled: true + +Lint/NonLocalExitFromIterator: + Enabled: true + +Lint/NumberConversion: + Enabled: false + +Lint/NumberedParameterAssignment: + Enabled: true + +Lint/OrAssignmentToConstant: + Enabled: true + +Lint/OrderedMagicComments: + Enabled: true + +Lint/OutOfRangeRegexpRef: + Enabled: true + +Lint/ParenthesesAsGroupedExpression: + Enabled: true + +Lint/PercentStringArray: + Enabled: false + +Lint/PercentSymbolArray: + Enabled: true + +Lint/RaiseException: + Enabled: true + +Lint/RandOne: + Enabled: true + +Lint/RedundantCopDisableDirective: + Enabled: false + +Lint/RedundantCopEnableDirective: + Enabled: false + +Lint/RedundantDirGlobSort: + Enabled: false + +Lint/RedundantRegexpQuantifiers: + Enabled: true + +Lint/RedundantRequireStatement: + Enabled: true + +Lint/RedundantSafeNavigation: + Enabled: false + +Lint/RedundantSplatExpansion: + Enabled: true + +Lint/RedundantStringCoercion: + Enabled: true + +Lint/RedundantWithIndex: + Enabled: true + +Lint/RedundantWithObject: + Enabled: true + +Lint/RefinementImportMethods: + Enabled: true + +Lint/RegexpAsCondition: + Enabled: true + +Lint/RequireParentheses: + Enabled: true + +Lint/RequireRangeParentheses: + Enabled: true + +Lint/RequireRelativeSelfPath: + Enabled: true + +Lint/RescueException: + Enabled: true + +Lint/RescueType: + Enabled: true + +Lint/ReturnInVoidContext: + Enabled: true + +Lint/SafeNavigationChain: + Enabled: true + AllowedMethods: + - present? + - blank? + - presence + - try + - try! + +Lint/SafeNavigationConsistency: + Enabled: true + AllowedMethods: + - present? + - blank? + - presence + - try + - try! + +Lint/SafeNavigationWithEmpty: + Enabled: true + +Lint/ScriptPermission: + Enabled: false + +Lint/SelfAssignment: + Enabled: true + +Lint/SendWithMixinArgument: + Enabled: false + +Lint/ShadowedArgument: + Enabled: true + IgnoreImplicitReferences: false + +Lint/ShadowedException: + Enabled: true + +Lint/ShadowingOuterLocalVariable: + Enabled: false + +Lint/StructNewOverride: + Enabled: false + +Lint/SuppressedException: + Enabled: false + +Lint/SymbolConversion: + Enabled: true + +Lint/ToEnumArguments: + Enabled: false + +Lint/ToJSON: + Enabled: false + +Lint/TopLevelReturnWithArgument: + Enabled: true + +Lint/TrailingCommaInAttributeDeclaration: + Enabled: true + +Lint/TripleQuotes: + Enabled: true + +Lint/UnderscorePrefixedVariableName: + Enabled: true + +Lint/UnexpectedBlockArity: + Enabled: false + +Lint/UnifiedInteger: + Enabled: true + +Lint/UnmodifiedReduceAccumulator: + Enabled: false + +Lint/UnreachableCode: + Enabled: true + +Lint/UnreachableLoop: + Enabled: false + +Lint/UnusedBlockArgument: + Enabled: false + +Lint/UnusedMethodArgument: + Enabled: false + +Lint/UriEscapeUnescape: + Enabled: true + +Lint/UriRegexp: + Enabled: true + +Lint/UselessAccessModifier: + Enabled: false + +Lint/UselessAssignment: + Enabled: true + +Lint/UselessElseWithoutRescue: + Enabled: false + +Lint/UselessMethodDefinition: + Enabled: false + +Lint/UselessRescue: + Enabled: true + +Lint/UselessRuby2Keywords: + Enabled: true + +Lint/UselessSetterCall: + Enabled: true + +Lint/UselessTimes: + Enabled: true + +Lint/Void: + Enabled: true + CheckForMethodsWithNoSideEffects: false + +Metrics/AbcSize: + Enabled: false + +Metrics/BlockLength: + Enabled: false + +Metrics/BlockNesting: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Metrics/CollectionLiteralLength: + Enabled: false + +Metrics/CyclomaticComplexity: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Metrics/ModuleLength: + Enabled: false + +Metrics/ParameterLists: + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false + +Migration/DepartmentName: + Enabled: true + +Naming/AccessorMethodName: + Enabled: false + +Naming/AsciiIdentifiers: + Enabled: false + +Naming/BinaryOperatorParameterName: + Enabled: true + +Naming/BlockForwarding: + Enabled: false + +Naming/BlockParameterName: + Enabled: true + MinNameLength: 1 + AllowNamesEndingInNumbers: true + AllowedNames: [] + ForbiddenNames: [] + +Naming/ClassAndModuleCamelCase: + Enabled: true + +Naming/ConstantName: + Enabled: true + +Naming/FileName: + Enabled: false + +Naming/HeredocDelimiterCase: + Enabled: true + EnforcedStyle: uppercase + +Naming/HeredocDelimiterNaming: + Enabled: false + +Naming/InclusiveLanguage: + Enabled: false + +Naming/MemoizedInstanceVariableName: + Enabled: false + +Naming/MethodName: + Enabled: false + +Naming/MethodParameterName: + Enabled: false + +Naming/RescuedExceptionsVariableName: + Enabled: false + +Naming/VariableName: + Enabled: true + EnforcedStyle: snake_case + +Naming/VariableNumber: + Enabled: false + +Security/CompoundHash: + Enabled: true + +Security/Eval: + Enabled: true + +Security/IoMethods: + Enabled: false + +Security/JSONLoad: + Enabled: true + +Security/MarshalLoad: + Enabled: false + +Security/Open: + Enabled: true + +Security/YAMLLoad: + Enabled: true + +Style/AccessModifierDeclarations: + Enabled: false + +Style/AccessorGrouping: + Enabled: false + +Style/Alias: + Enabled: true + EnforcedStyle: prefer_alias_method + +Style/AndOr: + Enabled: true + +Style/ArgumentsForwarding: + Enabled: true + +Style/ArrayCoercion: + Enabled: false + +Style/ArrayFirstLast: + Enabled: false + +Style/ArrayIntersect: + Enabled: false + +Style/ArrayJoin: + Enabled: true + +Style/AsciiComments: + Enabled: false + +Style/Attr: + Enabled: true + +Style/AutoResourceCleanup: + Enabled: false + +Style/BarePercentLiterals: + Enabled: true + EnforcedStyle: bare_percent + +Style/BeginBlock: + Enabled: true + +Style/BisectedAttrAccessor: + Enabled: false + +Style/BlockComments: + Enabled: true + +Style/BlockDelimiters: + Enabled: false + +Style/CaseEquality: + Enabled: false + +Style/CaseLikeIf: + Enabled: false + +Style/CharacterLiteral: + Enabled: true + +Style/ClassAndModuleChildren: + Enabled: false + +Style/ClassCheck: + Enabled: true + EnforcedStyle: is_a? + +Style/ClassEqualityComparison: + Enabled: true + +Style/ClassMethods: + Enabled: true + +Style/ClassMethodsDefinitions: + Enabled: false + +Style/ClassVars: + Enabled: false + +Style/CollectionCompact: + Enabled: false + +Style/CollectionMethods: + Enabled: false + +Style/ColonMethodCall: + Enabled: true + +Style/ColonMethodDefinition: + Enabled: true + +Style/CombinableLoops: + Enabled: false + +Style/CommandLiteral: + Enabled: true + EnforcedStyle: mixed + AllowInnerBackticks: false + +Style/CommentAnnotation: + Enabled: false + +Style/CommentedKeyword: + Enabled: false + +Style/ComparableClamp: + Enabled: true + +Style/ConcatArrayLiterals: + Enabled: false + +Style/ConditionalAssignment: + Enabled: true + EnforcedStyle: assign_to_condition + SingleLineConditionsOnly: true + IncludeTernaryExpressions: true + +Style/ConstantVisibility: + Enabled: false + +Style/Copyright: + Enabled: false + +Style/DataInheritance: + Enabled: false + +Style/DateTime: + Enabled: false + +Style/DefWithParentheses: + Enabled: true + +Style/Dir: + Enabled: true + +Style/DirEmpty: + Enabled: true + +Style/DisableCopsWithinSourceCodeDirective: + Enabled: false + +Style/DocumentDynamicEvalDefinition: + Enabled: false + +Style/Documentation: + Enabled: false + +Style/DocumentationMethod: + Enabled: false + +Style/DoubleCopDisableDirective: + Enabled: false + +Style/DoubleNegation: + Enabled: false + +Style/EachForSimpleLoop: + Enabled: true + +Style/EachWithObject: + Enabled: true + +Style/EmptyBlockParameter: + Enabled: true + +Style/EmptyCaseCondition: + Enabled: true + +Style/EmptyElse: + Enabled: true + AllowComments: true + EnforcedStyle: both + +Style/EmptyHeredoc: + Enabled: false + +Style/EmptyLambdaParameter: + Enabled: true + +Style/EmptyLiteral: + Enabled: true + +Style/EmptyMethod: + Enabled: true + EnforcedStyle: expanded + +Style/Encoding: + Enabled: true + +Style/EndBlock: + Enabled: true + +Style/EndlessMethod: + Enabled: false + +Style/EnvHome: + Enabled: false + +Style/EvalWithLocation: + Enabled: true + +Style/EvenOdd: + Enabled: false + +Style/ExactRegexpMatch: + Enabled: true +Style/ExpandPathArguments: + Enabled: false + +Style/ExplicitBlockArgument: + Enabled: false + +Style/ExponentialNotation: + Enabled: false + +Style/FetchEnvVar: + Enabled: false + +Style/FileEmpty: + Enabled: false + +Style/FileRead: + Enabled: true + +Style/FileWrite: + Enabled: true + +Style/FloatDivision: + Enabled: false + +Style/For: + Enabled: true + EnforcedStyle: each + +Style/FormatString: + Enabled: false + +Style/FormatStringToken: + Enabled: false + +Style/FrozenStringLiteralComment: + Enabled: false + +Style/GlobalStdStream: + Enabled: true + +Style/GlobalVars: + Enabled: true + AllowedVariables: [] + +Style/GuardClause: + Enabled: false + +Style/HashAsLastArrayItem: + Enabled: false + +Style/HashConversion: + Enabled: true + +Style/HashEachMethods: + Enabled: false + +Style/HashExcept: + Enabled: true + +Style/HashLikeCase: + Enabled: false + +Style/HashSyntax: + Enabled: true + EnforcedStyle: ruby19_no_mixed_keys + EnforcedShorthandSyntax: either + +Style/HashTransformKeys: + Enabled: false + +Style/HashTransformValues: + Enabled: false + +Style/IdenticalConditionalBranches: + Enabled: true + +Style/IfInsideElse: + Enabled: true + +Style/IfUnlessModifier: + Enabled: false + +Style/IfUnlessModifierOfIfUnless: + Enabled: true + +Style/IfWithBooleanLiteralBranches: + Enabled: true + +Style/IfWithSemicolon: + Enabled: true + +Style/ImplicitRuntimeError: + Enabled: false + +Style/InPatternThen: + Enabled: false + +Style/InfiniteLoop: + Enabled: true + +Style/InlineComment: + Enabled: false + +Style/InverseMethods: + Enabled: false + +Style/InvertibleUnlessCondition: + Enabled: false + +Style/IpAddresses: + Enabled: false + +Style/KeywordParametersOrder: + Enabled: true + +Style/Lambda: + Enabled: false + +Style/LambdaCall: + Enabled: true + EnforcedStyle: call + +Style/LineEndConcatenation: + Enabled: true + +Style/MagicCommentFormat: + Enabled: false + +Style/MapCompactWithConditionalBlock: + Enabled: true + +Style/MapIntoArray: + Enabled: false + +Style/MapToHash: + Enabled: false + +Style/MapToSet: + Enabled: false + +Style/MethodCallWithArgsParentheses: + Enabled: false + +Style/MethodCallWithoutArgsParentheses: + Enabled: true + AllowedMethods: [] + +Style/MethodCalledOnDoEndBlock: + Enabled: false + +Style/MethodDefParentheses: + Enabled: false + +Style/MinMax: + Enabled: false + +Style/MinMaxComparison: + Enabled: false + +Style/MissingElse: + Enabled: false + +Style/MissingRespondToMissing: + Enabled: true + +Style/MixinGrouping: + Enabled: true + EnforcedStyle: separated + +Style/MixinUsage: + Enabled: true + +Style/ModuleFunction: + Enabled: false + +Style/MultilineBlockChain: + Enabled: false + +Style/MultilineIfModifier: + Enabled: true + +Style/MultilineIfThen: + Enabled: true + +Style/MultilineInPatternThen: + Enabled: false + +Style/MultilineMemoization: + Enabled: true + EnforcedStyle: keyword + +Style/MultilineMethodSignature: + Enabled: false + +Style/MultilineTernaryOperator: + Enabled: false + +Style/MultilineWhenThen: + Enabled: true + +Style/MultipleComparison: + Enabled: false + +Style/MutableConstant: + Enabled: false + +Style/NegatedIf: + Enabled: false + +Style/NegatedIfElseCondition: + Enabled: false + +Style/NegatedUnless: + Enabled: false + +Style/NegatedWhile: + Enabled: true + +Style/NestedFileDirname: + Enabled: true + +Style/NestedModifier: + Enabled: true + +Style/NestedParenthesizedCalls: + Enabled: true + AllowedMethods: + - be + - be_a + - be_an + - be_between + - be_falsey + - be_kind_of + - be_instance_of + - be_truthy + - be_within + - eq + - eql + - end_with + - include + - match + - raise_error + - respond_to + - start_with + +Style/NestedTernaryOperator: + Enabled: true + +Style/Next: + Enabled: false + +Style/NilComparison: + Enabled: true + EnforcedStyle: predicate + +Style/NilLambda: + Enabled: true + +Style/NonNilCheck: + Enabled: true + IncludeSemanticChanges: false + +Style/Not: + Enabled: true + +Style/NumberedParameters: + Enabled: false + +Style/NumberedParametersLimit: + Enabled: false + +Style/NumericLiteralPrefix: + Enabled: true + EnforcedOctalStyle: zero_with_o + +Style/NumericLiterals: + Enabled: false + +Style/NumericPredicate: + Enabled: false + +Style/ObjectThen: + Enabled: false + +Style/OneLineConditional: + Enabled: true + +Style/OpenStructUse: + Enabled: false + +Style/OperatorMethodCall: + Enabled: false + +Style/OptionHash: + Enabled: false + +Style/OptionalArguments: + Enabled: true + +Style/OptionalBooleanParameter: + Enabled: false + +Style/OrAssignment: + Enabled: true + +Style/ParallelAssignment: + Enabled: false + +Style/ParenthesesAroundCondition: + Enabled: true + AllowSafeAssignment: true + AllowInMultilineConditions: false + +Style/PercentLiteralDelimiters: + Enabled: true + PreferredDelimiters: + default: () + '%i': '[]' + '%I': '[]' + '%r': '{}' + '%w': '[]' + '%W': '[]' + +Style/PercentQLiterals: + Enabled: false + +Style/PerlBackrefs: + Enabled: false + +Style/PreferredHashMethods: + Enabled: false + +Style/Proc: + Enabled: true + +Style/QuotedSymbols: + Enabled: true + EnforcedStyle: same_as_string_literals + +Style/RaiseArgs: + Enabled: false + +Style/RandomWithOffset: + Enabled: true + +Style/RedundantArgument: + Enabled: false + +Style/RedundantArrayConstructor: + Enabled: true + +Style/RedundantAssignment: + Enabled: true + +Style/RedundantBegin: + Enabled: true + +Style/RedundantCapitalW: + Enabled: false + +Style/RedundantCondition: + Enabled: true + +Style/RedundantConditional: + Enabled: true + +Style/RedundantConstantBase: + Enabled: false + +Style/RedundantCurrentDirectoryInPath: + Enabled: true + +Style/RedundantDoubleSplatHashBraces: + Enabled: true + +Style/RedundantEach: + Enabled: false + +Style/RedundantException: + Enabled: true + +Style/RedundantFetchBlock: + Enabled: false + +Style/RedundantFileExtensionInRequire: + Enabled: true + +Style/RedundantFilterChain: + Enabled: false + +Style/RedundantFreeze: + Enabled: true + +Style/RedundantHeredocDelimiterQuotes: + Enabled: true + +Style/RedundantInitialize: + Enabled: false + +Style/RedundantInterpolation: + Enabled: true + +Style/RedundantLineContinuation: + Enabled: true + +Style/RedundantParentheses: + Enabled: true + +Style/RedundantPercentQ: + Enabled: true + +Style/RedundantRegexpArgument: + Enabled: true + +Style/RedundantRegexpCharacterClass: + Enabled: true + +Style/RedundantRegexpConstructor: + Enabled: true + +Style/RedundantRegexpEscape: + Enabled: true + +Style/RedundantReturn: + Enabled: true + AllowMultipleReturnValues: false + +Style/RedundantSelf: + Enabled: true + +Style/RedundantSelfAssignment: + Enabled: false + +Style/RedundantSelfAssignmentBranch: + Enabled: false + +Style/RedundantSort: + Enabled: true + +Style/RedundantSortBy: + Enabled: true + +Style/RedundantStringEscape: + Enabled: true + +Style/RegexpLiteral: + Enabled: false + +Style/RequireOrder: + Enabled: false + +Style/RescueModifier: + Enabled: true + +Style/RescueStandardError: + Enabled: true + EnforcedStyle: implicit + +Style/ReturnNil: + Enabled: false + +Style/ReturnNilInPredicateMethodDefinition: + Enabled: false + +Style/SafeNavigation: + Enabled: true + ConvertCodeThatCanStartToReturnNil: false + AllowedMethods: + - present? + - blank? + - presence + - try + - try! + +Style/Sample: + Enabled: true + +Style/SelectByRegexp: + Enabled: false + +Style/SelfAssignment: + Enabled: true + +Style/Semicolon: + Enabled: true + AllowAsExpressionSeparator: false + +Style/Send: + Enabled: false + +Style/SendWithLiteralMethodName: + Enabled: false + +Style/SignalException: + Enabled: false + +Style/SingleArgumentDig: + Enabled: false + +Style/SingleLineBlockParams: + Enabled: false + +Style/SingleLineDoEndBlock: + Enabled: false + +Style/SingleLineMethods: + Enabled: true + AllowIfMethodIsEmpty: false + +Style/SlicingWithRange: + Enabled: true + +Style/SoleNestedConditional: + Enabled: false + +Style/SpecialGlobalVars: + Enabled: false + +Style/StabbyLambdaParentheses: + Enabled: true + EnforcedStyle: require_parentheses + +Style/StaticClass: + Enabled: false + +Style/StderrPuts: + Enabled: true + +Style/StringChars: + Enabled: true + +Style/StringConcatenation: + Enabled: false + +Style/StringHashKeys: + Enabled: false + +Style/StringLiterals: + Enabled: true + EnforcedStyle: double_quotes + ConsistentQuotesInMultiline: false + +Style/StringLiteralsInInterpolation: + Enabled: true + EnforcedStyle: double_quotes + +Style/StringMethods: + Enabled: false + +Style/Strip: + Enabled: true + +Style/StructInheritance: + Enabled: false + +Style/SuperArguments: + Enabled: true + +Style/SuperWithArgsParentheses: + Enabled: true + +Style/SwapValues: + Enabled: false + +Style/SymbolArray: + Enabled: false + +Style/SymbolLiteral: + Enabled: true + +Style/SymbolProc: + Enabled: false + +Style/TernaryParentheses: + Enabled: true + EnforcedStyle: require_parentheses_when_complex + AllowSafeAssignment: true + +Style/TopLevelMethodDefinition: + Enabled: false + +Style/TrailingBodyOnClass: + Enabled: true + +Style/TrailingBodyOnMethodDefinition: + Enabled: true + +Style/TrailingBodyOnModule: + Enabled: true + +Style/TrailingCommaInArguments: + Enabled: true + EnforcedStyleForMultiline: no_comma + +Style/TrailingCommaInArrayLiteral: + Enabled: true + EnforcedStyleForMultiline: no_comma + +Style/TrailingCommaInBlockArgs: + Enabled: true + +Style/TrailingCommaInHashLiteral: + Enabled: true + EnforcedStyleForMultiline: no_comma + +Style/TrailingMethodEndStatement: + Enabled: true + +Style/TrailingUnderscoreVariable: + Enabled: false + +Style/TrivialAccessors: + Enabled: true + ExactNameMatch: true + AllowPredicates: true + AllowDSLWriters: false + IgnoreClassMethods: true + AllowedMethods: + - to_ary + - to_a + - to_c + - to_enum + - to_h + - to_hash + - to_i + - to_int + - to_io + - to_open + - to_path + - to_proc + - to_r + - to_regexp + - to_str + - to_s + - to_sym + +Style/UnlessElse: + Enabled: true + +Style/UnlessLogicalOperators: + Enabled: true + EnforcedStyle: forbid_mixed_logical_operators + +Style/UnpackFirst: + Enabled: true + +Style/VariableInterpolation: + Enabled: true + +Style/WhenThen: + Enabled: true + +Style/WhileUntilDo: + Enabled: true + +Style/WhileUntilModifier: + Enabled: false + +Style/WordArray: + Enabled: false + +Style/YAMLFileRead: + Enabled: true + +Style/YodaCondition: + Enabled: true + EnforcedStyle: forbid_for_all_comparison_operators + +Style/YodaExpression: + Enabled: false -Rails/I18nLocaleTexts: +Style/ZeroLengthPredicate: Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 000000000000..d13e837c8ecb --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +4.0.6 diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000000..78dcfe60953c --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 24.14.1 diff --git a/Configuration.md b/Configuration.md index 294106744614..92ebf6cdd3a1 100644 --- a/Configuration.md +++ b/Configuration.md @@ -1,577 +1,5 @@ +Configuration documentation has now been moved to the [documentation portal](https://docs.pwpush.com/docs/config-strategies/). -# Overview +If you have any problems or feedback with the new documentation portal, let me know and file an issue. -Configure everything from defaults, features, branding, languages and more. - -# How to Configure the Application - -Password Pusher uses a centralized configuration that is stored in [config/settings.yml](https://github.com/pglombardo/PasswordPusher/blob/master/config/settings.yml). This file contains all of the settings that is configurable for the application. - -There are two ways to modify the settings in this file: - -1. Use environment variable that override this file -2. Modify the file itself - -For a few modifications, environment variables are the easy route. For more extensive configuration, it's suggested to maintain your own custom `settings.yml` file across updates. - -Read on for details on both methods. - -## Configuring via Environment variables - -The settings in the `config/settings.yml` file can be overridden by environment variables. A listing and description of these environment variables is available in this documentation below and also in the [settings.yml](https://github.com/pglombardo/PasswordPusher/blob/master/config/settings.yml) file itself. - -### Shell Example - -```sh -# Change the default language for the application to French -export PWP__DEFAULT_LOCALE='fr' -``` -### Docker Example - -```sh -# Change the default language for the application to French -docker run -d --env PWP__DEFAULT_LOCALE=fr -p "5100:5100" pglombardo/pwpush:release -``` - -_Tip: If you have to set a large number of environment variables for Docker, consider using a Docker env-file. There is an [example docker-env-file](https://github.com/pglombardo/PasswordPusher/blob/master/containers/docker/pwpush-docker-env-file) with instructions available._ - -## Configuring via a Custom `settings.yml` File - -If you prefer, you can take the [default settings.yml file](https://github.com/pglombardo/PasswordPusher/blob/master/config/settings.yml), modify it and apply it to the Password Pusher Docker container. - -Inside the Password Pusher Docker container: -* application code exists in the path `/opt/PasswordPusher/` -* the `settings.yml` file is located at `/opt/PasswordPusher/config/settings.yml` - -To replace this file with your own custom version, you can launch the Docker container with a bind mount option: - -```sh - docker run -d \ - --mount type=bind,source=/path/settings.yml,target=/opt/PasswordPusher/config/settings.yml \ - -p "5100:5100" pglombardo/pwpush:release -``` - -# Application Encryption - -Password Pusher encrypts sensitive data in the database. This requires a randomly generated encryption key for each application instance. - -To set a custom encryption key for your application, set the environment variable `PWPUSH_MASTER_KEY`: - - PWPUSH_MASTER_KEY=0c110f7f9d93d2122f36debf8a24bf835f33f248681714776b336849b801f693 - -## Generate a New Encryption Key - -Key generation can be done through the [helper tool](https://pwpush.com/pages/generate_key) or on the command line in the application source using `Lockbox.generate_key`: - -```ruby -bundle -rails c -Lockbox.generate_key -``` - -Notes: - -* If an encryption key isn't provided, a default key will be used. -* The best security for private instances of Password Pusher is to use your own custom encryption key although it is not required. -* The risk in using the default key is lessened if you keep your instance secure and your push expirations short. e.g. 1 day/1 view versus 100 days/100 views. -* Once a push expires, all encrypted data is deleted. -* Changing an encryption key where old pushes already exist will make those older pushes unreadable. In other words, the payloads will be garbled. New pushes going forward will work fine. - - -# Changing Application Defaults - -## Application General - -| Environment Variable | Description | Default Value | -| --------- | ------------------ | --- | -| PWP__DEFAULT_LOCALE | Sets the default language for the application. See the [documentation](https://github.com/pglombardo/PasswordPusher#internationalization). | `en` | -| PWP__RELATIVE_ROOT | Runs the application in a subfolder. e.g. With a value of `pwp` the front page will then be at `https://url/pwp` | `Not set` | -| PWP__SHOW_VERSION | Show the version in the footer | `true` | -| PWP__SHOW_GDPR_CONSENT_BANNER | Optionally enable or disable the GDPR cookie consent banner. | `true` | -| PWP__TIMEZONE | Set the application wide timezone. Use a valid timezone string (see note below). | `America/New_York` | -| SECRET_KEY_BASE | A secret key that is used for various security-related features, including session cookie encryption and other cryptographic operations. Use `/opt/PasswordPusher/bin/rails secret` to generate a random key string. [See the SECRET_KEY_BASE wiki page.](https://github.com/pglombardo/PasswordPusher/wiki/SECRET_KEY_BASE)| _Randomly Generated on boot_ | - -_Note_: The list of valid timezone strings can be found at [https://en.wikipedia.org/wiki/List_of_tz_database_time_zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). - -## Password Push Expiration Settings - -| Environment Variable | Description | Default Value | -| --------- | ------------------ | --- | -| PWP__PW__EXPIRE_AFTER_DAYS_DEFAULT | Controls the "Expire After Days" default value in Password#new | `7` | -| PWP__PW__EXPIRE_AFTER_DAYS_MIN | Controls the "Expire After Days" minimum value in Password#new | `1` | -| PWP__PW__EXPIRE_AFTER_DAYS_MAX | Controls the "Expire After Days" maximum value in Password#new | `90` | -| PWP__PW__EXPIRE_AFTER_VIEWS_DEFAULT | Controls the "Expire After Views" default value in Password#new | `5` | -| PWP__PW__EXPIRE_AFTER_VIEWS_MIN | Controls the "Expire After Views" minimum value in Password#new | `1` | -| PWP__PW__EXPIRE_AFTER_VIEWS_MAX | Controls the "Expire After Views" maximum value in Password#new | `100` | -| PWP__PW__ENABLE_DELETABLE_PUSHES | Can passwords be deleted by viewers? When true, passwords will have a link to optionally delete the password being viewed | `false` | -| PWP__PW__DELETABLE_PUSHES_DEFAULT | When the above is `true`, this sets the default value for the option. | `true` | -| PWP__PW__ENABLE_RETRIEVAL_STEP | When `true`, adds an option to have a preliminary step to retrieve passwords. | `true` | -| PWP__PW__RETRIEVAL_STEP_DEFAULT | Sets the default value for the retrieval step for newly created passwords. | `false` | -| PWP__PW__ENABLE_BLUR | Enables or disables the 'blur' effect when showing a push payload to the user. | `true` | - - -## Password Generator Settings - -| Environment Variable | Description | Default Value | -| --------- | ------------------ | --- | -| PWP__GEN__HAS_NUMBERS | Controls whether generated passwords have numbers | `true` | -| PWP__GEN__TITLE_CASED | Controls whether generated passwords will be title cased | `true` | -| PWP__GEN__USE_SEPARATORS | Controls whether generated passwords will use separators between syllables | `true` | -| PWP__GEN__CONSONANTS | The list of consonants to generate from | `bcdfghklmnprstvz` | -| PWP__GEN__VOWELS | The list of vowels to generate from | `aeiouy` | -| PWP__GEN__SEPARATORS | If `use_separators` is enabled above, the list of separators to use (randomly) | `-_=` | -| PWP__GEN__MAX_SYLLABLE_LENGTH | The maximum length of each syllable that a generated password can have | `3` | -| PWP__GEN__MIN_SYLLABLE_LENGTH | The minimum length of each syllable that a generated password can have | `1` | -| PWP__GEN__SYLLABLES_COUNT | The exact number of syllables that a generated password will have | `3` | - -# Enabling Logins - -To enable logins in your instance of Password Pusher, you must have an SMTP server available to send emails through. These emails are sent for events such as password reset, unlock, registration etc.. - -To use logins, you should be running a database backed version of Password Pusher. Logins will likely work in an ephemeral setup but aren't suggested since all data is wiped with every restart. - -_All_ of the following environments need to be set (except SMTP authentication if none) for application logins to function properly. - -| Environment Variable | Description | Default | -| --------- | ------------------ | --- | -| PWP__ENABLE_LOGINS | On/Off switch for logins. | `false` | -| PWP__ALLOW_ANONYMOUS | When false, requires a login for the front page (to push new passwords). | `true` | -| PWP__MAIL__RAISE_DELIVERY_ERRORS | Email delivery errors will be shown in the application | `true` | -| PWP__MAIL__SMTP_ADDRESS | Allows you to use a remote mail server. Just change it from its default "localhost" setting. | `smtp.domain.com` | -| PWP__MAIL__SMTP_PORT | Port of the SMTP server | `587` | -| PWP__MAIL__SMTP_USER_NAME | If your mail server requires authentication, set the username in this setting. | `smtp_username` | -| PWP__MAIL__SMTP_PASSWORD | If your mail server requires authentication, set the password in this setting. | `smtp_password` | -| PWP__MAIL__SMTP_AUTHENTICATION | If your mail server requires authentication, you need to specify the authentication type here. This is a string and one of :plain (will send the password in the clear), :login (will send password Base64 encoded) or :cram_md5 (combines a Challenge/Response mechanism to exchange information and a cryptographic Message Digest 5 algorithm to hash important information) | `plain` | -| PWP__MAIL__SMTP_STARTTLS | Use STARTTLS when connecting to your SMTP server and fail if unsupported. | `true` | -| PWP__MAIL__SMTP_ENABLE_STARTTLS_AUTO | Detects if STARTTLS is enabled in your SMTP server and starts to use it | `true` | -| PWP__MAIL__OPEN_TIMEOUT | Number of seconds to wait while attempting to open a connection. | `10` | -| PWP__MAIL__READ_TIMEOUT | Number of seconds to wait until timing-out a read(2) call. | `10` | -| PWP__HOST_DOMAIN | Used to build fully qualified URLs in emails. Where is your instance hosted? | `pwpush.com` | -| PWP__HOST_PROTOCOL | The protocol to access your Password Pusher instance. HTTPS advised. | `https` | -| PWP__MAIL__MAILER_SENDER | This is the "From" address in sent emails. | '"Company Name" ' | -| PWP__DISABLE_SIGNUPS| Once your user accounts are created, you can set this to disable any further user account creation. Sign up links and related backend functionality is disabled when `true`. | `false` | -| PWP__SIGNUP_EMAIL_REGEXP | The regular expression used to validate emails for new user signups. This can be modified to limit new account creation to a subset of domains. e.g. \A[^@\s]+@(hey\.com\|gmail\.com)\z. _Tip: use https://rubular.com to test out your regular expressions. It includes a guide to what each component means in regexp._ | `\A[^@\s]+@[^@\s]+\z` | - -## Shell Example - -``` -export PWP__ENABLE_LOGINS=true -export PWP__MAIL__RAISE_DELIVERY_ERRORS=true -export PWP__MAIL__SMTP_ADDRESS=smtp.mycompany.org -export PWP__MAIL__SMTP_PORT=587 -export PWP__MAIL__SMTP_USER_NAME=yolo -export PWP__MAIL__SMTP_PASSWORD=secret -export PWP__MAIL__SMTP_AUTHENTICATION=plain -export PWP__MAIL__SMTP_STARTTLS=true -export PWP__MAIL__OPEN_TIMEOUT=10 -export PWP__MAIL__READ_TIMEOUT=10 -export PWP__HOST_DOMAIN=pwpush.mycompany.org -export PWP__HOST_PROTOCOL=https -export PWP__MAIL__MAILER_SENDER='"Spiderman" ' -``` - -* See also this [Github discussion](https://github.com/pglombardo/PasswordPusher/issues/265#issuecomment-964432942). -* [External Documentation on mailer configuration](https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration) for the underlying technology if you need more details for configuration issues. - -# Enabling File Pushes - -To enable file uploads (File Pushes) in your instance of Password Pusher, there are a few requirements: - -1. you must have logins enabled (see above) -2. specify a place to store uploaded files -3. If you use cloud storage, configure the CORS configuration in your buckets (detailed below) - -The following settings enable/disable the feature and specify where to store uploaded files. - -This feature can store uploads on local disk (not valid for Docker containers), Amazon S3, Google Cloud Storage or Azure Storage. - -## General Settings - -| Environment Variable | Description | Value(s) | -| --------- | ------------------ | --- | -| PWP__ENABLE_FILE_PUSHES | On/Off switch for File Pushes. | `false` | -| PWP__FILES__STORAGE | Chooses the storage area for uploaded files. | `local`, `amazon`, `google` or `microsoft` | -| PWP__FILES__ENABLE_BLUR | Enables or disables the 'blur' effect when showing a text payload to the user. | `true` | -| PWP__FILES__ENABLE_DELETABLE_PUSHES | Can passwords be deleted by viewers? When true, passwords will have a link to optionally delete the password being viewed | `false` | -| PWP__FILES__DELETABLE_PUSHES_DEFAULT | When the above is `true`, this sets the default value for the option. | `true` | -| PWP__FILES__ENABLE_RETRIEVAL_STEP | When `true`, adds an option to have a preliminary step to retrieve passwords. | `true` | -| PWP__FILES__RETRIEVAL_STEP_DEFAULT | Sets the default value for the retrieval step for newly created passwords. | `false` | -| PWP__FILES__MAX_FILE_UPLOADS | Sets the maximum number of files that can be added to a single push. | `10` | - -## File Push Expiration Settings - -| Environment Variable | Description | Default Value | -| --------- | ------------------ | --- | -| PWP__FILES__EXPIRE_AFTER_DAYS_DEFAULT | Controls the "Expire After Days" default value in Password#new | `7` | -| PWP__FILES__EXPIRE_AFTER_DAYS_MIN | Controls the "Expire After Days" minimum value in Password#new | `1` | -| PWP__FILES__EXPIRE_AFTER_DAYS_MAX | Controls the "Expire After Days" maximum value in Password#new | `90` | -| PWP__FILES__EXPIRE_AFTER_VIEWS_DEFAULT | Controls the "Expire After Views" default value in Password#new | `5` | -| PWP__FILES__EXPIRE_AFTER_VIEWS_MIN | Controls the "Expire After Views" minimum value in Password#new | `1` | -| PWP__FILES__EXPIRE_AFTER_VIEWS_MAX | Controls the "Expire After Views" maximum value in Password#new | `100` | - -## Local Storage - -`PWP__FILES__STORAGE=local` - -The default location for local storage is `./storage`. - -If using containers and you prefer local storage, you can add a volume mount to the container at the path `/opt/PasswordPusher/storage`: - -`docker run -d -p "5100:5100" -v /var/lib/pwpush/files:/opt/PasswordPusher/storage pglombardo/pwpush:release` - -Please _make sure_ that the directory is writeable by the docker container. - -A CORS configuration is not required for local storage. - -## Amazon S3 - -To configure the application to store files in an Amazon S3 bucket, you have to: - -1. set the required environment variables detailed below (or the equivalent values in `settings.yml`) -2. apply a CORS configuration to your S3 bucket (see next section) - -| Environment Variable | Description | Value(s) | -| --------- | ------------------ | --- | -| PWP__FILES__STORAGE | Storage Provider Selection | `amazon` | -| PWP__FILES__S3__ENDPOINT | S3 Endpoint | None | -| PWP__FILES__S3__ACCESS_KEY_ID | Access Key ID | None | -| PWP__FILES__S3__SECRET_ACCESS_KEY | Secret Access Key| None | -| PWP__FILES__S3__REGION | S3 Region| None | -| PWP__FILES__S3__BUCKET | The S3 bucket name | None | - -### Amazon S3 CORS Configuration - -The application performs direct uploads from the browser to your Amazon S3 bucket. This provides better performance and reduces load on the application itself. - -For this to work, you have to add a CORS configuration to your bucket. - -This direct upload functionality is done using a library called ActiveStorage. For the full documentation on configuring CORS for ActiveStorage, [see here](https://edgeguides.rubyonrails.org/active_storage_overview.html#cross-origin-resource-sharing-cors-configuration). - -```json -[ - { - "AllowedHeaders": [ - "Content-Type", - "Content-MD5", - "Content-Disposition" - ], - "AllowedMethods": [ - "PUT" - ], - "AllowedOrigins": [ - "https://www.example.com" << Change to your URL - ], - "MaxAgeSeconds": 3600 - } -] -``` -## Google Cloud Storage - -To configure the application to store files in Google Cloud Storage, you have to: - -1. set the required environment variables detailed below (or the equivalent values in `settings.yml`) -2. apply a CORS configuration (see next section) - -| Environment Variable | Description | Value(s) | -| --------- | ------------------ | --- | -| PWP__FILES__STORAGE | Storage Provider Selection | `google` | -| PWP__FILES__GCS__PROJECT | GCS Project | None | -| PWP__FILES__GCS__CREDENTIALS | GCS Credentials | None | -| PWP__FILES__GCS__BUCKET | The GCS bucket name | None | - -### Google Cloud Storage CORS Configuration - -The application performs direct uploads from the browser to Google Cloud Storage. This provides better performance and reduces load on the application itself. - -For this to work, you have to add a CORS configuration. - -This direct upload functionality is done using a library called ActiveStorage. For the full documentation on configuring CORS for ActiveStorage, [see here](https://edgeguides.rubyonrails.org/active_storage_overview.html#cross-origin-resource-sharing-cors-configuration). - -```json -[ - { - "origin": ["https://www.example.com"], - "method": ["PUT"], - "responseHeader": ["Content-Type", "Content-MD5", "Content-Disposition"], - "maxAgeSeconds": 3600 - } -] -``` - - -## Azure Storage - -To configure the application to store files in Azure Storage, you have to: - -1. set the required environment variables detailed below (or the equivalent values in `settings.yml`) -2. apply a CORS configuration (see next section) - -| Environment Variable | Description | Value(s) | -| --------- | ------------------ | --- | -| PWP__FILES__STORAGE | Storage Provider Selection | `microsoft` | -| PWP__FILES__AS__STORAGE_ACCOUNT_NAME | Azure Storage Account Name | None | -| PWP__FILES__AS__STORAGE_ACCESS_KEY | Azure Storage Account Key | None | -| PWP__FILES__AS__CONTAINER | Azure Storage Container Name | None | - -### Azure Storage CORS Configuration - -The application performs direct uploads from the browser to Azure Storage. This provides better performance and reduces load on the application itself. - -For this to work, you have to add a CORS configuration. - -This direct upload functionality is done using a library called ActiveStorage. For the full documentation on configuring CORS for ActiveStorage, [see here](https://edgeguides.rubyonrails.org/active_storage_overview.html#cross-origin-resource-sharing-cors-configuration). - -```xml - - - https://www.example.com - PUT - Content-Type, Content-MD5, x-ms-blob-content-disposition, x-ms-blob-type - 3600 - - -``` - -# Enabling URL Pushes - -Similar to file pushes, URL pushes also require logins to be enabled. - -| Environment Variable | Description | Default | -| --------- | ------------------ | --- | -| PWP__ENABLE_URL_PUSHES | On/Off switch for URL Pushes. | `false` | - -## URL Push Expiration Settings - -| Environment Variable | Description | Default Value | -| --------- | ------------------ | --- | -| PWP__URL__EXPIRE_AFTER_DAYS_DEFAULT | Controls the "Expire After Days" default value in Password#new | `7` | -| PWP__URL__EXPIRE_AFTER_DAYS_MIN | Controls the "Expire After Days" minimum value in Password#new | `1` | -| PWP__URL__EXPIRE_AFTER_DAYS_MAX | Controls the "Expire After Days" maximum value in Password#new | `90` | -| PWP__URL__EXPIRE_AFTER_VIEWS_DEFAULT | Controls the "Expire After Views" default value in Password#new | `5` | -| PWP__URL__EXPIRE_AFTER_VIEWS_MIN | Controls the "Expire After Views" minimum value in Password#new | `1` | -| PWP__URL__EXPIRE_AFTER_VIEWS_MAX | Controls the "Expire After Views" maximum value in Password#new | `100` | -| PWP__URL__ENABLE_DELETABLE_PUSHES | Can passwords be deleted by viewers? When true, passwords will have a link to optionally delete the password being viewed | `false` | -| PWP__URL__DELETABLE_PUSHES_DEFAULT | When the above is `true`, this sets the default value for the option. | `true` | -| PWP__URL__ENABLE_RETRIEVAL_STEP | When `true`, adds an option to have a preliminary step to retrieve passwords. | `true` | -| PWP__URL__RETRIEVAL_STEP_DEFAULT | Sets the default value for the retrieval step for newly created passwords. | `false` | - -# Rebranding - -Password Pusher has the ability to be [re-branded](https://twitter.com/pwpush/status/1557658305325109253) with your own site title, tagline and logo. - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/branding%2Fpwpush-brand-example.png) - -This can be done with the following environment variables: - -| Environment Variable | Description | Default Value | -| --------- | ------------------ | --- | -| PWP__BRAND__TITLE | Title for the site. | `Password Pusher` | -| PWP__BRAND__TAGLINE | Tagline for the site. | `Go Ahead. Email Another Password.` | -| PWP__BRAND__DISCLAIMER | Disclaimer for the site. | `Undefined` | -| PWP__BRAND__SHOW_FOOTER_MENU | On/Off switch for the footer menu. | `true` | -| PWP__BRAND__LIGHT_LOGO | Site logo image for the light theme. | `logo-transparent-sm-bare.png` | -| PWP__BRAND__DARK_LOGO | Site logo image for the dark theme. | `logo-transparent-sm-bare.png` | - -The values for the `*_LOGO` images can either be: - -1. Fully qualified HTTP(s) URLS such as `https://pwpush.fra1.cdn.digitaloceanspaces.com/dev%2Facme-logo.jpg` (easiest) -2. Relative path that is mounted inside the container - -As an example for #2 above, say you place your logo images locally into `/var/lib/pwpush/logos/`. You would then mount that directory into the container: - -`docker run -d -p "5100:5100" -v /var/lib/pwpush/logos:/opt/PasswordPusher/public/logos pglombardo/pwpush:release` - -or alternatively for a `docker-compose.yml` file: - -```yaml -volumes: - # Example of a persistent volume for the storage directory (file uploads) - - /var/lib/pwpush/logos:/opt/PasswordPusher/public/logos:r -``` - -See [here](https://github.com/pglombardo/PasswordPusher/blob/master/containers/docker/docker-compose-postgres.yml) for a larger Docker Compose explanation. - -With this setup, you can then set your `LOGO` environment variables (or `settings.yml` options) to: - -``` -PWP__BRAND__LIGHT_LOGO=/logos/mylogo.png -``` - -## See Also - -* the `brand` section of [settings.yml](https://github.com/pglombardo/PasswordPusher/blob/master/config/settings.yml) for more details, examples and description. -* [this issue comment](https://github.com/pglombardo/PasswordPusher/issues/432#issuecomment-1282158006) on how to mount images into the contianer and set your environment variables accordingly - -# Change the Default Language - -The application comes with more than 24 languages bundled in which are selectable inside the application. The default language of the application is English. If you would like to change this default language, simply set the following environment variable for your application. - -```PWP__DEFAULT_LOCALE=is``` - -A list of supported languages (and their language codes) can be found in the [settings.yml](https://github.com/pglombardo/PasswordPusher/blob/master/config/settings.yml#L702-L734) file under `language_codes`. - -Choose which language you would like to have as the default language, and use the two letter code as the value for the environment variable. - -# Themes - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fquartz-theme-pwpush.com.png) - -Password Pusher supports **26 themes out of the box**. These themes are taken directly from the great [Bootswatch](https://bootswatch.com) project and are unmodified. - -As such, themes mostly work although there may be a rare edge cases where fonts may not be clear or something doesn't display correctly. If this is the case you can add custom CSS styles to fix any such issues. See the next section on how to add custom styling. - ----> Checkout the [Themes Gallery](Themes.md)! - -The Bootswatch themes are licensed under the MIT license. - -## Configuring a Theme - -To specify a theme for your Password Pusher instance, you must set __two__ environment variables:the `PWP__THEME` environment variable to specify the theme and `PWP_PRECOMPILE=true` environment variable to have CSS assets recompiled on container boot. - -**Make sure to set both `PWP__THEME` and `PWP_PRECOMPILE` for the selected theme to work.** 👍 - -| Environment Variable | Description | Possible Values | -| --------- | ------------------ | --- | -| PWP__THEME | Theme used for the application. | 'cerulean', 'cosmo', 'cyborg', 'darkly', 'flatly', 'journal', 'litera', 'lumen', 'lux', 'materia', 'minty', 'morph', 'pulse', 'quartz', 'sandstone', 'simplex', 'sketchy', 'slate', 'solar', 'spacelab', 'superhero', 'united', 'vapor', 'yeti', 'zephyr' | -| PWP_PRECOMPILE | Forces a rebuild of the theme CSS on boot. | `true` | - ----> See the [Themes Gallery](Themes.md) for examples of each. - -__Note:__ Since the theme is a boot level selection, the theme can only be selected by setting the `PWP__THEME` environment variable (and not modifying `settings.yml`). - -So to set the `quartz` theme for a Docker container: - -```bash -docker run --env PWP__THEME=quartz --env PWP_PRECOMPILE=true -p "5100:5100" pglombardo/pwpush:release -``` - -or alternatively for source code: - -```bash -export PWP__THEME=quartz -bin/rails asset:precompile # manually recompile assets -bin/rails server -``` - -## How to Precompile CSS Assets - -Password Pusher has a pre-compilation step of assets. This is used to fingerprint assets and pre-process CSS code for better performance. - -If using Docker containers, you can simply set the `PWP_PRECOMPILE=true` environment variable. On container boot, all assets will be precompiled and bundled into `/assets`. - -__Note: Precompiling all application assets for a new theme on container boot can add 30-90 seconds to the boot process (depending on the system). Make sure to allow this time in your health checks before declaring the container as unresponsive.__ - -To manually precompile assets run `bin/rails assets:precompile`. - -## Adding an entirely new theme from scratch - -The `PWP__THEME` environment variable simply causes the application to load a css file from `app/assets/stylesheets/themes/{$PWP__THEME}.css`. If you were to place a completely custom CSS file into that directory, you could then set the `PWP__THEME` environment variable to the filename that you added. - -For example: - -Add `app/assets/stylesheets/themes/mynewtheme.css` and set `PWP__THEME=mynewtheme`. - -This would cause that CSS file to be loaded and used as the theme for the site. Please refer to existing themes if you would like to author your theme for Password Pusher. - -Remember that after the new theme is configured, assets must be precompiled again. See the the previous section for instructions - -# How to Add Custom CSS - -Password Pusher supports adding custom CSS to the application. The application hosts a `custom.css` file located at `app/assets/stylesheets/custom.css`. This file is loaded last so it take precedence over all built in themes and styling. - -This file can either be modified directly or in the case of Docker containers, a new file mounted over the existing one. - -When changing this file inside a Docker container, make sure to set the precompile option `PWP_PRECOMPILE=true`. This will assure that the custom CSS is incorporated correctly. - -An example Docker command to override that file would be: - -``` -docker run -e PWP_PRECOMPILE=true --mount type=bind,source=/path/to/my/custom.css,target=/opt/PasswordPusher/app/assets/stylesheets/custom.css -p 5100:5100 pglombardo/pwpush:release -``` -or the `docker-compose.yml` equivalent: - -``` -version: '2.1' -services: - - pwpush: - image: docker.io/pglombardo/pwpush:release - ports: - - "5100:5100" - environment: - PWP_PRECOMPILE: 'true' - volumes: - - type: bind - source: /path/to/my/custom.css - target: /opt/PasswordPusher/app/assets/stylesheets/custom.css -``` - -Remember that when doing this, this new CSS code has to be precompiled. - -To do this in Docker containers, simply set the environment variable `PWP_PRECOMPILE=true`. For source code, run `bin/rails assets:precompile`. This compilation process will incorporate the custom CSS into the updated site theme. - -# Google Analytics - -| Environment Variable | Description | -| --------- | ------------------ | -| GA_ENABLE | The existence of this variable will enable the Google Analytics for the application. See `app/views/layouts/_ga.html.erb`.| -| GA_ACCOUNT | The Google Analytics account id. E.g. `UA-XXXXXXXX-X` | -| GA_DOMAIN | The domain where the application is hosted. E.g. `pwpush.com` | - -# Throttling - -Throttling enforces a minimum time interval -between subsequent HTTP requests from a particular client, as -well as by defining a maximum number of allowed HTTP requests -per a given time period (per second, minute, hourly, or daily). - -| Environment Variable | Description | Default Value | -| --------- | ------------------ | --- | -| PWP__THROTTLING__DAILY | The maximum number of allowed HTTP requests per day | `5000` | -| PWP__THROTTLING__HOURLY | The maximum number of allowed HTTP requests per hour | `600` | -| PWP__THROTTLING__MINUTE | The maximum number of allowed HTTP requests per minute | `60` | -| PWP__THROTTLING__SECOND | The maximum number of allowed HTTP requests per second | `20` | - - -# Logging - -| Environment Variable | Description | -| --------- | ------------------ | -| PWP__LOG_LEVEL | Set the logging level for the application. Valid values are: `debug`, `info`, `warn`, `error` and `fatal`. Note: lowercase. -| PWP__LOG_TO_STDOUT | Set to 'true' to have log output sent to STDOUT instead of log files. Default: `false` - - -# Forcing SSL Links - -See also the Proxies section below. - -| Environment Variable | Description | -| --------- | ------------------ | -| FORCE_SSL | (Deprecated) The existence of this variable will set `config.force_ssl` to `true` and generate HTTPS based secret URLs - -__Note:__ This is a legacy setting and is no longer suggested for use. If using a proxy, make sure to have your proxy forward the `X-Forwarded-Host`, `X-Forwarded-Port` and `X-Forwarded-Proto` HTTP headers. See the "Proxies" section for more information and instructions. -_ -# Proxies - -An occasional issue is that when using Password Pusher behind a proxy, the generated secret URLs are incorrect. They often have the backend URL & port instead of the public fully qualified URL - or use HTTP instead of HTTPS (or all of the preceding). - -To resolve this, make sure your proxy properly forwards the `X-Forwarded-Host`, `X-Forwarded-Port` and `X-Forwarded-Proto` headers. - -The values in these headers represent the front end request. When these headers are sent, Password Pusher can then build the correct URLs. - -## Nginx Example - -As an example, for nginx, the addition could be: - -```nginx -proxy_set_header X-Forwarded-Port $server_port; -proxy_set_header X-Forwarded-Host $host; -proxy_set_header X-Forwarded-Proto $scheme; -proxy_set_header X-Forwarded-Ssl on; -``` - -If you are unable to have these headers passed to the application for any reason, you could instead force an override of the base URL using the `PWP__OVERRIDE_BASE_URL` environment variable. - -| Environment Variable | Description | Example Value | -| --------- | ------------------ | --- | -| PWP__OVERRIDE_BASE_URL | Set this value (without a trailing slash) to force the base URL of generated links. | 'https://subdomain.domain.dev' +Thanks for using Password Pusher! diff --git a/Gemfile b/Gemfile index d876469821b4..51ad7fe86d72 100644 --- a/Gemfile +++ b/Gemfile @@ -1,122 +1,102 @@ # frozen_string_literal: true -source 'https://rubygems.org' - -ruby ENV['CUSTOM_RUBY_VERSION'] || '>=3.1.4' - -gem 'rails', '~> 7.1.3' +source "https://rubygems.org" + +ruby ENV["CUSTOM_RUBY_VERSION"] || ">=3.4.3" + +gem "rails", "~> 8.1.3" +gem "propshaft" +gem "importmap-rails" +gem "turbo-rails" +gem "stimulus-rails" +gem "jbuilder" +gem "tzinfo-data", platforms: %i[windows jruby] +gem "solid_cache" +gem "solid_queue" +gem "solid_cable" +gem "bootsnap", require: false +gem "kamal", require: false +gem "thruster", require: false +gem "cssbundling-rails" group :development do - gem 'listen' + gem "listen" + gem "pry-rails" + gem "web-console" - # Visual Studio Additions - gem 'rubocop' - gem 'rubocop-performance' - gem 'rubocop-rails' - gem 'ruby-debug-ide' + # A fully configurable and extendable Git hook manager + gem "overcommit", require: false - gem 'pry-rails' - - # Access an interactive console on exception pages or by - # calling 'console' anywhere in the code. - gem 'web-console', '>= 4.2.0' + gem "mailbin" end group :test do # Adds support for Capybara system testing and selenium driver - gem 'capybara', '>= 3.37.1', '< 4.0' - gem 'minitest' - gem 'minitest-rails', '>= 6.1.0' - gem 'minitest-reporters' - gem 'selenium-webdriver' - gem 'webdrivers', '~> 5.3', require: false + gem "capybara", ">= 3.37.1", "< 4.0" + gem "minitest" + gem "minitest-rails", ">= 6.1.0" + gem "minitest-reporters" + gem "selenium-webdriver", ">= 4.20.1" end group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem - gem 'debase', '>= 0.2.5.beta2', platforms: %i[mri mingw x64_mingw] - gem 'debug', platforms: %i[mri mingw x64_mingw] + gem "debug", platforms: %i[mri windows], require: "debug/prelude" + + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem "brakeman", require: false + + # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] + gem "rubocop-rails-omakase", require: false + + gem "i18n-tasks", "~> 1.1.2", require: false + gem "erb_lint", "~> 0.9.0", require: false + gem "standard", ">= 1.35.1" end -gem 'rack-attack' -gem 'rack-cors' - -# OSX: ../src/utils.h:33:10: fatal error: 'climits' file not found -# From: -# # 1. Install v8 ourselves -# $ brew install v8-315 -# # 2. Install libv8 using the v8 binary we just installed -# $ gem install libv8 -v '3.16.14.19' -- --with-system-v8 -# # 3. Install therubyracer using the v8 binary we just installed -# $ gem install therubyracer -- --with-v8-dir=/usr/local/opt/v8@315 -# # 4. Install the remaining dependencies -# $ bundle install -# gem 'therubyracer' -# -gem 'high_voltage' -gem 'kramdown', require: false -gem 'lockbox' - -# Reduces boot times through caching; required in config/boot.rb -gem 'bootsnap', '>= 1.4.4', require: false - -# Use SCSS for stylesheets -gem 'sass-rails', '~> 6.0', '>= 6.0.0' -gem 'terser', '~> 1.2' -# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder -gem 'bootstrap', '5.2.3' -gem 'json', '~> 2.7' # Legacy carry-over -gem 'will_paginate', '~> 4.0.0' -gem 'will_paginate-bootstrap-style' - -# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] -gem 'turbo-rails' -# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] -gem 'stimulus-rails' -# Build JSON APIs with ease [https://github.com/rails/jbuilder] -gem 'jbuilder' - -# Use Redis adapter to run Action Cable in production -# gem 'redis', '~> 4.0' -# Use ActiveModel has_secure_password -# gem 'bcrypt', '~> 3.1.7' - -gem 'apipie-rails' -gem 'config' -gem 'devise', '>= 4.9.0' -gem 'foreman' -gem 'lograge' -gem 'mail_form', '>= 1.9.0' -gem 'oj' -gem 'puma' -gem 'rollbar' -gem 'simple_token_authentication' - -gem 'devise-i18n' -gem 'i18n-tasks', '~> 1.0.13' # , group: :development -gem 'rails-i18n', '~> 7.0.8' -gem 'route_translator', '>= 13.0.0' -gem 'translation' +gem "rack-cors" +gem "high_voltage" +gem "kramdown", require: false +gem "lockbox" +gem "terser", "~> 1.2" +gem "bootstrap" +gem "json", "~> 2.21" # Legacy carry-over +gem "apipie-rails" +gem "config" +gem "devise", "~> 5.0" +gem "foreman" +gem "lograge" +gem "mail_form", ">= 1.9.0" +gem "oj" +gem "puma" +gem "kaminari", "~> 1.2" +gem "invisible_captcha", "~> 2.3" +gem "devise-i18n" +gem "rails-i18n", "~> 8.1.0" +gem "translation" +gem "local_time" # For File Uploads -gem 'aws-sdk-s3', require: false -gem 'azure-storage-blob', '~> 2.0', require: false -gem 'google-cloud-storage', '~> 1.48', require: false - -# Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] +gem "aws-sdk-s3", require: false +gem "azure-blob", "~> 0.8.0", require: false +gem "google-cloud-storage", "~> 1.62", require: false # Database backends -gem 'mysql2' -gem 'pg' -gem 'sqlite3', force_ruby_platform: true +gem "mysql2" +gem "pg" +gem "sqlite3", force_ruby_platform: true -group :production do - gem 'rack-throttle', '0.7.0' - gem 'rack-timeout' +group :production, :development do + gem "rack-attack" end -gem 'version', git: 'https://github.com/pglombardo/version.git', branch: 'master' +gem "rollbar" +gem "version", git: "https://github.com/pglombardo/version.git", branch: "master" +gem "madmin" +gem "rotp", "~> 6.2" +gem "rqrcode", "~> 3.2" +gem "turnout2024", require: "turnout" +gem "mission_control-jobs", "~> 1.3.1" +gem "overmind", "~> 2.5", group: :development -gem 'derailed_benchmarks', group: :development -gem 'stackprof', group: :development +gem "dotenv", "~> 3.2" diff --git a/Gemfile.lock b/Gemfile.lock index a68ca844f91e..62c7689c48e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/pglombardo/version.git - revision: 97678e0068542fb919f909b0d3a9e853ec7a24c2 + revision: 7e56f1552c19a1e5ce283c7add001c2a2322e385 branch: master specs: version (1.1.1) @@ -8,132 +8,129 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.1.3) - actionpack (= 7.1.3) - activesupport (= 7.1.3) + action_text-trix (2.1.19) + railties + actioncable (8.1.4) + actionpack (= 8.1.4) + activesupport (= 8.1.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.3) - actionpack (= 7.1.3) - activejob (= 7.1.3) - activerecord (= 7.1.3) - activestorage (= 7.1.3) - activesupport (= 7.1.3) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.1.3) - actionpack (= 7.1.3) - actionview (= 7.1.3) - activejob (= 7.1.3) - activesupport (= 7.1.3) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp + actionmailbox (8.1.4) + actionpack (= 8.1.4) + activejob (= 8.1.4) + activerecord (= 8.1.4) + activestorage (= 8.1.4) + activesupport (= 8.1.4) + mail (>= 2.8.0) + actionmailer (8.1.4) + actionpack (= 8.1.4) + actionview (= 8.1.4) + activejob (= 8.1.4) + activesupport (= 8.1.4) + mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.1.3) - actionview (= 7.1.3) - activesupport (= 7.1.3) + actionpack (8.1.4) + actionview (= 8.1.4) + activesupport (= 8.1.4) nokogiri (>= 1.8.5) - racc rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.3) - actionpack (= 7.1.3) - activerecord (= 7.1.3) - activestorage (= 7.1.3) - activesupport (= 7.1.3) + useragent (~> 0.16) + actiontext (8.1.4) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.4) + activerecord (= 8.1.4) + activestorage (= 8.1.4) + activesupport (= 8.1.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.3) - activesupport (= 7.1.3) + actionview (8.1.4) + activesupport (= 8.1.4) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.1.3) - activesupport (= 7.1.3) + activejob (8.1.4) + activesupport (= 8.1.4) globalid (>= 0.3.6) - activemodel (7.1.3) - activesupport (= 7.1.3) - activerecord (7.1.3) - activemodel (= 7.1.3) - activesupport (= 7.1.3) + activemodel (8.1.4) + activesupport (= 8.1.4) + activerecord (8.1.4) + activemodel (= 8.1.4) + activesupport (= 8.1.4) timeout (>= 0.4.0) - activestorage (7.1.3) - actionpack (= 7.1.3) - activejob (= 7.1.3) - activerecord (= 7.1.3) - activesupport (= 7.1.3) + activestorage (8.1.4) + actionpack (= 8.1.4) + activejob (= 8.1.4) + activerecord (= 8.1.4) + activesupport (= 8.1.4) marcel (~> 1.0) - activesupport (7.1.3) + activesupport (8.1.4) base64 bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) - ansi (1.5.0) - apipie-rails (1.3.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + ansi (1.6.0) + apipie-rails (1.5.1) actionpack (>= 5.0) activesupport (>= 5.0) - ast (2.4.2) - autoprefixer-rails (10.4.16.0) - execjs (~> 2) - aws-eventstream (1.3.0) - aws-partitions (1.889.0) - aws-sdk-core (3.191.1) + ast (2.4.3) + aws-eventstream (1.4.0) + aws-partitions (1.1290.0) + aws-sdk-core (3.257.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.77.0) - aws-sdk-core (~> 3, >= 3.191.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.143.0) - aws-sdk-core (~> 3, >= 3.191.0) + logger + rexml (~> 3.4, >= 3.4.2) + aws-sdk-kms (1.132.0) + aws-sdk-core (~> 3, >= 3.256.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.232.1) + aws-sdk-core (~> 3, >= 3.256.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) - azure-storage-blob (2.0.3) - azure-storage-common (~> 2.0) - nokogiri (~> 1, >= 1.10.8) - azure-storage-common (2.0.4) - faraday (~> 1.0) - faraday_middleware (~> 1.0, >= 1.0.0.rc1) - net-http-persistent (~> 4.0) - nokogiri (~> 1, >= 1.10.8) - base64 (0.2.0) - bcrypt (3.1.20) - benchmark-ips (2.13.0) - better_html (2.0.2) - actionview (>= 6.0) - activesupport (>= 6.0) + azure-blob (0.8.0) + cgi + rexml + base64 (0.3.0) + bcrypt (3.1.22) + bcrypt_pbkdf (1.1.2) + better_html (2.2.0) + actionview (>= 7.0) + activesupport (>= 7.0) ast (~> 2.0) erubi (~> 1.4) parser (>= 2.4) smart_properties - bigdecimal (3.1.6) + bigdecimal (4.1.3) bindex (0.8.1) - bootsnap (1.18.3) - msgpack (~> 1.2) - bootstrap (5.2.3) - autoprefixer-rails (>= 9.1.0) - popper_js (>= 2.11.6, < 3) - sassc-rails (>= 2.0.0) - builder (3.2.4) + bootsnap (1.26.0) + msgpack (~> 1.5) + bootstrap (5.3.8) + popper_js (>= 2.11.8, < 3) + brakeman (8.0.6) + racc + builder (3.3.0) capybara (3.40.0) addressable matrix @@ -143,520 +140,919 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) + cgi (0.5.2) + childprocess (5.1.0) + logger (~> 1.5) + chunky_png (1.4.0) coderay (1.1.3) - concurrent-ruby (1.2.3) - config (5.1.0) + concurrent-ruby (1.3.8) + config (5.6.1) deep_merge (~> 1.2, >= 1.2.1) - dry-validation (~> 1.0, >= 1.0.0) - connection_pool (2.4.1) - crass (1.0.6) - date (3.3.4) - dead_end (4.0.0) - debase (0.2.5.beta2) - debase-ruby_core_source (>= 0.10.12) - debase-ruby_core_source (3.3.1) - debug (1.9.1) + ostruct + connection_pool (3.0.2) + crass (1.0.7) + cssbundling-rails (1.4.3) + railties (>= 6.0.0) + date (3.5.1) + debug (1.11.1) irb (~> 1.10) reline (>= 0.3.8) declarative (0.0.20) deep_merge (1.2.2) - derailed_benchmarks (2.1.2) - benchmark-ips (~> 2) - dead_end - get_process_mem (~> 0) - heapy (~> 0) - memory_profiler (>= 0, < 2) - mini_histogram (>= 0.3.0) - rack (>= 1) - rack-test - rake (> 10, < 14) - ruby-statistics (>= 2.1) - thor (>= 0.19, < 2) - devise (4.9.3) + devise (5.0.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0) + railties (>= 7.0) responders warden (~> 1.2.3) - devise-i18n (1.12.0) - devise (>= 4.9.0) - digest-crc (0.6.5) + devise-i18n (1.16.1) + devise (>= 5.0.0) + rails-i18n + digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) - drb (2.2.0) - ruby2_keywords - dry-configurable (1.1.0) - dry-core (~> 1.0, < 2) - zeitwerk (~> 2.6) - dry-core (1.0.1) - concurrent-ruby (~> 1.0) - zeitwerk (~> 2.6) - dry-inflector (1.0.0) - dry-initializer (3.1.1) - dry-logic (1.5.0) - concurrent-ruby (~> 1.0) - dry-core (~> 1.0, < 2) - zeitwerk (~> 2.6) - dry-schema (1.13.3) - concurrent-ruby (~> 1.0) - dry-configurable (~> 1.0, >= 1.0.1) - dry-core (~> 1.0, < 2) - dry-initializer (~> 3.0) - dry-logic (>= 1.4, < 2) - dry-types (>= 1.7, < 2) - zeitwerk (~> 2.6) - dry-types (1.7.2) - bigdecimal (~> 3.0) - concurrent-ruby (~> 1.0) - dry-core (~> 1.0) - dry-inflector (~> 1.0) - dry-logic (~> 1.4) - zeitwerk (~> 2.6) - dry-validation (1.10.0) - concurrent-ruby (~> 1.0) - dry-core (~> 1.0, < 2) - dry-initializer (~> 3.0) - dry-schema (>= 1.12, < 2) - zeitwerk (~> 2.6) - erubi (1.12.0) - execjs (2.9.1) - faraday (1.10.3) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.1) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - faraday_middleware (1.2.0) - faraday (~> 1.0) - ffi (1.16.3) - foreman (0.87.2) - forwardable (1.3.3) - get_process_mem (0.2.7) - ffi (~> 1.0) + dotenv (3.2.0) + drb (2.2.3) + ed25519 (1.4.0) + erb (6.0.7) + erb_lint (0.9.0) + activesupport + better_html (>= 2.0.1) + parser (>= 2.7.1.4) + rainbow + rubocop (>= 1) + smart_properties + erubi (1.13.1) + et-orbi (1.4.2) + tzinfo + execjs (2.10.2) + json (>= 2) + faraday (2.14.4) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-follow_redirects (0.5.0) + faraday (>= 1, < 3) + faraday-net_http (3.4.4) + net-http (~> 0.5) + ffi (1.17.4) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86-linux-gnu) + ffi (1.17.4-x86-linux-musl) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) + fiddle (1.1.8) + foreman (0.90.0) + thor (~> 1.4) + forwardable (1.4.0) + fugit (1.14.0) + et-orbi (~> 1.4) + raabro (~> 1.4) gettext (3.4.9) erubi locale (>= 2.0.5) prime racc text (>= 1.3.0) - globalid (1.2.1) + globalid (1.4.0) activesupport (>= 6.1) - google-apis-core (0.13.0) - addressable (~> 2.5, >= 2.5.1) - googleauth (~> 1.9) - httpclient (>= 2.8.1, < 3.a) - mini_mime (~> 1.0) + google-apis-core (1.2.5) + addressable (~> 2.9) + faraday (~> 2.13) + faraday-follow_redirects (~> 0.3) + googleauth (~> 1.14) + mini_mime (~> 1.1) + multi_json (~> 1.11) representable (~> 3.0) - retriable (>= 2.0, < 4.a) - rexml - google-apis-iamcredentials_v1 (0.18.0) - google-apis-core (>= 0.12.0, < 2.a) - google-apis-storage_v1 (0.33.0) - google-apis-core (>= 0.12.0, < 2.a) - google-cloud-core (1.6.1) + retriable (>= 3.1, < 5.0) + google-apis-iamcredentials_v1 (0.28.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.67.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.9.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (2.1.1) + google-cloud-env (2.4.0) + base64 (~> 0.2) faraday (>= 1.0, < 3.a) - google-cloud-errors (1.3.1) - google-cloud-storage (1.48.1) + google-cloud-errors (1.7.0) + google-cloud-storage (1.62.0) addressable (~> 2.8) digest-crc (~> 0.4) - google-apis-core (~> 0.13) + google-apis-core (>= 0.18, < 2) google-apis-iamcredentials_v1 (~> 0.18) - google-apis-storage_v1 (~> 0.33) + google-apis-storage_v1 (>= 0.42) google-cloud-core (~> 1.6) googleauth (~> 1.9) mini_mime (~> 1.0) - googleauth (1.10.0) + google-logging-utils (0.2.0) + googleauth (1.17.4) faraday (>= 1.0, < 3.a) - google-cloud-env (~> 2.1) - jwt (>= 1.4, < 3.0) - multi_json (~> 1.11) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) os (>= 0.9, < 2.0) + pstore (~> 0.1) signet (>= 0.16, < 2.a) - heapy (0.2.0) - thor - high_voltage (3.1.2) - highline (3.0.1) - httpclient (2.8.3) - i18n (1.14.1) + high_voltage (5.0.0) + highline (3.1.2) + reline + i18n (1.15.2) concurrent-ruby (~> 1.0) - i18n-tasks (1.0.13) + i18n-tasks (1.1.2) activesupport (>= 4.0.2) ast (>= 2.1.0) - better_html (>= 1.0, < 3.0) erubi - highline (>= 2.0.0) + highline (>= 3.0.0) i18n parser (>= 3.2.2.1) + prism rails-i18n rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.8, >= 1.8.1) terminal-table (>= 1.5.1) - io-console (0.7.2) - irb (1.11.2) - rdoc + importmap-rails (2.2.3) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + iniparse (1.5.0) + invisible_captcha (2.3.0) + rails (>= 5.2) + io-console (0.9.4) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) reline (>= 0.4.2) - jbuilder (2.11.5) - actionview (>= 5.0.0) - activesupport (>= 5.0.0) + jbuilder (2.15.1) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) jmespath (1.6.2) - json (2.7.1) - jwt (2.7.1) - kramdown (2.4.0) - rexml - language_server-protocol (3.17.0.3) - listen (3.8.0) + json (2.21.2) + jwt (3.3.0) + base64 + kamal (2.12.0) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.4) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) + kramdown (2.5.2) + rexml (>= 3.4.4) + language_server-protocol (3.17.0.6) + lint_roller (1.1.0) + listen (3.10.0) + logger rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - locale (2.1.3) - lockbox (1.3.3) - lograge (0.14.0) + local_time (3.0.3) + locale (2.1.5) + fiddle + lockbox (2.2.0) + logger (1.7.0) + lograge (0.15.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.22.0) + loofah (2.25.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) - mail (2.8.1) + madmin (2.6.0) + importmap-rails + pagy (>= 3.5) + rails (>= 7.0.0) + stimulus-rails + turbo-rails + mail (2.9.1) + logger mini_mime (>= 0.1.1) net-imap net-pop net-smtp - mail_form (1.10.1) - actionmailer (>= 5.2) - activemodel (>= 5.2) - marcel (1.0.2) - matrix (0.4.2) - memory_profiler (1.0.1) - method_source (1.0.0) - mini_histogram (0.3.1) + mail_form (1.11.0) + actionpack (>= 7.0) + activemodel (>= 7.0) + mailbin (1.2.0) + importmap-rails + rails (>= 7.1.0) + turbo-rails + marcel (1.2.1) + matrix (0.4.3) + method_source (1.1.0) mini_mime (1.1.5) - mini_portile2 (2.8.5) - minitest (5.22.2) - minitest-rails (7.1.0) + mini_portile2 (2.8.9) + minitest (5.27.0) + minitest-rails (8.1.0) minitest (~> 5.20) - railties (~> 7.1.0) - minitest-reporters (1.6.1) + railties (>= 8.1.0, < 8.2.0) + minitest-reporters (1.8.0) ansi builder - minitest (>= 5.0) + minitest (>= 5.0, < 7) ruby-progressbar - msgpack (1.7.2) - multi_json (1.15.0) - multipart-post (2.4.0) - mutex_m (0.2.0) - mysql2 (0.5.6) - net-http-persistent (4.0.2) - connection_pool (~> 2.2) - net-imap (0.4.10) + mission_control-jobs (1.3.1) + actioncable (>= 7.1) + actionpack (>= 7.1) + activejob (>= 7.1) + activerecord (>= 7.1) + importmap-rails (>= 1.2.1) + irb (~> 1.13) + railties (>= 7.1) + stimulus-rails + turbo-rails + msgpack (1.8.5) + multi_json (1.21.2) + mysql2 (0.5.7) + bigdecimal + net-http (0.9.1) + uri (>= 0.11.1) + net-imap (0.6.7) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.2) + net-protocol (0.4.0) timeout - net-smtp (0.4.0.1) + net-scp (4.1.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-smtp (0.5.1) net-protocol - nio4r (2.7.0) - nokogiri (1.16.2) + net-ssh (7.3.3) + nio4r (2.7.5) + nokogiri (1.19.4) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.16.2-aarch64-linux) + nokogiri (1.19.4-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.4-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.16.2-arm-linux) + nokogiri (1.19.4-arm-linux-musl) racc (~> 1.4) - nokogiri (1.16.2-x86-linux) + nokogiri (1.19.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.2-x86_64-linux) + nokogiri (1.19.4-x86_64-linux-gnu) racc (~> 1.4) - oj (3.16.3) + nokogiri (1.19.4-x86_64-linux-musl) + racc (~> 1.4) + oj (3.17.6) bigdecimal (>= 3.0) + ostruct (>= 0.2) orm_adapter (0.5.0) os (1.1.4) - parallel (1.24.0) - parser (3.3.0.5) + ostruct (0.6.3) + overcommit (0.73.0) + childprocess (>= 0.6.3, < 6) + iniparse (~> 1.4) + rexml (>= 3.4.2) + overmind (2.5.1) + overmind (2.5.1-aarch64-linux) + overmind (2.5.1-arm-freebsd) + overmind (2.5.1-arm-linux) + overmind (2.5.1-arm64-darwin) + overmind (2.5.1-arm64-linux) + overmind (2.5.1-x86-freebsd) + overmind (2.5.1-x86-linux) + overmind (2.5.1-x86_64-freebsd) + pagy (43.6.3) + json + uri + yaml + parallel (2.2.0) + parser (3.3.12.0) ast (~> 2.4.1) racc - pg (1.5.4) + pg (1.6.3) + pg (1.6.3-aarch64-linux) + pg (1.6.3-aarch64-linux-musl) + pg (1.6.3-arm64-darwin) + pg (1.6.3-x86_64-linux) + pg (1.6.3-x86_64-linux-musl) popper_js (2.11.8) - prime (0.1.2) + pp (0.6.4) + prettyprint + prettyprint (0.2.0) + prime (0.1.4) forwardable singleton - pry (0.14.2) + prism (1.9.0) + propshaft (1.3.2) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + pry (0.16.0) coderay (~> 1.1) method_source (~> 1.0) - pry-rails (0.3.9) - pry (>= 0.10.4) - psych (5.1.2) - stringio - public_suffix (5.0.4) - puma (6.4.2) + reline (>= 0.6.0) + pry-rails (0.3.11) + pry (>= 0.13.0) + pstore (0.2.1) + public_suffix (7.0.5) + puma (8.0.2) nio4r (~> 2.0) - racc (1.7.3) - rack (3.0.9) - rack-attack (6.7.0) + raabro (1.5.0) + racc (1.8.1) + rack (3.2.7) + rack-accept (0.4.5) + rack (>= 0.4) + rack-attack (6.8.0) rack (>= 1.0, < 4) - rack-cors (2.0.1) - rack (>= 2.0.0) - rack-session (2.0.0) + rack-cors (3.0.0) + logger + rack (>= 3.0.14) + rack-session (2.1.2) + base64 (>= 0.1.0) rack (>= 3.0.0) - rack-test (2.1.0) + rack-test (2.2.0) rack (>= 1.3) - rack-throttle (0.7.0) - bundler (>= 1.0.0) - rack (>= 1.0.0) - rack-timeout (0.6.3) - rackup (2.1.0) + rackup (2.3.1) rack (>= 3) - webrick (~> 1.8) - rails (7.1.3) - actioncable (= 7.1.3) - actionmailbox (= 7.1.3) - actionmailer (= 7.1.3) - actionpack (= 7.1.3) - actiontext (= 7.1.3) - actionview (= 7.1.3) - activejob (= 7.1.3) - activemodel (= 7.1.3) - activerecord (= 7.1.3) - activestorage (= 7.1.3) - activesupport (= 7.1.3) + rails (8.1.4) + actioncable (= 8.1.4) + actionmailbox (= 8.1.4) + actionmailer (= 8.1.4) + actionpack (= 8.1.4) + actiontext (= 8.1.4) + actionview (= 8.1.4) + activejob (= 8.1.4) + activemodel (= 8.1.4) + activerecord (= 8.1.4) + activestorage (= 8.1.4) + activesupport (= 8.1.4) bundler (>= 1.15.0) - railties (= 7.1.3) - rails-dom-testing (2.2.0) + railties (= 8.1.4) + rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) - loofah (~> 2.21) - nokogiri (~> 1.14) - rails-i18n (7.0.8) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + rails-i18n (8.1.0) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 8) - railties (7.1.3) - actionpack (= 7.1.3) - activesupport (= 7.1.3) - irb + railties (>= 8.0.0, < 9) + railties (8.1.4) + actionpack (= 8.1.4) + activesupport (= 8.1.4) + irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.1.0) + rake (13.4.2) rb-fsevent (0.11.2) - rb-inotify (0.10.1) + rb-inotify (0.11.1) ffi (~> 1.0) - rdoc (6.6.2) - psych (>= 4.0.0) - regexp_parser (2.9.0) - reline (0.4.2) + rbs (4.2.0) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort + regexp_parser (2.13.0) + reline (0.7.0) io-console (~> 0.5) - representable (3.2.0) + representable (3.3.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - request_store (1.6.0) + request_store (1.7.0) rack (>= 1.4) - responders (3.1.1) - actionpack (>= 5.2) - railties (>= 5.2) - retriable (3.1.2) - rexml (3.2.6) - rollbar (3.5.1) - route_translator (14.1.1) - actionpack (>= 6.1, < 7.2) - activesupport (>= 6.1, < 7.2) - rubocop (1.60.2) + responders (3.2.1) + actionpack (>= 7.0) + railties (>= 7.0) + retriable (4.2.0) + rexml (3.4.4) + rollbar (3.8.0) + rotp (6.3.0) + rqrcode (3.2.0) + chunky_png (~> 1.0) + rqrcode_core (~> 2.0) + rqrcode_core (2.1.0) + rubocop (1.88.2) json (~> 2.3) - language_server-protocol (>= 3.17.0) - parallel (~> 1.10) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (>= 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.30.0, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.30.0) - parser (>= 3.2.1.0) - rubocop-performance (1.20.2) - rubocop (>= 1.48.1, < 2.0) - rubocop-ast (>= 1.30.0, < 2.0) - rubocop-rails (2.23.1) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.50.0) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.36.0) activesupport (>= 4.2.0) + lint_roller (~> 1.1) rack (>= 1.1) - rubocop (>= 1.33.0, < 2.0) - rubocop-ast (>= 1.30.0, < 2.0) - ruby-debug-ide (0.7.3) - rake (>= 0.8.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rails-omakase (1.1.0) + rubocop (>= 1.72) + rubocop-performance (>= 1.24) + rubocop-rails (>= 2.30) ruby-progressbar (1.13.0) - ruby-statistics (3.0.2) - ruby2_keywords (0.0.5) - rubyzip (2.3.2) - sass-rails (6.0.0) - sassc-rails (~> 2.1, >= 2.1.1) - sassc (2.4.0) - ffi (~> 1.9) - sassc-rails (2.1.2) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt - selenium-webdriver (4.10.0) + rubyzip (3.7.0) + securerandom (0.4.1) + selenium-webdriver (4.49.0) + base64 (~> 0.2) + logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) + rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) - signet (0.18.0) + signet (0.22.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) - simple_token_authentication (1.18.1) - actionmailer (>= 3.2.6, < 8) - actionpack (>= 3.2.6, < 8) - devise (>= 3.2, < 6) - singleton (0.2.0) + jwt (>= 1.5, < 4.0) + singleton (0.3.0) smart_properties (1.17.0) - sprockets (4.2.1) - concurrent-ruby (~> 1.0) - rack (>= 2.2.4, < 4) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - sprockets (>= 3.0.0) - sqlite3 (1.7.2) + solid_cable (4.1.0) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_cache (1.0.10) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_queue (1.7.0) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11) + railties (>= 7.1) + thor (>= 1.3.1) + sqlite3 (2.9.6) mini_portile2 (~> 2.8.0) - stackprof (0.2.26) - stimulus-rails (1.3.3) + sshkit (1.25.1) + base64 + logger + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + standard (1.56.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.88.0) + standard-custom (~> 1.0.0) + standard-performance (~> 1.8) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.9.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.26.0) + stimulus-rails (1.3.4) railties (>= 6.0.0) - stringio (3.1.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - terser (1.2.0) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + terser (1.2.8) execjs (>= 0.3.0, < 3) text (1.3.1) - thor (1.3.0) - tilt (2.3.0) - timeout (0.4.1) + thor (1.5.0) + thruster (0.1.26) + thruster (0.1.26-aarch64-linux) + thruster (0.1.26-arm64-darwin) + thruster (0.1.26-x86_64-linux) + tilt (2.9.0) + timeout (0.6.1) trailblazer-option (0.1.2) - translation (1.38) + translation (1.41) gettext (~> 3.2, >= 3.2.5, <= 3.4.9) - turbo-rails (1.5.0) - actionpack (>= 6.0.0) - activejob (>= 6.0.0) - railties (>= 6.0.0) + tsort (0.2.0) + turbo-rails (2.0.23) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + turnout2024 (3.0.2) + i18n (>= 0.7, < 2) + rack (>= 3, < 4) + rack-accept (~> 0.4) + tilt (>= 2.3, < 3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unicode-display_width (2.5.0) + unicode-display_width (3.3.0) + unicode-emoji (~> 4.3) + unicode-emoji (4.3.0) + uri (1.1.1) + useragent (0.16.11) warden (1.2.9) rack (>= 2.0.9) - web-console (4.2.1) - actionview (>= 6.0.0) - activemodel (>= 6.0.0) + web-console (4.3.0) + actionview (>= 8.0.0) bindex (>= 0.4.0) - railties (>= 6.0.0) - webdrivers (5.3.1) - nokogiri (~> 1.6) - rubyzip (>= 1.3.0) - selenium-webdriver (~> 4.0, < 4.11) - webrick (1.8.1) - websocket (1.2.10) - websocket-driver (0.7.6) + railties (>= 8.0.0) + websocket (1.2.11) + websocket-driver (0.8.2) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - will_paginate (4.0.0) - will_paginate-bootstrap-style (0.3.0) - will_paginate (~> 4.0, >= 4.0.0) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.13) + yaml (0.4.0) + zeitwerk (2.8.3) PLATFORMS - linux + aarch64-linux + aarch64-linux-android + aarch64-linux-gnu + aarch64-linux-musl + arm-freebsd + arm-linux + arm-linux-androideabi + arm-linux-gnu + arm-linux-gnueabihf + arm-linux-musl + arm-linux-musleabihf + arm64-darwin-23 + arm64-linux + riscv64-linux-android + riscv64-linux-gnu + riscv64-linux-musl ruby + x86-freebsd + x86-linux + x86-linux-gnu + x86-linux-musl + x86_64-freebsd + x86_64-linux + x86_64-linux-android + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES apipie-rails aws-sdk-s3 - azure-storage-blob (~> 2.0) - bootsnap (>= 1.4.4) - bootstrap (= 5.2.3) + azure-blob (~> 0.8.0) + bootsnap + bootstrap + brakeman capybara (>= 3.37.1, < 4.0) config - debase (>= 0.2.5.beta2) + cssbundling-rails debug - derailed_benchmarks - devise (>= 4.9.0) + devise (~> 5.0) devise-i18n + dotenv (~> 3.2) + erb_lint (~> 0.9.0) foreman - google-cloud-storage (~> 1.48) + google-cloud-storage (~> 1.62) high_voltage - i18n-tasks (~> 1.0.13) + i18n-tasks (~> 1.1.2) + importmap-rails + invisible_captcha (~> 2.3) jbuilder - json (~> 2.7) + json (~> 2.21) + kamal + kaminari (~> 1.2) kramdown listen + local_time lockbox lograge + madmin mail_form (>= 1.9.0) + mailbin minitest minitest-rails (>= 6.1.0) minitest-reporters + mission_control-jobs (~> 1.3.1) mysql2 oj + overcommit + overmind (~> 2.5) pg + propshaft pry-rails puma rack-attack rack-cors - rack-throttle (= 0.7.0) - rack-timeout - rails (~> 7.1.3) - rails-i18n (~> 7.0.8) + rails (~> 8.1.3) + rails-i18n (~> 8.1.0) rollbar - route_translator (>= 13.0.0) - rubocop - rubocop-performance - rubocop-rails - ruby-debug-ide - sass-rails (~> 6.0, >= 6.0.0) - selenium-webdriver - simple_token_authentication + rotp (~> 6.2) + rqrcode (~> 3.2) + rubocop-rails-omakase + selenium-webdriver (>= 4.20.1) + solid_cable + solid_cache + solid_queue sqlite3 - stackprof + standard (>= 1.35.1) stimulus-rails terser (~> 1.2) + thruster translation turbo-rails + turnout2024 tzinfo-data version! - web-console (>= 4.2.0) - webdrivers (~> 5.3) - will_paginate (~> 4.0.0) - will_paginate-bootstrap-style + web-console + +CHECKSUMS + action_text-trix (2.1.19) sha256=7012f59421009cf284aa651294896414d653a61a2417c9b8714c8476d2f74009 + actioncable (8.1.4) sha256=cfcd72f8c861e01ccdc96d772ad8c5ad6ed0ea7c571aef81a794b6027eaa7f11 + actionmailbox (8.1.4) sha256=9c4525e577ebb671ce558f3ea78b8a634f64e56a0e6e8d105b28b873793a06eb + actionmailer (8.1.4) sha256=f82d95d6762ae46ea38f221d3363639266af4f8e56d50e2bd367558674baf093 + actionpack (8.1.4) sha256=245493cc51be195a7645e6fc184880fa7e5b32ff1a08e053ee31f58c76e30ea4 + actiontext (8.1.4) sha256=33ee99bea7da6af911fc7d98b17013a8e1ec62aa01b20b20e9a5366f40dbf644 + actionview (8.1.4) sha256=eff1efd07c4d969abad288322b423bcb67fdbab5e324c91ca37352b80f226076 + activejob (8.1.4) sha256=4ed5b2ed8d4cc5c8f5f09becfef8346f202a4884eee0c8c73cae5ac378824099 + activemodel (8.1.4) sha256=9a0906c127bd118aa5f06157db12b8f750da83dccee72db2f658547a0ae7a0e8 + activerecord (8.1.4) sha256=3b4957ea7f246c9231bd7faa909312eac03a9101aaf87977b6c52733ff00b04f + activestorage (8.1.4) sha256=f4956f3ec9ee63e5e0b332b57c877cb627f82b56f4cc902279310d2aa285c7a8 + activesupport (8.1.4) sha256=891dd3fe43c4fc8391934b237b1189bb1f661c932a84a840043db9d46a4e80df + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + ansi (1.6.0) sha256=ac9ea0c0ea8d32fb4e271348e609963ac78882f34b73836c2a02b3622e666658 + apipie-rails (1.5.1) sha256=727c2a74d913cd0cb601d3675800672b7b5a6b0c61349805c0582431a12f2f45 + ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 + aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b + aws-partitions (1.1290.0) sha256=44613571eda66decddca594c10c9cee354ce819842b5724ca4fb5115e2f06811 + aws-sdk-core (3.257.0) sha256=a92ad19c7ff668cec6688ee3200ee562da80647a3bbeed9fda02e8b0ca6492c6 + aws-sdk-kms (1.132.0) sha256=094b0097bb3be9d5c1ac87e971ca7f5aebb746801dfb5f0f576480bf98dca69c + aws-sdk-s3 (1.232.1) sha256=124440f3a44fbc28cc3a2dec6eaeeea2a236bfc33772005010a19baa62654870 + aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00 + azure-blob (0.8.0) sha256=5c8d50e5c8b4fa9228f6a8d6bf003aba9f33d03c15f525235adeca8ad7879c10 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032 + bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6 + better_html (2.2.0) sha256=e68ab66ab09696b708333bbf35e8aa3c107500ba7892f528e2111624bdd8cf76 + bigdecimal (4.1.3) sha256=61ebe1e5e559bdc3cc6f2c0ee7f427321fc838f59611c294356eb04d6e21cf66 + bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e + bootsnap (1.26.0) sha256=ca96237015e6cd74a02963d5821cf00ac5ea134653b323e8cd6d702a7718bf1b + bootstrap (5.3.8) sha256=1c23b06df24ec28a0058ad90a0da93e260d2c0a5c453d7087f6bad428464742f + brakeman (8.0.6) sha256=759cc69341115e6c2dcd47b6fd8649a0b9bd540e3585ac8a0a94e31c66fee386 + builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f + bundler (4.0.12) sha256=7f8b757d28dfb636e7b24fba2344ac6dd13b5b24f4b46d62573d483f211825ac + capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef + cgi (0.5.2) sha256=61ca30298171190fd4fa0d8018e57ada456eae9b7a2b78526debf7f0a0e6f8bb + childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec + chunky_png (1.4.0) sha256=89d5b31b55c0cf4da3cf89a2b4ebc3178d8abe8cbaf116a1dba95668502fdcfe + coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b + concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1 + config (5.6.1) sha256=a9f0f0f9ffa6d12d43147a3fa1ab8486fe484c3098a350c6a2e0f32430e0d1cc + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295 + cssbundling-rails (1.4.3) sha256=53aecd5a7d24ac9c8fcd92975acd0e830fead4ee4583d3d3d49bb64651946e41 + date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 + debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6 + declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9 + deep_merge (1.2.2) sha256=83ced3a3d7f95f67de958d2ce41b1874e83c8d94fe2ddbff50c8b4b82323563a + devise (5.0.4) sha256=d605f2b85854e74e56ee789e2d398702bc2d06e6bcd894717a670a3199c74cc1 + devise-i18n (1.16.1) sha256=66321fa28111a78de25cbd7d4b5e7edcf67d3b73a419a8df563365849f68b2ba + digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07 + dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506 + erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92 + erb_lint (0.9.0) sha256=dfb5e40ad839e8d1f0d56ca85ec9a7ac4c9cd966ec281138282f35b323ca7c31 + erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 + et-orbi (1.4.2) sha256=bb555dae668419cb24caa2a293a170e58be6d4df1e017c51f5030bdc133cd20c + execjs (2.10.2) sha256=466cf2b49e50c0cfdff8fdb88a7a983dfab7ac0f6185c317ccb434a75c224c88 + faraday (2.14.4) sha256=9bb4408c44621b0dbaaaa39e5dbfc4dbe58cc6751b6eb94d16ae9e1f2f2fab6e + faraday-follow_redirects (0.5.0) sha256=5cde93c894b30943a5d2b93c2fe9284216a6b756f7af406a1e55f211d97d10ad + faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588 + ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf + ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df + ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 + ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 + ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 + ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b + ffi (1.17.4-x86-linux-gnu) sha256=38e150df5f4ca555e25beca4090823ae09657bceded154e3c52f8631c1ed72cf + ffi (1.17.4-x86-linux-musl) sha256=fbeec0fc7c795bcf86f623bb18d31ea1820f7bd580e1703a3d3740d527437809 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee + foreman (0.90.0) sha256=ff675e2d47b607ac58714a6d4ac3e1ee8f06f41d8db084531c31961e2c3f117c + forwardable (1.4.0) sha256=f1cd40cc9812937980e1c76f1aa053660990a7c9b6a98fc37d945468afcce838 + fugit (1.14.0) sha256=e311a9cc85a54bb67649df45060e721282a1e2c8ebad40e41874a3a7d576f37e + gettext (3.4.9) sha256=292864fe6a15c224cee4125a4a72fab426fdbb280e4cff3cfe44935f549b009a + globalid (1.4.0) sha256=037f12fbf1d9d7a014d501c2d5c77356fd4ddd96d7a7991d6700bba96706f427 + google-apis-core (1.2.5) sha256=e21562b5627a0fc6a0c3165e429c3afed0f6a628eaa514e83f7204dda1adff69 + google-apis-iamcredentials_v1 (0.28.0) sha256=0a92ffe6cc39c569554af2a77a25dfc61519ed8bbb64ab04cffdd352dc5ef106 + google-apis-storage_v1 (0.67.0) sha256=13befc89bf03512783f535d214590f15993b0c56411155fd8090ca607e1b8d20 + google-cloud-core (1.9.0) sha256=ab55409f51488e8deefb6edcc1ce4771dfb5da2fe7b3bc075709a030c2b682a4 + google-cloud-env (2.4.0) sha256=01cb35ecee5d1c4d1cd4deaed8bb5bddc838e296bcc2a35c5d1ddd07ce40d28d + google-cloud-errors (1.7.0) sha256=6e682f42d89aae08689f36f28495de629d756da076bafff0003bac7f8042ebb3 + google-cloud-storage (1.62.0) sha256=e2c3c08bf8fd40d50be92304084942203314d4fc0ee52028e99f9359c3ad1330 + google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b + googleauth (1.17.4) sha256=acc2cac2a6011048f149c922cbc6c9675719e165aa5000c7fa3876c480fe9ae3 + high_voltage (5.0.0) sha256=fbdd1a861af30d51d6a9f710b37709ab489211402685972945cd5eac6a4c0b23 + highline (3.1.2) sha256=67cbd34d19f6ef11a7ee1d82ffab5d36dfd5b3be861f450fc1716c7125f4bb4a + i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5 + i18n-tasks (1.1.2) sha256=4dcfba49e52a623f30661cb316cb80d84fbba5cb8c6d88ef5e02545fffa3637a + importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a + iniparse (1.5.0) sha256=36a165e98d8a250b7631c4a7f9afba32af78f089970cd6446a39771189c761f1 + invisible_captcha (2.3.0) sha256=309ee5a5e891ecfb732c85b12f1aa9252a648df6f2761b3b41205e824e30ff15 + io-console (0.9.4) sha256=a403304f32928ee85e56d457b4612ca43f2703b561a3258844992bd24085fbb1 + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 + jbuilder (2.15.1) sha256=2430bec28fb0cebacb5875b1009cf9d8bc3c303ccb810c4c8b062a4b51457637 + jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1 + json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a + jwt (3.3.0) sha256=44cc34fbc341c148233cf66641522b4bce0dfb1ce55e8073f05c56f44f4ee546 + kamal (2.12.0) sha256=c51d1ab085e515470f98d0c0f043637122b5ebf76e8b610cb1fbbed0b7f9b8fa + kaminari (1.2.2) sha256=c4076ff9adccc6109408333f87b5c4abbda5e39dc464bd4c66d06d9f73442a3e + kaminari-actionview (1.2.2) sha256=1330f6fc8b59a4a4ef6a549ff8a224797289ebf7a3a503e8c1652535287cc909 + kaminari-activerecord (1.2.2) sha256=0dd3a67bab356a356f36b3b7236bcb81cef313095365befe8e98057dd2472430 + kaminari-core (1.2.2) sha256=3bd26fec7370645af40ca73b9426a448d09b8a8ba7afa9ba3c3e0d39cdbb83ff + kramdown (2.5.2) sha256=1ba542204c66b6f9111ff00dcc26075b95b220b07f2905d8261740c82f7f02fa + language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0 + lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + listen (3.10.0) sha256=c6e182db62143aeccc2e1960033bebe7445309c7272061979bb098d03760c9d2 + local_time (3.0.3) sha256=c69a8974d993fdf6e60db02977ed23c070f203dcb3a1ff0de52ad3d2393f8303 + locale (2.1.5) sha256=1c6803e8aa6bdb2c29e91945d095050601bf6d58474993575adf6f3b89b32ef4 + lockbox (2.2.0) sha256=f80a20ee0161299b23a588d839d23a1232863112c4b6b9bc31eed28ff9d591af + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + lograge (0.15.0) sha256=34072c1f50b95019dc185137f5fec1a6759f5a1b6d55c28163d0cba204e6df16 + loofah (2.25.2) sha256=2007f746959ac65552456e04b433e83deb22759ab38c838b4445c70e43425918 + madmin (2.6.0) sha256=3d4f39182f4a572f3539bcda44fdbd9a78055f85b8ebc8f2d6450d567bcd85d8 + mail (2.9.1) sha256=06574eca475253d6c18145dd70af80d0eb970182d55053497c5f4d797ea160e8 + mail_form (1.11.0) sha256=54aaaf11b69e7e5bd4be220dfc80f2469bb809d6ed1f893b8d940f0b67232c5a + mailbin (1.2.0) sha256=55312e07e1de0a0eb41eec87e8325210891ff072cd2ff4dabea412b35f6a6c72 + marcel (1.2.1) sha256=1678e9360e32f9eafa917c80029e2f6d10b2715c66a4b87b6d0da9b9cd1f859f + matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b + method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5 + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289 + minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5 + minitest-rails (8.1.0) sha256=baf92080456ab3ffb352e038114728a25469b89001b76da0d0db2433d8192eca + minitest-reporters (1.8.0) sha256=8ce5280fb73ad3178ae525454df169b6f28c1b38b1d088ea91815d3a370ba384 + mission_control-jobs (1.3.1) sha256=3230be224069b39fee2893eecd27c1c334fb5467290bf575e88b5c9121287b26 + msgpack (1.8.5) sha256=97419eaa7b35dfe3e462c0e6c1a3dfdcbfc8756605c09e46e4c6daef127517ba + multi_json (1.21.2) sha256=245531eaf54bdba57aba515ff08b4503505c5e09911daa89c5a561e2f85bea8e + mysql2 (0.5.7) sha256=ba09ede515a0ae8a7192040a1b778c0fb0f025fa5877e9be895cd325fa5e9d7b + net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996 + net-imap (0.6.7) sha256=b5c9573be975d856de252ee851871724da66aa2d449b2482d5690bd12bc23660 + net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 + net-protocol (0.4.0) sha256=f9f90c8c6f64794a45160bf422177a336db12809de3beeeaa26b88fcd2e8c853 + net-scp (4.1.0) sha256=a99b0b92a1e5d360b0de4ffbf2dc0c91531502d3d4f56c28b0139a7c093d1a5d + net-sftp (4.0.0) sha256=65bb91c859c2f93b09826757af11b69af931a3a9155050f50d1b06d384526364 + net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 + net-ssh (7.3.3) sha256=831def58b2c51dcef66ec00d29397d4f210de89c19fe78f95873ca30f386e86a + nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 + nokogiri (1.19.4) sha256=50c951611c92bca05c51411aef45f1cbc50f2821c4802758c5c6d34696533ab5 + nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f + nokogiri (1.19.4-aarch64-linux-musl) sha256=35c65b9ce72b3bb03207bdbe7067915019dc18c1b9b59139684bd6690fdd01af + nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca + nokogiri (1.19.4-arm-linux-musl) sha256=588923c101bcfa78869734d247d25b598674323e7f22474fc468f6e5647311eb + nokogiri (1.19.4-arm64-darwin) sha256=a46db9853286e6597b36ebc6953817d15acf3a299583eb3f89fdc6f91dd63527 + nokogiri (1.19.4-x86_64-linux-gnu) sha256=379fae440b28915e3f19d752ce2dcf8465ed2b2fbefd2a7ca0dd497bc981a06a + nokogiri (1.19.4-x86_64-linux-musl) sha256=17dfb7c1fa194ae02fbf7c51a7afc8d278045ab3fdacfd86f91d02d7b274470b + oj (3.17.6) sha256=f511257e01a12030c3ceb8023cd2e96fca924ea9c741f57ea366816f1fd8e68c + orm_adapter (0.5.0) sha256=aa5d0be5d540cbb46d3a93e88061f4ece6a25f6e97d6a47122beb84fe595e9b9 + os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + overcommit (0.73.0) sha256=715ed299a3ed4a9439be381f9d48294d976811c6078bd7ba4b6191501e35a934 + overmind (2.5.1) sha256=df4676e787845173c3929029d2b2952055ffaf02fde0106054f98f4c2611ec9a + overmind (2.5.1-aarch64-linux) sha256=13a979e61278fb5a214600e844268460a86787a8f1a8ed962dcf6aa018f3f50e + overmind (2.5.1-arm-freebsd) sha256=97e667674be373d42280cd25f8a70f4592c54dc56450c46f7da19795897fc6f7 + overmind (2.5.1-arm-linux) sha256=5e12b5fb973dce118769648cf6549a48a0d424f947b40fbb30012f53308d23e1 + overmind (2.5.1-arm64-darwin) sha256=8b1c9ea304ef524eaf335b4524d5dbd58e02ed9492397243f61cc49a8bd4eca7 + overmind (2.5.1-arm64-linux) sha256=d80652563554513e063f1ddd8e10d2dff188b2b85994024c2063bae1cd230061 + overmind (2.5.1-x86-freebsd) sha256=7fe2bade9d4b3274e1a5b759489bb8a1f3182d842bbb3cc0f92f85b73a8fa5b6 + overmind (2.5.1-x86-linux) sha256=0ba519b106f6473500543fa945d7acd3ef8485a3f8a9c4838d9f0f1869fe5176 + overmind (2.5.1-x86_64-freebsd) sha256=37e63e97534e03c612a066d30c7fc9ada5e062333702f74c782078ed122544c9 + pagy (43.6.3) sha256=3b7cd5b5b08e975b85cb48c1e7b729d90125a46c9ac14bd830fde1a8c6719a85 + parallel (2.2.0) sha256=e1059c5fd7b649558a0aec38a769f06a42942bdb40503d005a59c352fe011cd8 + parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828 + pg (1.6.3) sha256=1388d0563e13d2758c1089e35e973a3249e955c659592d10e5b77c468f628a99 + pg (1.6.3-aarch64-linux) sha256=0698ad563e02383c27510b76bf7d4cd2de19cd1d16a5013f375dd473e4be72ea + pg (1.6.3-aarch64-linux-musl) sha256=06a75f4ea04b05140146f2a10550b8e0d9f006a79cdaf8b5b130cde40e3ecc2c + pg (1.6.3-arm64-darwin) sha256=7240330b572e6355d7c75a7de535edb5dfcbd6295d9c7777df4d9dddfb8c0e5f + pg (1.6.3-x86_64-linux) sha256=5d9e188c8f7a0295d162b7b88a768d8452a899977d44f3274d1946d67920ae8d + pg (1.6.3-x86_64-linux-musl) sha256=9c9c90d98c72f78eb04c0f55e9618fe55d1512128e411035fe229ff427864009 + popper_js (2.11.8) sha256=f4b0be717fc0d50bdb3dbbc55788525a9e0e8f640b76c9971fc34ee609eadbd2 + pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prime (0.1.4) sha256=4d755ebf7c2994a6f3a3fee0d072063be3fff2d4042ebff6cd5eebd4747a225e + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + propshaft (1.3.2) sha256=1d56a3e56a92c21bfc29caf07406b5386b00d4c47ddf357cf989a5a234b1389e + pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e + pry-rails (0.3.11) sha256=a69e28e24a34d75d1f60bcf241192a54253f8f7ef8a62cba1e75750a9653593d + pstore (0.2.1) sha256=03904d0f2c66579e96d1e6704cdabc0c88df7ea8ed8782d9f3569f6f6c702c1a + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb + raabro (1.5.0) sha256=3f998a7bc84f9c84df3ab580634d2e0a5bda4f0841168d56035f529c9877440a + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7 + rack-accept (0.4.5) sha256=66247b5449db64ebb93ae2ec4af4764b87d1ae8a7463c7c68893ac13fa8d4da2 + rack-attack (6.8.0) sha256=f2499fdebf85bcc05573a22dff57d24305ac14ec2e4156cd3c28d47cafeeecf2 + rack-cors (3.0.0) sha256=7b95be61db39606906b61b83bd7203fa802b0ceaaad8fcb2fef39e097bf53f68 + rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 + rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 + rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 + rails (8.1.4) sha256=5f3ecdf9a2a3b79b2e31b8fee3b64a00f7284d76e5631f87d0e58e665911a33b + rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d + rails-html-sanitizer (1.7.1) sha256=e797a7c9b01e567307e317c576b49ab4168017e63eea4dba9ce3cb587e2f22c2 + rails-i18n (8.1.0) sha256=52d5fd6c0abef28d84223cc05647f6ae0fd552637a1ede92deee9545755b6cf3 + railties (8.1.4) sha256=b5e7a8e8984e33114d68af62227971960aae1e6c02bd7c9d826ffad97fff583c + rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe + rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e + rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674 + rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469 + regexp_parser (2.13.0) sha256=d105d95d2138954e73914d2fbfb3d0a8f8605a49a3e89864c32e78b9107cb770 + reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d + representable (3.3.0) sha256=22beca7d07104d8dfe30667eb267756c764c8bdc6c0f1481922502ecc4f0d8cf + request_store (1.7.0) sha256=e1b75d5346a315f452242a68c937ef8e48b215b9453a77a6c0acdca2934c88cb + responders (3.2.1) sha256=c2772970cbda654c0e772d852e5a851e82cd8e96f5fa958650861ba75b8ffa84 + retriable (4.2.0) sha256=90c3b257472a1c02f2a3dfa64dd35a420f7a624cef1a5981e20fdac5730f8cdc + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rollbar (3.8.0) sha256=655d2783c84c854634f4a1045f34ff8d390596ee4c481f01fe1b214423047a3e + rotp (6.3.0) sha256=75d40087e65ed0d8022c33055a6306c1c400d1c12261932533b5d6cbcd868854 + rqrcode (3.2.0) sha256=64c1494ca6bb67d731330f38b50e3fd09eeab4f5dcd04b608e21218d1d0b9542 + rqrcode_core (2.1.0) sha256=f303b85df89c1b8fc5ee8dc19808c9dc4330e6329b660d99d4a8cbb36ca13051 + rubocop (1.88.2) sha256=8def251c90cd955feb4daa3edc0ab56893250c4ce90ef81e6c80c03f9a939bbf + rubocop-ast (1.50.0) sha256=b9ca88300da0803ee222ad20cdb30494c0a784eed06fdc35d254b06d662788db + rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 + rubocop-rails (2.36.0) sha256=105a5f5b0001ff2ef28a3af90da50862464e0775e8d0016d599079c0ca408bdb + rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d + ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 + rubyzip (3.7.0) sha256=65c19294da75297a939006f3516deacc33185fbd721ff1954f7a231db6d3e121 + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + selenium-webdriver (4.49.0) sha256=b430f091a3cababb356b6f6132e0e4f385b017819355f563557e1537819edc89 + signet (0.22.0) sha256=b76d495ccb07ad35dbc89f3e920665a9d8ed717141955034005d7843dcfe4780 + singleton (0.3.0) sha256=83ea1bca5f4aa34d00305ab842a7862ea5a8a11c73d362cb52379d94e9615778 + smart_properties (1.17.0) sha256=f9323f8122e932341756ddec8e0ac9ec6e238408a7661508be99439ca6d6384b + solid_cable (4.1.0) sha256=828e3d509207b82b4f05f4e9c85f696994710679b52d511aa7f97ebd212d3226 + solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41 + solid_queue (1.7.0) sha256=6566b70b801d1c317c81bba7bcdd5677c019afac584a30374b4164002ca356d3 + sqlite3 (2.9.6) sha256=956fe606956420d04ac7157d3ace620c8caba2135b2e05c76e483493da24d08e + sshkit (1.25.1) sha256=be3f10b9d6eb0b44d5eaba3f7cbe41bc6bb894bce4339688ac20124391455b78 + standard (1.56.0) sha256=ae2af4d9669589162ac69ed5ef59dcf9f346d4afc81f7e62b84339310dfcb787 + standard-custom (1.0.2) sha256=424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b + standard-performance (1.9.0) sha256=49483d31be448292951d80e5e67cdcb576c2502103c7b40aec6f1b6e9c88e3f2 + stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 + terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2 + terser (1.2.8) sha256=64931851d173bc5be0a90bd4570d751be5e83b2b063ccb750dbdc11a7b1d14db + text (1.3.1) sha256=2fbbbc82c1ce79c4195b13018a87cbb00d762bda39241bb3cdc32792759dd3f4 + thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 + thruster (0.1.26) sha256=6e45e807086b29d51404841bd1ad493b67cd95892fd65dc5afcdd32e82e94ce8 + thruster (0.1.26-aarch64-linux) sha256=2171cb34928c0250830008f535c4ab2ee57846cc3f5d3e96c3475f7b3de7a541 + thruster (0.1.26-arm64-darwin) sha256=40676164c433abf31313422305d9e9e9210cf941b1befad88e2428b3b8dcc36c + thruster (0.1.26-x86_64-linux) sha256=3117a6ee430663f845a0457699fe9a05232dcc6c396e2cc83504de5a223c60e8 + tilt (2.9.0) sha256=da5735d0280bba96e9a91041bb14aee435ccad5c17b0fa519249ae543d9aa3a5 + timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb + trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3 + translation (1.41) sha256=8a6e66fb158b7b9fdf53920db1ea587f2e00c2f705cfb0f18a8e714f6dc19f01 + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f + turnout2024 (3.0.2) sha256=01d5b484ff19b89bd5bc4a29515072daa8bbf7779328067f04e9b27f5795f33a + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc + unicode-display_width (3.3.0) sha256=4b7aa66a4b11db50f6f7e98411215cd0dd10eaecb665d36eec5bcacb3fa0b613 + unicode-emoji (4.3.0) sha256=11c02fa73290378c066bb0562cd4c87d8e1b706fbbe0059ca12746a4244de8ce + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 + version (1.1.1) + warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0 + web-console (4.3.0) sha256=e13b71301cdfc2093f155b5aa3a622db80b4672d1f2f713119cc7ec7ac6a6da4 + websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737 + websocket-driver (0.8.2) sha256=97c556b019bf3410b4961002ac501621e9322d3f8a7bc02161a09301cc4c4146 + websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 + xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e + yaml (0.4.0) sha256=240e69d1e6ce3584d6085978719a0faa6218ae426e034d8f9b02fb54d3471942 + zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5 RUBY VERSION - ruby 3.1.4p223 + ruby 4.0.3 BUNDLED WITH - 2.4.19 + 4.0.12 diff --git a/Procfile b/Procfile index 865eaaa7b76c..6490d07011a2 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,2 @@ -release: bundle exec rails db:migrate -web: bundle exec puma -C config/puma.rb -console: bundle exec rails console +web: bundle exec bin/thrust bin/rails server +worker: bundle exec rake solid_queue:start diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 000000000000..8ac967bacd25 --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,3 @@ +web: bin/rails server -p 5100 +worker: bundle exec rake solid_queue:start +css: corepack yarn build:css:watch diff --git a/README.md b/README.md index 3bc297ef1423..e803aab0c9b5 100644 --- a/README.md +++ b/README.md @@ -1,344 +1,291 @@
-[![Password Pusher Front Page](https://pwpush.fra1.cdn.digitaloceanspaces.com/branding/logos/horizontal-logo-small.png)](https://pwpush.com/) +[![Password Pusher](https://pwpush.fra1.cdn.digitaloceanspaces.com/branding/logos/horizontal-logo-small.png)](https://pwpush.com/) -__Simple & Secure Password Sharing with Auto-Expiration of Shared Items__ +**Share passwords, text, files & URLs securely with self-deleting links and full audit logs.** -[![](https://badgen.net/twitter/follow/pwpush)](https://twitter.com/pwpush) -![](https://badgen.net/github/stars/pglombardo/PasswordPusher) -[![](https://badgen.net/uptime-robot/month/m789048867-17b5770ccd78208645662f1f)](https://stats.uptimerobot.com/6xJjNtPr93) -[![](https://badgen.net/docker/pulls/pglombardo/pwpush)](https://hub.docker.com/repositories) +[![Try it free](https://img.shields.io/badge/Try_it_free-pwpush.com-0ea5e9?style=for-the-badge)](https://pwpush.com) +[![Documentation](https://img.shields.io/badge/Docs-docs.pwpush.com-64748b?style=for-the-badge)](https://docs.pwpush.com) -[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/pglombardo/PasswordPusher/ruby-tests.yml)](https://github.com/pglombardo/PasswordPusher/actions/workflows/ruby-tests.yml) -[![Dependencies Status](https://img.shields.io/badge/dependencies-up%20to%20date-brightgreen.svg)](https://github.com/pglombardo/pwpush-cli/pulls?utf8=%E2%9C%93&q=is%3Apr%20author%3Aapp%2Fdependabot) -[![Semantic Versions](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--versions-e10079.svg)](https://github.com/pglombardo/pwpush-cli/releases) +[![Docker pulls](https://img.shields.io/docker/pulls/pglombardo/pwpush)](https://hub.docker.com/r/pglombardo/pwpush) +[![Docker pulls](https://img.shields.io/docker/pulls/pglombardo/pwpush-ephemeral)](https://hub.docker.com/r/pglombardo/pwpush-ephemeral) [![License](https://img.shields.io/github/license/pglombardo/PasswordPusher)](https://github.com/pglombardo/PasswordPusher/blob/master/LICENSE) -
- ------- - -Give your users the tools to be secure by default. - -Password Pusher is an opensource application to communicate passwords over the web. Links to passwords expire after a certain number of views and/or time has passed. - -Hosted at [pwpush.com](https://pwpush.com) but you can also easily run your own private instance with just a few steps. - -* __Easy-to-install:__ Host your own via Docker, a cloud service or just use [pwpush.com](https://pwpush.com) -* __Opensource:__ No blackbox code. Only trusted, tested and reviewed opensource code. -* __Versatile:__ Push passwords, text, files or URLs that autoexpire and self delete. -* __Audit logging:__ Track and control what you've shared and see who has viewed it. -* __Encrypted storage:__ All sensitive data is stored encrypted and deleted entirely once expired. -* __Host your own:__ Database backed or ephemeral, easily run your own instance isolated from the world. -* __JSON API:__ Raw JSON API available for 3rd party tools or command line via `curl` or `wget`. -* __Command line interface:__ Automate your password distribution with CLI tools or custom scripts. -* __Logins__: Invite your colleagues and track what is pushed and who retrieved it. -* __Internationalized:__ 29 language translations are bundled in. Easily selectable via UI or URL -* __Themes:__ [26 themes](./Themes.md) bundled in courtesy of Bootswatch. Select with a simple environment variable. -* __Unbranded delivery page:__ No logos, superfluous text or unrelated links to confuse end users. -* __Customizable:__ Change text and default options via environment variables. -* __Light & dark themes:__ Via CSS @media integration, the default site theme follows your local preferences. -* __Rebrandable:__ Customize the site name, tagline and logo to fit your environment. -* __Custom CSS:__ Bundle in your own custom CSS to add your own design. -* __10 Years Old:__ Password Pusher has securely delivered millions and millions of passwords in its 10 year history. -* __Actively Maintained:__ I happily work for the good karma of the great IT/Security community. -* __Honest Software:__ Opensource written and maintained by [me](https://github.com/pglombardo) with the help of some great contributors. No organizations, corporations or evil agendas. - -💌 --> Sign up for [the newsletter](https://buttondown.email/pwpush?tag=github) to get updates on big releases, security issues, new features, integrations, tips and more. - -Password Pusher is also [on Twitter](https://twitter.com/pwpush), [Gettr](https://gettr.com/user/pwpush) and [on Facebook](https://www.facebook.com/pwpush) - ------ - -[![](./app/assets/images/features/front-page-thumb.png)](./app/assets/images/features/front-page-large.png) -[![](./app/assets/images/features/audit-log-thumb.png)](./app/assets/images/features/audit-log-large.png) -[![](./app/assets/images/features/secret-url-languages-thumb.png)](./app/assets/images/features/secret-url-languages-large.png) -[![](./app/assets/images/features/password-generator-thumb.png)](./app/assets/images/features/password-generator-large.png) -[![](./app/assets/images/features/dark-theme-thumb.png)](./app/assets/images/features/dark-theme.gif) -[![](./app/assets/images/features/preliminary-step-thumb.png)](./app/assets/images/features/preliminary-step.gif) - - -# ⚡️ Quickstart - -→ Go to [pwpush.com](https://pwpush.com) and try it out. - -_or_ - -→ Run your own instance with one command: `docker run -d -p "5100:5100" pglombardo/pwpush:release` then go to http://localhost:5100 - -_or_ - -→ Use one of the [3rd party tools](#3rd-party-tools) that interface with Password Pusher. - -# 💾 Run Your Own Instance +[![GitHub stars](https://img.shields.io/github/stars/pglombardo/PasswordPusher)](https://github.com/pglombardo/PasswordPusher) - 🎉 🎉 🎉 - - __We've recently introduced a single universal container. Migration for existing users is easy - please refer to [the documentation here](https://github.com/pglombardo/PasswordPusher/wiki/How-to-migrate-to-the-Universal-Container).__ - - 🎉 🎉 🎉 - -_Note: Password Pusher can be largely configured by environment variables so after you pick your deployment method below, make sure to read [the configuration page](Configuration.md). Take particular attention in setting your own custom encryption key which isn't required but provides the best security for your instance._ - -## On Docker - -Docker images of Password Pusher are available on [Docker hub](https://hub.docker.com/u/pglombardo). - -**➜ ephemeral** -_Temporary database that is wiped on container restart._ - - docker run -d -p "5100:5100" pglombardo/pwpush:release - -[Learn more](https://github.com/pglombardo/PasswordPusher/tree/master/containers/docker#ephemeral) - -**➜ using an External Postgres Database** -_Postgres database backed instance._ - - docker run -d -p "5100:5100" pglombardo/pwpush:release -e DATABASE_URL=postgres://pwpush_user:pwpush_passwd@postgres:5432/pwpush_db - -[Learn more](https://github.com/pglombardo/PasswordPusher/tree/master/containers/docker#postgres) + -**➜ using an External MariaDB (MySQL) Database** -_Mariadb database backed instance._ +--- - docker run -d -p "5100:5100" pglombardo/pwpush:release -e DATABASE_URL=mysql2://pwpush_user:pwpush_passwd@mysql:3306/pwpush_db +**v2.0 is released.** If you already self-host, see the **[upgrade guide](UPGRADE-2.0.md)** for migration notes and configuration changes. -[Learn more](https://github.com/pglombardo/PasswordPusher/tree/master/containers/docker#mysql) +--- -_Note: The `latest` Docker container tag builds nightly off of the latest code changes and can occasionally be unstable. Always use the ['release' or version'd tags](https://hub.docker.com/r/pglombardo/pwpush/tags?page=1&ordering=last_updated) if you prefer more stability in releases._ +## What is Password Pusher? -**See Also:** [Guide to DATABASE_URL](https://github.com/pglombardo/PasswordPusher/wiki/Guide-to-DATABASE_URL) +**Password Pusher** is an open source web app for sharing sensitive information safely. You push a password, note, file, or URL; the recipient gets a one-time link that expires after a set number of views and/or time. No more sending secrets over chat or email—encrypted, auditable, and auto-self-destruct. -## With Docker Compose +Use the [hosted service](https://pwpush.com) or run your own instance with Docker in minutes. -**➜ One-liner Password Pusher with a Postgres Database** +--- - curl -s -o docker-compose.yml https://raw.githubusercontent.com/pglombardo/PasswordPusher/master/containers/docker/docker-compose-postgres.yml && docker compose up -d +## Why Password Pusher? -**➜ One-liner Password Pusher with a MariaDB (MySQL) Database** +| | | +|---|---| +| **🔒 Secure by default** | Encrypted storage, optional passphrase, expiry by views and/or time. Sensitive data is removed entirely once expired. | +| **📋 Full audit trail** | See when links were created, viewed, and by whom (with logins). | +| **🏠 Self-host or use hosted** | Use [pwpush.com](https://pwpush.com) or deploy your own—Docker, Kubernetes, Helm, or cloud. | +| **🌐 Ready for teams** | 31 languages, light/dark theme, JSON API, [official CLI & Chrome Extension](#tools--extensions), and [many integrations](https://docs.pwpush.com/docs/3rd-party-tools/). | - curl -s -o docker-compose.yml https://raw.githubusercontent.com/pglombardo/PasswordPusher/master/containers/docker/docker-compose-mariadb.yml && docker compose up -d +--- -## On Kubernetes +## Financial Supporters -Instructions and explanation of a Kubernetes setup [can be found -here](https://github.com/pglombardo/PasswordPusher/tree/master/containers/kubernetes). +A heartfelt thank you to the companies and individuals who help keep Password Pusher running. Their support goes toward hosting, maintenance, support and continued development. -## On Kubernetes with Helm +| Supporter | | +|---|---| +| [SerpApi](https://serpapi.com/?utm_source=pwpush) | SerpApi is the world’s leading provider of search data, pioneering APIs that turn search engine results into structured, developer-ready information. | +| [WMACCESS](https://www.wmaccess.com/) | Provides securities information and tax-relevant financial data, including securities master data and income data for comprehensive tax reporting. | -A basic helm chart with instructions [can be found here](containers/helm/). +A special thank you as well to every customer who [subscribed to a plan on pwpush.com](https://eu.pwpush.com/pricing) or purchased [Self-Hosted Pro](https://eu.pwpush.com/self-hosted-pricing) — your support directly funds the ongoing development of Password Pusher. -## On Microsoft Azure +Interested in supporting Password Pusher? Reach out at [support@pwpush.com](mailto:support@pwpush.com). -_There used to be a 3rd party blog post with instructions but it's been deleted. If anyone has instructions they would like to contribute, it would be greatly appreciated._ +--- -See [issue #277](https://github.com/pglombardo/PasswordPusher/issues/277) +## Features -## On Heroku +### Security & privacy -One click deploy to [Heroku Cloud](https://www.heroku.com) without having to set up servers. +- **Encrypted at rest** — Sensitive data is stored encrypted and deleted when expired. +- **Expiry controls** — Limit by number of views and/or time; links can require a passphrase. +- **Two-factor authentication (MFA)** — TOTP (authenticator apps) with backup codes; admins can require MFA instance-wide with `PWP__REQUIRE_MFA=true`. +- **Audit logging** — Track what was shared and who viewed it (with optional logins). +- **Unbranded delivery page** — No logos, superfluous text or unrelated links to confuse push recipients. -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/pglombardo/PasswordPusher) +### Self-host & customize -_This option will deploy a production Password Pusher instance backed by a postgres database to Heroku. Heroku used to offer free dynos but that is [no longer the case](https://blog.heroku.com/next-chapter) from November 28, 2022. Hosting charges will be incurred._ +- **One-command deploy** — Docker Compose with automatic SSL/TLS +- **Database or ephemeral** — Use a database for persistence or run stateless. +- **Admin dashboard** — Manage your instance from a built-in admin UI. +- **White-label** — Custom theme, logo, site name, and [26 Bootswatch themes](https://docs.pwpush.com/docs/themes/) via env vars. +- **Custom CSS** — Add your own styles; light/dark follows system preference. -## On PikaPods +### Integrations & API -One click deploy to [PikaPods](https://www.pikapods.com/) from $1/month. Start free with $5 welcome credit. +- **JSON API v2** — Modern `/api/v2` endpoints for create/retrieve/audit/active/expired workflows. +- **Legacy API compatibility** — Existing `/p`, `/f`, `/r` API routes (v1 style) remain available for backwards compatibility. +- **CLI** — Official [pwpush CLI](https://github.com/pglombardo/pwpush-cli) plus [3rd-party tools](https://docs.pwpush.com/docs/3rd-party-tools/) and scripts. +- **31 languages** — UI and secret-URL pages in 31 languages (courtesy of [Translation.io](https://translation.io/?utm_source=pwpush)). -[![Run on PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=pwpush) +### Trust & community -## With Nginx +- **Open source** — Apache 2.0; no black box. Written and maintained by [myself](https://github.com/pglombardo) and the team at [Apnotic](https://apnotic.com) with the help of contributors. +- **15+ years in production** — Used to deliver millions of secrets; [actively maintained](https://github.com/pglombardo/PasswordPusher/graphs/contributors). +- **Trusted worldwide** — Used by thousands of companies around the globe. -See the prebuilt [Docker Compose example here](https://github.com/pglombardo/PasswordPusher/tree/master/containers/examples/pwpush-and-nginx). +--- -## From Source +## Screenshots -I generally don't suggest building this application from source code for casual use. The is due to the complexities in the toolset across platforms. Running from source code is best when you plan to develop the application. +| [![Front page](app/assets/images/features/front-page-thumb.png)](app/assets/images/features/front-page-large.png) | [![Audit log](app/assets/images/features/audit-log-thumb.png)](app/assets/images/features/audit-log-large.png) | [![Languages](app/assets/images/features/secret-url-languages-thumb.png)](app/assets/images/features/secret-url-languages-large.png) | +|:---:|:---:|:---:| +| **Create a push** | **Audit log** | **Multi-language URLs** | -For quick and easy, use the Docker containers instead. +| [![Password generator](app/assets/images/features/password-generator-thumb.png)](app/assets/images/features/password-generator-large.png) | [![Dark theme](app/assets/images/features/dark-theme-thumb.png)](app/assets/images/features/dark-theme.gif) | [![Preliminary step](app/assets/images/features/preliminary-step-thumb.png)](app/assets/images/features/preliminary-step.gif) | +|:---:|:---:|:---:| +| **Password generator** | **Dark theme** | **Optional preview step** | -But if you're resolute & brave, continue on! +--- -### Dependencies +## Editions -* Ruby 3.0 or greater -* Recent Node.js stable & Yarn -* Compiler tools: gcc g++ make -* Other: git +| | **Open source (this repo)** | **Pro (pwpush.com)** | +|---|---|---| +| **Try it** | [oss.pwpush.com](https://oss.pwpush.com) | [pwpush.com](https://pwpush.com) | +| **Use case** | Self-host or use OSS demo | Hosted Pro with extra features | +| **Details** | Full source here; you deploy or use the OSS demo. | Pro features are [periodically migrated](https://docs.pwpush.com/docs/editions/) to OSS. | -### SQLite3 backend +**Feature comparison:** [pwpush.com/features#matrix](https://pwpush.com/features#matrix) -* Make sure to install sqlite3 development libraries: `apt install libsqlite3-dev sqlite3` +### Self-Hosted Password Pusher Pro -```sh -git clone git@github.com:pglombardo/PasswordPusher.git -cd PasswordPusher -gem install bundler +Self-hosted **Pro** (with licensing) is now available. Pro features not yet in OSS will be available for self-hosted deployments. -bundle config set --local deployment 'true' -bundle install --without development production test -./bin/rails assets:precompile -./bin/rails db:setup -./bin/rails server -``` +--- -Then view the site @ [http://localhost:5100/](http://localhost:5100/). +## Quick Start -### Postgres, MySQL or Mariadb backend +### Use the hosted service -* Make sure to install related database driver development libraries: e.g. postgres-dev or libmariadb-dev +No setup: **[pwpush.com](https://pwpush.com)** — create a push and share the link. -```sh -git clone git@github.com:pglombardo/PasswordPusher.git -cd PasswordPusher -gem install bundler +### Run your own instance with Docker Compose -export RAILS_ENV=production +1. Point a DNS record to your server (e.g. `pwpush.example.com`). +2. Clone this repo or download [docker-compose.yml](https://raw.githubusercontent.com/pglombardo/PasswordPusher/refs/heads/master/docker-compose.yml). +3. In `docker-compose.yml`, uncomment and set: + - `TLS_DOMAIN: 'pwpush.example.com'` (for automatic Let’s Encrypt TLS). + - Optionally set `PWPUSH_MASTER_KEY` (see comments in the file; generate at [us.pwpush.com/generate_key](https://us.pwpush.com/generate_key)). +4. Run: -# Update the following line to point to your Postgres (or MySQL/Mariadb) instance -DATABASE_URL=postgresql://passwordpusher_user:passwordpusher_passwd@postgres:5432/passwordpusher_db - -bundle install --without development test -./bin/rails assets:precompile -./bin/rails db:setup -./bin/rails server --environment=production +```bash +docker compose up -d ``` -Then view the site @ [http://localhost:5100/](http://localhost:5100/). +Open `https://pwpush.example.com`. The Compose file includes persistent storage, health checks, and is suitable for production. +_Note: If you didn't set `TLS_DOMAIN`, uncomment port 5100 and visit the application on http://your-ip:5100_ -# 🔨 3rd Party Tools +### Cloud deploy & contributor setup -## Command Line Utilities +The repo includes ready-to-adapt configs for common platforms and local dev: -* The almost official [pwpush-cli](https://github.com/pglombardo/pwpush-cli) (in pre-beta): CLI for Password Pusher with authentication support +| File / path | Use | +|-------------|-----| +| [app.json](app.json) | Heroku-style deploy (env vars, `postdeploy` / `db:prepare`, process types). | +| [.do/deploy.template.yaml](.do/deploy.template.yaml) | [DigitalOcean App Platform](https://docs.digitalocean.com/products/app-platform/) spec (web + job, secrets placeholders). | +| [render.yaml](render.yaml) | [Render](https://render.com/) Blueprint (web + Postgres, health `/up`; optional worker commented). | +| [fly.toml](fly.toml) | [Fly.io](https://fly.io/) (`fly launch` — set `app` name; Dockerfile `containers/docker/Dockerfile`). | +| [railway.toml](railway.toml) | [Railway](https://railway.com/) config-as-code (Dockerfile path; set secrets in dashboard). | +| [.devcontainer/](.devcontainer/) | VS Code / GitHub Codespaces: Ruby + Postgres, repo mounted at `/workspace`, `bin/setup`. | -* [thekamilpro/kppwpush](https://github.com/thekamilpro/kppwpush): A PowerShell Module available in the [PowerShell Gallery](https://www.powershellgallery.com/packages/KpPwpush/0.0.1). See the livestream of its creation on [The Kamil Pro's channel](https://www.youtube.com/watch?v=f8_PZOx_KBY&feature=youtu.be). +Production image build: [containers/docker/Dockerfile](containers/docker/Dockerfile). Root [docker-compose.yml](docker-compose.yml) uses the published image; [.devcontainer/docker-compose.yml](.devcontainer/docker-compose.yml) is for development with a mounted working tree. -* [pgarm/pwposh](https://github.com/pgarm/pwposh): a PowerShell module available in the [PowerShell Gallery](https://www.powershellgallery.com/packages/PwPoSh/) +### Use the API, CLI, or integrations -* [lnfnunes/pwpush-cli](https://github.com/lnfnunes/pwpush-cli): a Node.js based CLI +For API usage, CLI tools, and integrations: -* [abkierstein/pwpush](https://github.com/abkierstein/pwpush): a Python based CLI +- **pwpush CLI**: [github.com/pglombardo/pwpush-cli](https://github.com/pglombardo/pwpush-cli) — Official command-line tool for pushing secrets from the terminal +- API v2 docs: [docs.pwpush.com/docs/api-v2](https://docs.pwpush.com/docs/api-v2/) +- OSS API endpoint reference in-app: `/api/v2/version` and `/api/v2/pushes` +- 3rd-party tools: [docs.pwpush.com/docs/3rd-party-tools](https://docs.pwpush.com/docs/3rd-party-tools/) -## GUIs +Quick API v2 smoke test: -* [Tachaeon/PWPush-Generator](https://github.com/Tachaeon/PWPush-Generator): A powershell GUI frontend for pwpush.com +```bash +curl -s https://YOUR_HOST/api/v2/version +``` -## Libraries & APIs +--- -* [oyale/PwPush-PHP](https://github.com/oyale/PwPush-PHP): a PHP library wrapper to easily push passwords to any Password Pusher instance +## Tools & Extensions -## Android Apps +Enhance Password Pusher with these official companion tools: -* [Pushie](https://play.google.com/store/apps/details?id=com.chesire.pushie) by [chesire](https://github.com/chesire) +| CLI | Chrome Extension | +|:---:|:---:| +| [![pwpush CLI](https://img.shields.io/badge/CLI-pwpush--cli-3776AB?style=for-the-badge&logo=python&logoColor=white)](https://github.com/pglombardo/pwpush-cli) | [![Chrome Extension](https://img.shields.io/badge/Chrome-Extension-4285F4?style=for-the-badge&logo=googlechrome&logoColor=white)](https://chromewebstore.google.com/detail/djdnbhbnimfpfmnocchfhjahinieaacg) | +| Command-line tool for pushing secrets from the terminal | Create pushes directly from your browser with self-hosted & APIv2 support | -## Application Integrations +--- -* [Slack: How to Add a Custom Slash Command](https://github.com/pglombardo/PasswordPusher/wiki/PasswordPusher-&-Slack:-Custom-Slash-Command) +## Documentation -* [Unraid Application](https://forums.unraid.net/topic/104128-support-passwordpusher-pwpush-corneliousjd-repo/) +Full docs: **[docs.pwpush.com](https://docs.pwpush.com)** — installation, configuration, API, themes, and more. -* [Alfred Workflow](http://www.packal.org/workflow/passwordpusher) for Mac users +--- -_See also the [Tools Page on pwpush.com](https://pwpush.com/en/pages/tools)._ +## Language translations -# 📡 The Password Pusher API +**[Translation.io](https://translation.io/?utm_source=pwpush)** has provided free translation tooling for the OSS version of Password Pusher. The app ships with **31 UI languages**. -* [JSON API Documentation](https://pwpush.com/api) -* [Walkthrough & Examples](https://github.com/pglombardo/PasswordPusher/wiki/Password-API) +[![Translation.io](app/assets/images/partners/translation-io-banner.png)](https://translation.io/?utm_source=pwpush) -# 🇮🇹 Internationalization +Consider [Translation.io](https://translation.io/?utm_source=pwpush) for your company or project’s translation needs. -Password Pusher is currently available in **29 languages** with more languages being added often as volunteers apply. +--- -From within the application, the language is selectable from a language menu. Out of the box and before any language menu selection is done, the default language for the application is English. +## Credits -## Changing the Default Language +### Security researchers -The default language can be changed by setting an environment variable with the appropriate language code: +- **Kullai Metikala** — [GitHub](https://github.com/kullaisec) \| [LinkedIn](https://www.linkedin.com/in/kullai-metikala-8378b122a/) +- [Positive Technologies](https://global.ptsecurity.com) +- **Igniter** — [GitHub](https://github.com/igniter07) - PWP__DEFAULT_LOCALE=es +### Translators -For more details, a list of supported language codes and further explanation, see the bottom of this [configuration file](https://github.com/pglombardo/PasswordPusher/blob/master/config/settings.yml). +| Name | Language | +|------|----------| +| [Oyale](https://github.com/oyale) | [Catalan](https://pwpush.com/ca), [Spanish](https://pwpush.com/es) | +| Finn Skaaning | [Danish](https://pwpush.com/da/p/ny) | +| [Mihail Tchetchelnitski](https://github.com/mtchetch) | [Finnish](https://pwpush.com/fi/p/uusi) | +| [Thibaut](https://github.com/tibo59) | [French](https://pwpush.com/fr/p/Nouveau) | +| Thomas Wölk | [German](https://pwpush.com/de/p/neu) — [GitHub](https://github.com/confluencepoint), [Twitter](https://twitter.com/confluencepoint) | +| Martin Otto | [German](https://pwpush.com/de/p/neu) | +| Robin Jørgensen | [Norwegian](https://pwpush.com/no/p/ny) | +| [Łukasz](https://github.com/drpt) | [Polish](https://pwpush.com/pl/p/nowy) | +| [Jair Henrique](https://github.com/jairhenrique/), [Fabrício Rodrigues](https://www.linkedin.com/in/ifabriciorodrigues/), [Ivan Freitas](https://github.com/IvanMFreitas), Sara Faria | [Portuguese (BR)](https://pwpush.com/pt-br/p/novo) | +| Pedro Marques | [European Portuguese](https://pwpush.com/pt-pt/p/novo) | +| johan323, Fredrik Arvas | [Swedish](https://pwpush.com/sv/p/ny) | -# 🛟 Help Out +Thanks also to [Translation.io](https://translation.io) for managing translations (free for open source). -[pwpush.com](https://pwpush.com) is hosted on Digital Ocean and is happily paid out of pocket by myself for more than 10 years. +### Containers & infrastructure -__But you could help out greatly__ by signing up to Digital Ocean with [this link](https://m.do.co/c/f4ea6ef24c13) (and get $200 credit). In return, Password Pusher gets a helpful hosting credit. +- [@fiskhest](https://github.com/fiskhest) — [Kubernetes manifests](https://github.com/pglombardo/PasswordPusher/tree/master/containers/kubernetes) +- [@sfarosu](https://github.com/sfarosu) — [Docker, Kubernetes & OpenShift support](https://github.com/pglombardo/PasswordPusher/pull/82) +- [sirux88](https://github.com/sirux88) — Docker cleanup and multistage builds -**tldr;** Sign up to Digital Ocean [with this link](https://m.do.co/c/f4ea6ef24c13), **get a $200 credit for free** and help Password Pusher out. +### Other -[![DigitalOcean Referral Badge](https://web-platforms.sfo2.cdn.digitaloceanspaces.com/WWW/Badge%201.svg)](https://www.digitalocean.com/?refcode=f4ea6ef24c13&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge) +- [@iandunn](https://github.com/iandunn) — Password form security +- [Kasper Grubbe](https://github.com/kaspergrubbe) — [JSON POST fix](https://github.com/pglombardo/PasswordPusher/pull/3) +- [JarvisAndPi](http://www.reddit.com/user/JarvisAndPi) — Favicon design -# 📼 Credits +More: [Contributors](https://github.com/pglombardo/PasswordPusher/graphs/contributors) -## Translators +--- -Thanks to our great translators! +## Stay updated -If you would like to volunteer and assist in translating, see [this page](https://pwpush.com/en/pages/translate). +- **Newsletter** — [Sign up](https://buttondown.email/pwpush?tag=github) for release notes, security updates, and tips. +- **Social** — [X](https://x.com/pwpush), [Reddit](https://www.reddit.com/r/pwpush), [Facebook](https://www.facebook.com/pwpush) -| Name | Language | | -|---|---|---| -| [Oyale](https://github.com/oyale) | [Catalan](https://pwpush.com/ca) | | -| Finn Skaaning | [Danish](https://pwpush.com/da/p/ny) | | -| [Mihail Tchetchelnitski](https://github.com/mtchetch) | [Finnish](https://pwpush.com/fi/p/uusi) | | -| [Thibaut](https://github.com/tibo59) | [French](https://pwpush.com/fr/p/Nouveau) | | -| Thomas Wölk | [German](https://pwpush.com/de/p/neu) | [Github](https://github.com/confluencepoint/), [Twitter](https://twitter.com/confluencepoint) | -| Martin Otto |[German](https://pwpush.com/de/p/neu) | | -| Robin Jørgensen |[Norwegian](https://pwpush.com/no/p/ny) | | -| [Łukasz](https://github.com/drpt)|[Polish](https://pwpush.com/pl/p/nowy) | | -| [Jair Henrique](https://github.com/jairhenrique/) | [Portuguese](https://pwpush.com/pt-br/p/novo) | | -| [Fabrício Rodrigues](https://www.linkedin.com/in/ifabriciorodrigues/)| [Portuguese](https://pwpush.com/pt-br/p/novo) | | -| [Ivan Freitas](https://github.com/IvanMFreitas)| [Portuguese](https://pwpush.com/pt-br/p/novo) | | -| Sara Faria| [Portuguese](https://pwpush.com/pt-br/p/novo) | | -| [Oyale](https://github.com/oyale) |[Spanish](https://pwpush.com/pt-br/p/novo) | | -| johan323 |[Swedish](https://pwpush.com/sv/p/ny) | | -| Fredrik Arvas|[Swedish](https://pwpush.com/sv/p/ny) | | -| Pedro Marques | [European Portuguese](https://pwpush.com/pt-pt/p/novo) | | +--- -Also thanks to [translation.io](https://translation.io) for their great service in managing translations. It's also generously free for opensource projects. +## Donations -## Containers +🛑 **Because of some abuse, the donation link is temporarily disabled. Contact support@pwpush.com if you'd like to donate** -Thanks to: +--> _To those testing various cards and failing, do what you do but please not here. We can't risk getting the account flagged in Stripe - we're not big enough to deal with that type of damage. I hope you can understand our position._ -* [@fiskhest](https://github.com/fiskhest) the [Kubernetes installation instructions and manifests](https://github.com/pglombardo/PasswordPusher/tree/master/containers/kubernetes). +Donations are **optional**. Password Pusher is and will remain open source and free to use. -* [@sfarosu](https://github.com/sfarosu) for [contributing](https://github.com/pglombardo/PasswordPusher/pull/82) the Docker, Kubernetes & OpenShift container support. +If it’s useful to you and you’d like to support development, donations are greatly appreciated and go toward hosting, maintenance, testing, and new features. -* [sirux88](https://github.com/sirux88) for cleaning up the Docker files and adding multistage builds. +| [![Donate QR](https://pwpush.fra1.cdn.digitaloceanspaces.com/misc/pwpush-donate-stripe-qr-small.png)](https://buy.stripe.com/7sI4gCgTT1tr6WY3cd) | [**Donate via Stripe**](https://buy.stripe.com/7sI4gCgTT1tr6WY3cd) | +|---|---| -## Other +You can also support the project with a [paid plan on pwpush.com](https://pwpush.com/pricing). -Thanks to: +**Note:** Password Pusher is operated by Apnotic, LLC. Donations support the project but are not tax-deductible charitable contributions. See [FAQ](https://docs.pwpush.com/docs/faq/) for more on [Apnotic](https://docs.pwpush.com/docs/faq/#what-is-apnotic) and [trust & security](https://docs.pwpush.com/docs/faq/#trust-is-a-concern--why-should-i-trust-and-use-password-pusher). -* [@iandunn](https://github.com/iandunn) for better password form security. +--- -* [Kasper 'kapöw' Grubbe](https://github.com/kaspergrubbe) for the [JSON POST fix](https://github.com/pglombardo/PasswordPusher/pull/3). +## Star history -* [JarvisAndPi](http://www.reddit.com/user/JarvisAndPi) for the favicon design +[![Star History Chart](https://star-history.dera.page/svg?repos=pglombardo/PasswordPusher&type=date&legend=top-left)](https://star-history.dera.page/#pglombardo/PasswordPusher&type=date&legend=top-left) -...and many more. See the [Contributors page](https://github.com/pglombardo/PasswordPusher/graphs/contributors) for more details. +--- -# 🛡 License +## License -[![License](https://img.shields.io/github/license/pglombardo/PasswordPusher)](https://github.com/pglombardo/PasswordPusher/blob/master/LICENSE) +This project is licensed under the **Apache License 2.0**. See [LICENSE](https://github.com/pglombardo/PasswordPusher/blob/master/LICENSE) for details. -This project is licensed under the terms of the `Apache License 2.0` license. See [LICENSE](https://github.com/pglombardo/PasswordPusher/blob/master/LICENSE) for more details. +--- -# 📃 Citation +## Citation ```bibtex @misc{PasswordPusher, author = {Peter Giacomo Lombardo}, - title = {An application to securely communicate passwords over the web. Passwords automatically expire after a certain number of views and/or time has passed.}, - year = {2022}, + title = {Password Pusher: Securely share sensitive information with automatic expiration and deletion. Track who, what and when with full audit logs.}, + year = {2026}, publisher = {GitHub}, - journal = {GitHub repository}, howpublished = {\url{https://github.com/pglombardo/PasswordPusher}} } ``` diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 000000000000..8860bbf13abf --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,108 @@ +# Release Guide + +Guide for Apnotic team members releasing a new version of the open source Password Pusher project ([pglombardo/PasswordPusher](https://github.com/pglombardo/PasswordPusher)). + +## Prerequisites + +- On `master` with all intended changes merged +- Clean working tree (no uncommitted or unpushed changes) +- `gh` CLI installed with the [gh-signoff](https://github.com/basecamp/gh-signoff) extension (`gh extension install basecamp/gh-signoff`) +- Git remote `oss` configured for `git@github.com:pglombardo/PasswordPusher.git` + +## Release a New Version + +### 1. Sign off the release commit + +From `master`, run the full CI suite and sign off the latest commit: + +```bash +bin/ci +``` + +This runs tests, linting, security checks, and signs the commit via `gh signoff` when everything passes. The repository must be pristine (nothing uncommitted or unpushed) for signoff to succeed. + +### 2. Bump the version + +Use the [version](https://github.com/pglombardo/version) gem rake tasks to update `VERSION`, commit the change, and create a local git tag: + +```bash +bin/rails version:bump # patch release (e.g. 2.7.0 → 2.7.1) +bin/rails version:bump:minor # minor release (e.g. 2.7.0 → 2.8.0) +bin/rails version:bump:major # major release (e.g. 2.7.0 → 3.0.0) +``` + +**Which bump to use:** + +- **Minor** (`version:bump:minor`) — new features or larger fixes +- **Patch** (`version:bump`) — dependency updates, security fixes, and small bug fixes +- **Major** (`version:bump:major`) — backward-incompatible breaking changes only. Extremely rare; requires explicit team agreement before use. + +The version task is configured in `Rakefile` to create tags with a `v` prefix (e.g. `v2.7.1`). + +### 3. Push to GitHub + +```bash +git push oss master +git push oss vX.Y.Z +``` + +Replace `vX.Y.Z` with the new version tag created by the bump task. + +Pushing the version tag triggers the [Docker Container Builds](.github/workflows/docker-containers.yml) GitHub Actions workflow, which builds and publishes images to Docker Hub: + +- `pglombardo/pwpush` +- `pglombardo/pwpush-worker` +- `pglombardo/pwpush-public-gateway` + +Each image is tagged with the semver version (e.g. `2.7.1`, `2.7`, `2`). + +### 4. Publish the GitHub release + +[Release Drafter](.github/workflows/release-drafter.yml) maintains a running draft release as PRs are merged to `master`. After pushing the version tag: + +1. Open [GitHub Releases](https://github.com/pglombardo/PasswordPusher/releases) and edit the draft release. +2. Confirm the release points to the correct version tag (`vX.Y.Z`), not `master`. +3. Review and edit the release notes. Release Drafter auto-generates notes from merged PR labels, but always add manual context for: + - Notable features and breaking changes + - Security fixes or advisories the community should know about + - Upgrade or migration notes (link to guides like [UPGRADE-2.0.md](UPGRADE-2.0.md) when relevant) +4. Publish the release. + +The release notes template in [.github/release-drafter.yml](.github/release-drafter.yml) includes Docker install instructions and useful links for the community. + +## Docker `stable` Tag (Separate Step) + +We maintain a Docker `stable` tag for users who prefer a conservative, proven release over the latest version tag. This is updated separately and **should not** be moved with every release. + +Update `stable` only after a release has been out for a few weeks with no reported issues from users. + +When ready, run: + +```bash +bin/move_up_stable_tag.sh X.Y.Z +``` + +Pass the version **without** the `v` prefix (e.g. `2.7.0`, not `v2.7.0`). + +This script: + +- Repoints the `stable` tag on Docker Hub for all three OSS images +- Updates the `stable` git tag on the `oss` remote +- Prints community announcement text you can paste (newsletter, social, etc.) + +**Requirements:** Docker logged in to Docker Hub, `jq` installed, and push access to the `oss` remote. + +## Quick Reference + +```bash +# On master, with a clean tree +bin/ci +bin/rails version:bump # or version:bump:minor / version:bump:major +git push oss master +git push oss vX.Y.Z + +# Edit and publish the Release Drafter draft on GitHub + +# Later, when proven stable (weeks later, no issues reported) +bin/move_up_stable_tag.sh X.Y.Z +``` diff --git a/Rakefile b/Rakefile index 444b77182b99..01bf6eca090b 100644 --- a/Rakefile +++ b/Rakefile @@ -3,13 +3,13 @@ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require File.expand_path('config/application', __dir__) +require File.expand_path("config/application", __dir__) PasswordPusher::Application.load_tasks # Add version gem rake tasks -require 'rake/version_task' +require "rake/version_task" Rake::VersionTask.new do |task| task.with_git_tag = true - task.git_tag_prefix = 'v' + task.git_tag_prefix = "v" end diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..7259a778d709 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,22 @@ +# Security Policy + +This page outlines the general procedure for reporting security vulnerabilities. + +All reports (and the time you spend to discover them) are very much appreciated. + +With the community, I hope to keep Password Pusher up to date with the latest developments and best security practices. + +As always, if you have any questions, feel free to [contact me](https://pwpush.com/feedbacks/new) anytime! + +## Supported Versions + +I encourage the safe reporting of security issues for the latest minor +version of the software. e.g. `v1.47.x` as of November 2024. + +This is a general rule but I would be open to hear any and all security reports that you feel are necessary. + +## Reporting a Vulnerability + +Please use the Github [Report a vulnerability](https://github.com/pglombardo/PasswordPusher/security/advisories/new) feature. + +_I kindly ask you to not report any potential vulnerabilities publicly as it may put existing users at risk. Your help in this is greatly appreciated!_ diff --git a/Themes.md b/Themes.md index a0d7321f2292..cd36277e1729 100644 --- a/Themes.md +++ b/Themes.md @@ -1,121 +1 @@ -# Themes - -This page serves as a gallery of the themes available in Password Pusher. To select a theme for your instance, simply set the `PWP__THEME` environment variable or update the corresponding value in your [config/settings.yml](https://github.com/pglombardo/PasswordPusher/blob/master/config/settings.yml) file. - -For more information on themes, see [the Themes section in the Configuration document](https://github.com/pglombardo/PasswordPusher/blob/master/Configuration.md#themes) which explains themes in their entirety and how to add custom CSS to further customize the application. - -These themes are bundled in Password Pusher but come from the great [Bootswatch](https://bootswatch.com) project and are licensed under the MIT License. - -# Branding - -Also note you can also "brand" the app by changing the logo, tagline and more. See the [Rebranding section in the Configuration document](https://github.com/pglombardo/PasswordPusher/blob/master/Configuration.md#rebranding) for more details. - -# Theme Gallery - -Onto the themes gallery! - -Set your theme with `PWP__THEME=` - -## Cerulean - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fcerulean-theme-pwpush.com.png) - -## Cosmo - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fcosmo-theme-pwpush.com.png) - -## Cyborg - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fcyborg-theme-pwpush.com.png) - -## Darkly - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fdarkly-theme-pwpush.com.png) - -## Default - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fdefault-theme-pwpush.com.png) - -## Flatly - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fflatly-theme-pwpush.com.png) - -## Journal - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fjournal-theme-pwpush.com.png) - -## Litera - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Flitera-theme-pwpush.com.png) - -## Lumen - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Flumen-theme-pwpush.com.png) - -## Lux - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Flux-theme-pwpush.com.png) - -## Materia - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fmateria-theme-pwpush.com.png) - -## Minty - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fminty-theme-pwpush.com.png) - -## Morph - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fmorph-theme-pwpush.com.png) - -## Pulse - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fpulse-theme-pwpush.com.png) - -## Quartz - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fquartz-theme-pwpush.com.png) - -## Sandstone - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fsandstone-theme-pwpush.com.png) - -## Simplex - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fsimplex-theme-pwpush.com.png) - -## Sketchy - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fsketchy-theme-pwpush.com.png) - -## Slate - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fslate-theme-pwpush.com.png) - -## Solar - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fsolar-theme-pwpush.com.png) - -## Spacelab - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fspacelab-theme-pwpush.com.png) - -## Superhero - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fsuperhero-theme-pwpush.com.png) - -## United - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Funited-theme-pwpush.com.png) - -## Vapor - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fvapor-theme-pwpush.com.png) - -## Yeti - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fyeti-theme-pwpush.com.png) - -## Zephyr - -![](https://pwpush.fra1.cdn.digitaloceanspaces.com/themes%2Fzephyr-theme-pwpush.com.png) +The Themes Gallery has been moved to the [documentation portal](https://docs.pwpush.com/docs/themes-gallery/). diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md new file mode 100644 index 000000000000..345ddf80c7f5 --- /dev/null +++ b/UPGRADE-2.0.md @@ -0,0 +1,96 @@ +# Upgrading to Password Pusher 2.0 (OSS) + +2.0 is the current release on `master`. It focuses on **better defaults** and **less setup for new installs**. If you already run 1.x, you only need to act where your **old config or expectations** conflict with the new defaults—everything else can stay as-is. + +**Config reference:** [Self-hosted configuration](https://docs.pwpush.com/docs/self-hosted-configuration/) +**Env vars & compose:** [docker-compose.yml](https://github.com/pglombardo/PasswordPusher/blob/master/docker-compose.yml) (primary install path; all `PWP__...` options are commented there) + +--- + +## Required steps for existing deployments + +Do these in order; skip any step that does not apply to you. + + +| # | Action | When | +| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | +| 1 | **Backup** database and any custom config/secrets. | Always | +| 2 | **Drop** `PWP__ENABLE_LOGINS` and any `enable_logins` from env/YAML; use **`disable_logins`**, **`allow_anonymous`**, and **`disable_signups`** instead (see docker-compose comments). | Always if you had it set | +| 3 | **If you rely on email** (signup confirmation, forgot password, unlock): set `PWP__ENABLE_USER_ACCOUNT_EMAILS=true` (or `enable_user_account_emails: true`) **and** confirm SMTP works. If you leave it off, those flows stay disabled. | Only if you used Devise mail before | +| 4 | **If you do not want** URL/file/QR pushes or the new retrieval-step default: set the relevant env vars to `false` explicitly (see docker-compose comments). 2.0 defaults several features **on** that were **off** in 1.x. | Only if defaults are too open for you | +| 5 | **Smoke-test** after deploy: login, create a push, API with token if you use it. | Always | + + +That’s the minimum. If nothing broke and behavior is acceptable, you’re done. + +--- + +## Optional (only if you need it) + +- **Anonymous vs logged-in only:** Use `PWP__ALLOW_ANONYMOUS` and `PWP__DISABLE_SIGNUPS` to match policy (replaces the old “logins off” mental model). +- **Disable logins (`disable_logins`):** When `true`, the Log In button is hidden and **GET/POST `/users/sign_in` return 404**—no new **web** sessions via the sign-in page. Existing browser sessions can still sign out. **The API is not affected:** Bearer/token authentication is unchanged, so anyone with a **previously created API token** can still authenticate intentionally—useful when you want to block the web login UI but keep programmatic access. Env: `PWP__DISABLE_LOGINS=true`. +- **GDPR banner:** Off by default in 2.0. Set `PWP__SHOW_GDPR_CONSENT_BANNER=true` if you still need it. +- **Custom `settings.yml`:** Still works. Prefer **environment variables** for new changes—see docker-compose. Long term, file-based config may move toward an in-app UI; env-based config is the forward-compatible path. +- **Fork with view overrides:** UI was restyled; re-test any customized templates. + +--- + +## What changed (reference) + +Use this only if you need context or to diff behavior. + + +| Topic | 1.x | 2.0 | +| -------------------------- | --------------------------------------- | ---------------------------------------------------------- | +| **Logins** | `enable_logins` could turn accounts off | Always on; use `allow_anonymous` / `disable_signups`; optional **`disable_logins`** hides login UI and blocks new session creation | +| **Devise email** | Often assumed when logins were on | **Opt-in** via `enable_user_account_emails` + SMTP | +| **URL / file / QR pushes** | Default off, tied to logins in docs | Default **on**; turn off explicitly if unwanted | +| **Retrieval step default** | Default off for pw/url/files | Default **on** for pw/url/files (QR stays off in defaults) | +| **GDPR banner** | Default on | Default **off** | +| **Configuration** | Many used `settings.yml` only | **Preferred:** env vars in compose; YAML still supported | + + +--- + +## Quick settings table + + +| Setting | Default (2.0) | Set to match old behavior | +| --------------------------------------------------------------- | ------------- | -------------------------------------------- | +| `enable_user_account_emails` | `false` | `true` + working SMTP if you need mail flows | +| `allow_anonymous` | `true` | `false` to require login to create pushes | +| `disable_logins` | `false` | `true` to hide Log In and block POST/GET **web** sign-in (404); **API token auth still works** | +| `enable_url_pushes` / `enable_file_pushes` / `enable_qr_pushes` | `true` each | `false` to disable | +| `pw` / `url` / `files` → `retrieval_step_default` | `true` | `false` for no extra step by default | +| `show_gdpr_consent_banner` | `false` | `true` to show banner | + + +All of the above have `PWP__...` equivalents—see [docker-compose.yml](https://github.com/pglombardo/PasswordPusher/blob/master/docker-compose.yml). + +--- + +## Further reading + +- [Self-hosted configuration](https://docs.pwpush.com/docs/self-hosted-configuration/) + +--- + +## Example: Fully anonymous installation (no login system) + +If you want a **fully anonymous** instance with **no login system exposed at all**—no sign-in page, no new accounts via the UI—set both: + +| Setting | Value | Env | +| ------------------ | ------- | ---------------------------- | +| `disable_logins` | `true` | `PWP__DISABLE_LOGINS=true` | +| `disable_signups` | `true` | `PWP__DISABLE_SIGNUPS=true` | + +Together they: + +- **Hide Log In** and **block GET/POST `/users/sign_in`** (404), so the app does not offer a web login flow. **API authentication is unchanged:** clients can still use a **previously issued API token** (Bearer) to act as that user—`disable_logins` does not revoke or block token auth. Revoke tokens in admin or stop issuing them if you need no API access either. +- **Hide Sign Up** and prevent new registrations through the UI. +- **Hide the Files tab** when `disable_logins` is true (file pushes require a signed-in user), so anonymous installs only expose password, URL, and QR flows as configured. + +Keep **`allow_anonymous: true`** (default) if visitors should create pushes without an account. Tighten with **`allow_anonymous: false`** only if you intend to lock creation down via another mechanism (e.g. API tokens only). See [docker-compose.yml](https://github.com/pglombardo/PasswordPusher/blob/master/docker-compose.yml) for commented `PWP__...` entries. + +**Not supported:** **`allow_anonymous: false`** together with **`disable_logins: true`** and **`disable_signups: true`**. That combination is unsupported—creation would require a signed-in user, but sign-in and sign-up are disabled, leaving no supported way to obtain a session. Do not deploy with all three set that way. + diff --git a/VERSION b/VERSION index d2829d87d659..edcfe40d1984 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.37.3 +2.14.0 diff --git a/app.json b/app.json index e6d4c11a143e..a823ed1be582 100644 --- a/app.json +++ b/app.json @@ -1,64 +1,134 @@ { "name": "PasswordPusher", - "description": "An application to communicate passwords over the web. Passwords expire after a certain number of views and/or time has passed.", + "description": "Share passwords, text, files, URLs, and QR payloads via secret links that expire by views and/or time. Encrypted storage, optional passphrase, audit logging, and JSON API.", + "website": "https://pwpush.com", "repository": "https://github.com/pglombardo/PasswordPusher", - "logo": "https://s3-eu-west-1.amazonaws.com/pwpush/pwpush_logo.png", - "keywords": ["password", "security", "expire"], + "logo": "https://pwpush.fra1.cdn.digitaloceanspaces.com/branding/logos/horizontal-logo-small.png", + "keywords": [ + "password", + "security", + "expire", + "file-sharing", + "url-sharing", + "qr-code", + "rails" + ], + "success_url": "/", + "stack": "heroku-24", + "buildpacks": [ + { + "url": "heroku/ruby" + } + ], + "formation": { + "web": { + "quantity": 1, + "size": "basic" + } + }, "addons": [ - "heroku-postgresql" + { + "plan": "heroku-postgresql", + "as": "DATABASE" + } ], "scripts": { - "postdeploy": "bundle exec rake db:migrate" + "postdeploy": "bundle exec rails db:prepare" }, "env": { - "BUNDLE_WITHOUT": "development:test", + "BUNDLE_WITHOUT": { + "description": "Bundler groups to exclude from production bundle.", + "value": "development:test" + }, + "RAILS_LOG_TO_STDOUT": { + "description": "Log to stdout for Heroku log drain.", + "value": "true" + }, + "RAILS_SERVE_STATIC_FILES": { + "description": "Serve static assets from Rails when no CDN is in front.", + "value": "true" + }, + "SECRET_KEY_BASE": { + "description": "Rails secret key base. Generate: rails secret", + "generator": "secret" + }, "PWPUSH_MASTER_KEY": { - "description": "Application Encryption Key", + "description": "Lockbox encryption key for payloads. Generate: rails runner 'puts Lockbox.generate_key'", "generator": "secret" }, "WEB_CONCURRENCY": { - "description": "The number of processes to run.", - "value": "3" - }, - "PWP__EXPIRE_AFTER_DAYS_DEFAULT": { - "description": "Default value for expire after days", - "value": "7" + "description": "Puma worker processes (memory vs throughput tradeoff).", + "value": "0" }, - "PWP__EXPIRE_AFTER_DAYS_MIN": { - "description": "Expire after days minimum value", - "value": "1" + "PWP__HOST_DOMAIN": { + "description": "Public hostname without scheme (e.g. myapp.herokuapp.com). Required for correct secret URLs.", + "required": false }, - "PWP__EXPIRE_AFTER_DAYS_MAX": { - "description": "Expire after days maximum value", - "value": "90" + "PWP__HOST_PROTOCOL": { + "description": "https recommended in production.", + "value": "https" }, - "PWP__EXPIRE_AFTER_VIEWS_DEFAULT": { - "description": "Expire after views default value", - "value": "5" + "PWP__OVERRIDE_BASE_URL": { + "description": "Full base URL if app is behind a proxy or custom domain (e.g. https://secrets.example.com).", + "required": false }, - "PWP__EXPIRE_AFTER_VIEWS_MIN": { - "description": "Expire after views minimum value", - "value": "1" + "PWP__ALLOWED_HOSTS": { + "description": "Space-separated hostnames Rails should accept (Host header). Set to your Heroku hostname and custom domains.", + "required": false }, - "PWP__EXPIRE_AFTER_VIEWS_MAX": { - "description": "Expire after views maximum value", - "value": "100" + "PWP__FILES__STORAGE": { + "description": "File push backend: local (ephemeral on Heroku) or s3. Use s3 for durable file pushes; see docs for PWP__FILES__S3__* vars.", + "value": "local" }, - "PWP__ENABLE_DELETABLE_PUSHES": { - "description": "When true, passwords will have a link to optionally delete the password being viewed. When pushing a new password, this option will also add a checkbox to conditionally enable/disable this feature on a per-password basis.", + "PWP__ALLOW_ANONYMOUS": { + "description": "Allow creating pushes without signing in.", "value": "true" }, - "PWP__DELETABLE_PUSHES_DEFAULT": { - "description": "When the option DELETABLE_PASSWORDS_ENABLED is set to true, this option does two things: 1. Sets the default check state for the \"Allow viewers to optionally delete password before expiration\" checkbox. 2. Sets the default value for newly pushed passwords if unspecified (such as with a json request)", + "PWP__DISABLE_SIGNUPS": { + "description": "Hide Sign Up and block new registrations via UI.", + "value": "false" + }, + "PWP__DISABLE_LOGINS": { + "description": "Hide Log In; sign_in returns 404. Use with allow_anonymous true for anonymous-only. Do not combine with allow_anonymous false.", + "value": "false" + }, + "PWP__REQUIRE_MFA": { + "description": "Require TOTP MFA for all signed-in users. Users without MFA are redirected to setup.", + "value": "false" + }, + "PWP__ENABLE_USER_ACCOUNT_EMAILS": { + "description": "Devise mail (confirm/reset). Requires SMTP; set PWP__MAIL__SMTP_* when true.", + "value": "false" + }, + "PWP__MAIL__SMTP_ADDRESS": { + "description": "SMTP host when account emails are enabled.", + "required": false + }, + "PWP__ENABLE_FILE_PUSHES": { + "description": "File pushes require a signed-in user unless disable_logins hides the feature.", "value": "true" }, - "PWP__ENABLE_RETRIEVAL_STEP": { - "description": "An option to require an extra click through to get to the actual password?", + "PWP__ENABLE_URL_PUSHES": { + "description": "URL push kind.", "value": "true" }, - "PWP__RETRIEVAL_STEP_DEFAULT": { - "description": "The default value for the 1-click retrieval step option.", + "PWP__ENABLE_QR_PUSHES": { + "description": "QR push kind.", "value": "true" + }, + "PWP__THEME": { + "description": "Bootswatch theme name. See https://docs.pwpush.com/docs/themes/", + "value": "default" + } + }, + "environments": { + "review": { + "formation": { + "web": { + "quantity": 1, + "size": "basic" + } + } } } } diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index 7fbd7f025f4e..decf0a9d77ca 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1,3 +1,2 @@ +//= link_tree ../builds //= link_tree ../images -//= link_directory ../stylesheets .css -//= link_tree ../javascripts .js diff --git a/app/assets/images/alfredapp.png b/app/assets/images/alfredapp.png deleted file mode 100644 index 90ac3736504d..000000000000 Binary files a/app/assets/images/alfredapp.png and /dev/null differ diff --git a/app/assets/images/black_wood.jpg b/app/assets/images/black_wood.jpg deleted file mode 100644 index 64301ae63510..000000000000 Binary files a/app/assets/images/black_wood.jpg and /dev/null differ diff --git a/app/assets/images/button_down.png b/app/assets/images/button_down.png deleted file mode 100644 index b0ab548a151f..000000000000 Binary files a/app/assets/images/button_down.png and /dev/null differ diff --git a/app/assets/images/button_over.png b/app/assets/images/button_over.png deleted file mode 100644 index 1329a26103d3..000000000000 Binary files a/app/assets/images/button_over.png and /dev/null differ diff --git a/app/assets/images/button_up.png b/app/assets/images/button_up.png deleted file mode 100644 index 62cb9b299de7..000000000000 Binary files a/app/assets/images/button_up.png and /dev/null differ diff --git a/app/assets/images/forkme.png b/app/assets/images/forkme.png deleted file mode 100644 index 8fe3a7e1f3a2..000000000000 Binary files a/app/assets/images/forkme.png and /dev/null differ diff --git a/app/assets/images/horizontal-logo-small.png b/app/assets/images/horizontal-logo-small.png deleted file mode 100644 index 52eaf30c9db8..000000000000 Binary files a/app/assets/images/horizontal-logo-small.png and /dev/null differ diff --git a/app/assets/images/logo-transparent-lg-dark.png b/app/assets/images/logo-transparent-lg-dark.png deleted file mode 100644 index d9e84f73a57a..000000000000 Binary files a/app/assets/images/logo-transparent-lg-dark.png and /dev/null differ diff --git a/app/assets/images/naked-logo.png b/app/assets/images/naked-logo.png index 943f6fa65f2a..f940e79a16ce 100644 Binary files a/app/assets/images/naked-logo.png and b/app/assets/images/naked-logo.png differ diff --git a/app/assets/images/partners/translation-io-banner.png b/app/assets/images/partners/translation-io-banner.png new file mode 100644 index 000000000000..a9bcdfb16c63 Binary files /dev/null and b/app/assets/images/partners/translation-io-banner.png differ diff --git a/app/assets/images/pwpush_logo.png b/app/assets/images/pwpush_logo.png index f5726db0b33f..70fc55a8ad0b 100644 Binary files a/app/assets/images/pwpush_logo.png and b/app/assets/images/pwpush_logo.png differ diff --git a/app/assets/images/square-logo-small.png b/app/assets/images/square-logo-small.png deleted file mode 100644 index 979637673951..000000000000 Binary files a/app/assets/images/square-logo-small.png and /dev/null differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js deleted file mode 100644 index 542b9afaa7ef..000000000000 --- a/app/assets/javascripts/application.js +++ /dev/null @@ -1,13670 +0,0 @@ -(() => { - var __create = Object.create; - var __defProp = Object.defineProperty; - var __getOwnPropDesc = Object.getOwnPropertyDescriptor; - var __getOwnPropNames = Object.getOwnPropertyNames; - var __getProtoOf = Object.getPrototypeOf; - var __hasOwnProp = Object.prototype.hasOwnProperty; - var __esm = (fn2, res) => function __init() { - return fn2 && (res = (0, fn2[__getOwnPropNames(fn2)[0]])(fn2 = 0)), res; - }; - var __commonJS = (cb, mod) => function __require() { - return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; - }; - var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); - }; - var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; - }; - var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( - // If the importer is in node compatibility mode or this is not an ESM - // file that has been converted to a CommonJS file using a Babel- - // compatible transform (i.e. "__esModule" has not been set), then set - // "default" to the CommonJS "module.exports" for node compatibility. - isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, - mod - )); - - // node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/adapters.js - var adapters_default; - var init_adapters = __esm({ - "node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/adapters.js"() { - adapters_default = { - logger: self.console, - WebSocket: self.WebSocket - }; - } - }); - - // node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/logger.js - var logger_default; - var init_logger = __esm({ - "node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/logger.js"() { - init_adapters(); - logger_default = { - log(...messages) { - if (this.enabled) { - messages.push(Date.now()); - adapters_default.logger.log("[ActionCable]", ...messages); - } - } - }; - } - }); - - // node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/connection_monitor.js - var now, secondsSince, ConnectionMonitor, connection_monitor_default; - var init_connection_monitor = __esm({ - "node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/connection_monitor.js"() { - init_logger(); - now = () => (/* @__PURE__ */ new Date()).getTime(); - secondsSince = (time) => (now() - time) / 1e3; - ConnectionMonitor = class { - constructor(connection) { - this.visibilityDidChange = this.visibilityDidChange.bind(this); - this.connection = connection; - this.reconnectAttempts = 0; - } - start() { - if (!this.isRunning()) { - this.startedAt = now(); - delete this.stoppedAt; - this.startPolling(); - addEventListener("visibilitychange", this.visibilityDidChange); - logger_default.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`); - } - } - stop() { - if (this.isRunning()) { - this.stoppedAt = now(); - this.stopPolling(); - removeEventListener("visibilitychange", this.visibilityDidChange); - logger_default.log("ConnectionMonitor stopped"); - } - } - isRunning() { - return this.startedAt && !this.stoppedAt; - } - recordPing() { - this.pingedAt = now(); - } - recordConnect() { - this.reconnectAttempts = 0; - this.recordPing(); - delete this.disconnectedAt; - logger_default.log("ConnectionMonitor recorded connect"); - } - recordDisconnect() { - this.disconnectedAt = now(); - logger_default.log("ConnectionMonitor recorded disconnect"); - } - // Private - startPolling() { - this.stopPolling(); - this.poll(); - } - stopPolling() { - clearTimeout(this.pollTimeout); - } - poll() { - this.pollTimeout = setTimeout( - () => { - this.reconnectIfStale(); - this.poll(); - }, - this.getPollInterval() - ); - } - getPollInterval() { - const { staleThreshold, reconnectionBackoffRate } = this.constructor; - const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10)); - const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate; - const jitter = jitterMax * Math.random(); - return staleThreshold * 1e3 * backoff * (1 + jitter); - } - reconnectIfStale() { - if (this.connectionIsStale()) { - logger_default.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`); - this.reconnectAttempts++; - if (this.disconnectedRecently()) { - logger_default.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`); - } else { - logger_default.log("ConnectionMonitor reopening"); - this.connection.reopen(); - } - } - } - get refreshedAt() { - return this.pingedAt ? this.pingedAt : this.startedAt; - } - connectionIsStale() { - return secondsSince(this.refreshedAt) > this.constructor.staleThreshold; - } - disconnectedRecently() { - return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold; - } - visibilityDidChange() { - if (document.visibilityState === "visible") { - setTimeout( - () => { - if (this.connectionIsStale() || !this.connection.isOpen()) { - logger_default.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`); - this.connection.reopen(); - } - }, - 200 - ); - } - } - }; - ConnectionMonitor.staleThreshold = 6; - ConnectionMonitor.reconnectionBackoffRate = 0.15; - connection_monitor_default = ConnectionMonitor; - } - }); - - // node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/internal.js - var internal_default; - var init_internal = __esm({ - "node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/internal.js"() { - internal_default = { - "message_types": { - "welcome": "welcome", - "disconnect": "disconnect", - "ping": "ping", - "confirmation": "confirm_subscription", - "rejection": "reject_subscription" - }, - "disconnect_reasons": { - "unauthorized": "unauthorized", - "invalid_request": "invalid_request", - "server_restart": "server_restart" - }, - "default_mount_path": "/cable", - "protocols": [ - "actioncable-v1-json", - "actioncable-unsupported" - ] - }; - } - }); - - // node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/connection.js - var message_types, protocols, supportedProtocols, indexOf, Connection, connection_default; - var init_connection = __esm({ - "node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/connection.js"() { - init_adapters(); - init_connection_monitor(); - init_internal(); - init_logger(); - ({ message_types, protocols } = internal_default); - supportedProtocols = protocols.slice(0, protocols.length - 1); - indexOf = [].indexOf; - Connection = class { - constructor(consumer2) { - this.open = this.open.bind(this); - this.consumer = consumer2; - this.subscriptions = this.consumer.subscriptions; - this.monitor = new connection_monitor_default(this); - this.disconnected = true; - } - send(data) { - if (this.isOpen()) { - this.webSocket.send(JSON.stringify(data)); - return true; - } else { - return false; - } - } - open() { - if (this.isActive()) { - logger_default.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`); - return false; - } else { - logger_default.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`); - if (this.webSocket) { - this.uninstallEventHandlers(); - } - this.webSocket = new adapters_default.WebSocket(this.consumer.url, protocols); - this.installEventHandlers(); - this.monitor.start(); - return true; - } - } - close({ allowReconnect } = { allowReconnect: true }) { - if (!allowReconnect) { - this.monitor.stop(); - } - if (this.isOpen()) { - return this.webSocket.close(); - } - } - reopen() { - logger_default.log(`Reopening WebSocket, current state is ${this.getState()}`); - if (this.isActive()) { - try { - return this.close(); - } catch (error2) { - logger_default.log("Failed to reopen WebSocket", error2); - } finally { - logger_default.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`); - setTimeout(this.open, this.constructor.reopenDelay); - } - } else { - return this.open(); - } - } - getProtocol() { - if (this.webSocket) { - return this.webSocket.protocol; - } - } - isOpen() { - return this.isState("open"); - } - isActive() { - return this.isState("open", "connecting"); - } - // Private - isProtocolSupported() { - return indexOf.call(supportedProtocols, this.getProtocol()) >= 0; - } - isState(...states) { - return indexOf.call(states, this.getState()) >= 0; - } - getState() { - if (this.webSocket) { - for (let state in adapters_default.WebSocket) { - if (adapters_default.WebSocket[state] === this.webSocket.readyState) { - return state.toLowerCase(); - } - } - } - return null; - } - installEventHandlers() { - for (let eventName in this.events) { - const handler = this.events[eventName].bind(this); - this.webSocket[`on${eventName}`] = handler; - } - } - uninstallEventHandlers() { - for (let eventName in this.events) { - this.webSocket[`on${eventName}`] = function() { - }; - } - } - }; - Connection.reopenDelay = 500; - Connection.prototype.events = { - message(event) { - if (!this.isProtocolSupported()) { - return; - } - const { identifier, message, reason, reconnect, type } = JSON.parse(event.data); - switch (type) { - case message_types.welcome: - this.monitor.recordConnect(); - return this.subscriptions.reload(); - case message_types.disconnect: - logger_default.log(`Disconnecting. Reason: ${reason}`); - return this.close({ allowReconnect: reconnect }); - case message_types.ping: - return this.monitor.recordPing(); - case message_types.confirmation: - this.subscriptions.confirmSubscription(identifier); - return this.subscriptions.notify(identifier, "connected"); - case message_types.rejection: - return this.subscriptions.reject(identifier); - default: - return this.subscriptions.notify(identifier, "received", message); - } - }, - open() { - logger_default.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`); - this.disconnected = false; - if (!this.isProtocolSupported()) { - logger_default.log("Protocol is unsupported. Stopping monitor and disconnecting."); - return this.close({ allowReconnect: false }); - } - }, - close(event) { - logger_default.log("WebSocket onclose event"); - if (this.disconnected) { - return; - } - this.disconnected = true; - this.monitor.recordDisconnect(); - return this.subscriptions.notifyAll("disconnected", { willAttemptReconnect: this.monitor.isRunning() }); - }, - error() { - logger_default.log("WebSocket onerror event"); - } - }; - connection_default = Connection; - } - }); - - // node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/subscription.js - var extend, Subscription; - var init_subscription = __esm({ - "node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/subscription.js"() { - extend = function(object, properties) { - if (properties != null) { - for (let key in properties) { - const value = properties[key]; - object[key] = value; - } - } - return object; - }; - Subscription = class { - constructor(consumer2, params = {}, mixin) { - this.consumer = consumer2; - this.identifier = JSON.stringify(params); - extend(this, mixin); - } - // Perform a channel action with the optional data passed as an attribute - perform(action, data = {}) { - data.action = action; - return this.send(data); - } - send(data) { - return this.consumer.send({ command: "message", identifier: this.identifier, data: JSON.stringify(data) }); - } - unsubscribe() { - return this.consumer.subscriptions.remove(this); - } - }; - } - }); - - // node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/subscription_guarantor.js - var SubscriptionGuarantor, subscription_guarantor_default; - var init_subscription_guarantor = __esm({ - "node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/subscription_guarantor.js"() { - init_logger(); - SubscriptionGuarantor = class { - constructor(subscriptions) { - this.subscriptions = subscriptions; - this.pendingSubscriptions = []; - } - guarantee(subscription) { - if (this.pendingSubscriptions.indexOf(subscription) == -1) { - logger_default.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`); - this.pendingSubscriptions.push(subscription); - } else { - logger_default.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`); - } - this.startGuaranteeing(); - } - forget(subscription) { - logger_default.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`); - this.pendingSubscriptions = this.pendingSubscriptions.filter((s) => s !== subscription); - } - startGuaranteeing() { - this.stopGuaranteeing(); - this.retrySubscribing(); - } - stopGuaranteeing() { - clearTimeout(this.retryTimeout); - } - retrySubscribing() { - this.retryTimeout = setTimeout( - () => { - if (this.subscriptions && typeof this.subscriptions.subscribe === "function") { - this.pendingSubscriptions.map((subscription) => { - logger_default.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`); - this.subscriptions.subscribe(subscription); - }); - } - }, - 500 - ); - } - }; - subscription_guarantor_default = SubscriptionGuarantor; - } - }); - - // node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/subscriptions.js - var Subscriptions; - var init_subscriptions = __esm({ - "node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/subscriptions.js"() { - init_subscription(); - init_subscription_guarantor(); - init_logger(); - Subscriptions = class { - constructor(consumer2) { - this.consumer = consumer2; - this.guarantor = new subscription_guarantor_default(this); - this.subscriptions = []; - } - create(channelName, mixin) { - const channel = channelName; - const params = typeof channel === "object" ? channel : { channel }; - const subscription = new Subscription(this.consumer, params, mixin); - return this.add(subscription); - } - // Private - add(subscription) { - this.subscriptions.push(subscription); - this.consumer.ensureActiveConnection(); - this.notify(subscription, "initialized"); - this.subscribe(subscription); - return subscription; - } - remove(subscription) { - this.forget(subscription); - if (!this.findAll(subscription.identifier).length) { - this.sendCommand(subscription, "unsubscribe"); - } - return subscription; - } - reject(identifier) { - return this.findAll(identifier).map((subscription) => { - this.forget(subscription); - this.notify(subscription, "rejected"); - return subscription; - }); - } - forget(subscription) { - this.guarantor.forget(subscription); - this.subscriptions = this.subscriptions.filter((s) => s !== subscription); - return subscription; - } - findAll(identifier) { - return this.subscriptions.filter((s) => s.identifier === identifier); - } - reload() { - return this.subscriptions.map((subscription) => this.subscribe(subscription)); - } - notifyAll(callbackName, ...args) { - return this.subscriptions.map((subscription) => this.notify(subscription, callbackName, ...args)); - } - notify(subscription, callbackName, ...args) { - let subscriptions; - if (typeof subscription === "string") { - subscriptions = this.findAll(subscription); - } else { - subscriptions = [subscription]; - } - return subscriptions.map((subscription2) => typeof subscription2[callbackName] === "function" ? subscription2[callbackName](...args) : void 0); - } - subscribe(subscription) { - if (this.sendCommand(subscription, "subscribe")) { - this.guarantor.guarantee(subscription); - } - } - confirmSubscription(identifier) { - logger_default.log(`Subscription confirmed ${identifier}`); - this.findAll(identifier).map((subscription) => this.guarantor.forget(subscription)); - } - sendCommand(subscription, command) { - const { identifier } = subscription; - return this.consumer.send({ command, identifier }); - } - }; - } - }); - - // node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/consumer.js - function createWebSocketURL(url) { - if (typeof url === "function") { - url = url(); - } - if (url && !/^wss?:/i.test(url)) { - const a = document.createElement("a"); - a.href = url; - a.href = a.href; - a.protocol = a.protocol.replace("http", "ws"); - return a.href; - } else { - return url; - } - } - var Consumer; - var init_consumer = __esm({ - "node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/consumer.js"() { - init_connection(); - init_subscriptions(); - Consumer = class { - constructor(url) { - this._url = url; - this.subscriptions = new Subscriptions(this); - this.connection = new connection_default(this); - } - get url() { - return createWebSocketURL(this._url); - } - send(data) { - return this.connection.send(data); - } - connect() { - return this.connection.open(); - } - disconnect() { - return this.connection.close({ allowReconnect: false }); - } - ensureActiveConnection() { - if (!this.connection.isActive()) { - return this.connection.open(); - } - } - }; - } - }); - - // node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/index.js - var src_exports = {}; - __export(src_exports, { - Connection: () => connection_default, - ConnectionMonitor: () => connection_monitor_default, - Consumer: () => Consumer, - INTERNAL: () => internal_default, - Subscription: () => Subscription, - SubscriptionGuarantor: () => subscription_guarantor_default, - Subscriptions: () => Subscriptions, - adapters: () => adapters_default, - createConsumer: () => createConsumer, - createWebSocketURL: () => createWebSocketURL, - getConfig: () => getConfig, - logger: () => logger_default - }); - function createConsumer(url = getConfig("url") || internal_default.default_mount_path) { - return new Consumer(url); - } - function getConfig(name) { - const element = document.head.querySelector(`meta[name='action-cable-${name}']`); - if (element) { - return element.getAttribute("content"); - } - } - var init_src = __esm({ - "node_modules/@hotwired/turbo-rails/node_modules/@rails/actioncable/src/index.js"() { - init_connection(); - init_connection_monitor(); - init_consumer(); - init_internal(); - init_subscription(); - init_subscriptions(); - init_subscription_guarantor(); - init_adapters(); - init_logger(); - } - }); - - // node_modules/omgopass/random.browser.js - var require_random_browser = __commonJS({ - "node_modules/omgopass/random.browser.js"(exports, module) { - var crypto = self.crypto || self.msCrypto; - module.exports = (size) => crypto.getRandomValues(new Uint8Array(size)); - } - }); - - // node_modules/omgopass/index.js - var require_omgopass = __commonJS({ - "node_modules/omgopass/index.js"(exports, module) { - var getRandomValues = require_random_browser(); - module.exports = ({ - syllablesCount = 3, - minSyllableLength = 2, - maxSyllableLength = 3, - hasNumbers = true, - titlecased = true, - separators = "", - vowels = "aeiouy", - consonants = "bcdfghjklmnpqrstvwxz" - } = {}) => produce(syllablesCount, (i) => { - let length = minSyllableLength + random(maxSyllableLength - minSyllableLength + 1); - let syllable = produce(length, (index) => { - let alpha = index % 2 ? vowels : consonants; - let char = alpha[random(alpha.length)]; - return titlecased && !index ? char.toUpperCase() : char; - }); - if (hasNumbers) - syllable += random(10); - return i && separators ? separators[random(separators.length)] + syllable : syllable; - }); - var produce = (number, callback) => { - for (var i = 0, result = ""; i < number; i++) - result += callback(i); - return result; - }; - var buffer = []; - var bufferSize = 65535; - var bufferIndex = bufferSize; - var random = (limit) => { - if (bufferIndex >= bufferSize) { - buffer = getRandomValues(bufferSize); - bufferIndex = 0; - } - return buffer[bufferIndex++] % limit; - }; - } - }); - - // node_modules/@hotwired/turbo/dist/turbo.es2017-esm.js - (function() { - if (window.Reflect === void 0 || window.customElements === void 0 || window.customElements.polyfillWrapFlushCallback) { - return; - } - const BuiltInHTMLElement = HTMLElement; - const wrapperForTheName = { - HTMLElement: function HTMLElement2() { - return Reflect.construct(BuiltInHTMLElement, [], this.constructor); - } - }; - window.HTMLElement = wrapperForTheName["HTMLElement"]; - HTMLElement.prototype = BuiltInHTMLElement.prototype; - HTMLElement.prototype.constructor = HTMLElement; - Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement); - })(); - (function(prototype) { - if (typeof prototype.requestSubmit == "function") - return; - prototype.requestSubmit = function(submitter) { - if (submitter) { - validateSubmitter(submitter, this); - submitter.click(); - } else { - submitter = document.createElement("input"); - submitter.type = "submit"; - submitter.hidden = true; - this.appendChild(submitter); - submitter.click(); - this.removeChild(submitter); - } - }; - function validateSubmitter(submitter, form) { - submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'"); - submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button"); - submitter.form == form || raise(DOMException, "The specified element is not owned by this form element", "NotFoundError"); - } - function raise(errorConstructor, message, name) { - throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name); - } - })(HTMLFormElement.prototype); - var submittersByForm = /* @__PURE__ */ new WeakMap(); - function findSubmitterFromClickTarget(target) { - const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null; - const candidate = element ? element.closest("input, button") : null; - return (candidate === null || candidate === void 0 ? void 0 : candidate.type) == "submit" ? candidate : null; - } - function clickCaptured(event) { - const submitter = findSubmitterFromClickTarget(event.target); - if (submitter && submitter.form) { - submittersByForm.set(submitter.form, submitter); - } - } - (function() { - if ("submitter" in Event.prototype) - return; - let prototype = window.Event.prototype; - if ("SubmitEvent" in window && /Apple Computer/.test(navigator.vendor)) { - prototype = window.SubmitEvent.prototype; - } else if ("SubmitEvent" in window) { - return; - } - addEventListener("click", clickCaptured, true); - Object.defineProperty(prototype, "submitter", { - get() { - if (this.type == "submit" && this.target instanceof HTMLFormElement) { - return submittersByForm.get(this.target); - } - } - }); - })(); - var FrameLoadingStyle; - (function(FrameLoadingStyle2) { - FrameLoadingStyle2["eager"] = "eager"; - FrameLoadingStyle2["lazy"] = "lazy"; - })(FrameLoadingStyle || (FrameLoadingStyle = {})); - var FrameElement = class _FrameElement extends HTMLElement { - static get observedAttributes() { - return ["disabled", "complete", "loading", "src"]; - } - constructor() { - super(); - this.loaded = Promise.resolve(); - this.delegate = new _FrameElement.delegateConstructor(this); - } - connectedCallback() { - this.delegate.connect(); - } - disconnectedCallback() { - this.delegate.disconnect(); - } - reload() { - return this.delegate.sourceURLReloaded(); - } - attributeChangedCallback(name) { - if (name == "loading") { - this.delegate.loadingStyleChanged(); - } else if (name == "complete") { - this.delegate.completeChanged(); - } else if (name == "src") { - this.delegate.sourceURLChanged(); - } else { - this.delegate.disabledChanged(); - } - } - get src() { - return this.getAttribute("src"); - } - set src(value) { - if (value) { - this.setAttribute("src", value); - } else { - this.removeAttribute("src"); - } - } - get loading() { - return frameLoadingStyleFromString(this.getAttribute("loading") || ""); - } - set loading(value) { - if (value) { - this.setAttribute("loading", value); - } else { - this.removeAttribute("loading"); - } - } - get disabled() { - return this.hasAttribute("disabled"); - } - set disabled(value) { - if (value) { - this.setAttribute("disabled", ""); - } else { - this.removeAttribute("disabled"); - } - } - get autoscroll() { - return this.hasAttribute("autoscroll"); - } - set autoscroll(value) { - if (value) { - this.setAttribute("autoscroll", ""); - } else { - this.removeAttribute("autoscroll"); - } - } - get complete() { - return !this.delegate.isLoading; - } - get isActive() { - return this.ownerDocument === document && !this.isPreview; - } - get isPreview() { - var _a, _b; - return (_b = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.documentElement) === null || _b === void 0 ? void 0 : _b.hasAttribute("data-turbo-preview"); - } - }; - function frameLoadingStyleFromString(style) { - switch (style.toLowerCase()) { - case "lazy": - return FrameLoadingStyle.lazy; - default: - return FrameLoadingStyle.eager; - } - } - function expandURL(locatable) { - return new URL(locatable.toString(), document.baseURI); - } - function getAnchor(url) { - let anchorMatch; - if (url.hash) { - return url.hash.slice(1); - } else if (anchorMatch = url.href.match(/#(.*)$/)) { - return anchorMatch[1]; - } - } - function getAction(form, submitter) { - const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formaction")) || form.getAttribute("action") || form.action; - return expandURL(action); - } - function getExtension(url) { - return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || ""; - } - function isHTML(url) { - return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/); - } - function isPrefixedBy(baseURL, url) { - const prefix = getPrefix(url); - return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix); - } - function locationIsVisitable(location2, rootLocation) { - return isPrefixedBy(location2, rootLocation) && isHTML(location2); - } - function getRequestURL(url) { - const anchor = getAnchor(url); - return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href; - } - function toCacheKey(url) { - return getRequestURL(url); - } - function urlsAreEqual(left2, right2) { - return expandURL(left2).href == expandURL(right2).href; - } - function getPathComponents(url) { - return url.pathname.split("/").slice(1); - } - function getLastPathComponent(url) { - return getPathComponents(url).slice(-1)[0]; - } - function getPrefix(url) { - return addTrailingSlash(url.origin + url.pathname); - } - function addTrailingSlash(value) { - return value.endsWith("/") ? value : value + "/"; - } - var FetchResponse = class { - constructor(response) { - this.response = response; - } - get succeeded() { - return this.response.ok; - } - get failed() { - return !this.succeeded; - } - get clientError() { - return this.statusCode >= 400 && this.statusCode <= 499; - } - get serverError() { - return this.statusCode >= 500 && this.statusCode <= 599; - } - get redirected() { - return this.response.redirected; - } - get location() { - return expandURL(this.response.url); - } - get isHTML() { - return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/); - } - get statusCode() { - return this.response.status; - } - get contentType() { - return this.header("Content-Type"); - } - get responseText() { - return this.response.clone().text(); - } - get responseHTML() { - if (this.isHTML) { - return this.response.clone().text(); - } else { - return Promise.resolve(void 0); - } - } - header(name) { - return this.response.headers.get(name); - } - }; - function activateScriptElement(element) { - if (element.getAttribute("data-turbo-eval") == "false") { - return element; - } else { - const createdScriptElement = document.createElement("script"); - const cspNonce = getMetaContent("csp-nonce"); - if (cspNonce) { - createdScriptElement.nonce = cspNonce; - } - createdScriptElement.textContent = element.textContent; - createdScriptElement.async = false; - copyElementAttributes(createdScriptElement, element); - return createdScriptElement; - } - } - function copyElementAttributes(destinationElement, sourceElement) { - for (const { name, value } of sourceElement.attributes) { - destinationElement.setAttribute(name, value); - } - } - function createDocumentFragment(html) { - const template = document.createElement("template"); - template.innerHTML = html; - return template.content; - } - function dispatch(eventName, { target, cancelable, detail } = {}) { - const event = new CustomEvent(eventName, { - cancelable, - bubbles: true, - composed: true, - detail - }); - if (target && target.isConnected) { - target.dispatchEvent(event); - } else { - document.documentElement.dispatchEvent(event); - } - return event; - } - function nextAnimationFrame() { - return new Promise((resolve) => requestAnimationFrame(() => resolve())); - } - function nextEventLoopTick() { - return new Promise((resolve) => setTimeout(() => resolve(), 0)); - } - function nextMicrotask() { - return Promise.resolve(); - } - function parseHTMLDocument(html = "") { - return new DOMParser().parseFromString(html, "text/html"); - } - function unindent(strings, ...values) { - const lines = interpolate(strings, values).replace(/^\n/, "").split("\n"); - const match = lines[0].match(/^\s+/); - const indent = match ? match[0].length : 0; - return lines.map((line) => line.slice(indent)).join("\n"); - } - function interpolate(strings, values) { - return strings.reduce((result, string, i) => { - const value = values[i] == void 0 ? "" : values[i]; - return result + string + value; - }, ""); - } - function uuid() { - return Array.from({ length: 36 }).map((_, i) => { - if (i == 8 || i == 13 || i == 18 || i == 23) { - return "-"; - } else if (i == 14) { - return "4"; - } else if (i == 19) { - return (Math.floor(Math.random() * 4) + 8).toString(16); - } else { - return Math.floor(Math.random() * 15).toString(16); - } - }).join(""); - } - function getAttribute(attributeName, ...elements) { - for (const value of elements.map((element) => element === null || element === void 0 ? void 0 : element.getAttribute(attributeName))) { - if (typeof value == "string") - return value; - } - return null; - } - function hasAttribute(attributeName, ...elements) { - return elements.some((element) => element && element.hasAttribute(attributeName)); - } - function markAsBusy(...elements) { - for (const element of elements) { - if (element.localName == "turbo-frame") { - element.setAttribute("busy", ""); - } - element.setAttribute("aria-busy", "true"); - } - } - function clearBusyState(...elements) { - for (const element of elements) { - if (element.localName == "turbo-frame") { - element.removeAttribute("busy"); - } - element.removeAttribute("aria-busy"); - } - } - function waitForLoad(element, timeoutInMilliseconds = 2e3) { - return new Promise((resolve) => { - const onComplete = () => { - element.removeEventListener("error", onComplete); - element.removeEventListener("load", onComplete); - resolve(); - }; - element.addEventListener("load", onComplete, { once: true }); - element.addEventListener("error", onComplete, { once: true }); - setTimeout(resolve, timeoutInMilliseconds); - }); - } - function getHistoryMethodForAction(action) { - switch (action) { - case "replace": - return history.replaceState; - case "advance": - case "restore": - return history.pushState; - } - } - function isAction(action) { - return action == "advance" || action == "replace" || action == "restore"; - } - function getVisitAction(...elements) { - const action = getAttribute("data-turbo-action", ...elements); - return isAction(action) ? action : null; - } - function getMetaElement(name) { - return document.querySelector(`meta[name="${name}"]`); - } - function getMetaContent(name) { - const element = getMetaElement(name); - return element && element.content; - } - function setMetaContent(name, content) { - let element = getMetaElement(name); - if (!element) { - element = document.createElement("meta"); - element.setAttribute("name", name); - document.head.appendChild(element); - } - element.setAttribute("content", content); - return element; - } - function findClosestRecursively(element, selector) { - var _a; - if (element instanceof Element) { - return element.closest(selector) || findClosestRecursively(element.assignedSlot || ((_a = element.getRootNode()) === null || _a === void 0 ? void 0 : _a.host), selector); - } - } - var FetchMethod; - (function(FetchMethod2) { - FetchMethod2[FetchMethod2["get"] = 0] = "get"; - FetchMethod2[FetchMethod2["post"] = 1] = "post"; - FetchMethod2[FetchMethod2["put"] = 2] = "put"; - FetchMethod2[FetchMethod2["patch"] = 3] = "patch"; - FetchMethod2[FetchMethod2["delete"] = 4] = "delete"; - })(FetchMethod || (FetchMethod = {})); - function fetchMethodFromString(method) { - switch (method.toLowerCase()) { - case "get": - return FetchMethod.get; - case "post": - return FetchMethod.post; - case "put": - return FetchMethod.put; - case "patch": - return FetchMethod.patch; - case "delete": - return FetchMethod.delete; - } - } - var FetchRequest = class { - constructor(delegate, method, location2, body = new URLSearchParams(), target = null) { - this.abortController = new AbortController(); - this.resolveRequestPromise = (_value) => { - }; - this.delegate = delegate; - this.method = method; - this.headers = this.defaultHeaders; - this.body = body; - this.url = location2; - this.target = target; - } - get location() { - return this.url; - } - get params() { - return this.url.searchParams; - } - get entries() { - return this.body ? Array.from(this.body.entries()) : []; - } - cancel() { - this.abortController.abort(); - } - async perform() { - const { fetchOptions } = this; - this.delegate.prepareRequest(this); - await this.allowRequestToBeIntercepted(fetchOptions); - try { - this.delegate.requestStarted(this); - const response = await fetch(this.url.href, fetchOptions); - return await this.receive(response); - } catch (error2) { - if (error2.name !== "AbortError") { - if (this.willDelegateErrorHandling(error2)) { - this.delegate.requestErrored(this, error2); - } - throw error2; - } - } finally { - this.delegate.requestFinished(this); - } - } - async receive(response) { - const fetchResponse = new FetchResponse(response); - const event = dispatch("turbo:before-fetch-response", { - cancelable: true, - detail: { fetchResponse }, - target: this.target - }); - if (event.defaultPrevented) { - this.delegate.requestPreventedHandlingResponse(this, fetchResponse); - } else if (fetchResponse.succeeded) { - this.delegate.requestSucceededWithResponse(this, fetchResponse); - } else { - this.delegate.requestFailedWithResponse(this, fetchResponse); - } - return fetchResponse; - } - get fetchOptions() { - var _a; - return { - method: FetchMethod[this.method].toUpperCase(), - credentials: "same-origin", - headers: this.headers, - redirect: "follow", - body: this.isSafe ? null : this.body, - signal: this.abortSignal, - referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href - }; - } - get defaultHeaders() { - return { - Accept: "text/html, application/xhtml+xml" - }; - } - get isSafe() { - return this.method === FetchMethod.get; - } - get abortSignal() { - return this.abortController.signal; - } - acceptResponseType(mimeType) { - this.headers["Accept"] = [mimeType, this.headers["Accept"]].join(", "); - } - async allowRequestToBeIntercepted(fetchOptions) { - const requestInterception = new Promise((resolve) => this.resolveRequestPromise = resolve); - const event = dispatch("turbo:before-fetch-request", { - cancelable: true, - detail: { - fetchOptions, - url: this.url, - resume: this.resolveRequestPromise - }, - target: this.target - }); - if (event.defaultPrevented) - await requestInterception; - } - willDelegateErrorHandling(error2) { - const event = dispatch("turbo:fetch-request-error", { - target: this.target, - cancelable: true, - detail: { request: this, error: error2 } - }); - return !event.defaultPrevented; - } - }; - var AppearanceObserver = class { - constructor(delegate, element) { - this.started = false; - this.intersect = (entries) => { - const lastEntry = entries.slice(-1)[0]; - if (lastEntry === null || lastEntry === void 0 ? void 0 : lastEntry.isIntersecting) { - this.delegate.elementAppearedInViewport(this.element); - } - }; - this.delegate = delegate; - this.element = element; - this.intersectionObserver = new IntersectionObserver(this.intersect); - } - start() { - if (!this.started) { - this.started = true; - this.intersectionObserver.observe(this.element); - } - } - stop() { - if (this.started) { - this.started = false; - this.intersectionObserver.unobserve(this.element); - } - } - }; - var StreamMessage = class { - static wrap(message) { - if (typeof message == "string") { - return new this(createDocumentFragment(message)); - } else { - return message; - } - } - constructor(fragment) { - this.fragment = importStreamElements(fragment); - } - }; - StreamMessage.contentType = "text/vnd.turbo-stream.html"; - function importStreamElements(fragment) { - for (const element of fragment.querySelectorAll("turbo-stream")) { - const streamElement = document.importNode(element, true); - for (const inertScriptElement of streamElement.templateElement.content.querySelectorAll("script")) { - inertScriptElement.replaceWith(activateScriptElement(inertScriptElement)); - } - element.replaceWith(streamElement); - } - return fragment; - } - var FormSubmissionState; - (function(FormSubmissionState2) { - FormSubmissionState2[FormSubmissionState2["initialized"] = 0] = "initialized"; - FormSubmissionState2[FormSubmissionState2["requesting"] = 1] = "requesting"; - FormSubmissionState2[FormSubmissionState2["waiting"] = 2] = "waiting"; - FormSubmissionState2[FormSubmissionState2["receiving"] = 3] = "receiving"; - FormSubmissionState2[FormSubmissionState2["stopping"] = 4] = "stopping"; - FormSubmissionState2[FormSubmissionState2["stopped"] = 5] = "stopped"; - })(FormSubmissionState || (FormSubmissionState = {})); - var FormEnctype; - (function(FormEnctype2) { - FormEnctype2["urlEncoded"] = "application/x-www-form-urlencoded"; - FormEnctype2["multipart"] = "multipart/form-data"; - FormEnctype2["plain"] = "text/plain"; - })(FormEnctype || (FormEnctype = {})); - function formEnctypeFromString(encoding) { - switch (encoding.toLowerCase()) { - case FormEnctype.multipart: - return FormEnctype.multipart; - case FormEnctype.plain: - return FormEnctype.plain; - default: - return FormEnctype.urlEncoded; - } - } - var FormSubmission = class _FormSubmission { - static confirmMethod(message, _element, _submitter) { - return Promise.resolve(confirm(message)); - } - constructor(delegate, formElement, submitter, mustRedirect = false) { - this.state = FormSubmissionState.initialized; - this.delegate = delegate; - this.formElement = formElement; - this.submitter = submitter; - this.formData = buildFormData(formElement, submitter); - this.location = expandURL(this.action); - if (this.method == FetchMethod.get) { - mergeFormDataEntries(this.location, [...this.body.entries()]); - } - this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement); - this.mustRedirect = mustRedirect; - } - get method() { - var _a; - const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || ""; - return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get; - } - get action() { - var _a; - const formElementAction = typeof this.formElement.action === "string" ? this.formElement.action : null; - if ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.hasAttribute("formaction")) { - return this.submitter.getAttribute("formaction") || ""; - } else { - return this.formElement.getAttribute("action") || formElementAction || ""; - } - } - get body() { - if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) { - return new URLSearchParams(this.stringFormData); - } else { - return this.formData; - } - } - get enctype() { - var _a; - return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype); - } - get isSafe() { - return this.fetchRequest.isSafe; - } - get stringFormData() { - return [...this.formData].reduce((entries, [name, value]) => { - return entries.concat(typeof value == "string" ? [[name, value]] : []); - }, []); - } - async start() { - const { initialized, requesting } = FormSubmissionState; - const confirmationMessage = getAttribute("data-turbo-confirm", this.submitter, this.formElement); - if (typeof confirmationMessage === "string") { - const answer = await _FormSubmission.confirmMethod(confirmationMessage, this.formElement, this.submitter); - if (!answer) { - return; - } - } - if (this.state == initialized) { - this.state = requesting; - return this.fetchRequest.perform(); - } - } - stop() { - const { stopping, stopped } = FormSubmissionState; - if (this.state != stopping && this.state != stopped) { - this.state = stopping; - this.fetchRequest.cancel(); - return true; - } - } - prepareRequest(request) { - if (!request.isSafe) { - const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token"); - if (token) { - request.headers["X-CSRF-Token"] = token; - } - } - if (this.requestAcceptsTurboStreamResponse(request)) { - request.acceptResponseType(StreamMessage.contentType); - } - } - requestStarted(_request) { - var _a; - this.state = FormSubmissionState.waiting; - (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", ""); - this.setSubmitsWith(); - dispatch("turbo:submit-start", { - target: this.formElement, - detail: { formSubmission: this } - }); - this.delegate.formSubmissionStarted(this); - } - requestPreventedHandlingResponse(request, response) { - this.result = { success: response.succeeded, fetchResponse: response }; - } - requestSucceededWithResponse(request, response) { - if (response.clientError || response.serverError) { - this.delegate.formSubmissionFailedWithResponse(this, response); - } else if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) { - const error2 = new Error("Form responses must redirect to another location"); - this.delegate.formSubmissionErrored(this, error2); - } else { - this.state = FormSubmissionState.receiving; - this.result = { success: true, fetchResponse: response }; - this.delegate.formSubmissionSucceededWithResponse(this, response); - } - } - requestFailedWithResponse(request, response) { - this.result = { success: false, fetchResponse: response }; - this.delegate.formSubmissionFailedWithResponse(this, response); - } - requestErrored(request, error2) { - this.result = { success: false, error: error2 }; - this.delegate.formSubmissionErrored(this, error2); - } - requestFinished(_request) { - var _a; - this.state = FormSubmissionState.stopped; - (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled"); - this.resetSubmitterText(); - dispatch("turbo:submit-end", { - target: this.formElement, - detail: Object.assign({ formSubmission: this }, this.result) - }); - this.delegate.formSubmissionFinished(this); - } - setSubmitsWith() { - if (!this.submitter || !this.submitsWith) - return; - if (this.submitter.matches("button")) { - this.originalSubmitText = this.submitter.innerHTML; - this.submitter.innerHTML = this.submitsWith; - } else if (this.submitter.matches("input")) { - const input = this.submitter; - this.originalSubmitText = input.value; - input.value = this.submitsWith; - } - } - resetSubmitterText() { - if (!this.submitter || !this.originalSubmitText) - return; - if (this.submitter.matches("button")) { - this.submitter.innerHTML = this.originalSubmitText; - } else if (this.submitter.matches("input")) { - const input = this.submitter; - input.value = this.originalSubmitText; - } - } - requestMustRedirect(request) { - return !request.isSafe && this.mustRedirect; - } - requestAcceptsTurboStreamResponse(request) { - return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement); - } - get submitsWith() { - var _a; - return (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("data-turbo-submits-with"); - } - }; - function buildFormData(formElement, submitter) { - const formData = new FormData(formElement); - const name = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("name"); - const value = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("value"); - if (name) { - formData.append(name, value || ""); - } - return formData; - } - function getCookieValue(cookieName) { - if (cookieName != null) { - const cookies = document.cookie ? document.cookie.split("; ") : []; - const cookie = cookies.find((cookie2) => cookie2.startsWith(cookieName)); - if (cookie) { - const value = cookie.split("=").slice(1).join("="); - return value ? decodeURIComponent(value) : void 0; - } - } - } - function responseSucceededWithoutRedirect(response) { - return response.statusCode == 200 && !response.redirected; - } - function mergeFormDataEntries(url, entries) { - const searchParams = new URLSearchParams(); - for (const [name, value] of entries) { - if (value instanceof File) - continue; - searchParams.append(name, value); - } - url.search = searchParams.toString(); - return url; - } - var Snapshot = class { - constructor(element) { - this.element = element; - } - get activeElement() { - return this.element.ownerDocument.activeElement; - } - get children() { - return [...this.element.children]; - } - hasAnchor(anchor) { - return this.getElementForAnchor(anchor) != null; - } - getElementForAnchor(anchor) { - return anchor ? this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`) : null; - } - get isConnected() { - return this.element.isConnected; - } - get firstAutofocusableElement() { - const inertDisabledOrHidden = "[inert], :disabled, [hidden], details:not([open]), dialog:not([open])"; - for (const element of this.element.querySelectorAll("[autofocus]")) { - if (element.closest(inertDisabledOrHidden) == null) - return element; - else - continue; - } - return null; - } - get permanentElements() { - return queryPermanentElementsAll(this.element); - } - getPermanentElementById(id2) { - return getPermanentElementById(this.element, id2); - } - getPermanentElementMapForSnapshot(snapshot) { - const permanentElementMap = {}; - for (const currentPermanentElement of this.permanentElements) { - const { id: id2 } = currentPermanentElement; - const newPermanentElement = snapshot.getPermanentElementById(id2); - if (newPermanentElement) { - permanentElementMap[id2] = [currentPermanentElement, newPermanentElement]; - } - } - return permanentElementMap; - } - }; - function getPermanentElementById(node, id2) { - return node.querySelector(`#${id2}[data-turbo-permanent]`); - } - function queryPermanentElementsAll(node) { - return node.querySelectorAll("[id][data-turbo-permanent]"); - } - var FormSubmitObserver = class { - constructor(delegate, eventTarget) { - this.started = false; - this.submitCaptured = () => { - this.eventTarget.removeEventListener("submit", this.submitBubbled, false); - this.eventTarget.addEventListener("submit", this.submitBubbled, false); - }; - this.submitBubbled = (event) => { - if (!event.defaultPrevented) { - const form = event.target instanceof HTMLFormElement ? event.target : void 0; - const submitter = event.submitter || void 0; - if (form && submissionDoesNotDismissDialog(form, submitter) && submissionDoesNotTargetIFrame(form, submitter) && this.delegate.willSubmitForm(form, submitter)) { - event.preventDefault(); - event.stopImmediatePropagation(); - this.delegate.formSubmitted(form, submitter); - } - } - }; - this.delegate = delegate; - this.eventTarget = eventTarget; - } - start() { - if (!this.started) { - this.eventTarget.addEventListener("submit", this.submitCaptured, true); - this.started = true; - } - } - stop() { - if (this.started) { - this.eventTarget.removeEventListener("submit", this.submitCaptured, true); - this.started = false; - } - } - }; - function submissionDoesNotDismissDialog(form, submitter) { - const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method"); - return method != "dialog"; - } - function submissionDoesNotTargetIFrame(form, submitter) { - if ((submitter === null || submitter === void 0 ? void 0 : submitter.hasAttribute("formtarget")) || form.hasAttribute("target")) { - const target = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formtarget")) || form.target; - for (const element of document.getElementsByName(target)) { - if (element instanceof HTMLIFrameElement) - return false; - } - return true; - } else { - return true; - } - } - var View = class { - constructor(delegate, element) { - this.resolveRenderPromise = (_value) => { - }; - this.resolveInterceptionPromise = (_value) => { - }; - this.delegate = delegate; - this.element = element; - } - scrollToAnchor(anchor) { - const element = this.snapshot.getElementForAnchor(anchor); - if (element) { - this.scrollToElement(element); - this.focusElement(element); - } else { - this.scrollToPosition({ x: 0, y: 0 }); - } - } - scrollToAnchorFromLocation(location2) { - this.scrollToAnchor(getAnchor(location2)); - } - scrollToElement(element) { - element.scrollIntoView(); - } - focusElement(element) { - if (element instanceof HTMLElement) { - if (element.hasAttribute("tabindex")) { - element.focus(); - } else { - element.setAttribute("tabindex", "-1"); - element.focus(); - element.removeAttribute("tabindex"); - } - } - } - scrollToPosition({ x, y }) { - this.scrollRoot.scrollTo(x, y); - } - scrollToTop() { - this.scrollToPosition({ x: 0, y: 0 }); - } - get scrollRoot() { - return window; - } - async render(renderer) { - const { isPreview, shouldRender, newSnapshot: snapshot } = renderer; - if (shouldRender) { - try { - this.renderPromise = new Promise((resolve) => this.resolveRenderPromise = resolve); - this.renderer = renderer; - await this.prepareToRenderSnapshot(renderer); - const renderInterception = new Promise((resolve) => this.resolveInterceptionPromise = resolve); - const options = { resume: this.resolveInterceptionPromise, render: this.renderer.renderElement }; - const immediateRender = this.delegate.allowsImmediateRender(snapshot, options); - if (!immediateRender) - await renderInterception; - await this.renderSnapshot(renderer); - this.delegate.viewRenderedSnapshot(snapshot, isPreview); - this.delegate.preloadOnLoadLinksForView(this.element); - this.finishRenderingSnapshot(renderer); - } finally { - delete this.renderer; - this.resolveRenderPromise(void 0); - delete this.renderPromise; - } - } else { - this.invalidate(renderer.reloadReason); - } - } - invalidate(reason) { - this.delegate.viewInvalidated(reason); - } - async prepareToRenderSnapshot(renderer) { - this.markAsPreview(renderer.isPreview); - await renderer.prepareToRender(); - } - markAsPreview(isPreview) { - if (isPreview) { - this.element.setAttribute("data-turbo-preview", ""); - } else { - this.element.removeAttribute("data-turbo-preview"); - } - } - async renderSnapshot(renderer) { - await renderer.render(); - } - finishRenderingSnapshot(renderer) { - renderer.finishRendering(); - } - }; - var FrameView = class extends View { - missing() { - this.element.innerHTML = `Content missing`; - } - get snapshot() { - return new Snapshot(this.element); - } - }; - var LinkInterceptor = class { - constructor(delegate, element) { - this.clickBubbled = (event) => { - if (this.respondsToEventTarget(event.target)) { - this.clickEvent = event; - } else { - delete this.clickEvent; - } - }; - this.linkClicked = (event) => { - if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) { - if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) { - this.clickEvent.preventDefault(); - event.preventDefault(); - this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent); - } - } - delete this.clickEvent; - }; - this.willVisit = (_event) => { - delete this.clickEvent; - }; - this.delegate = delegate; - this.element = element; - } - start() { - this.element.addEventListener("click", this.clickBubbled); - document.addEventListener("turbo:click", this.linkClicked); - document.addEventListener("turbo:before-visit", this.willVisit); - } - stop() { - this.element.removeEventListener("click", this.clickBubbled); - document.removeEventListener("turbo:click", this.linkClicked); - document.removeEventListener("turbo:before-visit", this.willVisit); - } - respondsToEventTarget(target) { - const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null; - return element && element.closest("turbo-frame, html") == this.element; - } - }; - var LinkClickObserver = class { - constructor(delegate, eventTarget) { - this.started = false; - this.clickCaptured = () => { - this.eventTarget.removeEventListener("click", this.clickBubbled, false); - this.eventTarget.addEventListener("click", this.clickBubbled, false); - }; - this.clickBubbled = (event) => { - if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) { - const target = event.composedPath && event.composedPath()[0] || event.target; - const link = this.findLinkFromClickTarget(target); - if (link && doesNotTargetIFrame(link)) { - const location2 = this.getLocationForLink(link); - if (this.delegate.willFollowLinkToLocation(link, location2, event)) { - event.preventDefault(); - this.delegate.followedLinkToLocation(link, location2); - } - } - } - }; - this.delegate = delegate; - this.eventTarget = eventTarget; - } - start() { - if (!this.started) { - this.eventTarget.addEventListener("click", this.clickCaptured, true); - this.started = true; - } - } - stop() { - if (this.started) { - this.eventTarget.removeEventListener("click", this.clickCaptured, true); - this.started = false; - } - } - clickEventIsSignificant(event) { - return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey); - } - findLinkFromClickTarget(target) { - return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])"); - } - getLocationForLink(link) { - return expandURL(link.getAttribute("href") || ""); - } - }; - function doesNotTargetIFrame(anchor) { - if (anchor.hasAttribute("target")) { - for (const element of document.getElementsByName(anchor.target)) { - if (element instanceof HTMLIFrameElement) - return false; - } - return true; - } else { - return true; - } - } - var FormLinkClickObserver = class { - constructor(delegate, element) { - this.delegate = delegate; - this.linkInterceptor = new LinkClickObserver(this, element); - } - start() { - this.linkInterceptor.start(); - } - stop() { - this.linkInterceptor.stop(); - } - willFollowLinkToLocation(link, location2, originalEvent) { - return this.delegate.willSubmitFormLinkToLocation(link, location2, originalEvent) && link.hasAttribute("data-turbo-method"); - } - followedLinkToLocation(link, location2) { - const form = document.createElement("form"); - const type = "hidden"; - for (const [name, value] of location2.searchParams) { - form.append(Object.assign(document.createElement("input"), { type, name, value })); - } - const action = Object.assign(location2, { search: "" }); - form.setAttribute("data-turbo", "true"); - form.setAttribute("action", action.href); - form.setAttribute("hidden", ""); - const method = link.getAttribute("data-turbo-method"); - if (method) - form.setAttribute("method", method); - const turboFrame = link.getAttribute("data-turbo-frame"); - if (turboFrame) - form.setAttribute("data-turbo-frame", turboFrame); - const turboAction = getVisitAction(link); - if (turboAction) - form.setAttribute("data-turbo-action", turboAction); - const turboConfirm = link.getAttribute("data-turbo-confirm"); - if (turboConfirm) - form.setAttribute("data-turbo-confirm", turboConfirm); - const turboStream = link.hasAttribute("data-turbo-stream"); - if (turboStream) - form.setAttribute("data-turbo-stream", ""); - this.delegate.submittedFormLinkToLocation(link, location2, form); - document.body.appendChild(form); - form.addEventListener("turbo:submit-end", () => form.remove(), { once: true }); - requestAnimationFrame(() => form.requestSubmit()); - } - }; - var Bardo = class { - static async preservingPermanentElements(delegate, permanentElementMap, callback) { - const bardo = new this(delegate, permanentElementMap); - bardo.enter(); - await callback(); - bardo.leave(); - } - constructor(delegate, permanentElementMap) { - this.delegate = delegate; - this.permanentElementMap = permanentElementMap; - } - enter() { - for (const id2 in this.permanentElementMap) { - const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id2]; - this.delegate.enteringBardo(currentPermanentElement, newPermanentElement); - this.replaceNewPermanentElementWithPlaceholder(newPermanentElement); - } - } - leave() { - for (const id2 in this.permanentElementMap) { - const [currentPermanentElement] = this.permanentElementMap[id2]; - this.replaceCurrentPermanentElementWithClone(currentPermanentElement); - this.replacePlaceholderWithPermanentElement(currentPermanentElement); - this.delegate.leavingBardo(currentPermanentElement); - } - } - replaceNewPermanentElementWithPlaceholder(permanentElement) { - const placeholder = createPlaceholderForPermanentElement(permanentElement); - permanentElement.replaceWith(placeholder); - } - replaceCurrentPermanentElementWithClone(permanentElement) { - const clone = permanentElement.cloneNode(true); - permanentElement.replaceWith(clone); - } - replacePlaceholderWithPermanentElement(permanentElement) { - const placeholder = this.getPlaceholderById(permanentElement.id); - placeholder === null || placeholder === void 0 ? void 0 : placeholder.replaceWith(permanentElement); - } - getPlaceholderById(id2) { - return this.placeholders.find((element) => element.content == id2); - } - get placeholders() { - return [...document.querySelectorAll("meta[name=turbo-permanent-placeholder][content]")]; - } - }; - function createPlaceholderForPermanentElement(permanentElement) { - const element = document.createElement("meta"); - element.setAttribute("name", "turbo-permanent-placeholder"); - element.setAttribute("content", permanentElement.id); - return element; - } - var Renderer = class { - constructor(currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) { - this.activeElement = null; - this.currentSnapshot = currentSnapshot; - this.newSnapshot = newSnapshot; - this.isPreview = isPreview; - this.willRender = willRender; - this.renderElement = renderElement; - this.promise = new Promise((resolve, reject) => this.resolvingFunctions = { resolve, reject }); - } - get shouldRender() { - return true; - } - get reloadReason() { - return; - } - prepareToRender() { - return; - } - finishRendering() { - if (this.resolvingFunctions) { - this.resolvingFunctions.resolve(); - delete this.resolvingFunctions; - } - } - async preservingPermanentElements(callback) { - await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback); - } - focusFirstAutofocusableElement() { - const element = this.connectedSnapshot.firstAutofocusableElement; - if (elementIsFocusable(element)) { - element.focus(); - } - } - enteringBardo(currentPermanentElement) { - if (this.activeElement) - return; - if (currentPermanentElement.contains(this.currentSnapshot.activeElement)) { - this.activeElement = this.currentSnapshot.activeElement; - } - } - leavingBardo(currentPermanentElement) { - if (currentPermanentElement.contains(this.activeElement) && this.activeElement instanceof HTMLElement) { - this.activeElement.focus(); - this.activeElement = null; - } - } - get connectedSnapshot() { - return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot; - } - get currentElement() { - return this.currentSnapshot.element; - } - get newElement() { - return this.newSnapshot.element; - } - get permanentElementMap() { - return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot); - } - }; - function elementIsFocusable(element) { - return element && typeof element.focus == "function"; - } - var FrameRenderer = class extends Renderer { - static renderElement(currentElement, newElement) { - var _a; - const destinationRange = document.createRange(); - destinationRange.selectNodeContents(currentElement); - destinationRange.deleteContents(); - const frameElement = newElement; - const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange(); - if (sourceRange) { - sourceRange.selectNodeContents(frameElement); - currentElement.appendChild(sourceRange.extractContents()); - } - } - constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) { - super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender); - this.delegate = delegate; - } - get shouldRender() { - return true; - } - async render() { - await nextAnimationFrame(); - this.preservingPermanentElements(() => { - this.loadFrameElement(); - }); - this.scrollFrameIntoView(); - await nextAnimationFrame(); - this.focusFirstAutofocusableElement(); - await nextAnimationFrame(); - this.activateScriptElements(); - } - loadFrameElement() { - this.delegate.willRenderFrame(this.currentElement, this.newElement); - this.renderElement(this.currentElement, this.newElement); - } - scrollFrameIntoView() { - if (this.currentElement.autoscroll || this.newElement.autoscroll) { - const element = this.currentElement.firstElementChild; - const block = readScrollLogicalPosition(this.currentElement.getAttribute("data-autoscroll-block"), "end"); - const behavior = readScrollBehavior(this.currentElement.getAttribute("data-autoscroll-behavior"), "auto"); - if (element) { - element.scrollIntoView({ block, behavior }); - return true; - } - } - return false; - } - activateScriptElements() { - for (const inertScriptElement of this.newScriptElements) { - const activatedScriptElement = activateScriptElement(inertScriptElement); - inertScriptElement.replaceWith(activatedScriptElement); - } - } - get newScriptElements() { - return this.currentElement.querySelectorAll("script"); - } - }; - function readScrollLogicalPosition(value, defaultValue) { - if (value == "end" || value == "start" || value == "center" || value == "nearest") { - return value; - } else { - return defaultValue; - } - } - function readScrollBehavior(value, defaultValue) { - if (value == "auto" || value == "smooth") { - return value; - } else { - return defaultValue; - } - } - var ProgressBar = class _ProgressBar { - static get defaultCSS() { - return unindent` - .turbo-progress-bar { - position: fixed; - display: block; - top: 0; - left: 0; - height: 3px; - background: #0076ff; - z-index: 2147483647; - transition: - width ${_ProgressBar.animationDuration}ms ease-out, - opacity ${_ProgressBar.animationDuration / 2}ms ${_ProgressBar.animationDuration / 2}ms ease-in; - transform: translate3d(0, 0, 0); - } - `; - } - constructor() { - this.hiding = false; - this.value = 0; - this.visible = false; - this.trickle = () => { - this.setValue(this.value + Math.random() / 100); - }; - this.stylesheetElement = this.createStylesheetElement(); - this.progressElement = this.createProgressElement(); - this.installStylesheetElement(); - this.setValue(0); - } - show() { - if (!this.visible) { - this.visible = true; - this.installProgressElement(); - this.startTrickling(); - } - } - hide() { - if (this.visible && !this.hiding) { - this.hiding = true; - this.fadeProgressElement(() => { - this.uninstallProgressElement(); - this.stopTrickling(); - this.visible = false; - this.hiding = false; - }); - } - } - setValue(value) { - this.value = value; - this.refresh(); - } - installStylesheetElement() { - document.head.insertBefore(this.stylesheetElement, document.head.firstChild); - } - installProgressElement() { - this.progressElement.style.width = "0"; - this.progressElement.style.opacity = "1"; - document.documentElement.insertBefore(this.progressElement, document.body); - this.refresh(); - } - fadeProgressElement(callback) { - this.progressElement.style.opacity = "0"; - setTimeout(callback, _ProgressBar.animationDuration * 1.5); - } - uninstallProgressElement() { - if (this.progressElement.parentNode) { - document.documentElement.removeChild(this.progressElement); - } - } - startTrickling() { - if (!this.trickleInterval) { - this.trickleInterval = window.setInterval(this.trickle, _ProgressBar.animationDuration); - } - } - stopTrickling() { - window.clearInterval(this.trickleInterval); - delete this.trickleInterval; - } - refresh() { - requestAnimationFrame(() => { - this.progressElement.style.width = `${10 + this.value * 90}%`; - }); - } - createStylesheetElement() { - const element = document.createElement("style"); - element.type = "text/css"; - element.textContent = _ProgressBar.defaultCSS; - if (this.cspNonce) { - element.nonce = this.cspNonce; - } - return element; - } - createProgressElement() { - const element = document.createElement("div"); - element.className = "turbo-progress-bar"; - return element; - } - get cspNonce() { - return getMetaContent("csp-nonce"); - } - }; - ProgressBar.animationDuration = 300; - var HeadSnapshot = class extends Snapshot { - constructor() { - super(...arguments); - this.detailsByOuterHTML = this.children.filter((element) => !elementIsNoscript(element)).map((element) => elementWithoutNonce(element)).reduce((result, element) => { - const { outerHTML } = element; - const details = outerHTML in result ? result[outerHTML] : { - type: elementType(element), - tracked: elementIsTracked(element), - elements: [] - }; - return Object.assign(Object.assign({}, result), { [outerHTML]: Object.assign(Object.assign({}, details), { elements: [...details.elements, element] }) }); - }, {}); - } - get trackedElementSignature() { - return Object.keys(this.detailsByOuterHTML).filter((outerHTML) => this.detailsByOuterHTML[outerHTML].tracked).join(""); - } - getScriptElementsNotInSnapshot(snapshot) { - return this.getElementsMatchingTypeNotInSnapshot("script", snapshot); - } - getStylesheetElementsNotInSnapshot(snapshot) { - return this.getElementsMatchingTypeNotInSnapshot("stylesheet", snapshot); - } - getElementsMatchingTypeNotInSnapshot(matchedType, snapshot) { - return Object.keys(this.detailsByOuterHTML).filter((outerHTML) => !(outerHTML in snapshot.detailsByOuterHTML)).map((outerHTML) => this.detailsByOuterHTML[outerHTML]).filter(({ type }) => type == matchedType).map(({ elements: [element] }) => element); - } - get provisionalElements() { - return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => { - const { type, tracked, elements } = this.detailsByOuterHTML[outerHTML]; - if (type == null && !tracked) { - return [...result, ...elements]; - } else if (elements.length > 1) { - return [...result, ...elements.slice(1)]; - } else { - return result; - } - }, []); - } - getMetaValue(name) { - const element = this.findMetaElementByName(name); - return element ? element.getAttribute("content") : null; - } - findMetaElementByName(name) { - return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => { - const { elements: [element] } = this.detailsByOuterHTML[outerHTML]; - return elementIsMetaElementWithName(element, name) ? element : result; - }, void 0); - } - }; - function elementType(element) { - if (elementIsScript(element)) { - return "script"; - } else if (elementIsStylesheet(element)) { - return "stylesheet"; - } - } - function elementIsTracked(element) { - return element.getAttribute("data-turbo-track") == "reload"; - } - function elementIsScript(element) { - const tagName = element.localName; - return tagName == "script"; - } - function elementIsNoscript(element) { - const tagName = element.localName; - return tagName == "noscript"; - } - function elementIsStylesheet(element) { - const tagName = element.localName; - return tagName == "style" || tagName == "link" && element.getAttribute("rel") == "stylesheet"; - } - function elementIsMetaElementWithName(element, name) { - const tagName = element.localName; - return tagName == "meta" && element.getAttribute("name") == name; - } - function elementWithoutNonce(element) { - if (element.hasAttribute("nonce")) { - element.setAttribute("nonce", ""); - } - return element; - } - var PageSnapshot = class _PageSnapshot extends Snapshot { - static fromHTMLString(html = "") { - return this.fromDocument(parseHTMLDocument(html)); - } - static fromElement(element) { - return this.fromDocument(element.ownerDocument); - } - static fromDocument({ head, body }) { - return new this(body, new HeadSnapshot(head)); - } - constructor(element, headSnapshot) { - super(element); - this.headSnapshot = headSnapshot; - } - clone() { - const clonedElement = this.element.cloneNode(true); - const selectElements = this.element.querySelectorAll("select"); - const clonedSelectElements = clonedElement.querySelectorAll("select"); - for (const [index, source] of selectElements.entries()) { - const clone = clonedSelectElements[index]; - for (const option of clone.selectedOptions) - option.selected = false; - for (const option of source.selectedOptions) - clone.options[option.index].selected = true; - } - for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) { - clonedPasswordInput.value = ""; - } - return new _PageSnapshot(clonedElement, this.headSnapshot); - } - get headElement() { - return this.headSnapshot.element; - } - get rootLocation() { - var _a; - const root = (_a = this.getSetting("root")) !== null && _a !== void 0 ? _a : "/"; - return expandURL(root); - } - get cacheControlValue() { - return this.getSetting("cache-control"); - } - get isPreviewable() { - return this.cacheControlValue != "no-preview"; - } - get isCacheable() { - return this.cacheControlValue != "no-cache"; - } - get isVisitable() { - return this.getSetting("visit-control") != "reload"; - } - getSetting(name) { - return this.headSnapshot.getMetaValue(`turbo-${name}`); - } - }; - var TimingMetric; - (function(TimingMetric2) { - TimingMetric2["visitStart"] = "visitStart"; - TimingMetric2["requestStart"] = "requestStart"; - TimingMetric2["requestEnd"] = "requestEnd"; - TimingMetric2["visitEnd"] = "visitEnd"; - })(TimingMetric || (TimingMetric = {})); - var VisitState; - (function(VisitState2) { - VisitState2["initialized"] = "initialized"; - VisitState2["started"] = "started"; - VisitState2["canceled"] = "canceled"; - VisitState2["failed"] = "failed"; - VisitState2["completed"] = "completed"; - })(VisitState || (VisitState = {})); - var defaultOptions = { - action: "advance", - historyChanged: false, - visitCachedSnapshot: () => { - }, - willRender: true, - updateHistory: true, - shouldCacheSnapshot: true, - acceptsStreamResponse: false - }; - var SystemStatusCode; - (function(SystemStatusCode2) { - SystemStatusCode2[SystemStatusCode2["networkFailure"] = 0] = "networkFailure"; - SystemStatusCode2[SystemStatusCode2["timeoutFailure"] = -1] = "timeoutFailure"; - SystemStatusCode2[SystemStatusCode2["contentTypeMismatch"] = -2] = "contentTypeMismatch"; - })(SystemStatusCode || (SystemStatusCode = {})); - var Visit = class { - constructor(delegate, location2, restorationIdentifier, options = {}) { - this.identifier = uuid(); - this.timingMetrics = {}; - this.followedRedirect = false; - this.historyChanged = false; - this.scrolled = false; - this.shouldCacheSnapshot = true; - this.acceptsStreamResponse = false; - this.snapshotCached = false; - this.state = VisitState.initialized; - this.delegate = delegate; - this.location = location2; - this.restorationIdentifier = restorationIdentifier || uuid(); - const { action, historyChanged, referrer, snapshot, snapshotHTML, response, visitCachedSnapshot, willRender, updateHistory, shouldCacheSnapshot, acceptsStreamResponse } = Object.assign(Object.assign({}, defaultOptions), options); - this.action = action; - this.historyChanged = historyChanged; - this.referrer = referrer; - this.snapshot = snapshot; - this.snapshotHTML = snapshotHTML; - this.response = response; - this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action); - this.visitCachedSnapshot = visitCachedSnapshot; - this.willRender = willRender; - this.updateHistory = updateHistory; - this.scrolled = !willRender; - this.shouldCacheSnapshot = shouldCacheSnapshot; - this.acceptsStreamResponse = acceptsStreamResponse; - } - get adapter() { - return this.delegate.adapter; - } - get view() { - return this.delegate.view; - } - get history() { - return this.delegate.history; - } - get restorationData() { - return this.history.getRestorationDataForIdentifier(this.restorationIdentifier); - } - get silent() { - return this.isSamePage; - } - start() { - if (this.state == VisitState.initialized) { - this.recordTimingMetric(TimingMetric.visitStart); - this.state = VisitState.started; - this.adapter.visitStarted(this); - this.delegate.visitStarted(this); - } - } - cancel() { - if (this.state == VisitState.started) { - if (this.request) { - this.request.cancel(); - } - this.cancelRender(); - this.state = VisitState.canceled; - } - } - complete() { - if (this.state == VisitState.started) { - this.recordTimingMetric(TimingMetric.visitEnd); - this.state = VisitState.completed; - this.followRedirect(); - if (!this.followedRedirect) { - this.adapter.visitCompleted(this); - this.delegate.visitCompleted(this); - } - } - } - fail() { - if (this.state == VisitState.started) { - this.state = VisitState.failed; - this.adapter.visitFailed(this); - } - } - changeHistory() { - var _a; - if (!this.historyChanged && this.updateHistory) { - const actionForHistory = this.location.href === ((_a = this.referrer) === null || _a === void 0 ? void 0 : _a.href) ? "replace" : this.action; - const method = getHistoryMethodForAction(actionForHistory); - this.history.update(method, this.location, this.restorationIdentifier); - this.historyChanged = true; - } - } - issueRequest() { - if (this.hasPreloadedResponse()) { - this.simulateRequest(); - } else if (this.shouldIssueRequest() && !this.request) { - this.request = new FetchRequest(this, FetchMethod.get, this.location); - this.request.perform(); - } - } - simulateRequest() { - if (this.response) { - this.startRequest(); - this.recordResponse(); - this.finishRequest(); - } - } - startRequest() { - this.recordTimingMetric(TimingMetric.requestStart); - this.adapter.visitRequestStarted(this); - } - recordResponse(response = this.response) { - this.response = response; - if (response) { - const { statusCode } = response; - if (isSuccessful(statusCode)) { - this.adapter.visitRequestCompleted(this); - } else { - this.adapter.visitRequestFailedWithStatusCode(this, statusCode); - } - } - } - finishRequest() { - this.recordTimingMetric(TimingMetric.requestEnd); - this.adapter.visitRequestFinished(this); - } - loadResponse() { - if (this.response) { - const { statusCode, responseHTML } = this.response; - this.render(async () => { - if (this.shouldCacheSnapshot) - this.cacheSnapshot(); - if (this.view.renderPromise) - await this.view.renderPromise; - if (isSuccessful(statusCode) && responseHTML != null) { - await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender, this); - this.performScroll(); - this.adapter.visitRendered(this); - this.complete(); - } else { - await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML), this); - this.adapter.visitRendered(this); - this.fail(); - } - }); - } - } - getCachedSnapshot() { - const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot(); - if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) { - if (this.action == "restore" || snapshot.isPreviewable) { - return snapshot; - } - } - } - getPreloadedSnapshot() { - if (this.snapshotHTML) { - return PageSnapshot.fromHTMLString(this.snapshotHTML); - } - } - hasCachedSnapshot() { - return this.getCachedSnapshot() != null; - } - loadCachedSnapshot() { - const snapshot = this.getCachedSnapshot(); - if (snapshot) { - const isPreview = this.shouldIssueRequest(); - this.render(async () => { - this.cacheSnapshot(); - if (this.isSamePage) { - this.adapter.visitRendered(this); - } else { - if (this.view.renderPromise) - await this.view.renderPromise; - await this.view.renderPage(snapshot, isPreview, this.willRender, this); - this.performScroll(); - this.adapter.visitRendered(this); - if (!isPreview) { - this.complete(); - } - } - }); - } - } - followRedirect() { - var _a; - if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) { - this.adapter.visitProposedToLocation(this.redirectedToLocation, { - action: "replace", - response: this.response, - shouldCacheSnapshot: false, - willRender: false - }); - this.followedRedirect = true; - } - } - goToSamePageAnchor() { - if (this.isSamePage) { - this.render(async () => { - this.cacheSnapshot(); - this.performScroll(); - this.changeHistory(); - this.adapter.visitRendered(this); - }); - } - } - prepareRequest(request) { - if (this.acceptsStreamResponse) { - request.acceptResponseType(StreamMessage.contentType); - } - } - requestStarted() { - this.startRequest(); - } - requestPreventedHandlingResponse(_request, _response) { - } - async requestSucceededWithResponse(request, response) { - const responseHTML = await response.responseHTML; - const { redirected, statusCode } = response; - if (responseHTML == void 0) { - this.recordResponse({ - statusCode: SystemStatusCode.contentTypeMismatch, - redirected - }); - } else { - this.redirectedToLocation = response.redirected ? response.location : void 0; - this.recordResponse({ statusCode, responseHTML, redirected }); - } - } - async requestFailedWithResponse(request, response) { - const responseHTML = await response.responseHTML; - const { redirected, statusCode } = response; - if (responseHTML == void 0) { - this.recordResponse({ - statusCode: SystemStatusCode.contentTypeMismatch, - redirected - }); - } else { - this.recordResponse({ statusCode, responseHTML, redirected }); - } - } - requestErrored(_request, _error) { - this.recordResponse({ - statusCode: SystemStatusCode.networkFailure, - redirected: false - }); - } - requestFinished() { - this.finishRequest(); - } - performScroll() { - if (!this.scrolled && !this.view.forceReloaded) { - if (this.action == "restore") { - this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop(); - } else { - this.scrollToAnchor() || this.view.scrollToTop(); - } - if (this.isSamePage) { - this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location); - } - this.scrolled = true; - } - } - scrollToRestoredPosition() { - const { scrollPosition } = this.restorationData; - if (scrollPosition) { - this.view.scrollToPosition(scrollPosition); - return true; - } - } - scrollToAnchor() { - const anchor = getAnchor(this.location); - if (anchor != null) { - this.view.scrollToAnchor(anchor); - return true; - } - } - recordTimingMetric(metric) { - this.timingMetrics[metric] = (/* @__PURE__ */ new Date()).getTime(); - } - getTimingMetrics() { - return Object.assign({}, this.timingMetrics); - } - getHistoryMethodForAction(action) { - switch (action) { - case "replace": - return history.replaceState; - case "advance": - case "restore": - return history.pushState; - } - } - hasPreloadedResponse() { - return typeof this.response == "object"; - } - shouldIssueRequest() { - if (this.isSamePage) { - return false; - } else if (this.action == "restore") { - return !this.hasCachedSnapshot(); - } else { - return this.willRender; - } - } - cacheSnapshot() { - if (!this.snapshotCached) { - this.view.cacheSnapshot(this.snapshot).then((snapshot) => snapshot && this.visitCachedSnapshot(snapshot)); - this.snapshotCached = true; - } - } - async render(callback) { - this.cancelRender(); - await new Promise((resolve) => { - this.frame = requestAnimationFrame(() => resolve()); - }); - await callback(); - delete this.frame; - } - cancelRender() { - if (this.frame) { - cancelAnimationFrame(this.frame); - delete this.frame; - } - } - }; - function isSuccessful(statusCode) { - return statusCode >= 200 && statusCode < 300; - } - var BrowserAdapter = class { - constructor(session2) { - this.progressBar = new ProgressBar(); - this.showProgressBar = () => { - this.progressBar.show(); - }; - this.session = session2; - } - visitProposedToLocation(location2, options) { - this.navigator.startVisit(location2, (options === null || options === void 0 ? void 0 : options.restorationIdentifier) || uuid(), options); - } - visitStarted(visit2) { - this.location = visit2.location; - visit2.loadCachedSnapshot(); - visit2.issueRequest(); - visit2.goToSamePageAnchor(); - } - visitRequestStarted(visit2) { - this.progressBar.setValue(0); - if (visit2.hasCachedSnapshot() || visit2.action != "restore") { - this.showVisitProgressBarAfterDelay(); - } else { - this.showProgressBar(); - } - } - visitRequestCompleted(visit2) { - visit2.loadResponse(); - } - visitRequestFailedWithStatusCode(visit2, statusCode) { - switch (statusCode) { - case SystemStatusCode.networkFailure: - case SystemStatusCode.timeoutFailure: - case SystemStatusCode.contentTypeMismatch: - return this.reload({ - reason: "request_failed", - context: { - statusCode - } - }); - default: - return visit2.loadResponse(); - } - } - visitRequestFinished(_visit) { - this.progressBar.setValue(1); - this.hideVisitProgressBar(); - } - visitCompleted(_visit) { - } - pageInvalidated(reason) { - this.reload(reason); - } - visitFailed(_visit) { - } - visitRendered(_visit) { - } - formSubmissionStarted(_formSubmission) { - this.progressBar.setValue(0); - this.showFormProgressBarAfterDelay(); - } - formSubmissionFinished(_formSubmission) { - this.progressBar.setValue(1); - this.hideFormProgressBar(); - } - showVisitProgressBarAfterDelay() { - this.visitProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay); - } - hideVisitProgressBar() { - this.progressBar.hide(); - if (this.visitProgressBarTimeout != null) { - window.clearTimeout(this.visitProgressBarTimeout); - delete this.visitProgressBarTimeout; - } - } - showFormProgressBarAfterDelay() { - if (this.formProgressBarTimeout == null) { - this.formProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay); - } - } - hideFormProgressBar() { - this.progressBar.hide(); - if (this.formProgressBarTimeout != null) { - window.clearTimeout(this.formProgressBarTimeout); - delete this.formProgressBarTimeout; - } - } - reload(reason) { - var _a; - dispatch("turbo:reload", { detail: reason }); - window.location.href = ((_a = this.location) === null || _a === void 0 ? void 0 : _a.toString()) || window.location.href; - } - get navigator() { - return this.session.navigator; - } - }; - var CacheObserver = class { - constructor() { - this.selector = "[data-turbo-temporary]"; - this.deprecatedSelector = "[data-turbo-cache=false]"; - this.started = false; - this.removeTemporaryElements = (_event) => { - for (const element of this.temporaryElements) { - element.remove(); - } - }; - } - start() { - if (!this.started) { - this.started = true; - addEventListener("turbo:before-cache", this.removeTemporaryElements, false); - } - } - stop() { - if (this.started) { - this.started = false; - removeEventListener("turbo:before-cache", this.removeTemporaryElements, false); - } - } - get temporaryElements() { - return [...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation]; - } - get temporaryElementsWithDeprecation() { - const elements = document.querySelectorAll(this.deprecatedSelector); - if (elements.length) { - console.warn(`The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`); - } - return [...elements]; - } - }; - var FrameRedirector = class { - constructor(session2, element) { - this.session = session2; - this.element = element; - this.linkInterceptor = new LinkInterceptor(this, element); - this.formSubmitObserver = new FormSubmitObserver(this, element); - } - start() { - this.linkInterceptor.start(); - this.formSubmitObserver.start(); - } - stop() { - this.linkInterceptor.stop(); - this.formSubmitObserver.stop(); - } - shouldInterceptLinkClick(element, _location, _event) { - return this.shouldRedirect(element); - } - linkClickIntercepted(element, url, event) { - const frame = this.findFrameElement(element); - if (frame) { - frame.delegate.linkClickIntercepted(element, url, event); - } - } - willSubmitForm(element, submitter) { - return element.closest("turbo-frame") == null && this.shouldSubmit(element, submitter) && this.shouldRedirect(element, submitter); - } - formSubmitted(element, submitter) { - const frame = this.findFrameElement(element, submitter); - if (frame) { - frame.delegate.formSubmitted(element, submitter); - } - } - shouldSubmit(form, submitter) { - var _a; - const action = getAction(form, submitter); - const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`); - const rootLocation = expandURL((_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/"); - return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation); - } - shouldRedirect(element, submitter) { - const isNavigatable = element instanceof HTMLFormElement ? this.session.submissionIsNavigatable(element, submitter) : this.session.elementIsNavigatable(element); - if (isNavigatable) { - const frame = this.findFrameElement(element, submitter); - return frame ? frame != element.closest("turbo-frame") : false; - } else { - return false; - } - } - findFrameElement(element, submitter) { - const id2 = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame"); - if (id2 && id2 != "_top") { - const frame = this.element.querySelector(`#${id2}:not([disabled])`); - if (frame instanceof FrameElement) { - return frame; - } - } - } - }; - var History = class { - constructor(delegate) { - this.restorationIdentifier = uuid(); - this.restorationData = {}; - this.started = false; - this.pageLoaded = false; - this.onPopState = (event) => { - if (this.shouldHandlePopState()) { - const { turbo } = event.state || {}; - if (turbo) { - this.location = new URL(window.location.href); - const { restorationIdentifier } = turbo; - this.restorationIdentifier = restorationIdentifier; - this.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier); - } - } - }; - this.onPageLoad = async (_event) => { - await nextMicrotask(); - this.pageLoaded = true; - }; - this.delegate = delegate; - } - start() { - if (!this.started) { - addEventListener("popstate", this.onPopState, false); - addEventListener("load", this.onPageLoad, false); - this.started = true; - this.replace(new URL(window.location.href)); - } - } - stop() { - if (this.started) { - removeEventListener("popstate", this.onPopState, false); - removeEventListener("load", this.onPageLoad, false); - this.started = false; - } - } - push(location2, restorationIdentifier) { - this.update(history.pushState, location2, restorationIdentifier); - } - replace(location2, restorationIdentifier) { - this.update(history.replaceState, location2, restorationIdentifier); - } - update(method, location2, restorationIdentifier = uuid()) { - const state = { turbo: { restorationIdentifier } }; - method.call(history, state, "", location2.href); - this.location = location2; - this.restorationIdentifier = restorationIdentifier; - } - getRestorationDataForIdentifier(restorationIdentifier) { - return this.restorationData[restorationIdentifier] || {}; - } - updateRestorationData(additionalData) { - const { restorationIdentifier } = this; - const restorationData = this.restorationData[restorationIdentifier]; - this.restorationData[restorationIdentifier] = Object.assign(Object.assign({}, restorationData), additionalData); - } - assumeControlOfScrollRestoration() { - var _a; - if (!this.previousScrollRestoration) { - this.previousScrollRestoration = (_a = history.scrollRestoration) !== null && _a !== void 0 ? _a : "auto"; - history.scrollRestoration = "manual"; - } - } - relinquishControlOfScrollRestoration() { - if (this.previousScrollRestoration) { - history.scrollRestoration = this.previousScrollRestoration; - delete this.previousScrollRestoration; - } - } - shouldHandlePopState() { - return this.pageIsLoaded(); - } - pageIsLoaded() { - return this.pageLoaded || document.readyState == "complete"; - } - }; - var Navigator = class { - constructor(delegate) { - this.delegate = delegate; - } - proposeVisit(location2, options = {}) { - if (this.delegate.allowsVisitingLocationWithAction(location2, options.action)) { - if (locationIsVisitable(location2, this.view.snapshot.rootLocation)) { - this.delegate.visitProposedToLocation(location2, options); - } else { - window.location.href = location2.toString(); - } - } - } - startVisit(locatable, restorationIdentifier, options = {}) { - this.stop(); - this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, Object.assign({ referrer: this.location }, options)); - this.currentVisit.start(); - } - submitForm(form, submitter) { - this.stop(); - this.formSubmission = new FormSubmission(this, form, submitter, true); - this.formSubmission.start(); - } - stop() { - if (this.formSubmission) { - this.formSubmission.stop(); - delete this.formSubmission; - } - if (this.currentVisit) { - this.currentVisit.cancel(); - delete this.currentVisit; - } - } - get adapter() { - return this.delegate.adapter; - } - get view() { - return this.delegate.view; - } - get history() { - return this.delegate.history; - } - formSubmissionStarted(formSubmission) { - if (typeof this.adapter.formSubmissionStarted === "function") { - this.adapter.formSubmissionStarted(formSubmission); - } - } - async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) { - if (formSubmission == this.formSubmission) { - const responseHTML = await fetchResponse.responseHTML; - if (responseHTML) { - const shouldCacheSnapshot = formSubmission.isSafe; - if (!shouldCacheSnapshot) { - this.view.clearSnapshotCache(); - } - const { statusCode, redirected } = fetchResponse; - const action = this.getActionForFormSubmission(formSubmission); - const visitOptions = { - action, - shouldCacheSnapshot, - response: { statusCode, responseHTML, redirected } - }; - this.proposeVisit(fetchResponse.location, visitOptions); - } - } - } - async formSubmissionFailedWithResponse(formSubmission, fetchResponse) { - const responseHTML = await fetchResponse.responseHTML; - if (responseHTML) { - const snapshot = PageSnapshot.fromHTMLString(responseHTML); - if (fetchResponse.serverError) { - await this.view.renderError(snapshot, this.currentVisit); - } else { - await this.view.renderPage(snapshot, false, true, this.currentVisit); - } - this.view.scrollToTop(); - this.view.clearSnapshotCache(); - } - } - formSubmissionErrored(formSubmission, error2) { - console.error(error2); - } - formSubmissionFinished(formSubmission) { - if (typeof this.adapter.formSubmissionFinished === "function") { - this.adapter.formSubmissionFinished(formSubmission); - } - } - visitStarted(visit2) { - this.delegate.visitStarted(visit2); - } - visitCompleted(visit2) { - this.delegate.visitCompleted(visit2); - } - locationWithActionIsSamePage(location2, action) { - const anchor = getAnchor(location2); - const currentAnchor = getAnchor(this.view.lastRenderedLocation); - const isRestorationToTop = action === "restore" && typeof anchor === "undefined"; - return action !== "replace" && getRequestURL(location2) === getRequestURL(this.view.lastRenderedLocation) && (isRestorationToTop || anchor != null && anchor !== currentAnchor); - } - visitScrolledToSamePageLocation(oldURL, newURL) { - this.delegate.visitScrolledToSamePageLocation(oldURL, newURL); - } - get location() { - return this.history.location; - } - get restorationIdentifier() { - return this.history.restorationIdentifier; - } - getActionForFormSubmission({ submitter, formElement }) { - return getVisitAction(submitter, formElement) || "advance"; - } - }; - var PageStage; - (function(PageStage2) { - PageStage2[PageStage2["initial"] = 0] = "initial"; - PageStage2[PageStage2["loading"] = 1] = "loading"; - PageStage2[PageStage2["interactive"] = 2] = "interactive"; - PageStage2[PageStage2["complete"] = 3] = "complete"; - })(PageStage || (PageStage = {})); - var PageObserver = class { - constructor(delegate) { - this.stage = PageStage.initial; - this.started = false; - this.interpretReadyState = () => { - const { readyState } = this; - if (readyState == "interactive") { - this.pageIsInteractive(); - } else if (readyState == "complete") { - this.pageIsComplete(); - } - }; - this.pageWillUnload = () => { - this.delegate.pageWillUnload(); - }; - this.delegate = delegate; - } - start() { - if (!this.started) { - if (this.stage == PageStage.initial) { - this.stage = PageStage.loading; - } - document.addEventListener("readystatechange", this.interpretReadyState, false); - addEventListener("pagehide", this.pageWillUnload, false); - this.started = true; - } - } - stop() { - if (this.started) { - document.removeEventListener("readystatechange", this.interpretReadyState, false); - removeEventListener("pagehide", this.pageWillUnload, false); - this.started = false; - } - } - pageIsInteractive() { - if (this.stage == PageStage.loading) { - this.stage = PageStage.interactive; - this.delegate.pageBecameInteractive(); - } - } - pageIsComplete() { - this.pageIsInteractive(); - if (this.stage == PageStage.interactive) { - this.stage = PageStage.complete; - this.delegate.pageLoaded(); - } - } - get readyState() { - return document.readyState; - } - }; - var ScrollObserver = class { - constructor(delegate) { - this.started = false; - this.onScroll = () => { - this.updatePosition({ x: window.pageXOffset, y: window.pageYOffset }); - }; - this.delegate = delegate; - } - start() { - if (!this.started) { - addEventListener("scroll", this.onScroll, false); - this.onScroll(); - this.started = true; - } - } - stop() { - if (this.started) { - removeEventListener("scroll", this.onScroll, false); - this.started = false; - } - } - updatePosition(position) { - this.delegate.scrollPositionChanged(position); - } - }; - var StreamMessageRenderer = class { - render({ fragment }) { - Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), () => document.documentElement.appendChild(fragment)); - } - enteringBardo(currentPermanentElement, newPermanentElement) { - newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true)); - } - leavingBardo() { - } - }; - function getPermanentElementMapForFragment(fragment) { - const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement); - const permanentElementMap = {}; - for (const permanentElementInDocument of permanentElementsInDocument) { - const { id: id2 } = permanentElementInDocument; - for (const streamElement of fragment.querySelectorAll("turbo-stream")) { - const elementInStream = getPermanentElementById(streamElement.templateElement.content, id2); - if (elementInStream) { - permanentElementMap[id2] = [permanentElementInDocument, elementInStream]; - } - } - } - return permanentElementMap; - } - var StreamObserver = class { - constructor(delegate) { - this.sources = /* @__PURE__ */ new Set(); - this.started = false; - this.inspectFetchResponse = (event) => { - const response = fetchResponseFromEvent(event); - if (response && fetchResponseIsStream(response)) { - event.preventDefault(); - this.receiveMessageResponse(response); - } - }; - this.receiveMessageEvent = (event) => { - if (this.started && typeof event.data == "string") { - this.receiveMessageHTML(event.data); - } - }; - this.delegate = delegate; - } - start() { - if (!this.started) { - this.started = true; - addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false); - } - } - stop() { - if (this.started) { - this.started = false; - removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false); - } - } - connectStreamSource(source) { - if (!this.streamSourceIsConnected(source)) { - this.sources.add(source); - source.addEventListener("message", this.receiveMessageEvent, false); - } - } - disconnectStreamSource(source) { - if (this.streamSourceIsConnected(source)) { - this.sources.delete(source); - source.removeEventListener("message", this.receiveMessageEvent, false); - } - } - streamSourceIsConnected(source) { - return this.sources.has(source); - } - async receiveMessageResponse(response) { - const html = await response.responseHTML; - if (html) { - this.receiveMessageHTML(html); - } - } - receiveMessageHTML(html) { - this.delegate.receivedMessageFromStream(StreamMessage.wrap(html)); - } - }; - function fetchResponseFromEvent(event) { - var _a; - const fetchResponse = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.fetchResponse; - if (fetchResponse instanceof FetchResponse) { - return fetchResponse; - } - } - function fetchResponseIsStream(response) { - var _a; - const contentType = (_a = response.contentType) !== null && _a !== void 0 ? _a : ""; - return contentType.startsWith(StreamMessage.contentType); - } - var ErrorRenderer = class extends Renderer { - static renderElement(currentElement, newElement) { - const { documentElement, body } = document; - documentElement.replaceChild(newElement, body); - } - async render() { - this.replaceHeadAndBody(); - this.activateScriptElements(); - } - replaceHeadAndBody() { - const { documentElement, head } = document; - documentElement.replaceChild(this.newHead, head); - this.renderElement(this.currentElement, this.newElement); - } - activateScriptElements() { - for (const replaceableElement of this.scriptElements) { - const parentNode = replaceableElement.parentNode; - if (parentNode) { - const element = activateScriptElement(replaceableElement); - parentNode.replaceChild(element, replaceableElement); - } - } - } - get newHead() { - return this.newSnapshot.headSnapshot.element; - } - get scriptElements() { - return document.documentElement.querySelectorAll("script"); - } - }; - var PageRenderer = class extends Renderer { - static renderElement(currentElement, newElement) { - if (document.body && newElement instanceof HTMLBodyElement) { - document.body.replaceWith(newElement); - } else { - document.documentElement.appendChild(newElement); - } - } - get shouldRender() { - return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical; - } - get reloadReason() { - if (!this.newSnapshot.isVisitable) { - return { - reason: "turbo_visit_control_is_reload" - }; - } - if (!this.trackedElementsAreIdentical) { - return { - reason: "tracked_element_mismatch" - }; - } - } - async prepareToRender() { - await this.mergeHead(); - } - async render() { - if (this.willRender) { - await this.replaceBody(); - } - } - finishRendering() { - super.finishRendering(); - if (!this.isPreview) { - this.focusFirstAutofocusableElement(); - } - } - get currentHeadSnapshot() { - return this.currentSnapshot.headSnapshot; - } - get newHeadSnapshot() { - return this.newSnapshot.headSnapshot; - } - get newElement() { - return this.newSnapshot.element; - } - async mergeHead() { - const mergedHeadElements = this.mergeProvisionalElements(); - const newStylesheetElements = this.copyNewHeadStylesheetElements(); - this.copyNewHeadScriptElements(); - await mergedHeadElements; - await newStylesheetElements; - } - async replaceBody() { - await this.preservingPermanentElements(async () => { - this.activateNewBody(); - await this.assignNewBody(); - }); - } - get trackedElementsAreIdentical() { - return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature; - } - async copyNewHeadStylesheetElements() { - const loadingElements = []; - for (const element of this.newHeadStylesheetElements) { - loadingElements.push(waitForLoad(element)); - document.head.appendChild(element); - } - await Promise.all(loadingElements); - } - copyNewHeadScriptElements() { - for (const element of this.newHeadScriptElements) { - document.head.appendChild(activateScriptElement(element)); - } - } - async mergeProvisionalElements() { - const newHeadElements = [...this.newHeadProvisionalElements]; - for (const element of this.currentHeadProvisionalElements) { - if (!this.isCurrentElementInElementList(element, newHeadElements)) { - document.head.removeChild(element); - } - } - for (const element of newHeadElements) { - document.head.appendChild(element); - } - } - isCurrentElementInElementList(element, elementList) { - for (const [index, newElement] of elementList.entries()) { - if (element.tagName == "TITLE") { - if (newElement.tagName != "TITLE") { - continue; - } - if (element.innerHTML == newElement.innerHTML) { - elementList.splice(index, 1); - return true; - } - } - if (newElement.isEqualNode(element)) { - elementList.splice(index, 1); - return true; - } - } - return false; - } - removeCurrentHeadProvisionalElements() { - for (const element of this.currentHeadProvisionalElements) { - document.head.removeChild(element); - } - } - copyNewHeadProvisionalElements() { - for (const element of this.newHeadProvisionalElements) { - document.head.appendChild(element); - } - } - activateNewBody() { - document.adoptNode(this.newElement); - this.activateNewBodyScriptElements(); - } - activateNewBodyScriptElements() { - for (const inertScriptElement of this.newBodyScriptElements) { - const activatedScriptElement = activateScriptElement(inertScriptElement); - inertScriptElement.replaceWith(activatedScriptElement); - } - } - async assignNewBody() { - await this.renderElement(this.currentElement, this.newElement); - } - get newHeadStylesheetElements() { - return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot); - } - get newHeadScriptElements() { - return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot); - } - get currentHeadProvisionalElements() { - return this.currentHeadSnapshot.provisionalElements; - } - get newHeadProvisionalElements() { - return this.newHeadSnapshot.provisionalElements; - } - get newBodyScriptElements() { - return this.newElement.querySelectorAll("script"); - } - }; - var SnapshotCache = class { - constructor(size) { - this.keys = []; - this.snapshots = {}; - this.size = size; - } - has(location2) { - return toCacheKey(location2) in this.snapshots; - } - get(location2) { - if (this.has(location2)) { - const snapshot = this.read(location2); - this.touch(location2); - return snapshot; - } - } - put(location2, snapshot) { - this.write(location2, snapshot); - this.touch(location2); - return snapshot; - } - clear() { - this.snapshots = {}; - } - read(location2) { - return this.snapshots[toCacheKey(location2)]; - } - write(location2, snapshot) { - this.snapshots[toCacheKey(location2)] = snapshot; - } - touch(location2) { - const key = toCacheKey(location2); - const index = this.keys.indexOf(key); - if (index > -1) - this.keys.splice(index, 1); - this.keys.unshift(key); - this.trim(); - } - trim() { - for (const key of this.keys.splice(this.size)) { - delete this.snapshots[key]; - } - } - }; - var PageView = class extends View { - constructor() { - super(...arguments); - this.snapshotCache = new SnapshotCache(10); - this.lastRenderedLocation = new URL(location.href); - this.forceReloaded = false; - } - renderPage(snapshot, isPreview = false, willRender = true, visit2) { - const renderer = new PageRenderer(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender); - if (!renderer.shouldRender) { - this.forceReloaded = true; - } else { - visit2 === null || visit2 === void 0 ? void 0 : visit2.changeHistory(); - } - return this.render(renderer); - } - renderError(snapshot, visit2) { - visit2 === null || visit2 === void 0 ? void 0 : visit2.changeHistory(); - const renderer = new ErrorRenderer(this.snapshot, snapshot, ErrorRenderer.renderElement, false); - return this.render(renderer); - } - clearSnapshotCache() { - this.snapshotCache.clear(); - } - async cacheSnapshot(snapshot = this.snapshot) { - if (snapshot.isCacheable) { - this.delegate.viewWillCacheSnapshot(); - const { lastRenderedLocation: location2 } = this; - await nextEventLoopTick(); - const cachedSnapshot = snapshot.clone(); - this.snapshotCache.put(location2, cachedSnapshot); - return cachedSnapshot; - } - } - getCachedSnapshotForLocation(location2) { - return this.snapshotCache.get(location2); - } - get snapshot() { - return PageSnapshot.fromElement(this.element); - } - }; - var Preloader = class { - constructor(delegate) { - this.selector = "a[data-turbo-preload]"; - this.delegate = delegate; - } - get snapshotCache() { - return this.delegate.navigator.view.snapshotCache; - } - start() { - if (document.readyState === "loading") { - return document.addEventListener("DOMContentLoaded", () => { - this.preloadOnLoadLinksForView(document.body); - }); - } else { - this.preloadOnLoadLinksForView(document.body); - } - } - preloadOnLoadLinksForView(element) { - for (const link of element.querySelectorAll(this.selector)) { - this.preloadURL(link); - } - } - async preloadURL(link) { - const location2 = new URL(link.href); - if (this.snapshotCache.has(location2)) { - return; - } - try { - const response = await fetch(location2.toString(), { headers: { "VND.PREFETCH": "true", Accept: "text/html" } }); - const responseText = await response.text(); - const snapshot = PageSnapshot.fromHTMLString(responseText); - this.snapshotCache.put(location2, snapshot); - } catch (_) { - } - } - }; - var Session = class { - constructor() { - this.navigator = new Navigator(this); - this.history = new History(this); - this.preloader = new Preloader(this); - this.view = new PageView(this, document.documentElement); - this.adapter = new BrowserAdapter(this); - this.pageObserver = new PageObserver(this); - this.cacheObserver = new CacheObserver(); - this.linkClickObserver = new LinkClickObserver(this, window); - this.formSubmitObserver = new FormSubmitObserver(this, document); - this.scrollObserver = new ScrollObserver(this); - this.streamObserver = new StreamObserver(this); - this.formLinkClickObserver = new FormLinkClickObserver(this, document.documentElement); - this.frameRedirector = new FrameRedirector(this, document.documentElement); - this.streamMessageRenderer = new StreamMessageRenderer(); - this.drive = true; - this.enabled = true; - this.progressBarDelay = 500; - this.started = false; - this.formMode = "on"; - } - start() { - if (!this.started) { - this.pageObserver.start(); - this.cacheObserver.start(); - this.formLinkClickObserver.start(); - this.linkClickObserver.start(); - this.formSubmitObserver.start(); - this.scrollObserver.start(); - this.streamObserver.start(); - this.frameRedirector.start(); - this.history.start(); - this.preloader.start(); - this.started = true; - this.enabled = true; - } - } - disable() { - this.enabled = false; - } - stop() { - if (this.started) { - this.pageObserver.stop(); - this.cacheObserver.stop(); - this.formLinkClickObserver.stop(); - this.linkClickObserver.stop(); - this.formSubmitObserver.stop(); - this.scrollObserver.stop(); - this.streamObserver.stop(); - this.frameRedirector.stop(); - this.history.stop(); - this.started = false; - } - } - registerAdapter(adapter) { - this.adapter = adapter; - } - visit(location2, options = {}) { - const frameElement = options.frame ? document.getElementById(options.frame) : null; - if (frameElement instanceof FrameElement) { - frameElement.src = location2.toString(); - frameElement.loaded; - } else { - this.navigator.proposeVisit(expandURL(location2), options); - } - } - connectStreamSource(source) { - this.streamObserver.connectStreamSource(source); - } - disconnectStreamSource(source) { - this.streamObserver.disconnectStreamSource(source); - } - renderStreamMessage(message) { - this.streamMessageRenderer.render(StreamMessage.wrap(message)); - } - clearCache() { - this.view.clearSnapshotCache(); - } - setProgressBarDelay(delay) { - this.progressBarDelay = delay; - } - setFormMode(mode) { - this.formMode = mode; - } - get location() { - return this.history.location; - } - get restorationIdentifier() { - return this.history.restorationIdentifier; - } - historyPoppedToLocationWithRestorationIdentifier(location2, restorationIdentifier) { - if (this.enabled) { - this.navigator.startVisit(location2, restorationIdentifier, { - action: "restore", - historyChanged: true - }); - } else { - this.adapter.pageInvalidated({ - reason: "turbo_disabled" - }); - } - } - scrollPositionChanged(position) { - this.history.updateRestorationData({ scrollPosition: position }); - } - willSubmitFormLinkToLocation(link, location2) { - return this.elementIsNavigatable(link) && locationIsVisitable(location2, this.snapshot.rootLocation); - } - submittedFormLinkToLocation() { - } - willFollowLinkToLocation(link, location2, event) { - return this.elementIsNavigatable(link) && locationIsVisitable(location2, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location2, event); - } - followedLinkToLocation(link, location2) { - const action = this.getActionForLink(link); - const acceptsStreamResponse = link.hasAttribute("data-turbo-stream"); - this.visit(location2.href, { action, acceptsStreamResponse }); - } - allowsVisitingLocationWithAction(location2, action) { - return this.locationWithActionIsSamePage(location2, action) || this.applicationAllowsVisitingLocation(location2); - } - visitProposedToLocation(location2, options) { - extendURLWithDeprecatedProperties(location2); - this.adapter.visitProposedToLocation(location2, options); - } - visitStarted(visit2) { - if (!visit2.acceptsStreamResponse) { - markAsBusy(document.documentElement); - } - extendURLWithDeprecatedProperties(visit2.location); - if (!visit2.silent) { - this.notifyApplicationAfterVisitingLocation(visit2.location, visit2.action); - } - } - visitCompleted(visit2) { - clearBusyState(document.documentElement); - this.notifyApplicationAfterPageLoad(visit2.getTimingMetrics()); - } - locationWithActionIsSamePage(location2, action) { - return this.navigator.locationWithActionIsSamePage(location2, action); - } - visitScrolledToSamePageLocation(oldURL, newURL) { - this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL); - } - willSubmitForm(form, submitter) { - const action = getAction(form, submitter); - return this.submissionIsNavigatable(form, submitter) && locationIsVisitable(expandURL(action), this.snapshot.rootLocation); - } - formSubmitted(form, submitter) { - this.navigator.submitForm(form, submitter); - } - pageBecameInteractive() { - this.view.lastRenderedLocation = this.location; - this.notifyApplicationAfterPageLoad(); - } - pageLoaded() { - this.history.assumeControlOfScrollRestoration(); - } - pageWillUnload() { - this.history.relinquishControlOfScrollRestoration(); - } - receivedMessageFromStream(message) { - this.renderStreamMessage(message); - } - viewWillCacheSnapshot() { - var _a; - if (!((_a = this.navigator.currentVisit) === null || _a === void 0 ? void 0 : _a.silent)) { - this.notifyApplicationBeforeCachingSnapshot(); - } - } - allowsImmediateRender({ element }, options) { - const event = this.notifyApplicationBeforeRender(element, options); - const { defaultPrevented, detail: { render } } = event; - if (this.view.renderer && render) { - this.view.renderer.renderElement = render; - } - return !defaultPrevented; - } - viewRenderedSnapshot(_snapshot, _isPreview) { - this.view.lastRenderedLocation = this.history.location; - this.notifyApplicationAfterRender(); - } - preloadOnLoadLinksForView(element) { - this.preloader.preloadOnLoadLinksForView(element); - } - viewInvalidated(reason) { - this.adapter.pageInvalidated(reason); - } - frameLoaded(frame) { - this.notifyApplicationAfterFrameLoad(frame); - } - frameRendered(fetchResponse, frame) { - this.notifyApplicationAfterFrameRender(fetchResponse, frame); - } - applicationAllowsFollowingLinkToLocation(link, location2, ev) { - const event = this.notifyApplicationAfterClickingLinkToLocation(link, location2, ev); - return !event.defaultPrevented; - } - applicationAllowsVisitingLocation(location2) { - const event = this.notifyApplicationBeforeVisitingLocation(location2); - return !event.defaultPrevented; - } - notifyApplicationAfterClickingLinkToLocation(link, location2, event) { - return dispatch("turbo:click", { - target: link, - detail: { url: location2.href, originalEvent: event }, - cancelable: true - }); - } - notifyApplicationBeforeVisitingLocation(location2) { - return dispatch("turbo:before-visit", { - detail: { url: location2.href }, - cancelable: true - }); - } - notifyApplicationAfterVisitingLocation(location2, action) { - return dispatch("turbo:visit", { detail: { url: location2.href, action } }); - } - notifyApplicationBeforeCachingSnapshot() { - return dispatch("turbo:before-cache"); - } - notifyApplicationBeforeRender(newBody, options) { - return dispatch("turbo:before-render", { - detail: Object.assign({ newBody }, options), - cancelable: true - }); - } - notifyApplicationAfterRender() { - return dispatch("turbo:render"); - } - notifyApplicationAfterPageLoad(timing = {}) { - return dispatch("turbo:load", { - detail: { url: this.location.href, timing } - }); - } - notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) { - dispatchEvent(new HashChangeEvent("hashchange", { - oldURL: oldURL.toString(), - newURL: newURL.toString() - })); - } - notifyApplicationAfterFrameLoad(frame) { - return dispatch("turbo:frame-load", { target: frame }); - } - notifyApplicationAfterFrameRender(fetchResponse, frame) { - return dispatch("turbo:frame-render", { - detail: { fetchResponse }, - target: frame, - cancelable: true - }); - } - submissionIsNavigatable(form, submitter) { - if (this.formMode == "off") { - return false; - } else { - const submitterIsNavigatable = submitter ? this.elementIsNavigatable(submitter) : true; - if (this.formMode == "optin") { - return submitterIsNavigatable && form.closest('[data-turbo="true"]') != null; - } else { - return submitterIsNavigatable && this.elementIsNavigatable(form); - } - } - } - elementIsNavigatable(element) { - const container = findClosestRecursively(element, "[data-turbo]"); - const withinFrame = findClosestRecursively(element, "turbo-frame"); - if (this.drive || withinFrame) { - if (container) { - return container.getAttribute("data-turbo") != "false"; - } else { - return true; - } - } else { - if (container) { - return container.getAttribute("data-turbo") == "true"; - } else { - return false; - } - } - } - getActionForLink(link) { - return getVisitAction(link) || "advance"; - } - get snapshot() { - return this.view.snapshot; - } - }; - function extendURLWithDeprecatedProperties(url) { - Object.defineProperties(url, deprecatedLocationPropertyDescriptors); - } - var deprecatedLocationPropertyDescriptors = { - absoluteURL: { - get() { - return this.toString(); - } - } - }; - var Cache = class { - constructor(session2) { - this.session = session2; - } - clear() { - this.session.clearCache(); - } - resetCacheControl() { - this.setCacheControl(""); - } - exemptPageFromCache() { - this.setCacheControl("no-cache"); - } - exemptPageFromPreview() { - this.setCacheControl("no-preview"); - } - setCacheControl(value) { - setMetaContent("turbo-cache-control", value); - } - }; - var StreamActions = { - after() { - this.targetElements.forEach((e) => { - var _a; - return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e.nextSibling); - }); - }, - append() { - this.removeDuplicateTargetChildren(); - this.targetElements.forEach((e) => e.append(this.templateContent)); - }, - before() { - this.targetElements.forEach((e) => { - var _a; - return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e); - }); - }, - prepend() { - this.removeDuplicateTargetChildren(); - this.targetElements.forEach((e) => e.prepend(this.templateContent)); - }, - remove() { - this.targetElements.forEach((e) => e.remove()); - }, - replace() { - this.targetElements.forEach((e) => e.replaceWith(this.templateContent)); - }, - update() { - this.targetElements.forEach((targetElement) => { - targetElement.innerHTML = ""; - targetElement.append(this.templateContent); - }); - } - }; - var session = new Session(); - var cache = new Cache(session); - var { navigator: navigator$1 } = session; - function start() { - session.start(); - } - function registerAdapter(adapter) { - session.registerAdapter(adapter); - } - function visit(location2, options) { - session.visit(location2, options); - } - function connectStreamSource(source) { - session.connectStreamSource(source); - } - function disconnectStreamSource(source) { - session.disconnectStreamSource(source); - } - function renderStreamMessage(message) { - session.renderStreamMessage(message); - } - function clearCache() { - console.warn("Please replace `Turbo.clearCache()` with `Turbo.cache.clear()`. The top-level function is deprecated and will be removed in a future version of Turbo.`"); - session.clearCache(); - } - function setProgressBarDelay(delay) { - session.setProgressBarDelay(delay); - } - function setConfirmMethod(confirmMethod) { - FormSubmission.confirmMethod = confirmMethod; - } - function setFormMode(mode) { - session.setFormMode(mode); - } - var Turbo = /* @__PURE__ */ Object.freeze({ - __proto__: null, - navigator: navigator$1, - session, - cache, - PageRenderer, - PageSnapshot, - FrameRenderer, - start, - registerAdapter, - visit, - connectStreamSource, - disconnectStreamSource, - renderStreamMessage, - clearCache, - setProgressBarDelay, - setConfirmMethod, - setFormMode, - StreamActions - }); - var TurboFrameMissingError = class extends Error { - }; - var FrameController = class { - constructor(element) { - this.fetchResponseLoaded = (_fetchResponse) => { - }; - this.currentFetchRequest = null; - this.resolveVisitPromise = () => { - }; - this.connected = false; - this.hasBeenLoaded = false; - this.ignoredAttributes = /* @__PURE__ */ new Set(); - this.action = null; - this.visitCachedSnapshot = ({ element: element2 }) => { - const frame = element2.querySelector("#" + this.element.id); - if (frame && this.previousFrameElement) { - frame.replaceChildren(...this.previousFrameElement.children); - } - delete this.previousFrameElement; - }; - this.element = element; - this.view = new FrameView(this, this.element); - this.appearanceObserver = new AppearanceObserver(this, this.element); - this.formLinkClickObserver = new FormLinkClickObserver(this, this.element); - this.linkInterceptor = new LinkInterceptor(this, this.element); - this.restorationIdentifier = uuid(); - this.formSubmitObserver = new FormSubmitObserver(this, this.element); - } - connect() { - if (!this.connected) { - this.connected = true; - if (this.loadingStyle == FrameLoadingStyle.lazy) { - this.appearanceObserver.start(); - } else { - this.loadSourceURL(); - } - this.formLinkClickObserver.start(); - this.linkInterceptor.start(); - this.formSubmitObserver.start(); - } - } - disconnect() { - if (this.connected) { - this.connected = false; - this.appearanceObserver.stop(); - this.formLinkClickObserver.stop(); - this.linkInterceptor.stop(); - this.formSubmitObserver.stop(); - } - } - disabledChanged() { - if (this.loadingStyle == FrameLoadingStyle.eager) { - this.loadSourceURL(); - } - } - sourceURLChanged() { - if (this.isIgnoringChangesTo("src")) - return; - if (this.element.isConnected) { - this.complete = false; - } - if (this.loadingStyle == FrameLoadingStyle.eager || this.hasBeenLoaded) { - this.loadSourceURL(); - } - } - sourceURLReloaded() { - const { src } = this.element; - this.ignoringChangesToAttribute("complete", () => { - this.element.removeAttribute("complete"); - }); - this.element.src = null; - this.element.src = src; - return this.element.loaded; - } - completeChanged() { - if (this.isIgnoringChangesTo("complete")) - return; - this.loadSourceURL(); - } - loadingStyleChanged() { - if (this.loadingStyle == FrameLoadingStyle.lazy) { - this.appearanceObserver.start(); - } else { - this.appearanceObserver.stop(); - this.loadSourceURL(); - } - } - async loadSourceURL() { - if (this.enabled && this.isActive && !this.complete && this.sourceURL) { - this.element.loaded = this.visit(expandURL(this.sourceURL)); - this.appearanceObserver.stop(); - await this.element.loaded; - this.hasBeenLoaded = true; - } - } - async loadResponse(fetchResponse) { - if (fetchResponse.redirected || fetchResponse.succeeded && fetchResponse.isHTML) { - this.sourceURL = fetchResponse.response.url; - } - try { - const html = await fetchResponse.responseHTML; - if (html) { - const document2 = parseHTMLDocument(html); - const pageSnapshot = PageSnapshot.fromDocument(document2); - if (pageSnapshot.isVisitable) { - await this.loadFrameResponse(fetchResponse, document2); - } else { - await this.handleUnvisitableFrameResponse(fetchResponse); - } - } - } finally { - this.fetchResponseLoaded = () => { - }; - } - } - elementAppearedInViewport(element) { - this.proposeVisitIfNavigatedWithAction(element, element); - this.loadSourceURL(); - } - willSubmitFormLinkToLocation(link) { - return this.shouldInterceptNavigation(link); - } - submittedFormLinkToLocation(link, _location, form) { - const frame = this.findFrameElement(link); - if (frame) - form.setAttribute("data-turbo-frame", frame.id); - } - shouldInterceptLinkClick(element, _location, _event) { - return this.shouldInterceptNavigation(element); - } - linkClickIntercepted(element, location2) { - this.navigateFrame(element, location2); - } - willSubmitForm(element, submitter) { - return element.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(element, submitter); - } - formSubmitted(element, submitter) { - if (this.formSubmission) { - this.formSubmission.stop(); - } - this.formSubmission = new FormSubmission(this, element, submitter); - const { fetchRequest } = this.formSubmission; - this.prepareRequest(fetchRequest); - this.formSubmission.start(); - } - prepareRequest(request) { - var _a; - request.headers["Turbo-Frame"] = this.id; - if ((_a = this.currentNavigationElement) === null || _a === void 0 ? void 0 : _a.hasAttribute("data-turbo-stream")) { - request.acceptResponseType(StreamMessage.contentType); - } - } - requestStarted(_request) { - markAsBusy(this.element); - } - requestPreventedHandlingResponse(_request, _response) { - this.resolveVisitPromise(); - } - async requestSucceededWithResponse(request, response) { - await this.loadResponse(response); - this.resolveVisitPromise(); - } - async requestFailedWithResponse(request, response) { - await this.loadResponse(response); - this.resolveVisitPromise(); - } - requestErrored(request, error2) { - console.error(error2); - this.resolveVisitPromise(); - } - requestFinished(_request) { - clearBusyState(this.element); - } - formSubmissionStarted({ formElement }) { - markAsBusy(formElement, this.findFrameElement(formElement)); - } - formSubmissionSucceededWithResponse(formSubmission, response) { - const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter); - frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter); - frame.delegate.loadResponse(response); - if (!formSubmission.isSafe) { - session.clearCache(); - } - } - formSubmissionFailedWithResponse(formSubmission, fetchResponse) { - this.element.delegate.loadResponse(fetchResponse); - session.clearCache(); - } - formSubmissionErrored(formSubmission, error2) { - console.error(error2); - } - formSubmissionFinished({ formElement }) { - clearBusyState(formElement, this.findFrameElement(formElement)); - } - allowsImmediateRender({ element: newFrame }, options) { - const event = dispatch("turbo:before-frame-render", { - target: this.element, - detail: Object.assign({ newFrame }, options), - cancelable: true - }); - const { defaultPrevented, detail: { render } } = event; - if (this.view.renderer && render) { - this.view.renderer.renderElement = render; - } - return !defaultPrevented; - } - viewRenderedSnapshot(_snapshot, _isPreview) { - } - preloadOnLoadLinksForView(element) { - session.preloadOnLoadLinksForView(element); - } - viewInvalidated() { - } - willRenderFrame(currentElement, _newElement) { - this.previousFrameElement = currentElement.cloneNode(true); - } - async loadFrameResponse(fetchResponse, document2) { - const newFrameElement = await this.extractForeignFrameElement(document2.body); - if (newFrameElement) { - const snapshot = new Snapshot(newFrameElement); - const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false); - if (this.view.renderPromise) - await this.view.renderPromise; - this.changeHistory(); - await this.view.render(renderer); - this.complete = true; - session.frameRendered(fetchResponse, this.element); - session.frameLoaded(this.element); - this.fetchResponseLoaded(fetchResponse); - } else if (this.willHandleFrameMissingFromResponse(fetchResponse)) { - this.handleFrameMissingFromResponse(fetchResponse); - } - } - async visit(url) { - var _a; - const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams(), this.element); - (_a = this.currentFetchRequest) === null || _a === void 0 ? void 0 : _a.cancel(); - this.currentFetchRequest = request; - return new Promise((resolve) => { - this.resolveVisitPromise = () => { - this.resolveVisitPromise = () => { - }; - this.currentFetchRequest = null; - resolve(); - }; - request.perform(); - }); - } - navigateFrame(element, url, submitter) { - const frame = this.findFrameElement(element, submitter); - frame.delegate.proposeVisitIfNavigatedWithAction(frame, element, submitter); - this.withCurrentNavigationElement(element, () => { - frame.src = url; - }); - } - proposeVisitIfNavigatedWithAction(frame, element, submitter) { - this.action = getVisitAction(submitter, element, frame); - if (this.action) { - const pageSnapshot = PageSnapshot.fromElement(frame).clone(); - const { visitCachedSnapshot } = frame.delegate; - frame.delegate.fetchResponseLoaded = (fetchResponse) => { - if (frame.src) { - const { statusCode, redirected } = fetchResponse; - const responseHTML = frame.ownerDocument.documentElement.outerHTML; - const response = { statusCode, redirected, responseHTML }; - const options = { - response, - visitCachedSnapshot, - willRender: false, - updateHistory: false, - restorationIdentifier: this.restorationIdentifier, - snapshot: pageSnapshot - }; - if (this.action) - options.action = this.action; - session.visit(frame.src, options); - } - }; - } - } - changeHistory() { - if (this.action) { - const method = getHistoryMethodForAction(this.action); - session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier); - } - } - async handleUnvisitableFrameResponse(fetchResponse) { - console.warn(`The response (${fetchResponse.statusCode}) from is performing a full page visit due to turbo-visit-control.`); - await this.visitResponse(fetchResponse.response); - } - willHandleFrameMissingFromResponse(fetchResponse) { - this.element.setAttribute("complete", ""); - const response = fetchResponse.response; - const visit2 = async (url, options = {}) => { - if (url instanceof Response) { - this.visitResponse(url); - } else { - session.visit(url, options); - } - }; - const event = dispatch("turbo:frame-missing", { - target: this.element, - detail: { response, visit: visit2 }, - cancelable: true - }); - return !event.defaultPrevented; - } - handleFrameMissingFromResponse(fetchResponse) { - this.view.missing(); - this.throwFrameMissingError(fetchResponse); - } - throwFrameMissingError(fetchResponse) { - const message = `The response (${fetchResponse.statusCode}) did not contain the expected and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.`; - throw new TurboFrameMissingError(message); - } - async visitResponse(response) { - const wrapped = new FetchResponse(response); - const responseHTML = await wrapped.responseHTML; - const { location: location2, redirected, statusCode } = wrapped; - return session.visit(location2, { response: { redirected, statusCode, responseHTML } }); - } - findFrameElement(element, submitter) { - var _a; - const id2 = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target"); - return (_a = getFrameElementById(id2)) !== null && _a !== void 0 ? _a : this.element; - } - async extractForeignFrameElement(container) { - let element; - const id2 = CSS.escape(this.id); - try { - element = activateElement(container.querySelector(`turbo-frame#${id2}`), this.sourceURL); - if (element) { - return element; - } - element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id2}]`), this.sourceURL); - if (element) { - await element.loaded; - return await this.extractForeignFrameElement(element); - } - } catch (error2) { - console.error(error2); - return new FrameElement(); - } - return null; - } - formActionIsVisitable(form, submitter) { - const action = getAction(form, submitter); - return locationIsVisitable(expandURL(action), this.rootLocation); - } - shouldInterceptNavigation(element, submitter) { - const id2 = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target"); - if (element instanceof HTMLFormElement && !this.formActionIsVisitable(element, submitter)) { - return false; - } - if (!this.enabled || id2 == "_top") { - return false; - } - if (id2) { - const frameElement = getFrameElementById(id2); - if (frameElement) { - return !frameElement.disabled; - } - } - if (!session.elementIsNavigatable(element)) { - return false; - } - if (submitter && !session.elementIsNavigatable(submitter)) { - return false; - } - return true; - } - get id() { - return this.element.id; - } - get enabled() { - return !this.element.disabled; - } - get sourceURL() { - if (this.element.src) { - return this.element.src; - } - } - set sourceURL(sourceURL) { - this.ignoringChangesToAttribute("src", () => { - this.element.src = sourceURL !== null && sourceURL !== void 0 ? sourceURL : null; - }); - } - get loadingStyle() { - return this.element.loading; - } - get isLoading() { - return this.formSubmission !== void 0 || this.resolveVisitPromise() !== void 0; - } - get complete() { - return this.element.hasAttribute("complete"); - } - set complete(value) { - this.ignoringChangesToAttribute("complete", () => { - if (value) { - this.element.setAttribute("complete", ""); - } else { - this.element.removeAttribute("complete"); - } - }); - } - get isActive() { - return this.element.isActive && this.connected; - } - get rootLocation() { - var _a; - const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`); - const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/"; - return expandURL(root); - } - isIgnoringChangesTo(attributeName) { - return this.ignoredAttributes.has(attributeName); - } - ignoringChangesToAttribute(attributeName, callback) { - this.ignoredAttributes.add(attributeName); - callback(); - this.ignoredAttributes.delete(attributeName); - } - withCurrentNavigationElement(element, callback) { - this.currentNavigationElement = element; - callback(); - delete this.currentNavigationElement; - } - }; - function getFrameElementById(id2) { - if (id2 != null) { - const element = document.getElementById(id2); - if (element instanceof FrameElement) { - return element; - } - } - } - function activateElement(element, currentURL) { - if (element) { - const src = element.getAttribute("src"); - if (src != null && currentURL != null && urlsAreEqual(src, currentURL)) { - throw new Error(`Matching element has a source URL which references itself`); - } - if (element.ownerDocument !== document) { - element = document.importNode(element, true); - } - if (element instanceof FrameElement) { - element.connectedCallback(); - element.disconnectedCallback(); - return element; - } - } - } - var StreamElement = class _StreamElement extends HTMLElement { - static async renderElement(newElement) { - await newElement.performAction(); - } - async connectedCallback() { - try { - await this.render(); - } catch (error2) { - console.error(error2); - } finally { - this.disconnect(); - } - } - async render() { - var _a; - return (_a = this.renderPromise) !== null && _a !== void 0 ? _a : this.renderPromise = (async () => { - const event = this.beforeRenderEvent; - if (this.dispatchEvent(event)) { - await nextAnimationFrame(); - await event.detail.render(this); - } - })(); - } - disconnect() { - try { - this.remove(); - } catch (_a) { - } - } - removeDuplicateTargetChildren() { - this.duplicateChildren.forEach((c) => c.remove()); - } - get duplicateChildren() { - var _a; - const existingChildren = this.targetElements.flatMap((e) => [...e.children]).filter((c) => !!c.id); - const newChildrenIds = [...((_a = this.templateContent) === null || _a === void 0 ? void 0 : _a.children) || []].filter((c) => !!c.id).map((c) => c.id); - return existingChildren.filter((c) => newChildrenIds.includes(c.id)); - } - get performAction() { - if (this.action) { - const actionFunction = StreamActions[this.action]; - if (actionFunction) { - return actionFunction; - } - this.raise("unknown action"); - } - this.raise("action attribute is missing"); - } - get targetElements() { - if (this.target) { - return this.targetElementsById; - } else if (this.targets) { - return this.targetElementsByQuery; - } else { - this.raise("target or targets attribute is missing"); - } - } - get templateContent() { - return this.templateElement.content.cloneNode(true); - } - get templateElement() { - if (this.firstElementChild === null) { - const template = this.ownerDocument.createElement("template"); - this.appendChild(template); - return template; - } else if (this.firstElementChild instanceof HTMLTemplateElement) { - return this.firstElementChild; - } - this.raise("first child element must be a