Compare commits

...

13 Commits

Author SHA1 Message Date
Jason Cameron
7882bdd656 chore: bump Playwright dev dependency to version 1.56.0 2025-11-14 16:53:47 -08:00
Jason Cameron
d8c2ef9801 chore: update playwright cache key to version 1.56.0 2025-11-14 16:50:29 -08:00
Jason Cameron
71bb9803ac chore: update playwright to version 1.56.0 in configuration files 2025-11-14 16:38:28 -08:00
Xe Iaso
68fcc0c44f feat(lib): expose WEIGH matches as prometheus metrics (#1277)
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-11-14 17:12:59 -05:00
Esteban Gimbernat
6a7f80e6f5 (feat) Add cluster support to redis/vaultkey store (#1276)
* (feat) Add cluster support to redis/vaultkey store

* (chore) Update CHANGELOG.md

* (fix) Disable maintenance notification on the Valkey store

* (fix) Valkey text fix and allow maintnotifications in spelling.
2025-11-14 08:22:22 -05:00
Henri Vasserman
a5bb6d2751 test: ipv4 in v6 address checking (#1271)
* test: ipv4 in v6 address checking

* fix(lib/policy): unmap 4in6 addresses in RemoteAddrChecker

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

* docs: update CHANGELOG

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

* docs: perfect CHANGELOG

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-11-14 03:39:50 +00:00
kouhaidev
1e298f5d0e fix(run): mark openrc service script as executable (#1272)
Signed-off-by: Kouhai <66407198+kouhaidev@users.noreply.github.com>
2025-11-13 22:14:21 -05:00
Xe Iaso
a4770956a8 fix(docs): use node:lts (#1274)
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-11-14 03:14:00 +00:00
Josh Deprez
316905bf1d Add Renovate to Docker clients (#1267)
Renovate-bot looks at the container APIs directly to learn about new image versions and digests. The [default User-Agent](https://docs.renovatebot.com/self-hosted-configuration/#useragent) is `Renovate/${renovateVersion} (https://github.com/renovatebot/renovate)`
2025-11-12 03:22:00 +00:00
dependabot[bot]
1a12171d74 build(deps): bump the github-actions group with 3 updates (#1262)
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-11-09 18:08:06 -08:00
Denys Nykula
4f50d3245e feat(localization): Add Ukrainian language translation (#1044) 2025-11-08 18:46:20 +00:00
Xe Iaso
49c9333359 fix(data): add services folder to embedded filesystem (#1259)
* fix(data): add services folder to embedded filesystem

Also includes a regression test to ensure this does not happen again.

Assisted-By: GLM 4.6 via Claude Code

* docs: update CHANGELOG

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

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-11-08 18:08:48 +00:00
Xe Iaso
c7e4cd1032 fix(data/docker-client): allow some more OCI clients through (#1258)
* fix(data/docker-client): allow some more OCI clients through

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

* Update metadata

check-spelling run (pull_request) for Xe/more-docker-client-programs

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(data/docker-client): add containerd

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-11-08 17:50:56 +00:00
28 changed files with 300 additions and 116 deletions

View File

@@ -1,12 +1,12 @@
services:
playwright:
image: mcr.microsoft.com/playwright:v1.52.0-noble
image: mcr.microsoft.com/playwright:v1.56.0-noble
init: true
network_mode: service:workspace
command:
- /bin/sh
- -c
- npx -y playwright@1.52.0 run-server --port 9001 --host 0.0.0.0
- npx -y playwright@1.56.0 run-server --port 9001 --host 0.0.0.0
valkey:
image: valkey/valkey:8

View File

@@ -8,3 +8,4 @@ msgbox
xeact
ABee
tencent
maintnotifications

View File

@@ -36,6 +36,7 @@ botstopper
BPort
Brightbot
broked
buildah
byteslice
Bytespider
cachebuster
@@ -199,7 +200,6 @@ licstart
lightpanda
limsa
Linting
linuxbrew
LLU
loadbalancer
lol
@@ -226,6 +226,7 @@ nobots
NONINFRINGEMENT
nosleep
nullglob
oci
OCOB
ogtag
oklch

View File

@@ -22,11 +22,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: latest
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable

View File

@@ -26,11 +26,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: latest
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable
@@ -38,7 +38,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
with:
images: ghcr.io/${{ github.repository }}

View File

@@ -36,11 +36,11 @@ jobs:
run: |
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: latest
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable
@@ -55,7 +55,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
with:
images: ${{ env.IMAGE }}

View File

@@ -33,7 +33,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
with:
images: ghcr.io/techarohq/anubis/docs
tags: |

View File

@@ -22,7 +22,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
with:
images: ghcr.io/techarohq/anubis/docs
tags: |

View File

@@ -24,11 +24,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: latest
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable
@@ -38,7 +38,7 @@ jobs:
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/go.sum') }}
key: ${{ runner.os }}-playwright-1.56.0
- name: install node deps
run: |
@@ -46,8 +46,8 @@ jobs:
- name: install playwright browsers
run: |
npx --no-install playwright@1.52.0 install --with-deps
npx --no-install playwright@1.52.0 run-server --port 9001 &
npx playwright@1.56.0 install --with-deps
npx playwright@1.56.0 run-server --port 9001 &
- name: Build
run: npm run build

View File

@@ -25,11 +25,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: latest
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable

View File

@@ -26,11 +26,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: latest
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable

View File

@@ -23,3 +23,38 @@
all:
- '"Docker-Distribution-Api-Version" in headers'
- '!(userAgent.contains("Mozilla"))'
- name: allow-go-containerregistry-client
action: ALLOW
expression:
all:
- path.startsWith("/v2/")
- userAgent.contains("go-containerregistry/")
- name: allow-buildah
action: ALLOW
expression:
all:
- path.startsWith("/v2/")
- userAgent.contains("Buildah/")
- name: allow-podman
action: ALLOW
expression:
all:
- path.startsWith("/v2/")
- userAgent.contains("containers/")
- name: allow-containerd
action: ALLOW
expression:
all:
- path.startsWith("/v2/")
- userAgent.contains("containerd/")
- name: allow-renovate
action: ALLOW
expression:
all:
- path.startsWith("/v2/")
- userAgent.contains("Renovate/")

View File

@@ -3,6 +3,6 @@ package data
import "embed"
var (
//go:embed botPolicies.yaml all:apps all:bots all:clients all:common all:crawlers all:meta
//go:embed botPolicies.yaml all:apps all:bots all:clients all:common all:crawlers all:meta all:services
BotPolicies embed.FS
)

38
data/embed_test.go Normal file
View File

@@ -0,0 +1,38 @@
package data
import (
"path/filepath"
"strings"
"testing"
)
// TestBotPoliciesEmbed ensures all YAML files in the directory tree
// are accessible in the embedded BotPolicies filesystem.
func TestBotPoliciesEmbed(t *testing.T) {
yamlFiles, err := filepath.Glob("./**/*.yaml")
if err != nil {
t.Fatalf("Failed to glob YAML files: %v", err)
}
if len(yamlFiles) == 0 {
t.Fatal("No YAML files found in directory tree")
}
t.Logf("Found %d YAML files to verify", len(yamlFiles))
for _, filePath := range yamlFiles {
embeddedPath := strings.TrimPrefix(filePath, "./")
t.Run(embeddedPath, func(t *testing.T) {
content, err := BotPolicies.ReadFile(embeddedPath)
if err != nil {
t.Errorf("Failed to read %s from embedded filesystem: %v", embeddedPath, err)
return
}
if len(content) == 0 {
t.Errorf("File %s exists in embedded filesystem but is empty", embeddedPath)
}
})
}
}

View File

@@ -1,4 +1,4 @@
FROM docker.io/library/node AS build
FROM docker.io/library/node:lts AS build
WORKDIR /app
COPY . .

View File

@@ -13,6 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- This changes the project to: -->
- Expose WEIGHT rule matches as Prometheus metrics.
- Allow more OCI registry clients [based on feedback](https://github.com/TecharoHQ/anubis/pull/1253#issuecomment-3506744184).
- Expose services directory in the embedded `(data)` filesystem.
- Add Ukrainian locale ([#1044](https://github.com/TecharoHQ/anubis/pull/1044)).
- Allow Renovate as an OCI registry client.
- Properly handle 4in6 addresses so that IP matching works with those addresses.
- Add support to simple Valkey/Redis cluster mode
- Bump the Playwright dev dependency to 1.56.0
## v1.23.1: Lyse Hext - Echo 1
- Fix `SERVE_ROBOTS_TXT` setting after the double slash fix broke it.

View File

@@ -99,7 +99,7 @@ const (
actionChallenge action = "CHALLENGE"
placeholderIP = "fd11:5ee:bad:c0de::"
playwrightVersion = "1.52.0"
playwrightVersion = "1.56.0"
)
type action string

View File

@@ -576,6 +576,7 @@ func (s *Server) check(r *http.Request, lg *slog.Logger) (policy.CheckResult, *p
return cr("bot/"+b.Name, b.Action, weight), &b, nil
case config.RuleWeigh:
lg.Debug("adjusting weight", "name", b.Name, "delta", b.Weight.Adjust)
policy.Applications.WithLabelValues("bot/"+b.Name, "WEIGH").Add(1)
weight += b.Weight.Adjust
}
}

View File

@@ -18,6 +18,7 @@
"pt-BR",
"ru",
"tr",
"uk",
"vi",
"zh-CN",
"zh-TW",

View File

@@ -0,0 +1,66 @@
{
"loading": "Завантаження...",
"why_am_i_seeing": "Чому я це бачу?",
"protected_by": "Захищено засобами",
"protected_from": "за авторством",
"made_with": "Зроблено з ❤️ у 🇨🇦",
"mascot_design": "Дизайн персонажа від",
"ai_companies_explanation": "Ви це бачите, оскільки адміністрація сайту налаштувала Anubis, щоб захистити сервер від тиску ШІ-компаній, які агресивно сканують вебсайти. Їхня діяльність спричиняє перебої в роботі вебсайтів, що робить матеріали недоступними для всіх.",
"anubis_compromise": "Anubis — це компроміс. Anubis втілює схему доказу виконаної роботи подібно до Hashcash — засобу боротьби зі спамом. По ідеї, додаткове навантаження не обтяжує справжню людину, котра робить небагато запитів, а от масове сканування таким чином стає суттєво дорожчим.",
"hack_purpose": "Це тимчасове рішення, котре дозволяє приділити більше часу розпізнанню й виокремленню автоматизованих браузерів (наприклад, за тим, як вони промальовують шрифти), щоб сторінку перевірки доказу виконаної роботи не доводилося показувати ймовірно справжнім користувачам.",
"simplified_explanation": "Це засіб боротьби з ботами й зловмисними запитами, подібний до капчі. Проте замість того, щоб просити вас щось зробити, він пропонує вашому браузеру розв'язати обчислювальне завдання. Ця концепція називається <a href=\"https://en.wikipedia.org/wiki/Proof_of_work\">доказом виконаної роботи</a>. Завдання обчислюється кілька секунд, після чого вам надається доступ до сайту. Дякуємо за розуміння й терплячість.",
"jshelter_note": "Зауважте, Anubis потребує сучасного JavaScript-функціоналу, котрий може бути недоступним при використанні розширень на зразок JShelter. Будь ласка, вимкніть JShelter чи інші подібні розширення для цього домену.",
"version_info": "Цей вебсайт застосовує Anubis версії",
"try_again": "Повторіть спробу",
"go_home": "Перейдіть на головну сторінку",
"contact_webmaster": "або, якщо ви певні в помилковості блокування, сконтактуйте з адміністрацією за адресою",
"connection_security": "Зачекайте хвилинку, поки ми перевіримо безпеку вашого з'єднання.",
"javascript_required": "На жаль, вам потрібно ввімкнути JavaScript, щоб пройти цю перевірку. Це необхідно, оскільки ШІ-компанії нехтують суспільним договором, завдяки якому можливо утримувати вебсайти. Робота над рішенням без використання JS триває.",
"benchmark_requires_js": "Щоб запустити тестування продуктивності, ввімкніть JavaScript.",
"difficulty": "Складність:",
"algorithm": "Алгоритм:",
"compare": "Порівняти:",
"time": "Час",
"iters": "Ітерації",
"time_a": "Час A",
"iters_a": "Ітерації A",
"time_b": "Час B",
"iters_b": "Ітерації B",
"static_check_endpoint": "Це просто сторінка перевірки для вашого зворотного проксі.",
"authorization_required": "Необхідно авторизуватися",
"cookies_disabled": "У вашому браузері вимкнено кукі. Anubis використовує кукі, щоб упевнитись, що ви дійсно людина. Це законний інтерес. Будь ласка, ввімкніть кукі для цього домену",
"access_denied": "Доступ заборонено: код помилки",
"dronebl_entry": "DroneBL містить пункт",
"see_dronebl_lookup": "див.",
"internal_server_error": "Внутрішня помилка сервера: адміністрація хибно налаштувала Anubis. Будь ласка, сконтактуйте з адміністрацією й попросіть глянути логи довкола",
"invalid_redirect": "Хибне переспрямування",
"redirect_not_parseable": "Не вдається розпізнати URL-адресу переспрямування",
"redirect_domain_not_allowed": "Заборонений домен переспрямування",
"missing_required_forwarded_headers": "Бракує обов'язкових заголовків X-Forwarded-*",
"failed_to_sign_jwt": "не вдається підписати JWT",
"invalid_invocation": "Хибний виклик MakeChallenge",
"client_error_browser": "Помилка клієнта: переконайтесь, що використовуєте браузер актуальної версії, й повторіть спробу.",
"oh_noes": "Йой!",
"benchmarking_anubis": "Тестування продуктивності Anubis!",
"you_are_not_a_bot": "Ви не бот!",
"making_sure_not_bot": "Перевірка, чи ви не бот!",
"celphase": "CELPHASE",
"js_web_crypto_error": "Ваш браузер не надає web.crypto. Ви певні, що дивитесь це через захищений контекст?",
"js_web_workers_error": "Ваш браузер не підтримує Web Workers (Anubis використовує їх, щоб ваш браузер не зависав на час перевірки). Можливо, у вас встановлено розширення на зразок JShelter?",
"js_cookies_error": "Ваш браузер не зберігає кукі. Anubis записує підписаний токен до кукі, щоб занотувати, що клієнт пройшов перевірку. Будь ласка, ввімкніть збереження кукі для цього домену. Назви кукі, які записує Anubis, можуть змінюватися без попередження. Назви й значення кукі не є частиною публічного API.",
"js_context_not_secure": "Ваш контекст незахищений!",
"js_context_not_secure_msg": "Спробуйте з'єднатися через HTTPS або попросіть адміністрацію налаштувати HTTPS. Докладніше — в <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a>.",
"js_calculating": "Обчислення...",
"js_missing_feature": "Бракує функціоналу",
"js_challenge_error": "Помилка перевірки!",
"js_challenge_error_msg": "Не вдалося визначити алгоритм перевірки. Спробуйте оновити сторінку.",
"js_calculating_difficulty": "Обчислення...<br/>Складність:",
"js_speed": "Швидкість:",
"js_verification_longer": "Перевірка триває довше, ніж очікувалося. Будь ласка, не оновлюйте сторінку.",
"js_success": "Успіх!",
"js_done_took": "Готово! Знадобилося",
"js_iterations": "ітерацій",
"js_finished_reading": "Читання завершено, продовжити →",
"js_calculation_error": "Помилка обчислення!",
"js_calculation_error_msg": "Не вдалося обчислити перевірку:"
}

View File

@@ -27,6 +27,7 @@ func TestLocalizationService(t *testing.T) {
"pt-BR": "Carregando...",
"tr": "Yükleniyor...",
"ru": "Загрузка...",
"uk": "Завантаження...",
"vi": "Đang nạp...",
"zh-CN": "加载中...",
"zh-TW": "載入中...",

View File

@@ -51,6 +51,11 @@ func (rac *RemoteAddrChecker) Check(r *http.Request) (bool, error) {
return false, fmt.Errorf("%w: %s is not an IP address: %w", ErrMisconfiguration, host, err)
}
// Convert IPv4-mapped IPv6 addresses to IPv4
if addr.Is6() && addr.Is4In6() {
addr = addr.Unmap()
}
return rac.prefixTable.Contains(addr), nil
}

View File

@@ -21,6 +21,20 @@ func TestRemoteAddrChecker(t *testing.T) {
ok: true,
err: nil,
},
{
name: "match_ipv4_in_ipv6",
cidrs: []string{"0.0.0.0/0"},
ip: "::ffff:1.1.1.1",
ok: true,
err: nil,
},
{
name: "match_ipv4_in_ipv6_hex",
cidrs: []string{"0.0.0.0/0"},
ip: "::ffff:101:101",
ok: true,
err: nil,
},
{
name: "match_ipv6",
cidrs: []string{"::/0"},

View File

@@ -5,80 +5,98 @@ import (
"encoding/json"
"errors"
"fmt"
"time"
"github.com/TecharoHQ/anubis/lib/store"
valkey "github.com/redis/go-redis/v9"
)
var (
ErrNoURL = errors.New("valkey.Config: no URL defined")
ErrBadURL = errors.New("valkey.Config: URL is invalid")
"github.com/redis/go-redis/v9/maintnotifications"
)
func init() {
store.Register("valkey", Factory{})
}
type Factory struct{}
func (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)
}
opts, err := valkey.ParseURL(config.URL)
if err != nil {
return nil, fmt.Errorf("%w: %w", store.ErrBadConfig, err)
}
rdb := valkey.NewClient(opts)
if _, err := rdb.Ping(ctx).Result(); err != nil {
return nil, fmt.Errorf("can't ping valkey instance: %w", err)
}
return &Store{
rdb: rdb,
}, 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
}
// Errors kept as-is so other code/tests still pass.
var (
ErrNoURL = errors.New("valkey.Config: no URL defined")
ErrBadURL = errors.New("valkey.Config: URL is invalid")
)
// Config is what Anubis unmarshals from the "parameters" JSON.
type Config struct {
URL string `json:"url"`
URL string `json:"url"`
Cluster bool `json:"cluster,omitempty"`
}
func (c Config) Valid() error {
var errs []error
if c.URL == "" {
errs = append(errs, ErrNoURL)
return ErrNoURL
}
// Just validate that it's a valid Redis URL.
if _, err := valkey.ParseURL(c.URL); err != nil {
errs = append(errs, ErrBadURL)
}
if len(errs) != 0 {
return fmt.Errorf("valkey.Config: invalid config: %w", errors.Join(errs...))
return fmt.Errorf("%w: %v", ErrBadURL, err)
}
return nil
}
// redisClient is satisfied by *valkey.Client and *valkey.ClusterClient.
type redisClient interface {
Get(ctx context.Context, key string) *valkey.StringCmd
Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *valkey.StatusCmd
Del(ctx context.Context, keys ...string) *valkey.IntCmd
Ping(ctx context.Context) *valkey.StatusCmd
}
type Factory struct{}
func (Factory) Valid(data json.RawMessage) error {
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return err
}
return cfg.Valid()
}
func (Factory) Build(ctx context.Context, data json.RawMessage) (store.Interface, error) {
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, err
}
if err := cfg.Valid(); err != nil {
return nil, err
}
opts, err := valkey.ParseURL(cfg.URL)
if err != nil {
return nil, fmt.Errorf("valkey.Factory: %w", err)
}
var client redisClient
if cfg.Cluster {
// Cluster mode: use the parsed Addr as the seed node.
clusterOpts := &valkey.ClusterOptions{
Addrs: []string{opts.Addr},
// Explicitly disable maintenance notifications
// This prevents the client from sending CLIENT MAINT_NOTIFICATIONS ON
MaintNotificationsConfig: &maintnotifications.Config{
Mode: maintnotifications.ModeDisabled,
},
}
client = valkey.NewClusterClient(clusterOpts)
} else {
opts.MaintNotificationsConfig = &maintnotifications.Config{
Mode: maintnotifications.ModeDisabled,
}
client = valkey.NewClient(opts)
}
// Optional but nice: fail fast if the cluster/single node is unreachable.
if err := client.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("valkey.Factory: ping failed: %w", err)
}
return &Store{client: client}, nil
}

View File

@@ -2,52 +2,46 @@ package valkey
import (
"context"
"fmt"
"time"
"github.com/TecharoHQ/anubis/lib/store"
valkey "github.com/redis/go-redis/v9"
)
// Store implements store.Interface on top of Redis/Valkey.
type Store struct {
rdb *valkey.Client
client redisClient
}
func (s *Store) Delete(ctx context.Context, key string) error {
n, err := s.rdb.Del(ctx, key).Result()
if err != nil {
return fmt.Errorf("can't delete from valkey: %w", err)
}
switch n {
case 0:
return fmt.Errorf("%w: %d key(s) deleted", store.ErrNotFound, n)
default:
return nil
}
}
var _ store.Interface = (*Store)(nil)
func (s *Store) Get(ctx context.Context, key string) ([]byte, error) {
result, err := s.rdb.Get(ctx, key).Result()
if err != nil {
if valkey.HasErrorPrefix(err, "redis: nil") {
return nil, fmt.Errorf("%w: %w", store.ErrNotFound, err)
cmd := s.client.Get(ctx, key)
if err := cmd.Err(); err != nil {
if err == valkey.Nil {
return nil, store.ErrNotFound
}
return nil, fmt.Errorf("can't fetch from valkey: %w", err)
return nil, err
}
return []byte(result), nil
return cmd.Bytes()
}
func (s *Store) Set(ctx context.Context, key string, value []byte, expiry time.Duration) error {
if _, err := s.rdb.Set(ctx, key, string(value), expiry).Result(); err != nil {
return fmt.Errorf("can't set %q in valkey: %w", key, err)
}
return s.client.Set(ctx, key, value, expiry).Err()
}
func (s *Store) Delete(ctx context.Context, key string) error {
res := s.client.Del(ctx, key)
if err := res.Err(); err != nil {
return err
}
if n, _ := res.Result(); n == 0 {
return store.ErrNotFound
}
return nil
}
// IsPersistent tells Anubis this backend is “real” storage, not in-memory.
func (s *Store) IsPersistent() bool {
return true
}

16
package-lock.json generated
View File

@@ -16,7 +16,7 @@
"cssnano": "^7.1.2",
"cssnano-preset-advanced": "^7.0.10",
"esbuild": "^0.25.12",
"playwright": "^1.52.0",
"playwright": "^1.56.0",
"postcss-cli": "^11.0.1",
"postcss-import": "^16.1.1",
"postcss-import-url": "^7.2.0",
@@ -1603,13 +1603,13 @@
}
},
"node_modules/playwright": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.52.0"
"playwright-core": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -1622,9 +1622,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@@ -21,7 +21,7 @@
"cssnano": "^7.1.2",
"cssnano-preset-advanced": "^7.0.10",
"esbuild": "^0.25.12",
"playwright": "^1.52.0",
"playwright": "^1.56.0",
"postcss-cli": "^11.0.1",
"postcss-import": "^16.1.1",
"postcss-import-url": "^7.2.0",
@@ -31,4 +31,4 @@
"@aws-crypto/sha256-js": "^5.2.0",
"preact": "^10.27.2"
}
}
}

0
run/openrc/anubis.initd Normal file → Executable file
View File