Compare commits

...

18 Commits

Author SHA1 Message Date
Xe Iaso
63d557b96e Update metadata
check-spelling run (pull_request) for Xe/actorify

Signed-off-by: check-spelling-bot <check-spelling-bot@users.noreply.github.com>
on-behalf-of: @check-spelling <check-spelling-bot@check-spelling.dev>
2025-09-12 18:30:49 +00:00
Xe Iaso
d718792881 docs(internal/actorify): document package
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-12 18:27:24 +00:00
Xe Iaso
b28840f1a9 feat(store/bbolt): implement actor pattern
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-12 18:01:55 +00:00
Xe Iaso
63591866aa fix(decaymap): fix lock convoy (#1106)
* fix(decaymap): fix lock convoy

Ref #1103

This uses the actor pattern to delay deletion instead of making things
fight over a lock. It also properly fixes locking logic to prevent the
convoy problem.

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs: update CHANGELOG

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-12 16:43:08 +00:00
Xe Iaso
f79d36d21e docs: update CHANGELOG properly
It helps if you save your editor buffer!

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-11 14:07:52 +00:00
Xe Iaso
f5b5243b5e docs: update CHANGELOG
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-11 14:04:32 +00:00
Xe Iaso
2011b83a44 chore: port client-side JS to TypeScript (#1100)
* chore(challenge/preact): port to typescript

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(js/algorithms): port to typescript

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(js/worker): port to typescript

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(web): fix TypeScript build logic

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(web): port bench.mjs to typescript

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(web): port main.mjs to typescript

Signed-off-by: Xe Iaso <me@xeiaso.net>

* Update metadata

check-spelling run (pull_request) for Xe/use-typescript

Signed-off-by: check-spelling-bot <check-spelling-bot@users.noreply.github.com>
on-behalf-of: @check-spelling <check-spelling-bot@check-spelling.dev>

* fix(js/algorithms/fast): handle old browsers

Closes #1082

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Signed-off-by: check-spelling-bot <check-spelling-bot@users.noreply.github.com>
2025-09-11 10:03:10 -04:00
Martin
8ed89a6c6e feat(lib): Add option for adding difficulty field to JWT claims (#1063)
* Add option for difficulty JWT field

* Add DIFFICULTY_IN_JWT option to docs

* Add missing_required_forwarded_headers to lt translation via Google Translate

* docs(CHANGELOG): move CHANGELOG entry to the top

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2025-09-11 13:50:33 +00:00
Xe Iaso
9430d0e6a5 fix(cmd/containerbuild): support commas in --docker-tags (#1099)
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-08 22:19:42 +00:00
Xe Iaso
8b9dafac51 security: npm audit fix for GHSA-hfm8-9jrf-7g9w et. al (#1098)
* security: npm audit fix for GHSA-hfm8-9jrf-7g9w et. al

Closes #1097

I'm not sure that this is required, but I'd sleep better at night not
finding out that it is required the hard way.

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: bump postcss version

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-08 14:17:59 -04:00
dependabot[bot]
9997130a7c build(deps): bump the github-actions group with 4 updates (#1093)
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-09-07 22:01:27 -04:00
Jason Cameron
e239083944 docs: add reminder for verified signatures in PR template (#1092) 2025-09-07 16:15:26 -04:00
Jason Cameron
abf6c8de57 feat: Warn on missing signing keys when persisting challenges (#1088) 2025-09-07 15:43:58 -04:00
Xe Iaso
7e1b5d9951 fix: demote temporal assurance checks
* fix(challenge): demote temporal assurance to 80% instead of 95%

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(challenge/preact): wait a little longer to be extra safe

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(challenge/metarefresh): wait a little longer to be extra safe

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs(CHANGELOG): add fix notes

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-07 16:10:54 +00:00
Xe Iaso
98945fb56f feat(lib/store): add s3api storage backend (#1089)
* feat(lib/store): add s3api storage backend

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs(store/s3api): replace fake S3 API keys with the bee movie script

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs(store/s3api): fix spelling sin

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(store/s3api): remove vestigal experiment

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(store/s3api): support IsPersistent call

Ref #1088

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(test): go mod tidy

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-07 09:24:14 -04:00
Jason Cameron
82099d9e05 fix(robots2policy): handle multiple user agents under one block (#925) 2025-09-06 22:35:19 -04:00
dependabot[bot]
87c2f1e0e6 build(deps): bump the github-actions group across 1 directory with 8 updates (#1071)
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-09-06 22:30:43 -04:00
Jason Cameron
f0199d014f docs: document some missing env vars (#1087) 2025-09-07 01:34:42 +00:00
64 changed files with 2386 additions and 634 deletions

View File

@@ -9,3 +9,4 @@ Checklist:
- [ ] Added a description of the changes to the `[Unreleased]` section of docs/docs/CHANGELOG.md - [ ] Added a description of the changes to the `[Unreleased]` section of docs/docs/CHANGELOG.md
- [ ] Added test cases to [the relevant parts of the codebase](https://anubis.techaro.lol/docs/developer/code-quality) - [ ] Added test cases to [the relevant parts of the codebase](https://anubis.techaro.lol/docs/developer/code-quality)
- [ ] Ran integration tests `npm run test:integration` (unsupported on Windows, please use WSL) - [ ] Ran integration tests `npm run test:integration` (unsupported on Windows, please use WSL)
- [ ] All of my commits have [verified signatures](https://anubis.techaro.lol/docs/developer/signed-commits)

View File

@@ -5,4 +5,5 @@ ubuntu
workarounds workarounds
rjack rjack
msgbox msgbox
xeact xeact
ABee

View File

@@ -1,4 +1,7 @@
acs acs
Actorified
actorifiedstore
actorify
Aibrew Aibrew
alibaba alibaba
alrest alrest
@@ -140,6 +143,7 @@ headermap
healthcheck healthcheck
healthz healthz
hec hec
Hetzner
hmc hmc
homelab homelab
hostable hostable
@@ -156,6 +160,7 @@ ifm
Imagesift Imagesift
imgproxy imgproxy
impressum impressum
inbox
inp inp
internets internets
IPTo IPTo
@@ -213,6 +218,7 @@ nicksnyder
nobots nobots
NONINFRINGEMENT NONINFRINGEMENT
nosleep nosleep
nullglob
OCOB OCOB
ogtag ogtag
oklch oklch
@@ -237,7 +243,6 @@ pki
podkova podkova
podman podman
poststart poststart
poxied
prebaked prebaked
privkey privkey
promauto promauto
@@ -250,7 +255,6 @@ pwuser
qualys qualys
qwant qwant
qwantbot qwantbot
QWEN
rac rac
rawler rawler
rcvar rcvar
@@ -279,10 +283,10 @@ Seo
setsebool setsebool
shellcheck shellcheck
shirou shirou
shopt
Sidetrade Sidetrade
simprint simprint
sitemap sitemap
Slackware
sls sls
Smartphone Smartphone
sni sni
@@ -360,6 +364,7 @@ XOriginal
XReal XReal
yae yae
YAMLTo YAMLTo
Yda
yeet yeet
yeetfile yeetfile
yourdomain yourdomain

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
fetch-tags: true fetch-tags: true
fetch-depth: 0 fetch-depth: 0
@@ -25,7 +25,7 @@ jobs:
uses: Homebrew/actions/setup-homebrew@main uses: Homebrew/actions/setup-homebrew@main
- name: Setup Homebrew cellar cache - name: Setup Homebrew cellar cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: | path: |
/home/linuxbrew/.linuxbrew/Cellar /home/linuxbrew/.linuxbrew/Cellar
@@ -47,7 +47,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
with: with:
images: ghcr.io/${{ github.repository }} images: ghcr.io/${{ github.repository }}

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
fetch-tags: true fetch-tags: true
fetch-depth: 0 fetch-depth: 0
@@ -35,7 +35,7 @@ jobs:
uses: Homebrew/actions/setup-homebrew@main uses: Homebrew/actions/setup-homebrew@main
- name: Setup Homebrew cellar cache - name: Setup Homebrew cellar cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: | path: |
/home/linuxbrew/.linuxbrew/Cellar /home/linuxbrew/.linuxbrew/Cellar
@@ -56,7 +56,7 @@ jobs:
brew bundle brew bundle
- name: Log into registry - name: Log into registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -64,7 +64,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
with: with:
images: ${{ env.IMAGE }} images: ${{ env.IMAGE }}
@@ -78,7 +78,7 @@ jobs:
SLOG_LEVEL: debug SLOG_LEVEL: debug
- name: Generate artifact attestation - name: Generate artifact attestation
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with: with:
subject-name: ${{ env.IMAGE }} subject-name: ${{ env.IMAGE }}
subject-digest: ${{ steps.build.outputs.digest }} subject-digest: ${{ steps.build.outputs.digest }}

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
@@ -25,7 +25,7 @@ jobs:
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Log into registry - name: Log into registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with: with:
registry: ghcr.io registry: ghcr.io
username: techarohq username: techarohq
@@ -33,7 +33,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
with: with:
images: ghcr.io/techarohq/anubis/docs images: ghcr.io/techarohq/anubis/docs
tags: | tags: |
@@ -53,14 +53,14 @@ jobs:
push: true push: true
- name: Apply k8s manifests to limsa lominsa - name: Apply k8s manifests to limsa lominsa
uses: actions-hub/kubectl@b5b19eeb6a0ffde16637e398f8b96ef01eb8fdb7 # v1.33.3 uses: actions-hub/kubectl@af345ed727f0268738e65be48422e463cc67c220 # v1.34.0
env: env:
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }} KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
with: with:
args: apply -k docs/manifest args: apply -k docs/manifest
- name: Apply k8s manifests to limsa lominsa - name: Apply k8s manifests to limsa lominsa
uses: actions-hub/kubectl@b5b19eeb6a0ffde16637e398f8b96ef01eb8fdb7 # v1.33.3 uses: actions-hub/kubectl@af345ed727f0268738e65be48422e463cc67c220 # v1.34.0
env: env:
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }} KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
with: with:

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
@@ -22,7 +22,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
with: with:
images: ghcr.io/techarohq/anubis/docs images: ghcr.io/techarohq/anubis/docs
tags: | tags: |

View File

@@ -15,7 +15,7 @@ jobs:
#runs-on: alrest-techarohq #runs-on: alrest-techarohq
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
@@ -28,7 +28,7 @@ jobs:
uses: Homebrew/actions/setup-homebrew@main uses: Homebrew/actions/setup-homebrew@main
- name: Setup Homebrew cellar cache - name: Setup Homebrew cellar cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: | path: |
/home/linuxbrew/.linuxbrew/Cellar /home/linuxbrew/.linuxbrew/Cellar
@@ -49,7 +49,7 @@ jobs:
brew bundle brew bundle
- name: Setup Golang caches - name: Setup Golang caches
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: | path: |
~/.cache/go-build ~/.cache/go-build
@@ -59,7 +59,7 @@ jobs:
${{ runner.os }}-golang- ${{ runner.os }}-golang-
- name: Cache playwright binaries - name: Cache playwright binaries
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
id: playwright-cache id: playwright-cache
with: with:
path: | path: |

View File

@@ -14,7 +14,7 @@ jobs:
#runs-on: alrest-techarohq #runs-on: alrest-techarohq
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
fetch-tags: true fetch-tags: true
@@ -29,7 +29,7 @@ jobs:
uses: Homebrew/actions/setup-homebrew@main uses: Homebrew/actions/setup-homebrew@main
- name: Setup Homebrew cellar cache - name: Setup Homebrew cellar cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: | path: |
/home/linuxbrew/.linuxbrew/Cellar /home/linuxbrew/.linuxbrew/Cellar
@@ -50,7 +50,7 @@ jobs:
brew bundle brew bundle
- name: Setup Golang caches - name: Setup Golang caches
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: | path: |
~/.cache/go-build ~/.cache/go-build

View File

@@ -15,7 +15,7 @@ jobs:
#runs-on: alrest-techarohq #runs-on: alrest-techarohq
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
fetch-tags: true fetch-tags: true
@@ -30,7 +30,7 @@ jobs:
uses: Homebrew/actions/setup-homebrew@main uses: Homebrew/actions/setup-homebrew@main
- name: Setup Homebrew cellar cache - name: Setup Homebrew cellar cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: | path: |
/home/linuxbrew/.linuxbrew/Cellar /home/linuxbrew/.linuxbrew/Cellar
@@ -51,7 +51,7 @@ jobs:
brew bundle brew bundle
- name: Setup Golang caches - name: Setup Golang caches
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: | path: |
~/.cache/go-build ~/.cache/go-build

View File

@@ -24,15 +24,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with: with:
node-version: latest node-version: latest
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: stable go-version: stable

View File

@@ -18,13 +18,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
fetch-tags: true fetch-tags: true
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Log into registry - name: Log into registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}

View File

@@ -20,7 +20,7 @@ jobs:
- ci@ppc64le.techaro.lol - ci@ppc64le.techaro.lol
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
fetch-tags: true fetch-tags: true
fetch-depth: 0 fetch-depth: 0
@@ -33,7 +33,7 @@ jobs:
name: id_rsa name: id_rsa
known_hosts: ${{ secrets.CI_SSH_KNOWN_HOSTS }} known_hosts: ${{ secrets.CI_SSH_KNOWN_HOSTS }}
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: stable go-version: stable

View File

@@ -16,12 +16,12 @@ jobs:
security-events: write security-events: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Install the latest version of uv - name: Install the latest version of uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: Run zizmor 🌈 - name: Run zizmor 🌈
run: uvx zizmor --format sarif . > results.sarif run: uvx zizmor --format sarif . > results.sarif
@@ -29,7 +29,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file - name: Upload SARIF file
uses: github/codeql-action/upload-sarif@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 uses: github/codeql-action/upload-sarif@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1
with: with:
sarif_file: results.sarif sarif_file: results.sarif
category: zizmor category: zizmor

View File

@@ -51,6 +51,7 @@ var (
cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for") cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for")
cookiePrefix = flag.String("cookie-prefix", anubis.CookieName, "prefix for browser cookies created by Anubis") cookiePrefix = flag.String("cookie-prefix", anubis.CookieName, "prefix for browser cookies created by Anubis")
cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support") cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support")
difficultyInJWT = flag.Bool("difficulty-in-jwt", false, "if true, adds a difficulty field in the JWT claims")
useSimplifiedExplanation = flag.Bool("use-simplified-explanation", false, "if true, replaces the text when clicking \"Why am I seeing this?\" with a more simplified text for a non-tech-savvy audience.") useSimplifiedExplanation = flag.Bool("use-simplified-explanation", false, "if true, replaces the text when clicking \"Why am I seeing this?\" with a more simplified text for a non-tech-savvy audience.")
forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header") forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header")
hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set") hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set")
@@ -317,6 +318,16 @@ func main() {
log.Fatalf("can't parse policy file: %v", err) log.Fatalf("can't parse policy file: %v", err)
} }
// Warn if persistent storage is used without a configured signing key
if policy.Store.IsPersistent() {
if *hs512Secret == "" && *ed25519PrivateKeyHex == "" && *ed25519PrivateKeyHexFile == "" {
slog.Warn("[misconfiguration] persistent storage backend is configured, but no private key is set. " +
"Challenges will be invalidated when Anubis restarts. " +
"Set HS512_SECRET, ED25519_PRIVATE_KEY_HEX, or ED25519_PRIVATE_KEY_HEX_FILE to ensure challenges survive service restarts. " +
"See: https://anubis.techaro.lol/docs/admin/installation#key-generation")
}
}
ruleErrorIDs := make(map[string]string) ruleErrorIDs := make(map[string]string)
for _, rule := range policy.Bots { for _, rule := range policy.Bots {
if rule.Action != config.RuleDeny { if rule.Action != config.RuleDeny {
@@ -423,6 +434,7 @@ func main() {
CookieSecure: *cookieSecure, CookieSecure: *cookieSecure,
PublicUrl: *publicUrl, PublicUrl: *publicUrl,
JWTRestrictionHeader: *jwtRestrictionHeader, JWTRestrictionHeader: *jwtRestrictionHeader,
DifficultyInJWT: *difficultyInJWT,
}) })
if err != nil { if err != nil {
log.Fatalf("can't construct libanubis.Server: %v", err) log.Fatalf("can't construct libanubis.Server: %v", err)

View File

@@ -46,6 +46,11 @@ func main() {
) )
} }
if strings.Contains(*dockerTags, ",") {
newTags := strings.Join(strings.Split(*dockerTags, ","), "\n")
dockerTags = &newTags
}
setOutput("docker_image", strings.SplitN(*dockerTags, "\n", 2)[0]) setOutput("docker_image", strings.SplitN(*dockerTags, "\n", 2)[0])
version, err := run("git describe --tags --always --dirty") version, err := run("git describe --tags --always --dirty")

View File

@@ -29,7 +29,7 @@ var (
) )
type RobotsRule struct { type RobotsRule struct {
UserAgent string UserAgents []string
Disallows []string Disallows []string
Allows []string Allows []string
CrawlDelay int CrawlDelay int
@@ -130,10 +130,26 @@ func main() {
} }
} }
func createRuleFromAccumulated(userAgents, disallows, allows []string, crawlDelay int) RobotsRule {
rule := RobotsRule{
UserAgents: make([]string, len(userAgents)),
Disallows: make([]string, len(disallows)),
Allows: make([]string, len(allows)),
CrawlDelay: crawlDelay,
}
copy(rule.UserAgents, userAgents)
copy(rule.Disallows, disallows)
copy(rule.Allows, allows)
return rule
}
func parseRobotsTxt(input io.Reader) ([]RobotsRule, error) { func parseRobotsTxt(input io.Reader) ([]RobotsRule, error) {
scanner := bufio.NewScanner(input) scanner := bufio.NewScanner(input)
var rules []RobotsRule var rules []RobotsRule
var currentRule *RobotsRule var currentUserAgents []string
var currentDisallows []string
var currentAllows []string
var currentCrawlDelay int
for scanner.Scan() { for scanner.Scan() {
line := strings.TrimSpace(scanner.Text()) line := strings.TrimSpace(scanner.Text())
@@ -154,38 +170,42 @@ func parseRobotsTxt(input io.Reader) ([]RobotsRule, error) {
switch directive { switch directive {
case "user-agent": case "user-agent":
// Start a new rule section // If we have accumulated rules with directives and encounter a new user-agent,
if currentRule != nil { // flush the current rules
rules = append(rules, *currentRule) if len(currentUserAgents) > 0 && (len(currentDisallows) > 0 || len(currentAllows) > 0 || currentCrawlDelay > 0) {
} rule := createRuleFromAccumulated(currentUserAgents, currentDisallows, currentAllows, currentCrawlDelay)
currentRule = &RobotsRule{ rules = append(rules, rule)
UserAgent: value, // Reset for next group
Disallows: make([]string, 0), currentUserAgents = nil
Allows: make([]string, 0), currentDisallows = nil
currentAllows = nil
currentCrawlDelay = 0
} }
currentUserAgents = append(currentUserAgents, value)
case "disallow": case "disallow":
if currentRule != nil && value != "" { if len(currentUserAgents) > 0 && value != "" {
currentRule.Disallows = append(currentRule.Disallows, value) currentDisallows = append(currentDisallows, value)
} }
case "allow": case "allow":
if currentRule != nil && value != "" { if len(currentUserAgents) > 0 && value != "" {
currentRule.Allows = append(currentRule.Allows, value) currentAllows = append(currentAllows, value)
} }
case "crawl-delay": case "crawl-delay":
if currentRule != nil { if len(currentUserAgents) > 0 {
if delay, err := parseIntSafe(value); err == nil { if delay, err := parseIntSafe(value); err == nil {
currentRule.CrawlDelay = delay currentCrawlDelay = delay
} }
} }
} }
} }
// Don't forget the last rule // Don't forget the last group of rules
if currentRule != nil { if len(currentUserAgents) > 0 {
rules = append(rules, *currentRule) rule := createRuleFromAccumulated(currentUserAgents, currentDisallows, currentAllows, currentCrawlDelay)
rules = append(rules, rule)
} }
// Mark blacklisted user agents (those with "Disallow: /") // Mark blacklisted user agents (those with "Disallow: /")
@@ -211,10 +231,11 @@ func convertToAnubisRules(robotsRules []RobotsRule) []AnubisRule {
var anubisRules []AnubisRule var anubisRules []AnubisRule
ruleCounter := 0 ruleCounter := 0
// Process each robots rule individually
for _, robotsRule := range robotsRules { for _, robotsRule := range robotsRules {
userAgent := robotsRule.UserAgent userAgents := robotsRule.UserAgents
// Handle crawl delay as weight adjustment (do this first before any continues) // Handle crawl delay
if robotsRule.CrawlDelay > 0 && *crawlDelay > 0 { if robotsRule.CrawlDelay > 0 && *crawlDelay > 0 {
ruleCounter++ ruleCounter++
rule := AnubisRule{ rule := AnubisRule{
@@ -223,20 +244,32 @@ func convertToAnubisRules(robotsRules []RobotsRule) []AnubisRule {
Weight: &config.Weight{Adjust: *crawlDelay}, Weight: &config.Weight{Adjust: *crawlDelay},
} }
if userAgent == "*" { if len(userAgents) == 1 && userAgents[0] == "*" {
rule.Expression = &config.ExpressionOrList{ rule.Expression = &config.ExpressionOrList{
All: []string{"true"}, // Always applies All: []string{"true"}, // Always applies
} }
} else { } else if len(userAgents) == 1 {
rule.Expression = &config.ExpressionOrList{ rule.Expression = &config.ExpressionOrList{
All: []string{fmt.Sprintf("userAgent.contains(%q)", userAgent)}, All: []string{fmt.Sprintf("userAgent.contains(%q)", userAgents[0])},
}
} else {
// Multiple user agents - use any block
var expressions []string
for _, ua := range userAgents {
if ua == "*" {
expressions = append(expressions, "true")
} else {
expressions = append(expressions, fmt.Sprintf("userAgent.contains(%q)", ua))
}
}
rule.Expression = &config.ExpressionOrList{
Any: expressions,
} }
} }
anubisRules = append(anubisRules, rule) anubisRules = append(anubisRules, rule)
} }
// Handle blacklisted user agents (complete deny/challenge) // Handle blacklisted user agents
if robotsRule.IsBlacklist { if robotsRule.IsBlacklist {
ruleCounter++ ruleCounter++
rule := AnubisRule{ rule := AnubisRule{
@@ -244,21 +277,36 @@ func convertToAnubisRules(robotsRules []RobotsRule) []AnubisRule {
Action: *userAgentDeny, Action: *userAgentDeny,
} }
if userAgent == "*" { if len(userAgents) == 1 {
// This would block everything - convert to a weight adjustment instead userAgent := userAgents[0]
rule.Name = fmt.Sprintf("%s-global-restriction-%d", *policyName, ruleCounter) if userAgent == "*" {
rule.Action = "WEIGH" // This would block everything - convert to a weight adjustment instead
rule.Weight = &config.Weight{Adjust: 20} // Increase difficulty significantly rule.Name = fmt.Sprintf("%s-global-restriction-%d", *policyName, ruleCounter)
rule.Expression = &config.ExpressionOrList{ rule.Action = "WEIGH"
All: []string{"true"}, // Always applies rule.Weight = &config.Weight{Adjust: 20} // Increase difficulty significantly
rule.Expression = &config.ExpressionOrList{
All: []string{"true"}, // Always applies
}
} else {
rule.Expression = &config.ExpressionOrList{
All: []string{fmt.Sprintf("userAgent.contains(%q)", userAgent)},
}
} }
} else { } else {
// Multiple user agents - use any block
var expressions []string
for _, ua := range userAgents {
if ua == "*" {
expressions = append(expressions, "true")
} else {
expressions = append(expressions, fmt.Sprintf("userAgent.contains(%q)", ua))
}
}
rule.Expression = &config.ExpressionOrList{ rule.Expression = &config.ExpressionOrList{
All: []string{fmt.Sprintf("userAgent.contains(%q)", userAgent)}, Any: expressions,
} }
} }
anubisRules = append(anubisRules, rule) anubisRules = append(anubisRules, rule)
continue
} }
// Handle specific disallow rules // Handle specific disallow rules
@@ -276,9 +324,33 @@ func convertToAnubisRules(robotsRules []RobotsRule) []AnubisRule {
// Build CEL expression // Build CEL expression
var conditions []string var conditions []string
// Add user agent condition if not wildcard // Add user agent conditions
if userAgent != "*" { if len(userAgents) == 1 && userAgents[0] == "*" {
conditions = append(conditions, fmt.Sprintf("userAgent.contains(%q)", userAgent)) // Wildcard user agent - no user agent condition needed
} else if len(userAgents) == 1 {
conditions = append(conditions, fmt.Sprintf("userAgent.contains(%q)", userAgents[0]))
} else {
// For multiple user agents, we need to use a more complex expression
// This is a limitation - we can't easily combine any for user agents with all for path
// So we'll create separate rules for each user agent
for _, ua := range userAgents {
if ua == "*" {
continue // Skip wildcard as it's handled separately
}
ruleCounter++
subRule := AnubisRule{
Name: fmt.Sprintf("%s-disallow-%d", *policyName, ruleCounter),
Action: *baseAction,
Expression: &config.ExpressionOrList{
All: []string{
fmt.Sprintf("userAgent.contains(%q)", ua),
buildPathCondition(disallow),
},
},
}
anubisRules = append(anubisRules, subRule)
}
continue
} }
// Add path condition // Add path condition
@@ -291,7 +363,6 @@ func convertToAnubisRules(robotsRules []RobotsRule) []AnubisRule {
anubisRules = append(anubisRules, rule) anubisRules = append(anubisRules, rule)
} }
} }
return anubisRules return anubisRules

View File

@@ -78,6 +78,12 @@ func TestDataFileConversion(t *testing.T) {
expectedFile: "complex.yaml", expectedFile: "complex.yaml",
options: TestOptions{format: "yaml", crawlDelayWeight: 5}, options: TestOptions{format: "yaml", crawlDelayWeight: 5},
}, },
{
name: "consecutive_user_agents",
robotsFile: "consecutive.robots.txt",
expectedFile: "consecutive.yaml",
options: TestOptions{format: "yaml", crawlDelayWeight: 3},
},
} }
for _, tc := range testCases { for _, tc := range testCases {

View File

@@ -25,6 +25,6 @@
- action: CHALLENGE - action: CHALLENGE
expression: expression:
all: all:
- userAgent.contains("Googlebot") - userAgent.contains("Googlebot")
- path.startsWith("/search") - path.startsWith("/search")
name: robots-txt-policy-disallow-7 name: robots-txt-policy-disallow-7

View File

@@ -20,8 +20,8 @@
- action: CHALLENGE - action: CHALLENGE
expression: expression:
all: all:
- userAgent.contains("Googlebot") - userAgent.contains("Googlebot")
- path.startsWith("/search/") - path.startsWith("/search/")
name: robots-txt-policy-disallow-6 name: robots-txt-policy-disallow-6
- action: WEIGH - action: WEIGH
expression: userAgent.contains("Bingbot") expression: userAgent.contains("Bingbot")
@@ -31,14 +31,14 @@
- action: CHALLENGE - action: CHALLENGE
expression: expression:
all: all:
- userAgent.contains("Bingbot") - userAgent.contains("Bingbot")
- path.startsWith("/search/") - path.startsWith("/search/")
name: robots-txt-policy-disallow-8 name: robots-txt-policy-disallow-8
- action: CHALLENGE - action: CHALLENGE
expression: expression:
all: all:
- userAgent.contains("Bingbot") - userAgent.contains("Bingbot")
- path.startsWith("/admin/") - path.startsWith("/admin/")
name: robots-txt-policy-disallow-9 name: robots-txt-policy-disallow-9
- action: DENY - action: DENY
expression: userAgent.contains("BadBot") expression: userAgent.contains("BadBot")
@@ -54,18 +54,18 @@
- action: CHALLENGE - action: CHALLENGE
expression: expression:
all: all:
- userAgent.contains("TestBot") - userAgent.contains("TestBot")
- path.matches("^/.*/admin") - path.matches("^/.*/admin")
name: robots-txt-policy-disallow-13 name: robots-txt-policy-disallow-13
- action: CHALLENGE - action: CHALLENGE
expression: expression:
all: all:
- userAgent.contains("TestBot") - userAgent.contains("TestBot")
- path.matches("^/temp.*\\.html") - path.matches("^/temp.*\\.html")
name: robots-txt-policy-disallow-14 name: robots-txt-policy-disallow-14
- action: CHALLENGE - action: CHALLENGE
expression: expression:
all: all:
- userAgent.contains("TestBot") - userAgent.contains("TestBot")
- path.matches("^/file.\\.log") - path.matches("^/file.\\.log")
name: robots-txt-policy-disallow-15 name: robots-txt-policy-disallow-15

View File

@@ -0,0 +1,25 @@
# Test consecutive user agents that should be grouped into any: blocks
User-agent: *
Disallow: /admin
Crawl-delay: 10
# Multiple consecutive user agents - should be grouped
User-agent: BadBot
User-agent: SpamBot
User-agent: EvilBot
Disallow: /
# Single user agent - should be separate
User-agent: GoodBot
Disallow: /private
# Multiple consecutive user agents with crawl delay
User-agent: SlowBot1
User-agent: SlowBot2
Crawl-delay: 5
# Multiple consecutive user agents with specific path
User-agent: SearchBot1
User-agent: SearchBot2
User-agent: SearchBot3
Disallow: /search

View File

@@ -0,0 +1,47 @@
- action: WEIGH
expression: "true"
name: robots-txt-policy-crawl-delay-1
weight:
adjust: 3
- action: CHALLENGE
expression: path.startsWith("/admin")
name: robots-txt-policy-disallow-2
- action: DENY
expression:
any:
- userAgent.contains("BadBot")
- userAgent.contains("SpamBot")
- userAgent.contains("EvilBot")
name: robots-txt-policy-blacklist-3
- action: CHALLENGE
expression:
all:
- userAgent.contains("GoodBot")
- path.startsWith("/private")
name: robots-txt-policy-disallow-4
- action: WEIGH
expression:
any:
- userAgent.contains("SlowBot1")
- userAgent.contains("SlowBot2")
name: robots-txt-policy-crawl-delay-5
weight:
adjust: 3
- action: CHALLENGE
expression:
all:
- userAgent.contains("SearchBot1")
- path.startsWith("/search")
name: robots-txt-policy-disallow-7
- action: CHALLENGE
expression:
all:
- userAgent.contains("SearchBot2")
- path.startsWith("/search")
name: robots-txt-policy-disallow-8
- action: CHALLENGE
expression:
all:
- userAgent.contains("SearchBot3")
- path.startsWith("/search")
name: robots-txt-policy-disallow-9

View File

@@ -1,12 +1,12 @@
[ [
{ {
"action": "CHALLENGE",
"expression": "path.startsWith(\"/admin/\")", "expression": "path.startsWith(\"/admin/\")",
"name": "robots-txt-policy-disallow-1" "name": "robots-txt-policy-disallow-1",
"action": "CHALLENGE"
}, },
{ {
"action": "CHALLENGE",
"expression": "path.startsWith(\"/private\")", "expression": "path.startsWith(\"/private\")",
"name": "robots-txt-policy-disallow-2" "name": "robots-txt-policy-disallow-2",
"action": "CHALLENGE"
} }
] ]

View File

@@ -14,6 +14,12 @@ func Zilch[T any]() T {
type Impl[K comparable, V any] struct { type Impl[K comparable, V any] struct {
data map[K]decayMapEntry[V] data map[K]decayMapEntry[V]
lock sync.RWMutex lock sync.RWMutex
// deleteCh receives decay-deletion requests from readers.
deleteCh chan deleteReq[K]
// stopCh stops the background cleanup worker.
stopCh chan struct{}
wg sync.WaitGroup
} }
type decayMapEntry[V any] struct { type decayMapEntry[V any] struct {
@@ -21,30 +27,38 @@ type decayMapEntry[V any] struct {
expiry time.Time expiry time.Time
} }
// deleteReq is a request to remove a key if its expiry timestamp still matches
// the observed one. This prevents racing with concurrent Set updates.
type deleteReq[K comparable] struct {
key K
expiry time.Time
}
// New creates a new DecayMap of key type K and value type V. // New creates a new DecayMap of key type K and value type V.
// //
// Key types must be comparable to work with maps. // Key types must be comparable to work with maps.
func New[K comparable, V any]() *Impl[K, V] { func New[K comparable, V any]() *Impl[K, V] {
return &Impl[K, V]{ m := &Impl[K, V]{
data: make(map[K]decayMapEntry[V]), data: make(map[K]decayMapEntry[V]),
deleteCh: make(chan deleteReq[K], 1024),
stopCh: make(chan struct{}),
} }
m.wg.Add(1)
go m.cleanupWorker()
return m
} }
// expire forcibly expires a key by setting its time-to-live one second in the past. // expire forcibly expires a key by setting its time-to-live one second in the past.
func (m *Impl[K, V]) expire(key K) bool { func (m *Impl[K, V]) expire(key K) bool {
m.lock.RLock() // Use a single write lock to avoid RUnlock->Lock convoy.
m.lock.Lock()
defer m.lock.Unlock()
val, ok := m.data[key] val, ok := m.data[key]
m.lock.RUnlock()
if !ok { if !ok {
return false return false
} }
m.lock.Lock()
val.expiry = time.Now().Add(-1 * time.Second) val.expiry = time.Now().Add(-1 * time.Second)
m.data[key] = val m.data[key] = val
m.lock.Unlock()
return true return true
} }
@@ -53,19 +67,14 @@ func (m *Impl[K, V]) expire(key K) bool {
// If the value does not exist, return false. Return true after // If the value does not exist, return false. Return true after
// deletion. // deletion.
func (m *Impl[K, V]) Delete(key K) bool { func (m *Impl[K, V]) Delete(key K) bool {
m.lock.RLock() // Use a single write lock to avoid RUnlock->Lock convoy.
_, ok := m.data[key]
m.lock.RUnlock()
if !ok {
return false
}
m.lock.Lock() m.lock.Lock()
delete(m.data, key) defer m.lock.Unlock()
m.lock.Unlock() _, ok := m.data[key]
if ok {
return true delete(m.data, key)
}
return ok
} }
// Get gets a value from the DecayMap by key. // Get gets a value from the DecayMap by key.
@@ -81,13 +90,12 @@ func (m *Impl[K, V]) Get(key K) (V, bool) {
} }
if time.Now().After(value.expiry) { if time.Now().After(value.expiry) {
m.lock.Lock() // Defer decay deletion to the background worker to avoid convoy.
// Since previously reading m.data[key], the value may have been updated. select {
// Delete the entry only if the expiry time is still the same. case m.deleteCh <- deleteReq[K]{key: key, expiry: value.expiry}:
if m.data[key].expiry.Equal(value.expiry) { default:
delete(m.data, key) // Channel full: drop request; a future Cleanup() or Get will retry.
} }
m.lock.Unlock()
return Zilch[V](), false return Zilch[V](), false
} }
@@ -125,3 +133,64 @@ func (m *Impl[K, V]) Len() int {
defer m.lock.RUnlock() defer m.lock.RUnlock()
return len(m.data) return len(m.data)
} }
// Close stops the background cleanup worker. It's optional to call; maps live
// for the process lifetime in many cases. Call in tests or when you know you no
// longer need the map to avoid goroutine leaks.
func (m *Impl[K, V]) Close() {
close(m.stopCh)
m.wg.Wait()
}
// cleanupWorker batches decay deletions to minimize lock contention.
func (m *Impl[K, V]) cleanupWorker() {
defer m.wg.Done()
batch := make([]deleteReq[K], 0, 64)
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
flush := func() {
if len(batch) == 0 {
return
}
m.applyDeletes(batch)
// reset batch without reallocating
batch = batch[:0]
}
for {
select {
case req := <-m.deleteCh:
batch = append(batch, req)
case <-ticker.C:
flush()
case <-m.stopCh:
// Drain any remaining requests then exit
for {
select {
case req := <-m.deleteCh:
batch = append(batch, req)
default:
flush()
return
}
}
}
}
}
func (m *Impl[K, V]) applyDeletes(batch []deleteReq[K]) {
now := time.Now()
m.lock.Lock()
for _, req := range batch {
entry, ok := m.data[req.key]
if !ok {
continue
}
// Only delete if the expiry is unchanged and already past.
if entry.expiry.Equal(req.expiry) && now.After(entry.expiry) {
delete(m.data, req.key)
}
}
m.lock.Unlock()
}

View File

@@ -7,6 +7,7 @@ import (
func TestImpl(t *testing.T) { func TestImpl(t *testing.T) {
dm := New[string, string]() dm := New[string, string]()
t.Cleanup(dm.Close)
dm.Set("test", "hi", 5*time.Minute) dm.Set("test", "hi", 5*time.Minute)
@@ -28,10 +29,24 @@ func TestImpl(t *testing.T) {
if ok { if ok {
t.Error("got value even though it was supposed to be expired") t.Error("got value even though it was supposed to be expired")
} }
// Deletion of expired entries after Get is deferred to a background worker.
// Assert it eventually disappears from the map.
deadline := time.Now().Add(200 * time.Millisecond)
for time.Now().Before(deadline) {
if dm.Len() == 0 {
break
}
time.Sleep(5 * time.Millisecond)
}
if dm.Len() != 0 {
t.Fatalf("expected background cleanup to remove expired key; len=%d", dm.Len())
}
} }
func TestCleanup(t *testing.T) { func TestCleanup(t *testing.T) {
dm := New[string, string]() dm := New[string, string]()
t.Cleanup(dm.Close)
dm.Set("test1", "hi1", 1*time.Second) dm.Set("test1", "hi1", 1*time.Second)
dm.Set("test2", "hi2", 2*time.Second) dm.Set("test2", "hi2", 2*time.Second)

View File

@@ -11,10 +11,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
- Document `SLOG_LEVEL` environment variable in installation guide ([#1086](https://github.com/TecharoHQ/anubis/pull/1086))
<!-- This changes the project to: --> <!-- This changes the project to: -->
- Fix lock convoy problem in decaymap ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)).
- Fix lock convoy problem in bbolt by implementing the actor pattern ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)).
- Document missing environment variables in installation guide: `SLOG_LEVEL`, `COOKIE_PREFIX`, `FORCED_LANGUAGE`, and `TARGET_DISABLE_KEEPALIVE` ([#1086](https://github.com/TecharoHQ/anubis/pull/1086)).
- Add validation warning when persistent storage is used without setting signing keys.
- Fixed `robots2policy` to properly group consecutive user agents into `any:` instead of only processing the last one ([#925](https://github.com/TecharoHQ/anubis/pull/925)).
- Add the [`s3api` storage backend](./admin/policies.mdx#s3api) to allow Anubis to use S3 API compatible object storage as its storage backend.
- Make `cmd/containerbuild` support commas for separating elements of the `--docker-tags` argument as well as newlines.
- Add the `DIFFICULTY_IN_JWT` option, which allows one to add the `difficulty` field in the JWT claims which indicates the difficulty of the token ([#1063](https://github.com/TecharoHQ/anubis/pull/1063)).
- Ported the client-side JS to TypeScript to avoid egregious errors in the future.
- Fixes concurrency problems with very old browsers ([#1082](https://github.com/TecharoHQ/anubis/issues/1082)).
### Bug Fixes
Sometimes the enhanced temporal assurance in [#1038](https://github.com/TecharoHQ/anubis/pull/1038) and [#1068](https://github.com/TecharoHQ/anubis/pull/1068) could backfire because Chromium and its ilk randomize the amount of time they wait in order to avoid a timing side channel attack. This has been fixed by both increasing the amount of time a client has to wait for the metarefresh and preact challenges as well as making the server side logic more permissive.
## v1.22.0: Yda Hext ## v1.22.0: Yda Hext
> Someone has to make an effort at reconciliation if these conflicts are ever going to end. > Someone has to make an effort at reconciliation if these conflicts are ever going to end.

View File

@@ -59,7 +59,7 @@ Currently the following settings are configurable via the policy file:
Anubis uses these environment variables for configuration: Anubis uses these environment variables for configuration:
| Environment Variable | Default value | Explanation | | Environment Variable | Default value | Explanation |
|:-------------------------------|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | :----------------------------- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints (everything starting with `/.within.website/x/anubis/`). For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. | | `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints (everything starting with `/.within.website/x/anubis/`). For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. |
| `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` | | `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` |
| `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. | | `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. |
@@ -67,10 +67,12 @@ Anubis uses these environment variables for configuration:
| `COOKIE_DYNAMIC_DOMAIN` | false | If set to true, automatically set cookie domain fields based on the hostname of the request. EG: if you are making a request to `anubis.techaro.lol`, the Anubis cookie will be valid for any subdomain of `techaro.lol`. | | `COOKIE_DYNAMIC_DOMAIN` | false | If set to true, automatically set cookie domain fields based on the hostname of the request. EG: if you are making a request to `anubis.techaro.lol`, the Anubis cookie will be valid for any subdomain of `techaro.lol`. |
| `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. | | `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. |
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. | | `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
| `COOKIE_PREFIX` | `anubis-cookie` | The prefix used for browser cookies created by Anubis. Useful for customization or avoiding conflicts with other applications. |
| `COOKIE_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false | | `COOKIE_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false |
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. | | `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
| `ED25519_PRIVATE_KEY_HEX` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. When running multiple instances on the same base domain, the key must be the same across all instances. See below for details. | | `DIFFICULTY_IN_JWT` | `false` | If set to `true`, adds the `difficulty` field into JWT claims, which indicates the difficulty the token has been generated. This may be useful for statistics and debugging. |
| `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. | | `ED25519_PRIVATE_KEY_HEX` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. See below for details. |
| `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. |
| `JWT_RESTRICTION_HEADER` | `X-Real-IP` | If set, the JWT is only valid if the current value of this header matches the value when the JWT was created. You can use it e.g. to restrict a JWT to the source IP of the user using `X-Real-IP`. | | `JWT_RESTRICTION_HEADER` | `X-Real-IP` | If set, the JWT is only valid if the current value of this header matches the value when the JWT was created. You can use it e.g. to restrict a JWT to the source IP of the user using `X-Real-IP`. |
| `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more information. | | `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more information. |
| `METRICS_BIND_NETWORK` | `tcp` | The address family that the Anubis metrics server listens on. See `BIND_NETWORK` for more information. | | `METRICS_BIND_NETWORK` | `tcp` | The address family that the Anubis metrics server listens on. See `BIND_NETWORK` for more information. |
@@ -99,12 +101,14 @@ If you don't know or understand what these settings mean, ignore them. These are
::: :::
| Environment Variable | Default value | Explanation | | Environment Variable | Default value | Explanation |
| :---------------------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | :---------------------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. | | `FORCED_LANGUAGE` | unset | If set, forces Anubis to display challenge pages in the specified language instead of using the browser's Accept-Language header. Use ISO 639-1 language codes (e.g., `de` for German, `fr` for French). |
| `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. | | `HS512_SECRET` | unset | Secret string for JWT HS512 algorithm. If this is not set, Anubis will use ED25519 as defined via the variables above. The longer the better; 128 chars should suffice. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. |
| `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. | | `TARGET_DISABLE_KEEPALIVE` | `false` | If `true`, disables HTTP keep-alive for connections to the target backend. Useful for backends that don't handle keep-alive properly. |
| `HS512_SECRET` | unset | Secret string for JWT HS512 algorithm. If this is not set, Anubis will use ED25519 as defined via the variables above. The longer the better; 128 chars should suffice. | | `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. |
| `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. |
| `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. |
</details> </details>

View File

@@ -196,6 +196,83 @@ store:
path: /data/anubis.bdb path: /data/anubis.bdb
``` ```
### `s3api`
A network-backed storage layer backed by [object storage](https://en.wikipedia.org/wiki/Object_storage), specifically using the [S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_Reference.html). This can be backed by any S3-compatible object storage service such as:
- [AWS S3](https://aws.amazon.com/s3/)
- [Cloudflare R2](https://www.cloudflare.com/developer-platform/products/r2/)
- [Hetzner Object Storage](https://www.hetzner.com/storage/object-storage/)
- [Minio](https://www.min.io/)
- [Tigris](https://www.tigrisdata.com/)
If you are using a cloud platform, they likely provide an S3 compatible object storage service. If not, you may want to choose [one of the fastest options](https://www.tigrisdata.com/blog/benchmark-small-objects/).
| Should I use this backend? | Yes/no |
| :------------------------------------------------------------ | :----- |
| Are you running only one instance of Anubis for this service? | 🚫 No |
| Does your service get a lot of traffic? | ✅ Yes |
| Do you want to store data persistently when Anubis restarts? | ✅ Yes |
| Do you run Anubis without mutable filesystem storage? | ✅ Yes |
:::note
Using this backend will cause a lot of S3 operations, at least one for creating challenges, one for invalidating challenges, one for updating challenges to prevent double-spends, and one for removing challenges.
:::
#### Configuration
The `s3api` backend takes the following configuration options:
| Name | Type | Example | Description |
| :----------- | :------ | :------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ |
| `bucketName` | string | The name of the dedicated bucket for Anubis to store information in. |
| `pathStyle` | boolean | `false` | If true, use path-style S3 API operations. Please consult your storage provider's documentation if you don't know what you should put here. |
:::note
You should probably enable a lifecycle expiration rule for buckets containing Anubis data. Here is an example policy:
```json
{
"Rules": [
{
"Status": "Enabled",
"Expiration": {
"Days": 7
}
}
]
}
```
Adjust this as facts and circumstances demand, but 7 days should be enough for anyone.
:::
Example:
Assuming your environment looks like this:
```sh
# All of the following are fake credentials that look like real ones.
AWS_ACCESS_KEY_ID=accordingToAllKnownRulesOfAviation
AWS_SECRET_ACCESS_KEY=thereIsNoWayABeeShouldBeAbleToFly
AWS_REGION=yow
AWS_ENDPOINT_URL_S3=https://yow.s3.probably-not-malware.lol
```
Then your configuration would look like this:
```yaml
store:
backend: s3api
parameters:
bucketName: techaro-prod-anubis
pathStyle: false
```
### `valkey` ### `valkey`
[Valkey](https://valkey.io/) is an in-memory key/value store that clients access over the network. This allows multiple instances of Anubis to share information and does not require each instance of Anubis to have persistent filesystem storage. [Valkey](https://valkey.io/) is an in-memory key/value store that clients access over the network. This allows multiple instances of Anubis to share information and does not require each instance of Anubis to have persistent filesystem storage.

677
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "^3.8.1", "@docusaurus/module-type-aliases": "^3.0.1",
"@docusaurus/tsconfig": "^3.8.1", "@docusaurus/tsconfig": "^3.8.1",
"@docusaurus/types": "^3.8.1", "@docusaurus/types": "^3.8.1",
"typescript": "~5.6.2" "typescript": "~5.6.2"
@@ -45,4 +45,4 @@
"engines": { "engines": {
"node": ">=18.0" "node": ">=18.0"
} }
} }

18
go.mod
View File

@@ -5,6 +5,9 @@ go 1.24.2
require ( require (
github.com/TecharoHQ/thoth-proto v0.4.0 github.com/TecharoHQ/thoth-proto v0.4.0
github.com/a-h/templ v0.3.924 github.com/a-h/templ v0.3.924
github.com/aws/aws-sdk-go-v2 v1.38.3
github.com/aws/aws-sdk-go-v2/config v1.31.6
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3
github.com/cespare/xxhash/v2 v2.3.0 github.com/cespare/xxhash/v2 v2.3.0
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456 github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
github.com/gaissmai/bart v0.23.0 github.com/gaissmai/bart v0.23.0
@@ -49,6 +52,21 @@ require (
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
github.com/aws/smithy-go v1.23.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
github.com/cavaliergopher/cpio v1.0.1 // indirect github.com/cavaliergopher/cpio v1.0.1 // indirect

36
go.sum
View File

@@ -51,6 +51,42 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=
github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
github.com/aws/aws-sdk-go-v2/config v1.31.6 h1:a1t8fXY4GT4xjyJExz4knbuoxSCacB5hT/WgtfPyLjo=
github.com/aws/aws-sdk-go-v2/config v1.31.6/go.mod h1:5ByscNi7R+ztvOGzeUaIu49vkMk2soq5NaH5PYe33MQ=
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg=
github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 h1:wbjnrrMnKew78/juW7I2BtKQwa1qlf6EjQgS69uYY14=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6/go.mod h1:AtiqqNrDioJXuUgz3+3T0mBWN7Hro2n9wll2zRUc0ww=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 h1:R0tNFJqfjHL3900cqhXuwQ+1K4G0xc9Yf8EDbFXCKEw=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6/go.mod h1:y/7sDdu+aJvPtGXr4xYosdpq9a6T9Z0jkXfugmti0rI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 h1:hncKj/4gR+TPauZgTAsxOxNcvBayhUlYZ6LO/BYiQ30=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6/go.mod h1:OiIh45tp6HdJDDJGnja0mw8ihQGz3VGrUflLqSL0SmM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 h1:LHS1YAIJXJ4K9zS+1d/xa9JAA9sL2QyXIQCQFQW/X08=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6/go.mod h1:c9PCiTEuh0wQID5/KqA32J+HAgZxN9tOGXKCiYJjTZI=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 h1:nEXUSAwyUfLTgnc9cxlDWy637qsq4UWwp3sNAfl0Z3Y=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6/go.mod h1:HGzIULx4Ge3Do2V0FaiYKcyKzOqwrhUZgCI77NisswQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3 h1:ETkfWcXP2KNPLecaDa++5bsQhCRa5M5sLUJa5DWYIIg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3/go.mod h1:+/3ZTqoYb3Ur7DObD00tarKMLMuKg8iqz5CHEanqTnw=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 h1:8OLZnVJPvjnrxEwHFg9hVUof/P4sibH+Ea4KKuqAGSg=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1/go.mod h1:27M3BpVi0C02UiQh1w9nsBEit6pLhlaH3NHna6WUbDE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 h1:gKWSTnqudpo8dAxqBqZnDoDWCiEh/40FziUjr/mo6uA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2/go.mod h1:x7+rkNmRoEN1U13A6JE2fXne9EWyJy54o3n6d4mGaXQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl7fiGcfJTIo8BszI=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c=
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=

View File

@@ -0,0 +1,107 @@
// Package actorify lets you transform a parallel operation into a serialized
// operation via the Actor pattern[1].
//
// [1]: https://en.wikipedia.org/wiki/Actor_model
package actorify
import (
"context"
"errors"
)
func z[Z any]() Z {
var z Z
return z
}
var (
// ErrActorDied is returned when the actor inbox or reply channel was closed.
ErrActorDied = errors.New("actorify: the actor inbox or reply channel was closed")
)
// Handler is a function alias for the underlying logic the Actor should call.
type Handler[Input, Output any] func(ctx context.Context, input Input) (Output, error)
// Actor is a serializing wrapper that runs a function in a background goroutine.
// Whenever the Call method is invoked, a message is sent to the actor's inbox and then
// the callee waits for a response. Depending on how busy the actor is, this may take
// a moment.
type Actor[Input, Output any] struct {
handler Handler[Input, Output]
inbox chan *message[Input, Output]
}
type message[Input, Output any] struct {
ctx context.Context
arg Input
reply chan reply[Output]
}
type reply[Output any] struct {
output Output
err error
}
// New constructs a new Actor and starts its background thread. Cancel the context and you cancel
// the Actor.
func New[Input, Output any](ctx context.Context, handler Handler[Input, Output]) *Actor[Input, Output] {
result := &Actor[Input, Output]{
handler: handler,
inbox: make(chan *message[Input, Output], 32),
}
go result.handle(ctx)
return result
}
func (a *Actor[Input, Output]) handle(ctx context.Context) {
for {
select {
case <-ctx.Done():
close(a.inbox)
return
case msg, ok := <-a.inbox:
if !ok {
if msg.reply != nil {
close(msg.reply)
}
return
}
result, err := a.handler(msg.ctx, msg.arg)
reply := reply[Output]{
output: result,
err: err,
}
msg.reply <- reply
}
}
}
// Call calls the Actor with a given Input and returns the handler's Output.
//
// This only works with unary functions by design. If you need to have more inputs, define
// a struct type to use as a container.
func (a *Actor[Input, Output]) Call(ctx context.Context, input Input) (Output, error) {
replyCh := make(chan reply[Output])
a.inbox <- &message[Input, Output]{
arg: input,
reply: replyCh,
}
select {
case reply, ok := <-replyCh:
if !ok {
return z[Output](), ErrActorDied
}
return reply.output, reply.err
case <-ctx.Done():
return z[Output](), context.Cause(ctx)
}
}

View File

@@ -501,6 +501,12 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
var tokenString string var tokenString string
// check if JWTRestrictionHeader is set and header is in request // check if JWTRestrictionHeader is set and header is in request
claims := jwt.MapClaims{
"challenge": chall.ID,
"method": rule.Challenge.Algorithm,
"policyRule": rule.Hash(),
"action": string(cr.Rule),
}
if s.opts.JWTRestrictionHeader != "" { if s.opts.JWTRestrictionHeader != "" {
if r.Header.Get(s.opts.JWTRestrictionHeader) == "" { if r.Header.Get(s.opts.JWTRestrictionHeader) == "" {
lg.Error("JWTRestrictionHeader is set in config but not found in request, please check your reverse proxy config.") lg.Error("JWTRestrictionHeader is set in config but not found in request, please check your reverse proxy config.")
@@ -508,22 +514,13 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
s.respondWithError(w, r, "failed to sign JWT") s.respondWithError(w, r, "failed to sign JWT")
return return
} else { } else {
tokenString, err = s.signJWT(jwt.MapClaims{ claims["restriction"] = internal.SHA256sum(r.Header.Get(s.opts.JWTRestrictionHeader))
"challenge": chall.ID,
"method": rule.Challenge.Algorithm,
"policyRule": rule.Hash(),
"action": string(cr.Rule),
"restriction": internal.SHA256sum(r.Header.Get(s.opts.JWTRestrictionHeader)),
})
} }
} else {
tokenString, err = s.signJWT(jwt.MapClaims{
"challenge": chall.ID,
"method": rule.Challenge.Algorithm,
"policyRule": rule.Hash(),
"action": string(cr.Rule),
})
} }
if s.opts.DifficultyInJWT {
claims["difficulty"] = rule.Challenge.Difficulty
}
tokenString, err = s.signJWT(claims)
if err != nil { if err != nil {
lg.Error("failed to sign JWT", "err", err) lg.Error("failed to sign JWT", "err", err)

View File

@@ -43,7 +43,7 @@ func (i *Impl) Issue(r *http.Request, lg *slog.Logger, in *challenge.IssueInput)
} }
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error { func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error {
wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 950 * time.Millisecond) wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 800 * time.Millisecond)
if time.Now().Before(wantTime) { if time.Now().Before(wantTime) {
return challenge.NewError("validate", "insufficent time", fmt.Errorf("%w: wanted user to wait until at least %s", challenge.ErrFailed, wantTime.Format(time.RFC3339))) return challenge.NewError("validate", "insufficent time", fmt.Errorf("%w: wanted user to wait until at least %s", challenge.ErrFailed, wantTime.Format(time.RFC3339)))

View File

@@ -13,6 +13,6 @@ templ page(redir string, difficulty int, loc *localization.SimpleLocalizer) {
<img style="display:none;" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version }/> <img style="display:none;" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version }/>
<p id="status">{ loc.T("loading") }</p> <p id="status">{ loc.T("loading") }</p>
<p>{ loc.T("connection_security") }</p> <p>{ loc.T("connection_security") }</p>
<meta http-equiv="refresh" content={ fmt.Sprintf("%d; url=%s", difficulty, redir) }/> <meta http-equiv="refresh" content={ fmt.Sprintf("%d; url=%s", difficulty+1, redir) }/>
</div> </div>
} }

View File

@@ -93,9 +93,9 @@ func page(redir string, difficulty int, loc *localization.SimpleLocalizer) templ
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty, redir)) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty+1, redir))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 16, Col: 83} return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 16, Col: 85}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {

View File

@@ -40,9 +40,9 @@ for the JavaScript code in this page.
mkdir -p static/js mkdir -p static/js
for file in js/*.jsx; do for file in js/*.tsx; do
filename="${file##*/}" # Extracts "app.jsx" from "./js/app.jsx" filename="${file##*/}" # Extracts "app.jsx" from "./js/app.jsx"
output="${filename%.jsx}.js" # Changes "app.jsx" to "app.js" output="${filename%.tsx}.js" # Changes "app.jsx" to "app.js"
echo $output echo $output
esbuild "${file}" --minify --bundle --outfile=static/"${output}" --banner:js="${LICENSE}" esbuild "${file}" --minify --bundle --outfile=static/"${output}" --banner:js="${LICENSE}"

View File

@@ -1,62 +0,0 @@
import { render, h, Fragment } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { g, j, u, x } from "./xeact.js";
import { Sha256 } from '@aws-crypto/sha256-js';
/** @jsx h */
/** @jsxFrag Fragment */
function toHexString(arr) {
return Array.from(arr)
.map((c) => c.toString(16).padStart(2, "0"))
.join("");
}
const App = () => {
const [state, setState] = useState(null);
const [imageURL, setImageURL] = useState(null);
const [passed, setPassed] = useState(false);
const [challenge, setChallenge] = useState(null);
useEffect(() => {
setState(j("preact_info"));
});
useEffect(() => {
setImageURL(state.pensive_url);
const hash = new Sha256('');
hash.update(state.challenge);
setChallenge(toHexString(hash.digestSync()));
}, [state]);
useEffect(() => {
const timer = setTimeout(() => {
setPassed(true);
}, state.difficulty * 100);
return () => clearTimeout(timer);
}, [challenge]);
useEffect(() => {
window.location.href = u(state.redir, {
result: challenge,
});
}, [passed]);
return (
<>
{imageURL !== null && (
<img src={imageURL} style="width:100%;max-width:256px;" />
)}
{state !== null && (
<>
<p id="status">{state.loading_message}</p>
<p>{state.connection_security_message}</p>
</>
)}
</>
);
};
x(g("app"));
render(<App />, g("app"));

View File

@@ -0,0 +1,87 @@
import { render, h, Fragment } from "preact";
import { useState, useEffect } from "preact/hooks";
import { g, j, r, u, x } from "./xeact.js";
import { Sha256 } from "@aws-crypto/sha256-js";
/** @jsx h */
/** @jsxFrag Fragment */
function toHexString(arr: Uint8Array) {
return Array.from(arr)
.map((c) => c.toString(16).padStart(2, "0"))
.join("");
}
interface PreactInfo {
redir: string;
challenge: string;
difficulty: number;
connection_security_message: string;
loading_message: string;
pensive_url: string;
}
const App = () => {
const [state, setState] = useState<PreactInfo>();
const [imageURL, setImageURL] = useState<string | null>(null);
const [passed, setPassed] = useState<boolean>(false);
const [challenge, setChallenge] = useState<string | null>(null);
useEffect(() => {
setState(j("preact_info"));
});
useEffect(() => {
if (state === undefined) {
return;
}
setImageURL(state?.pensive_url);
const hash = new Sha256("");
hash.update(state.challenge);
setChallenge(toHexString(hash.digestSync()));
}, [state]);
useEffect(() => {
if (state === undefined) {
return;
}
const timer = setTimeout(() => {
setPassed(true);
}, state?.difficulty * 125);
return () => clearTimeout(timer);
}, [challenge]);
useEffect(() => {
if (state === undefined) {
return;
}
if (challenge === null) {
return;
}
window.location.href = u(state.redir, {
result: challenge,
});
}, [passed]);
return (
<>
{imageURL !== null && (
<img src={imageURL} style={{ width: "100%", maxWidth: "256px" }} />
)}
{state !== undefined && (
<>
<p id="status">{state.loading_message}</p>
<p>{state.connection_security_message}</p>
</>
)}
</>
);
};
x(g("app"));
render(<App />, g("app"));

View File

@@ -57,7 +57,7 @@ func (i *impl) Issue(r *http.Request, lg *slog.Logger, in *challenge.IssueInput)
} }
func (i *impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error { func (i *impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error {
wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 95 * time.Millisecond) wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 80 * time.Millisecond)
if time.Now().Before(wantTime) { if time.Now().Before(wantTime) {
return challenge.NewError("validate", "insufficent time", fmt.Errorf("%w: wanted user to wait until at least %s", challenge.ErrFailed, wantTime.Format(time.RFC3339))) return challenge.NewError("validate", "insufficent time", fmt.Errorf("%w: wanted user to wait until at least %s", challenge.ErrFailed, wantTime.Format(time.RFC3339)))

View File

@@ -46,6 +46,7 @@ type Options struct {
Logger *slog.Logger Logger *slog.Logger
PublicUrl string PublicUrl string
JWTRestrictionHeader string JWTRestrictionHeader string
DifficultyInJWT bool
} }
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int) (*policy.ParsedConfig, error) { func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {

View File

@@ -62,5 +62,6 @@
"js_iterations": "iteracijų", "js_iterations": "iteracijų",
"js_finished_reading": "Viską perskaičiau, tęskime →", "js_finished_reading": "Viską perskaičiau, tęskime →",
"js_calculation_error": "Skaičiavimo klaida!", "js_calculation_error": "Skaičiavimo klaida!",
"js_calculation_error_msg": "Nepavyko įveikti iššūkio:" "js_calculation_error_msg": "Nepavyko įveikti iššūkio:",
"missing_required_forwarded_headers": "Trūksta privalomų X-Forwarded-* antraščių"
} }

View File

@@ -0,0 +1,82 @@
package store
import (
"context"
"time"
"github.com/TecharoHQ/anubis/internal/actorify"
)
type unit struct{}
type ActorifiedStore struct {
Interface
deleteActor *actorify.Actor[string, unit]
getActor *actorify.Actor[string, []byte]
setActor *actorify.Actor[*actorSetReq, unit]
cancel context.CancelFunc
}
type actorSetReq struct {
key string
value []byte
expiry time.Duration
}
func NewActorifiedStore(backend Interface) *ActorifiedStore {
ctx, cancel := context.WithCancel(context.Background())
result := &ActorifiedStore{
Interface: backend,
cancel: cancel,
}
result.deleteActor = actorify.New(ctx, result.actorDelete)
result.getActor = actorify.New(ctx, backend.Get)
result.setActor = actorify.New(ctx, result.actorSet)
return result
}
func (a *ActorifiedStore) Close() { a.cancel() }
func (a *ActorifiedStore) Delete(ctx context.Context, key string) error {
if _, err := a.deleteActor.Call(ctx, key); err != nil {
return err
}
return nil
}
func (a *ActorifiedStore) Get(ctx context.Context, key string) ([]byte, error) {
return a.getActor.Call(ctx, key)
}
func (a *ActorifiedStore) Set(ctx context.Context, key string, value []byte, expiry time.Duration) error {
if _, err := a.setActor.Call(ctx, &actorSetReq{
key: key,
value: value,
expiry: expiry,
}); err != nil {
return err
}
return nil
}
func (a *ActorifiedStore) actorDelete(ctx context.Context, key string) (unit, error) {
if err := a.Interface.Delete(ctx, key); err != nil {
return unit{}, err
}
return unit{}, nil
}
func (a *ActorifiedStore) actorSet(ctx context.Context, req *actorSetReq) (unit, error) {
if err := a.Interface.Set(ctx, req.key, req.value, req.expiry); err != nil {
return unit{}, err
}
return unit{}, nil
}

View File

@@ -6,5 +6,6 @@ package all
import ( import (
_ "github.com/TecharoHQ/anubis/lib/store/bbolt" _ "github.com/TecharoHQ/anubis/lib/store/bbolt"
_ "github.com/TecharoHQ/anubis/lib/store/memory" _ "github.com/TecharoHQ/anubis/lib/store/memory"
_ "github.com/TecharoHQ/anubis/lib/store/s3api"
_ "github.com/TecharoHQ/anubis/lib/store/valkey" _ "github.com/TecharoHQ/anubis/lib/store/valkey"
) )

View File

@@ -11,10 +11,9 @@ import (
"go.etcd.io/bbolt" "go.etcd.io/bbolt"
) )
// Sentinel error values used for testing and in admin-visible error messages. // Sentinel error value used for testing and in admin-visible error messages.
var ( var (
ErrBucketDoesNotExist = errors.New("bbolt: bucket does not exist") ErrNotExists = errors.New("bbolt: value does not exist in store")
ErrNotExists = errors.New("bbolt: value does not exist in store")
) )
// Store implements store.Interface backed by bbolt[1]. // Store implements store.Interface backed by bbolt[1].
@@ -150,6 +149,10 @@ func (s *Store) cleanup(ctx context.Context) error {
}) })
} }
func (s *Store) IsPersistent() bool {
return true
}
func (s *Store) cleanupThread(ctx context.Context) { func (s *Store) cleanupThread(ctx context.Context) {
t := time.NewTicker(time.Hour) t := time.NewTicker(time.Hour)
defer t.Stop() defer t.Stop()

View File

@@ -48,7 +48,7 @@ func (Factory) Build(ctx context.Context, data json.RawMessage) (store.Interface
go result.cleanupThread(ctx) go result.cleanupThread(ctx)
return result, nil return store.NewActorifiedStore(result), nil
} }
// Valid parses and validates the bbolt store Config or returns // Valid parses and validates the bbolt store Config or returns

View File

@@ -37,6 +37,11 @@ type Interface interface {
// Set puts a value into the store that expires according to its expiry. // Set puts a value into the store that expires according to its expiry.
Set(ctx context.Context, key string, value []byte, expiry time.Duration) error Set(ctx context.Context, key string, value []byte, expiry time.Duration) error
// IsPersistent returns true if this storage backend persists data across
// service restarts (e.g., bbolt, valkey). Returns false for volatile storage
// like in-memory backends.
IsPersistent() bool
} }
func z[T any]() T { return *new(T) } func z[T any]() T { return *new(T) }
@@ -88,3 +93,7 @@ func (j *JSON[T]) Set(ctx context.Context, key string, value T, expiry time.Dura
return nil return nil
} }
func (j *JSON[T]) IsPersistent() bool {
return j.Underlying.IsPersistent()
}

View File

@@ -48,6 +48,10 @@ func (i *impl) Set(_ context.Context, key string, value []byte, expiry time.Dura
return nil return nil
} }
func (i *impl) IsPersistent() bool {
return false
}
func (i *impl) cleanupThread(ctx context.Context) { func (i *impl) cleanupThread(ctx context.Context) {
t := time.NewTicker(5 * time.Minute) t := time.NewTicker(5 * time.Minute)
defer t.Stop() defer t.Stop()

107
lib/store/s3api/factory.go Normal file
View File

@@ -0,0 +1,107 @@
package s3api
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/TecharoHQ/anubis/lib/store"
awsConfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
var (
ErrNoRegion = errors.New("s3api.Config: no region env var name defined")
ErrNoAccessKeyID = errors.New("s3api.Config: no access key id env var name defined")
ErrNoSecretAccessKey = errors.New("s3api.Config: no secret access key env var name defined")
ErrNoBucketName = errors.New("s3api.Config: no bucket name env var name defined")
)
func init() {
store.Register("s3api", Factory{})
}
// S3API is the subset of the AWS S3 client used by this store. It enables mocking in tests.
type S3API interface {
PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error)
HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error)
}
// Factory builds an S3-backed store. Tests can inject a Mock via Client.
// Factory can optionally carry a preconstructed S3 client (e.g., a mock in tests).
type Factory struct {
Client S3API
}
func (f Factory) Build(ctx context.Context, data json.RawMessage) (store.Interface, error) {
var config Config
if err := json.Unmarshal([]byte(data), &config); err != nil {
return nil, fmt.Errorf("%w: %w", store.ErrBadConfig, err)
}
if err := config.Valid(); err != nil {
return nil, fmt.Errorf("%w: %w", store.ErrBadConfig, err)
}
if config.BucketName == "" {
return nil, fmt.Errorf("%w: %s", store.ErrBadConfig, ErrNoBucketName)
}
// If a client was injected (e.g., tests), use it directly.
if f.Client != nil {
return &Store{
s3: f.Client,
bucket: config.BucketName,
}, nil
}
cfg, err := awsConfig.LoadDefaultConfig(ctx)
if err != nil {
return nil, fmt.Errorf("can't load AWS config from environment: %w", err)
}
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.UsePathStyle = config.PathStyle
})
return &Store{
s3: client,
bucket: config.BucketName,
}, nil
}
func (Factory) Valid(data json.RawMessage) error {
var config Config
if err := json.Unmarshal([]byte(data), &config); err != nil {
return fmt.Errorf("%w: %w", store.ErrBadConfig, err)
}
if err := config.Valid(); err != nil {
return fmt.Errorf("%w: %w", store.ErrBadConfig, err)
}
return nil
}
type Config struct {
PathStyle bool `json:"pathStyle"`
BucketName string `json:"bucketName"`
}
func (c Config) Valid() error {
var errs []error
if c.BucketName == "" {
errs = append(errs, ErrNoBucketName)
}
if len(errs) != 0 {
return fmt.Errorf("s3api.Config: invalid config: %w", errors.Join(errs...))
}
return nil
}

78
lib/store/s3api/s3api.go Normal file
View File

@@ -0,0 +1,78 @@
package s3api
import (
"bytes"
"context"
"fmt"
"io"
"strconv"
"strings"
"time"
"github.com/TecharoHQ/anubis/lib/store"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type Store struct {
s3 S3API
bucket string
}
func (s *Store) Delete(ctx context.Context, key string) error {
normKey := strings.ReplaceAll(key, ":", "/")
// Emulate not found by probing first.
if _, err := s.s3.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &s.bucket, Key: &normKey}); err != nil {
return fmt.Errorf("%w: %w", store.ErrNotFound, err)
}
if _, err := s.s3.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: &s.bucket, Key: &normKey}); err != nil {
return fmt.Errorf("can't delete from s3: %w", err)
}
return nil
}
func (s *Store) Get(ctx context.Context, key string) ([]byte, error) {
normKey := strings.ReplaceAll(key, ":", "/")
out, err := s.s3.GetObject(ctx, &s3.GetObjectInput{
Bucket: &s.bucket,
Key: &normKey,
})
if err != nil {
return nil, fmt.Errorf("%w: %w", store.ErrNotFound, err)
}
defer out.Body.Close()
if msStr, ok := out.Metadata["x-anubis-expiry-ms"]; ok && msStr != "" {
if ms, err := strconv.ParseInt(msStr, 10, 64); err == nil {
if time.Now().UnixMilli() >= ms {
_, _ = s.s3.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: &s.bucket, Key: &normKey})
return nil, store.ErrNotFound
}
}
}
b, err := io.ReadAll(out.Body)
if err != nil {
return nil, fmt.Errorf("can't read s3 object: %w", err)
}
return b, nil
}
func (s *Store) Set(ctx context.Context, key string, value []byte, expiry time.Duration) error {
normKey := strings.ReplaceAll(key, ":", "/")
// S3 has no native TTL; we store object with metadata X-Anubis-Expiry as epoch seconds.
var meta map[string]string
if expiry > 0 {
exp := time.Now().Add(expiry).UnixMilli()
meta = map[string]string{"x-anubis-expiry-ms": fmt.Sprintf("%d", exp)}
}
_, err := s.s3.PutObject(ctx, &s3.PutObjectInput{
Bucket: &s.bucket,
Key: &normKey,
Body: bytes.NewReader(value),
Metadata: meta,
})
if err != nil {
return fmt.Errorf("can't put s3 object: %w", err)
}
return nil
}
func (Store) IsPersistent() bool { return true }

View File

@@ -0,0 +1,140 @@
package s3api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"sync"
"testing"
"time"
"github.com/TecharoHQ/anubis/lib/store/storetest"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// mockS3 is an in-memory mock of the methods we use.
type mockS3 struct {
mu sync.RWMutex
bucket string
data map[string][]byte
meta map[string]map[string]string
}
func (m *mockS3) PutObject(ctx context.Context, in *s3.PutObjectInput, _ ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.data == nil {
m.data = map[string][]byte{}
}
if m.meta == nil {
m.meta = map[string]map[string]string{}
}
b, _ := io.ReadAll(in.Body)
m.data[aws.ToString(in.Key)] = bytes.Clone(b)
if in.Metadata != nil {
m.meta[aws.ToString(in.Key)] = map[string]string{}
for k, v := range in.Metadata {
m.meta[aws.ToString(in.Key)][k] = v
}
}
m.bucket = aws.ToString(in.Bucket)
return &s3.PutObjectOutput{}, nil
}
func (m *mockS3) GetObject(ctx context.Context, in *s3.GetObjectInput, _ ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
m.mu.RLock()
defer m.mu.RUnlock()
b, ok := m.data[aws.ToString(in.Key)]
if !ok {
return nil, fmt.Errorf("not found")
}
out := &s3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader(b))}
if md, ok := m.meta[aws.ToString(in.Key)]; ok {
out.Metadata = md
}
return out, nil
}
func (m *mockS3) DeleteObject(ctx context.Context, in *s3.DeleteObjectInput, _ ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.data, aws.ToString(in.Key))
delete(m.meta, aws.ToString(in.Key))
return &s3.DeleteObjectOutput{}, nil
}
func (m *mockS3) HeadObject(ctx context.Context, in *s3.HeadObjectInput, _ ...func(*s3.Options)) (*s3.HeadObjectOutput, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if _, ok := m.data[aws.ToString(in.Key)]; !ok {
return nil, fmt.Errorf("not found")
}
return &s3.HeadObjectOutput{}, nil
}
func TestImpl(t *testing.T) {
mock := &mockS3{}
f := Factory{Client: mock}
data, _ := json.Marshal(Config{
BucketName: "bucket",
})
storetest.Common(t, f, json.RawMessage(data))
}
func TestKeyNormalization(t *testing.T) {
mock := &mockS3{}
f := Factory{Client: mock}
data, _ := json.Marshal(Config{
BucketName: "anubis",
})
s, err := f.Build(t.Context(), json.RawMessage(data))
if err != nil {
t.Fatal(err)
}
key := "a:b:c"
val := []byte("value")
if err := s.Set(t.Context(), key, val, 0); err != nil {
t.Fatalf("Set failed: %v", err)
}
// Ensure mock saw normalized key
mock.mu.RLock()
_, hasRaw := mock.data["a:b:c"]
got, hasNorm := mock.data["a/b/c"]
mock.mu.RUnlock()
if hasRaw {
t.Fatalf("mock contains raw key with colon; normalization failed")
}
if !hasNorm || !bytes.Equal(got, val) {
t.Fatalf("normalized key missing or wrong value: got=%q", string(got))
}
// Get using colon key should work
out, err := s.Get(t.Context(), key)
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if !bytes.Equal(out, val) {
t.Fatalf("Get returned wrong value: got=%q", string(out))
}
// Delete using colon key should delete normalized object
if err := s.Delete(t.Context(), key); err != nil {
t.Fatalf("Delete failed: %v", err)
}
// Give any async cleanup in tests a tick (not needed for mock, but harmless)
time.Sleep(1 * time.Millisecond)
mock.mu.RLock()
_, exists := mock.data["a/b/c"]
mock.mu.RUnlock()
if exists {
t.Fatalf("normalized key still exists after Delete")
}
}

View File

@@ -47,3 +47,7 @@ func (s *Store) Set(ctx context.Context, key string, value []byte, expiry time.D
return nil return nil
} }
func (s *Store) IsPersistent() bool {
return true
}

710
package-lock.json generated
View File

@@ -19,7 +19,7 @@
"playwright": "^1.52.0", "playwright": "^1.52.0",
"postcss-cli": "^11.0.1", "postcss-cli": "^11.0.1",
"postcss-import": "^16.1.1", "postcss-import": "^16.1.1",
"postcss-import-url": "^7.2.0", "postcss-import-url": "^1.0.0",
"postcss-url": "^10.1.3" "postcss-url": "^10.1.3"
} }
}, },
@@ -553,6 +553,13 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true,
"license": "ISC"
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -651,6 +658,13 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true,
"license": "MIT"
},
"node_modules/boolbase": { "node_modules/boolbase": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -774,6 +788,18 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/cli": {
"version": "0.4.4-2",
"resolved": "https://registry.npmjs.org/cli/-/cli-0.4.4-2.tgz",
"integrity": "sha512-zvFHTz+T8S4gejPHNVtdqc0mDnWmZcwd5juDF4ScZkPerNdl/9aiWcBv3l57v81jzq+n89eYLkRJdvc5aWJROA==",
"dev": true,
"dependencies": {
"glob": ">= 3.1.4"
},
"engines": {
"node": ">=0.2.5"
}
},
"node_modules/cliui": { "node_modules/cliui": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -833,6 +859,17 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/config-chain": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ini": "^1.3.4",
"proto-list": "~1.2.1"
}
},
"node_modules/css-declaration-sorter": { "node_modules/css-declaration-sorter": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz",
@@ -1047,6 +1084,23 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/deep-equal": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.1.2.tgz",
"integrity": "sha512-rUCt39nKM7s6qUyYgp/reJmtXjgkOS/JbLO24DioMZaBNkD3b7C7cD3zJjSyjclEElNTpetAIRD6fMIbBIbX1Q==",
"dev": true,
"license": "MIT"
},
"node_modules/dependency-graph": { "node_modules/dependency-graph": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz",
@@ -1057,6 +1111,15 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/diff": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz",
"integrity": "sha512-0bTLzyr1S59cPsgAD/lR+ivvHTbgPb+k/mUR6WGqma1J6QDU+kUegI8uQFuH/cMUNK7JGN3Tk1Y5Jf2MO85WrA==",
"dev": true,
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/dom-serializer": { "node_modules/dom-serializer": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -1195,6 +1258,16 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -1222,10 +1295,19 @@
"url": "https://github.com/sponsors/rawify" "url": "https://github.com/sponsors/rawify"
} }
}, },
"node_modules/fresh": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.1.0.tgz",
"integrity": "sha512-ROG9M8tikYOuOJsvRBggh10WiQ/JebnldAwuCaQyFoiAUIE9XrYVnpznIjOQGZfCMzxzEBYHQr/LHJp3tcndzQ==",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/fs-extra": { "node_modules/fs-extra": {
"version": "11.3.0", "version": "11.3.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz",
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1237,6 +1319,13 @@
"node": ">=14.14" "node": ">=14.14"
} }
}, },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true,
"license": "ISC"
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
@@ -1272,6 +1361,28 @@
"node": "6.* || 8.* || >= 10.*" "node": "6.* || 8.* || >= 10.*"
} }
}, },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": { "node_modules/glob-parent": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@@ -1285,6 +1396,19 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/glob/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -1292,6 +1416,35 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/growl": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz",
"integrity": "sha512-VWv7s1EI41AG2LiCr7uAuxWikLDN1SQOuEUc37d/P34NAIIYgkvWYngNw0d9d9iCrDFL0SYCE9UQpxhIjjtuLg==",
"dev": true
},
"node_modules/has-ansi": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
"integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^2.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/has-ansi/node_modules/ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -1312,6 +1465,32 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true,
"license": "ISC"
},
"node_modules/is-binary-path": { "node_modules/is-binary-path": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -1391,10 +1570,68 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/jade": {
"version": "0.26.3",
"resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz",
"integrity": "sha512-mkk3vzUHFjzKjpCXeu+IjXeZD+QOTjUUdubgmHtHTDwvAO2ZTkMTTVrapts5CWz3JvJryh/4KWZpjeZrCepZ3A==",
"deprecated": "Jade has been renamed to pug, please install the latest version of pug instead of jade",
"dev": true,
"dependencies": {
"commander": "0.6.1",
"mkdirp": "0.3.0"
},
"bin": {
"jade": "bin/jade"
}
},
"node_modules/jade/node_modules/commander": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz",
"integrity": "sha512-0fLycpl1UMTGX257hRsu/arL/cUbcvQM4zMKwvLvzXtfdezIV4yotPS2dYtknF+NmEfWSoCEF6+hj9XLm/6hEw==",
"dev": true,
"engines": {
"node": ">= 0.4.x"
}
},
"node_modules/jade/node_modules/mkdirp": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
"integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==",
"deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)",
"dev": true,
"license": "MIT/X11",
"engines": {
"node": "*"
}
},
"node_modules/js-base64": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/js-beautify": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.4.2.tgz",
"integrity": "sha512-0o7oku1AcG66QoDIoSLCBENbyFgV6WHoqnZhC8oL4URTWYDzIXWo3tTGTLrLh6jR91miKS5YC+WBZeYC5iZMQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"config-chain": "~1.1.5",
"mkdirp": "0.3.5",
"nopt": "~2.1.1"
},
"bin": {
"css-beautify": "js/bin/css-beautify.js",
"html-beautify": "js/bin/html-beautify.js",
"js-beautify": "js/bin/js-beautify.js"
}
},
"node_modules/jsonfile": { "node_modules/jsonfile": {
"version": "6.1.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1417,13 +1654,6 @@
"url": "https://github.com/sponsors/antonk52" "url": "https://github.com/sponsors/antonk52"
} }
}, },
"node_modules/lodash.assign": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
"integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.memoize": { "node_modules/lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -1431,13 +1661,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.trim": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/lodash.trim/-/lodash.trim-4.5.1.tgz",
"integrity": "sha512-nJAlRl/K+eiOehWKDzoBVrSMhK0K3A3YQsUNXHQa5yIrKBAhsZgSu3KoAFoFT+mEgiyBHddZ0pRk1ITpIp90Wg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.uniq": { "node_modules/lodash.uniq": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@@ -1445,6 +1668,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lru-cache": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz",
"integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==",
"dev": true,
"license": "ISC"
},
"node_modules/make-dir": { "node_modules/make-dir": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -1494,6 +1724,95 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/mkdirp": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
"integrity": "sha512-8OCq0De/h9ZxseqzCH8Kw/Filf5pF/vMI6+BH7Lu0jXz2pqYCjTAQRolSxRIi+Ax+oCCjlxoJMP0YQ4XlrQNHg==",
"deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)",
"dev": true,
"license": "MIT"
},
"node_modules/mocha": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.0.tgz",
"integrity": "sha512-Bmjo5ZIr+RcxCKRLFpE7tpGiYemqCkWNVBx31seyUv+c45MahZcBBcoRN33yMhvOBmiq0ABhpENk19WtM3BcOw==",
"deprecated": "Mocha v1.x is no longer supported.",
"dev": true,
"dependencies": {
"commander": "2.0.0",
"debug": "*",
"diff": "1.0.7",
"glob": "3.2.3",
"growl": "1.7.x",
"jade": "0.26.3",
"mkdirp": "0.3.5"
},
"bin": {
"_mocha": "bin/_mocha",
"mocha": "bin/mocha"
},
"engines": {
"node": ">= 0.4.x"
}
},
"node_modules/mocha/node_modules/commander": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz",
"integrity": "sha512-qebjpyeaA/nJ4w3EO2cV2++/zEkccPnjWogzA2rff+Lk8ILI75vULeTmyd4wPxWdKwtP3J+G39IXVZadh0UHyw==",
"dev": true,
"engines": {
"node": ">= 0.6.x"
}
},
"node_modules/mocha/node_modules/glob": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz",
"integrity": "sha512-WPaLsMHD1lYEqAmIQI6VOJSPwuBdGShDWnj1yUo0vQqEO809R8W3LM9OVU13CnnDhyv/EiNwOtxEW74SmrzS6w==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "BSD",
"dependencies": {
"graceful-fs": "~2.0.0",
"inherits": "2",
"minimatch": "~0.2.11"
},
"engines": {
"node": "*"
}
},
"node_modules/mocha/node_modules/graceful-fs": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz",
"integrity": "sha512-hcj/NTUWv+C3MbqrVb9F+aH6lvTwEHJdx2foBxlrVq5h6zE8Bfu4pv4CAAqbDcZrw/9Ak5lsRXlY9Ao8/F0Tuw==",
"deprecated": "please upgrade to graceful-fs 4 for compatibility with current and future versions of Node.js",
"dev": true,
"license": "BSD",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/mocha/node_modules/minimatch": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
"integrity": "sha512-zZ+Jy8lVWlvqqeM8iZB7w7KmQkoJn8djM585z88rywrEbzoqawVa9FR5p2hwD+y74nfuKOjmNvi9gtWJNLqHvA==",
"deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue",
"dev": true,
"license": "MIT",
"dependencies": {
"lru-cache": "2",
"sigmund": "~1.0.0"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -1521,6 +1840,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/nopt": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.2.tgz",
"integrity": "sha512-x8vXm7BZ2jE1Txrxh/hO74HTuYZQEbo8edoRcANgdZ4+PCV+pbjd/xdummkmjjC7LU5EjPzlu8zEq/oxWylnKA==",
"dev": true,
"license": "MIT",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
}
},
"node_modules/normalize-path": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -1554,6 +1886,26 @@
"url": "https://github.com/fb55/nth-check?sponsor=1" "url": "https://github.com/fb55/nth-check?sponsor=1"
} }
}, },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@@ -1561,6 +1913,83 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/phpfn": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/phpfn/-/phpfn-1.0.0.tgz",
"integrity": "sha512-hwl2fXpXDOHCOlFDkNYXwD4FUKsddgXooF7Cb8eyynt82Ej9DLVfL6P/2d6L0uQghJq1X6DUnTd0rKM3yC8oOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"phpjs": "latest"
}
},
"node_modules/phpjs": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/phpjs/-/phpjs-1.3.2.tgz",
"integrity": "sha512-S/V298ABWBDLsWgssVl91JmexMvTmmBR4oufeHvQU3W63+xOBluVtbVEoMyxv6ZdFuj/fx6BXe/WC6gWnO+lig==",
"deprecated": "phpjs is no longer maintained. Please use Locutus instead: https://locutus.io",
"dev": true,
"dependencies": {
"cli": "0.4.4-2",
"deep-equal": "0.1.2",
"glob": "3.2.1",
"js-beautify": "1.4.2",
"mocha": "1.17.0",
"send": "0.1.0",
"underscore": "1.5.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/phpjs/node_modules/glob": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/glob/-/glob-3.2.1.tgz",
"integrity": "sha512-wvxQZUqjkvW//FJMr/DCmPlAOFcrmf2ojnUddQTdgAQ5XkKL8ILfob0Rz+Ch/fSiols6EtiHRJS3i9W0kBRZmQ==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "BSD",
"dependencies": {
"graceful-fs": "~1.2.0",
"inherits": "1",
"minimatch": "~0.2.11"
},
"engines": {
"node": "*"
}
},
"node_modules/phpjs/node_modules/graceful-fs": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz",
"integrity": "sha512-iiTUZ5vZ+2ZV+h71XAgwCSu6+NAizhFU3Yw8aC/hH5SQ3SnISqEqAek40imAFGtDcwJKNhXvSY+hzIolnLwcdQ==",
"deprecated": "please upgrade to graceful-fs 4 for compatibility with current and future versions of Node.js",
"dev": true,
"license": "BSD",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/phpjs/node_modules/inherits": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz",
"integrity": "sha512-Al67oatbRSo3RV5hRqIoln6Y5yMVbJSIn4jEJNL7VCImzq/kLr7vvb6sFRJXqr8rpHc/2kJOM+y0sPKN47VdzA==",
"dev": true
},
"node_modules/phpjs/node_modules/minimatch": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
"integrity": "sha512-zZ+Jy8lVWlvqqeM8iZB7w7KmQkoJn8djM585z88rywrEbzoqawVa9FR5p2hwD+y74nfuKOjmNvi9gtWJNLqHvA==",
"deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue",
"dev": true,
"license": "MIT",
"dependencies": {
"lru-cache": "2",
"sigmund": "~1.0.0"
},
"engines": {
"node": "*"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1825,23 +2254,116 @@
} }
}, },
"node_modules/postcss-import-url": { "node_modules/postcss-import-url": {
"version": "7.2.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/postcss-import-url/-/postcss-import-url-7.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-import-url/-/postcss-import-url-1.0.0.tgz",
"integrity": "sha512-El61K/5+Rv753G9mBiHyQlOIN2mBfN0YHPMXLlgIo/m1+tPDLM32wd97WoUjc8FHUnC6EyyfVA8RDuKoyuVl0Q==", "integrity": "sha512-sXZVBws7VJZDc3P60oTI/7hR5I5EZnjIrmm9QFQY6iwhdmRHi4o9deYoAcnV6jaKrPzzaqO8VGrxf6X2yxUfHQ==",
"dev": true,
"license": "Beerware",
"dependencies": {
"bluebird": "^3.0.2",
"http-https": "^1.0.0",
"is-url": "^1.2.1",
"phpfn": "^1.0.0",
"postcss": "^5.0.2"
}
},
"node_modules/postcss-import-url/node_modules/ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postcss-import-url/node_modules/ansi-styles": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
"integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postcss-import-url/node_modules/chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"http-https": "^1.0.0", "ansi-styles": "^2.2.1",
"is-url": "^1.2.4", "escape-string-regexp": "^1.0.2",
"lodash.assign": "^4.2.0", "has-ansi": "^2.0.0",
"lodash.trim": "^4.5.1", "strip-ansi": "^3.0.0",
"resolve-relative-url": "^1.0.0" "supports-color": "^2.0.0"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=0.10.0"
}
},
"node_modules/postcss-import-url/node_modules/chalk/node_modules/supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/postcss-import-url/node_modules/has-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
"integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postcss-import-url/node_modules/postcss": {
"version": "5.2.18",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
"integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^1.1.3",
"js-base64": "^2.1.9",
"source-map": "^0.5.6",
"supports-color": "^3.2.3"
}, },
"peerDependencies": { "engines": {
"postcss": "^8.0.0" "node": ">=0.12"
}
},
"node_modules/postcss-import-url/node_modules/strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^2.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postcss-import-url/node_modules/supports-color": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
"integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^1.0.0"
},
"engines": {
"node": ">=0.8.0"
} }
}, },
"node_modules/postcss-load-config": { "node_modules/postcss-load-config": {
@@ -2347,21 +2869,20 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/punycode": { "node_modules/proto-list": {
"version": "1.3.2", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
"integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
"dev": true, "dev": true,
"license": "MIT" "license": "ISC"
}, },
"node_modules/querystring": { "node_modules/range-parser": {
"version": "0.2.0", "version": "0.0.4",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-0.0.4.tgz",
"integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", "integrity": "sha512-okJVEq9DbZyg+5lD8pr6ooQmeA0uu8DYIyAU7VK1WUUK7hctI1yw2ZHhKiKjB6RXaDrYRmTR4SsIHkyiQpaLMA==",
"deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=0.4.x" "node": "*"
} }
}, },
"node_modules/read-cache": { "node_modules/read-cache": {
@@ -2418,16 +2939,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/resolve-relative-url": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-relative-url/-/resolve-relative-url-1.0.0.tgz",
"integrity": "sha512-zpcelQBAmrwckiyRmym9os1goECU3EzuTU/UrYkGzXV0i14n8FkyGUvwkOYA5klqVLq1Hz/EiFZMS7bZQdd+EA==",
"dev": true,
"license": "MIT",
"dependencies": {
"url": "0.10.x"
}
},
"node_modules/sax": { "node_modules/sax": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
@@ -2445,6 +2956,34 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/send": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.1.0.tgz",
"integrity": "sha512-D/GaJQQYx7ICNq9Te5V4wHetfDQdFk3HJ4oBfDUBNW7XQmLbJ8sQDm/wFvVUUpKN8tluOnO1dFdM8KODn6D79w==",
"dev": true,
"dependencies": {
"debug": "*",
"fresh": "0.1.0",
"mime": "1.2.6",
"range-parser": "0.0.4"
}
},
"node_modules/send/node_modules/mime": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.2.6.tgz",
"integrity": "sha512-S4yfg1ehMduQ5F3NeTUUWJesnut4RvymaRSatO4etOm68yZE98oCg2GtgG0coGYx03GCv240sezMvRwFk8DUKw==",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/sigmund": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
"integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==",
"dev": true,
"license": "ISC"
},
"node_modules/slash": { "node_modules/slash": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
@@ -2458,6 +2997,16 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2560,14 +3109,14 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.14", "version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fdir": "^6.4.4", "fdir": "^6.5.0",
"picomatch": "^4.0.2" "picomatch": "^4.0.3"
}, },
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
@@ -2577,11 +3126,14 @@
} }
}, },
"node_modules/tinyglobby/node_modules/fdir": { "node_modules/tinyglobby/node_modules/fdir": {
"version": "6.4.6", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": { "peerDependencies": {
"picomatch": "^3 || ^4" "picomatch": "^3 || ^4"
}, },
@@ -2592,9 +3144,9 @@
} }
}, },
"node_modules/tinyglobby/node_modules/picomatch": { "node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2623,6 +3175,12 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/underscore": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.2.tgz",
"integrity": "sha512-yejOFsRnTJs0N9CK5Apzf6maDO2djxGoLLrlZlvGs2o9ZQuhIhDL18rtFyy4FBIbOkzA6+4hDgXbgz5EvDQCXQ==",
"dev": true
},
"node_modules/universalify": { "node_modules/universalify": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -2664,17 +3222,6 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/url": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
"integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"punycode": "1.3.2",
"querystring": "0.2.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -2700,6 +3247,13 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/xxhashjs": { "node_modules/xxhashjs": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz", "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz",
@@ -2721,9 +3275,9 @@
} }
}, },
"node_modules/yaml": { "node_modules/yaml": {
"version": "2.8.0", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {

View File

@@ -24,7 +24,7 @@
"playwright": "^1.52.0", "playwright": "^1.52.0",
"postcss-cli": "^11.0.1", "postcss-cli": "^11.0.1",
"postcss-import": "^16.1.1", "postcss-import": "^16.1.1",
"postcss-import-url": "^7.2.0", "postcss-import-url": "^1.0.0",
"postcss-url": "^10.1.3" "postcss-url": "^10.1.3"
}, },
"dependencies": { "dependencies": {

View File

@@ -5,7 +5,7 @@ go 1.24.5
replace github.com/TecharoHQ/anubis => .. replace github.com/TecharoHQ/anubis => ..
require ( require (
github.com/TecharoHQ/anubis v1.21.3 github.com/TecharoHQ/anubis v1.22.0
github.com/docker/docker v28.3.2+incompatible github.com/docker/docker v28.3.2+incompatible
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456 github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
@@ -18,6 +18,24 @@ require (
github.com/TecharoHQ/thoth-proto v0.4.0 // indirect github.com/TecharoHQ/thoth-proto v0.4.0 // indirect
github.com/a-h/templ v0.3.924 // indirect github.com/a-h/templ v0.3.924 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/aws/aws-sdk-go-v2 v1.38.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.6 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
github.com/aws/smithy-go v1.23.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect

View File

@@ -16,6 +16,42 @@ github.com/a-h/templ v0.3.924 h1:t5gZqTneXqvehpNZsgtnlOscnBboNh9aASBH2MgV/0k=
github.com/a-h/templ v0.3.924/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334= github.com/a-h/templ v0.3.924/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=
github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
github.com/aws/aws-sdk-go-v2/config v1.31.6 h1:a1t8fXY4GT4xjyJExz4knbuoxSCacB5hT/WgtfPyLjo=
github.com/aws/aws-sdk-go-v2/config v1.31.6/go.mod h1:5ByscNi7R+ztvOGzeUaIu49vkMk2soq5NaH5PYe33MQ=
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg=
github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 h1:wbjnrrMnKew78/juW7I2BtKQwa1qlf6EjQgS69uYY14=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6/go.mod h1:AtiqqNrDioJXuUgz3+3T0mBWN7Hro2n9wll2zRUc0ww=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 h1:R0tNFJqfjHL3900cqhXuwQ+1K4G0xc9Yf8EDbFXCKEw=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6/go.mod h1:y/7sDdu+aJvPtGXr4xYosdpq9a6T9Z0jkXfugmti0rI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 h1:hncKj/4gR+TPauZgTAsxOxNcvBayhUlYZ6LO/BYiQ30=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6/go.mod h1:OiIh45tp6HdJDDJGnja0mw8ihQGz3VGrUflLqSL0SmM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 h1:LHS1YAIJXJ4K9zS+1d/xa9JAA9sL2QyXIQCQFQW/X08=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6/go.mod h1:c9PCiTEuh0wQID5/KqA32J+HAgZxN9tOGXKCiYJjTZI=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 h1:nEXUSAwyUfLTgnc9cxlDWy637qsq4UWwp3sNAfl0Z3Y=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6/go.mod h1:HGzIULx4Ge3Do2V0FaiYKcyKzOqwrhUZgCI77NisswQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3 h1:ETkfWcXP2KNPLecaDa++5bsQhCRa5M5sLUJa5DWYIIg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3/go.mod h1:+/3ZTqoYb3Ur7DObD00tarKMLMuKg8iqz5CHEanqTnw=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 h1:8OLZnVJPvjnrxEwHFg9hVUof/P4sibH+Ea4KKuqAGSg=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1/go.mod h1:27M3BpVi0C02UiQh1w9nsBEit6pLhlaH3NHna6WUbDE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 h1:gKWSTnqudpo8dAxqBqZnDoDWCiEh/40FziUjr/mo6uA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2/go.mod h1:x7+rkNmRoEN1U13A6JE2fXne9EWyJy54o3n6d4mGaXQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl7fiGcfJTIo8BszI=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c=
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=

View File

@@ -39,9 +39,18 @@ for the JavaScript code in this page.
mkdir -p static/locales mkdir -p static/locales
cp ../lib/localization/locales/*.json static/locales/ cp ../lib/localization/locales/*.json static/locales/
for file in js/*.mjs js/worker/*.mjs; do shopt -s nullglob globstar
esbuild "${file}" --sourcemap --bundle --minify --outfile=static/"${file}" --banner:js="${LICENSE}"
gzip -f -k -n static/${file} for file in js/**/*.ts js/**/*.mjs; do
zstd -f -k --ultra -22 static/${file} out="static/${file}"
brotli -fZk static/${file} if [[ "$file" == *.ts ]]; then
out="static/${file%.ts}.mjs"
fi
mkdir -p "$(dirname "$out")"
esbuild "$file" --sourcemap --bundle --minify --outfile="$out" --banner:js="$LICENSE"
gzip -f -k -n "$out"
zstd -f -k --ultra -22 "$out"
brotli -fZk "$out"
done done

View File

@@ -1,11 +1,21 @@
type ProgressCallback = (nonce: number) => void;
interface ProcessOptions {
basePrefix: string;
version: string;
}
const getHardwareConcurrency = () =>
navigator.hardwareConcurrency !== undefined ? navigator.hardwareConcurrency : 1;
export default function process( export default function process(
{ basePrefix, version }, options: ProcessOptions,
data, data: string,
difficulty = 5, difficulty: number = 5,
signal = null, signal: AbortSignal | null = null,
progressCallback = null, progressCallback?: ProgressCallback,
threads = Math.trunc(Math.max(navigator.hardwareConcurrency / 2, 1)), threads: number = Math.trunc(Math.max(getHardwareConcurrency() / 2, 1)),
) { ): Promise<string> {
console.debug("fast algo"); console.debug("fast algo");
let workerMethod = window.crypto !== undefined ? "webcrypto" : "purejs"; let workerMethod = window.crypto !== undefined ? "webcrypto" : "purejs";
@@ -16,13 +26,17 @@ export default function process(
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let webWorkerURL = `${basePrefix}/.within.website/x/cmd/anubis/static/js/worker/sha256-${workerMethod}.mjs?cacheBuster=${version}`; let webWorkerURL = `${options.basePrefix}/.within.website/x/cmd/anubis/static/js/worker/sha256-${workerMethod}.mjs?cacheBuster=${options.version}`;
console.log(webWorkerURL); const workers: Worker[] = [];
const workers = [];
let settled = false; let settled = false;
const onAbort = () => {
console.log("PoW aborted");
cleanup();
reject(new DOMException("Aborted", "AbortError"));
};
const cleanup = () => { const cleanup = () => {
if (settled) { if (settled) {
return; return;
@@ -34,12 +48,6 @@ export default function process(
} }
}; };
const onAbort = () => {
console.log("PoW aborted");
cleanup();
reject(new DOMException("Aborted", "AbortError"));
};
if (signal != null) { if (signal != null) {
if (signal.aborted) { if (signal.aborted) {
return onAbort(); return onAbort();

View File

@@ -1,4 +1,4 @@
import fast from "./fast.mjs"; import fast from "./fast";
export default { export default {
fast: fast, fast: fast,

View File

@@ -1,20 +1,24 @@
import algorithms from "./algorithms/index.mjs"; import algorithms from "./algorithms";
const defaultDifficulty = 4; const defaultDifficulty = 4;
const status = document.getElementById("status"); const status: HTMLParagraphElement = document.getElementById("status") as HTMLParagraphElement;
const difficultyInput = document.getElementById("difficulty-input"); const difficultyInput: HTMLInputElement = document.getElementById("difficulty-input") as HTMLInputElement;
const algorithmSelect = document.getElementById("algorithm-select"); const algorithmSelect: HTMLSelectElement = document.getElementById("algorithm-select") as HTMLSelectElement;
const compareSelect = document.getElementById("compare-select"); const compareSelect: HTMLSelectElement = document.getElementById("compare-select") as HTMLSelectElement;
const header = document.getElementById("table-header"); const header: HTMLTableRowElement = document.getElementById("table-header") as HTMLTableRowElement;
const headerCompare = document.getElementById("table-header-compare"); const headerCompare: HTMLTableSectionElement = document.getElementById("table-header-compare") as HTMLTableSectionElement;
const results = document.getElementById("results"); const results: HTMLTableRowElement = document.getElementById("results") as HTMLTableRowElement;
const setupControls = () => { const setupControls = () => {
difficultyInput.value = defaultDifficulty; if (defaultDifficulty == null) {
return;
}
difficultyInput.value = defaultDifficulty.toString();
for (const alg of Object.keys(algorithms)) { for (const alg of Object.keys(algorithms)) {
const option1 = document.createElement("option"); const option1 = document.createElement("option");
algorithmSelect.append(option1); algorithmSelect?.append(option1);
const option2 = document.createElement("option"); const option2 = document.createElement("option");
compareSelect.append(option2); compareSelect.append(option2);
option1.value = option1.innerText = option2.value = option2.innerText = alg; option1.value = option1.innerText = option2.value = option2.innerText = alg;
@@ -116,13 +120,13 @@ const benchmarkLoop = async (controller) => {
await benchmarkLoop(controller); await benchmarkLoop(controller);
}; };
let controller = null; let controller: AbortController | null = null;
const reset = () => { const reset = () => {
stats.time = stats.iters = 0; stats.time = stats.iters = 0;
comparison.time = comparison.iters = 0; comparison.time = comparison.iters = 0;
results.innerHTML = status.innerText = ""; results.innerHTML = status.innerText = "";
const table = results.parentElement; const table = results.parentElement as HTMLElement;
if (compareSelect.value !== "NONE") { if (compareSelect.value !== "NONE") {
table.style.gridTemplateColumns = "repeat(4,auto)"; table.style.gridTemplateColumns = "repeat(4,auto)";
header.style.display = "none"; header.style.display = "none";

View File

@@ -1,12 +1,21 @@
import algorithms from "./algorithms/index.mjs"; import algorithms from "./algorithms";
// from Xeact // from Xeact
const u = (url = "", params = {}) => { const u = (url: string = "", params: Record<string, any> = {}) => {
let result = new URL(url, window.location.href); let result = new URL(url, window.location.href);
Object.entries(params).forEach(([k, v]) => result.searchParams.set(k, v)); Object.entries(params).forEach(([k, v]) => result.searchParams.set(k, v));
return result.toString(); return result.toString();
}; };
const j = (id: string): any | null => {
const elem = document.getElementById(id);
if (elem === null) {
return null;
}
return JSON.parse(elem.textContent);
};
const imageURL = (mood, cacheBuster, basePrefix) => const imageURL = (mood, cacheBuster, basePrefix) =>
u(`${basePrefix}/.within.website/x/cmd/anubis/static/img/${mood}.webp`, { u(`${basePrefix}/.within.website/x/cmd/anubis/static/img/${mood}.webp`, {
cacheBuster, cacheBuster,
@@ -14,9 +23,10 @@ const imageURL = (mood, cacheBuster, basePrefix) =>
// Detect available languages by loading the manifest // Detect available languages by loading the manifest
const getAvailableLanguages = async () => { const getAvailableLanguages = async () => {
const basePrefix = JSON.parse( const basePrefix = j("anubis_base_prefix");
document.getElementById("anubis_base_prefix").textContent, if (basePrefix === null) {
); return;
}
try { try {
const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/manifest.json`); const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/manifest.json`);
@@ -38,9 +48,11 @@ const getBrowserLanguage = async () =>
// Load translations from JSON files // Load translations from JSON files
const loadTranslations = async (lang) => { const loadTranslations = async (lang) => {
const basePrefix = JSON.parse( const basePrefix = j("anubis_base_prefix");
document.getElementById("anubis_base_prefix").textContent, if (basePrefix === null) {
); return;
}
try { try {
const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/${lang}.json`); const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/${lang}.json`);
return await response.json(); return await response.json();
@@ -54,9 +66,10 @@ const loadTranslations = async (lang) => {
}; };
const getRedirectUrl = () => { const getRedirectUrl = () => {
const publicUrl = JSON.parse( const publicUrl = j("anubis_public_url");
document.getElementById("anubis_public_url").textContent, if (publicUrl === null) {
); return;
}
if (publicUrl && window.location.href.startsWith(publicUrl)) { if (publicUrl && window.location.href.startsWith(publicUrl)) {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('redir'); return urlParams.get('redir');
@@ -91,16 +104,14 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
value: navigator.cookieEnabled, value: navigator.cookieEnabled,
}, },
]; ];
const status = document.getElementById("status");
const image = document.getElementById("image"); const status: HTMLParagraphElement = document.getElementById("status") as HTMLParagraphElement;
const title = document.getElementById("title"); const image: HTMLImageElement = document.getElementById("image") as HTMLImageElement;
const progress = document.getElementById("progress"); const title: HTMLHeadingElement = document.getElementById("title") as HTMLHeadingElement;
const anubisVersion = JSON.parse( const progress: HTMLDivElement = document.getElementById("progress") as HTMLDivElement;
document.getElementById("anubis_version").textContent,
); const anubisVersion = j("anubis_version");
const basePrefix = JSON.parse( const basePrefix = j("anubis_base_prefix");
document.getElementById("anubis_base_prefix").textContent,
);
const details = document.querySelector("details"); const details = document.querySelector("details");
let userReadDetails = false; let userReadDetails = false;
@@ -132,9 +143,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
} }
} }
const { challenge, rules } = JSON.parse( const { challenge, rules } = j("anubis_challenge");
document.getElementById("anubis_challenge").textContent,
);
const process = algorithms[rules.algorithm]; const process = algorithms[rules.algorithm];
if (!process) { if (!process) {
@@ -182,7 +191,9 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
const probability = Math.pow(1 - likelihood, iters); const probability = Math.pow(1 - likelihood, iters);
const distance = (1 - Math.pow(probability, 2)) * 100; const distance = (1 - Math.pow(probability, 2)) * 100;
progress["aria-valuenow"] = distance; progress["aria-valuenow"] = distance;
progress.firstElementChild.style.width = `${distance}%`; if (progress.firstElementChild !== null) {
(progress.firstElementChild as HTMLElement).style.width = `${distance}%`;
}
if (probability < 0.1 && !showingApology) { if (probability < 0.1 && !showingApology) {
status.append( status.append(
@@ -197,7 +208,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
console.log({ hash, nonce }); console.log({ hash, nonce });
if (userReadDetails) { if (userReadDetails) {
const container = document.getElementById("progress"); const container: HTMLDivElement = document.getElementById("progress") as HTMLDivElement;
// Style progress bar as a continue button // Style progress bar as a continue button
container.style.display = "flex"; container.style.display = "flex";

View File

@@ -6,7 +6,7 @@ const calculateSHA256 = (text) => {
return hash.digest(); return hash.digest();
}; };
function toHexString(arr) { function toHexString(arr: Uint8Array): string {
return Array.from(arr) return Array.from(arr)
.map((c) => c.toString(16).padStart(2, "0")) .map((c) => c.toString(16).padStart(2, "0"))
.join(""); .join("");

View File

@@ -1,10 +1,11 @@
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const calculateSHA256 = async (input) => {
const calculateSHA256 = async (input: string) => {
const data = encoder.encode(input); const data = encoder.encode(input);
return await crypto.subtle.digest("SHA-256", data); return await crypto.subtle.digest("SHA-256", data);
}; };
const toHexString = (byteArray) => { const toHexString = (byteArray: Uint8Array) => {
return byteArray.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), ""); return byteArray.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
}; };