From fe6747888bc8df6763023ef366c974b63d87e13f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 12:51:53 +0000 Subject: [PATCH 01/10] Run every database test from one script on disposable databases infra/neon/tests/run-local.sh builds a local-mode database (bootstrap.sql, every migration, after-core.sql) and a stub-mode one (local_auth_stub.sql and every migration), then runs each SQL test on its own copy of the one its header names, and the lock order test the same way. Tests written for a Neon branch are skipped and said to be. It reads the connection from the PG* variables and accepts only a loopback server, since the auth stand-ins create cluster roles that must never reach Neon. Every database it makes carries a random run id that no database had when the run began, creating one fails rather than reuse a taken name, and cleanup (also on an interrupt) drops only those. --create builds one database for the local API and worker tests. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS --- infra/neon/tests/run-local.sh | 164 ++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100755 infra/neon/tests/run-local.sh diff --git a/infra/neon/tests/run-local.sh b/infra/neon/tests/run-local.sh new file mode 100755 index 00000000..6024b0e4 --- /dev/null +++ b/infra/neon/tests/run-local.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# Runs the database tests in this folder against a local PostgreSQL server: +# every *.sql test on its own fresh database, then every *.mjs test +# (task_lock_order.mjs) the same way. CI runs it, and so can you before +# pushing a migration: +# +# PGHOST=127.0.0.1 PGPORT=5432 PGUSER=commune PGPASSWORD=commune \ +# infra/neon/tests/run-local.sh [test-name ...] +# +# With names (grants, task_lock_order, ...) it runs only those tests. The +# *.mjs tests need the local API's dependencies installed (they use its pg). +# +# The connection comes from the usual PG* variables. PGHOST defaults to +# 127.0.0.1 and must be a loopback address: the auth stand-ins below create +# the authenticated and anonymous roles, which must never reach Neon. The role +# needs to create databases and roles (a local superuser, as in CI). +# +# Each test runs on a copy of one of two databases built from the migrations, +# one per local auth mode: +# local infra/local/bootstrap.sql, every migration, infra/local/after-core.sql +# (what the local API runs on) +# stub infra/neon/tests/local_auth_stub.sql and every migration +# A test's leading comment decides: one naming the stub runs on stub, one +# naming bootstrap.sql or after-core.sql runs on local, and any other runs on +# stub. One written for a "Neon branch" is skipped, as it needs hosted auth. +# +# It only ever drops databases it created in the same run. Their names start +# with SQL_TEST_PREFIX (default commune_sqltest) and a random run id that no +# database had when the run began, so an interrupted run cleans up after +# itself and never touches anything else. +# +# infra/neon/tests/run-local.sh --create +# builds one new database the same way and leaves it for the caller (the +# local API and reminder worker tests use this). It refuses a name in use. +set -euo pipefail +export LC_ALL=C + +here=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$here/../../.." && pwd) +export PGHOST="${PGHOST:-127.0.0.1}" PGPORT="${PGPORT:-5432}" +case "$PGHOST" in + 127.0.0.1|localhost) ;; + *) echo "run-local.sh: PGHOST must be 127.0.0.1 or localhost, not $PGHOST" >&2; exit 2 ;; +esac + +valid_name() { + [[ "$1" =~ ^[a-z_][a-z0-9_]*$ ]] && [ "${#1}" -le 63 ] +} +# Names of the databases carrying this run's id (validated, so safe to quote). +run_databases() { + psql -X -At -d postgres -c "SELECT datname FROM pg_database WHERE starts_with(datname, '${1}_') ORDER BY 1" +} + +work=$(mktemp -d "${TMPDIR:-/tmp}/run-local.XXXXXX") +run="" +cleanup() { + local db + if [ -n "$run" ]; then + for db in $(run_databases "$run" 2>/dev/null); do + dropdb --if-exists --force "$db" >/dev/null 2>&1 || echo "run-local.sh: could not drop $db" >&2 + done + fi + rm -rf "$work" +} +trap cleanup EXIT +trap 'exit 130' INT TERM + +# Applies every file for one auth mode to a new, empty database. +migrate() { + local db="$1" mode="$2" file + local files=() + if [ "$mode" = local ]; then files+=("$root/infra/local/bootstrap.sql"); else files+=("$here/local_auth_stub.sql"); fi + files+=("$root"/infra/neon/migrations/*.sql) + if [ "$mode" = local ]; then files+=("$root/infra/local/after-core.sql"); fi + for file in "${files[@]}"; do + if ! psql -X -q -v ON_ERROR_STOP=1 -d "$db" -f "$file" >"$work/build.log" 2>&1; then + echo "FAILED building the $mode database at ${file#"$root"/}:" >&2 + tail -n 20 "$work/build.log" | sed 's/^/ /' >&2 + return 1 + fi + done +} + +# local, stub or neon, from the test's leading comment lines. +mode_of() { + local header + header=$(sed -n -E -e '/^(--|\/\/)/!q' -e p "$1") + case "$header" in + *"Neon branch"*) echo neon ;; + *local_auth_stub*|*"auth stub"*|*"local stub"*) echo stub ;; + *bootstrap.sql*|*after-core*) echo local ;; + *) echo stub ;; + esac +} + +if [ "${1:-}" = "--create" ]; then + if [ "$#" -ne 3 ] || ! valid_name "$2" || { [ "$3" != local ] && [ "$3" != stub ]; }; then + echo "usage: run-local.sh --create " >&2; exit 2 + fi + createdb "$2" # fails if the name is taken + if ! migrate "$2" "$3"; then dropdb --force "$2"; exit 1; fi + echo "created $2 ($3)" + exit 0 +fi + +prefix="${SQL_TEST_PREFIX:-commune_sqltest}" +if ! valid_name "$prefix" || [ "${#prefix}" -gt 40 ]; then + echo "run-local.sh: SQL_TEST_PREFIX must be lower-case letters, digits or _, at most 40 characters" >&2; exit 2 +fi + +tests=() +if [ "$#" -gt 0 ]; then + for name in "$@"; do + if [ -f "$here/$name.sql" ] && [ "$name" != local_auth_stub ]; then tests+=("$here/$name.sql") + elif [ -f "$here/$name.mjs" ]; then tests+=("$here/$name.mjs") + else echo "run-local.sh: no test called $name in ${here#"$root"/}" >&2; exit 2; fi + done +else + for test in "$here"/*.sql "$here"/*.mjs; do + [ "$(basename "$test")" = local_auth_stub.sql ] || tests+=("$test") + done +fi + +candidate="${prefix}_$(od -An -N3 -tx1 /dev/urandom | tr -d ' \n')" +if [ -n "$(run_databases "$candidate")" ]; then + echo "run-local.sh: databases named ${candidate}_* already exist; run again for a new id" >&2; exit 2 +fi +run="$candidate" + +echo "Building the local and stub databases from $(ls "$root"/infra/neon/migrations/*.sql | wc -l | tr -d ' ') migrations" +# Local first: on a fresh server the local database then sees only the roles +# bootstrap.sql makes, as the local API does. +for mode in local stub; do + createdb "${run}_$mode" + migrate "${run}_$mode" "$mode" +done + +pass=0; fail=0; skip=0; n=0 +for test in "${tests[@]}"; do + name=$(basename "$test"); name="${name%.*}" + mode=$(mode_of "$test") + if [ "$mode" = neon ]; then + echo "SKIP $name (needs a Neon branch)"; skip=$((skip + 1)); continue + fi + n=$((n + 1)); db="${run}_$n" + createdb -T "${run}_$mode" "$db" + if [ "${test##*.}" = sql ]; then + # A hung lock wait fails the test instead of the whole run. + PGOPTIONS="${PGOPTIONS:-} -c statement_timeout=120s" \ + psql -X -q -v ON_ERROR_STOP=1 -d "$db" -f "$test" >"$work/test.log" 2>&1 && ok=1 || ok=0 + else + # pg takes the user and password from the PG* variables. + DATABASE_URL="postgres://127.0.0.1:$PGPORT/$db" node "$test" >"$work/test.log" 2>&1 && ok=1 || ok=0 + fi + if [ "$ok" = 1 ]; then + echo "PASS $name ($mode)"; pass=$((pass + 1)) + else + echo "FAIL $name ($mode)"; tail -n 20 "$work/test.log" | sed 's/^/ /'; fail=$((fail + 1)) + fi + dropdb --if-exists --force "$db" >/dev/null 2>&1 || true +done + +echo "Database tests: $pass passed, $fail failed, $skip skipped" +[ "$fail" -eq 0 ] From 3acde32d8eddc8f5efa52a21ef1d627259e496b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 12:56:09 +0000 Subject: [PATCH 02/10] Keep a local sign-in lockout in force however many other sign-ins fail The local API's sign-in throttle emptied its whole list once it held more than 1,000 entries, so failing to sign in as 1,001 other addresses lifted every running lockout and gave a guesser ten more tries. Entries now stay in order of their latest failure and each failure drops only the expired ones from the front, so a lockout runs its full 15 minutes. apps/local-api/tests/sign-in-lockout.mjs locks an account out, fails 1,001 other sign-ins and checks the account is still locked (it got 200 for the right password before this change). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS --- apps/local-api/src/server.ts | 18 +++++++-- apps/local-api/tests/sign-in-lockout.mjs | 49 ++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 apps/local-api/tests/sign-in-lockout.mjs diff --git a/apps/local-api/src/server.ts b/apps/local-api/src/server.ts index c5017832..d279e9b4 100644 --- a/apps/local-api/src/server.ts +++ b/apps/local-api/src/server.ts @@ -180,7 +180,12 @@ async function actor(req: IncomingMessage): Promise { return user; } -// A bounded local throttle protects password verification from quick guessing. +// A local throttle protects password verification from quick guessing: ten +// failures for one email from one client lock that pair out until 15 minutes +// after the latest. Entries stay in order of their latest failure, so expired +// ones are always first and each failure drops them. A lockout still running +// is never dropped, however many other failures arrive; each one costs a +// password hash, which bounds how many can build up in 15 minutes. const failures = new Map(); function checkThrottle(req: IncomingMessage, address: string): string { const key = `${req.socket.remoteAddress ?? "?"}:${address}`; @@ -191,11 +196,16 @@ function checkThrottle(req: IncomingMessage, address: string): string { return key; } function failed(key: string): void { - if (failures.size > 1000) failures.clear(); + const now = Date.now(); + for (const [stale, item] of failures) { + if (item.until > now) break; + failures.delete(stale); + } const existing = failures.get(key); + failures.delete(key); failures.set(key, { - count: existing && existing.until > Date.now() ? existing.count + 1 : 1, - until: Date.now() + 15 * 60 * 1000, + count: existing && existing.until > now ? existing.count + 1 : 1, + until: now + 15 * 60 * 1000, }); } diff --git a/apps/local-api/tests/sign-in-lockout.mjs b/apps/local-api/tests/sign-in-lockout.mjs new file mode 100644 index 00000000..43cd8aa6 --- /dev/null +++ b/apps/local-api/tests/sign-in-lockout.mjs @@ -0,0 +1,49 @@ +import assert from "node:assert/strict"; +import { randomUUID } from "node:crypto"; + +// Ten wrong passwords lock that email out of signing in for 15 minutes, and +// failures for other addresses never lift the lock early. Creates an account, +// so it refuses the normal application API. About 1,000 failed sign-ins, each +// a password hash, so it takes half a minute or so. +const base = process.env.API_BASE; +if (!base) throw new Error("Set API_BASE to an isolated local test API, not the user's database."); +const url = new URL(base); +if (url.protocol !== "http:" || !["127.0.0.1", "localhost"].includes(url.hostname) || !url.port || + url.port === "3002" || url.pathname !== "/" || url.search || url.hash || url.username || url.password) { + throw new Error("Sign-in lockout tests require an isolated loopback API port (not 3002)."); +} +const suffix = randomUUID().slice(0, 8); +const password = "DisposableLockoutFixture-2026!"; +async function call(path, input) { + const response = await fetch(`${base}/api${path}`, { + method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input), + }); + return { status: response.status, ...(await response.json()) }; +} +const signIn = (email, attempt = password) => call("/auth/login", { email, password: attempt }); + +const avery = `lockout-avery-${suffix}@example.test`; +assert.equal((await call("/auth/signup", { name: "Avery Lockout", email: avery, password })).status, 201); +for (let attempt = 1; attempt <= 10; attempt += 1) { + assert.equal((await signIn(avery, `Wrong-guess-${attempt}`)).status, 401, `wrong password ${attempt}`); +} +// Locked: even the right password is refused without being checked. +const locked = await signIn(avery); +assert.equal(locked.status, 429, JSON.stringify(locked)); +assert.equal(locked.error, "Try signing in again later"); + +// The lockout list used to be emptied once it held more than 1,000 entries, +// so 1,001 failures for other addresses gave Avery's guesser ten more tries. +const others = Array.from({ length: 1001 }, (_, index) => `lockout-other-${index}-${suffix}@example.test`); +for (let start = 0; start < others.length; start += 8) { + const statuses = await Promise.all(others.slice(start, start + 8).map(async (email) => (await signIn(email)).status)); + assert.ok(statuses.every((status) => status === 401), `other addresses: ${statuses.join(", ")}`); +} +assert.equal((await signIn(avery)).status, 429, "Failures for other addresses lifted the lockout"); + +// Other people sign in as usual. +const blake = `lockout-blake-${suffix}@example.test`; +assert.equal((await call("/auth/signup", { name: "Blake Lockout", email: blake, password })).status, 201); +assert.equal((await signIn(blake, "Wrong-guess-1")).status, 401); +assert.equal((await signIn(blake)).status, 200); +console.log("Sign-in lockout: ten failures lock an address out, and failures for 1,001 other addresses leave it locked."); From b1a86ae722d428bb244bbebb1040b9631ad604a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 12:56:09 +0000 Subject: [PATCH 03/10] Send invitation email from the local API only when asked to Setting RESEND_API_KEY, RESEND_FROM and APP_URL, perhaps for the worker, was enough for the local API to email real invitations from a development machine. It now also needs LOCAL_API_SEND_EMAIL=yes, and logs at startup whether invitation email is on or off and why. Without the opt-in the sender gets the existing "not configured here" answer (503) and can copy the link as before. The decision lives in apps/local-api/src/invitation-email.ts so tests/invitation-email-opt-in.mjs can check it without a server. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS --- apps/local-api/src/invitation-email.ts | 18 ++++++++++ apps/local-api/src/server.ts | 9 ++++- .../tests/invitation-email-opt-in.mjs | 34 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 apps/local-api/src/invitation-email.ts create mode 100644 apps/local-api/tests/invitation-email-opt-in.mjs diff --git a/apps/local-api/src/invitation-email.ts b/apps/local-api/src/invitation-email.ts new file mode 100644 index 00000000..c81b35b0 --- /dev/null +++ b/apps/local-api/src/invitation-email.ts @@ -0,0 +1,18 @@ +// The local API emails an invitation only when told to in so many words. The +// Resend settings alone are not enough: they may be exported in a shell for +// the worker, and an invitation made while testing would then reach a real +// inbox. LOCAL_API_SEND_EMAIL=yes is the opt-in, and the server logs which +// way it is set when it starts. +type Env = Record; + +export function invitationEmailSetting(env: Env): { send: boolean; notice: string } { + const configured = Boolean(env.RESEND_API_KEY && env.RESEND_FROM && env.APP_URL); + if (env.LOCAL_API_SEND_EMAIL !== "yes") { + return { send: false, notice: configured + ? "Invitation email is off: the Resend settings are ignored unless LOCAL_API_SEND_EMAIL=yes is set as well." + : "Invitation email is off. Invitation links can still be copied and shared." }; + } + return configured + ? { send: true, notice: "Invitation email is on (LOCAL_API_SEND_EMAIL=yes): invitations are emailed through Resend to the address invited." } + : { send: false, notice: "Invitation email is off: LOCAL_API_SEND_EMAIL=yes also needs RESEND_API_KEY, RESEND_FROM and APP_URL." }; +} diff --git a/apps/local-api/src/server.ts b/apps/local-api/src/server.ts index d279e9b4..afb86001 100644 --- a/apps/local-api/src/server.ts +++ b/apps/local-api/src/server.ts @@ -4,6 +4,7 @@ import type { PoolClient } from "pg"; import { changePassword, createAccount, getUser, login, logout, logoutOthers, setSessionCookie, type User } from "./auth.js"; import { pool, withActor } from "./db.js"; import { InvitationEmailError, invitationMailConfig, sendInvitationEmail } from "../../reminder-worker/src/invitations.js"; +import { invitationEmailSetting } from "./invitation-email.js"; const host = "127.0.0.1"; const port = Number(process.env.API_PORT ?? "3002"); @@ -839,11 +840,17 @@ route("GET", /^\/api\/invites\/([0-9a-f]{64})$/, async (req, res, match) => { } : null }); }); +// Real email needs LOCAL_API_SEND_EMAIL=yes as well as the Resend settings. +// Without both, the sender is told email isn't set up here (503). +const invitationEmail = invitationEmailSetting(process.env); +console.log(invitationEmail.notice); + route("POST", /^\/api\/invitation-email$/, async (req, res) => { const user = await actor(req); const input = await body(req); try { - send(res, await sendInvitationEmail(pool, user.id, text(input.token, "token", 64), invitationMailConfig())); + send(res, await sendInvitationEmail(pool, user.id, text(input.token, "token", 64), + invitationEmail.send ? invitationMailConfig() : null)); } catch (error) { if (error instanceof InvitationEmailError) throw new ApiError(error.status, error.message); throw error; diff --git a/apps/local-api/tests/invitation-email-opt-in.mjs b/apps/local-api/tests/invitation-email-opt-in.mjs new file mode 100644 index 00000000..65551199 --- /dev/null +++ b/apps/local-api/tests/invitation-email-opt-in.mjs @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { invitationEmailSetting } from "../src/invitation-email.ts"; + +// Whether the local API may email an invitation. No API or database needed: +// node --test apps/local-api/tests/invitation-email-opt-in.mjs +const resend = { RESEND_API_KEY: "test-only", RESEND_FROM: "Commune ", APP_URL: "https://example.test" }; + +test("the Resend settings alone send nothing, and the log says how to opt in", () => { + const setting = invitationEmailSetting(resend); + assert.equal(setting.send, false); + assert.equal(setting.notice, "Invitation email is off: the Resend settings are ignored unless LOCAL_API_SEND_EMAIL=yes is set as well."); +}); + +test("sends only with the opt-in and every Resend setting", () => { + const setting = invitationEmailSetting({ ...resend, LOCAL_API_SEND_EMAIL: "yes" }); + assert.equal(setting.send, true); + assert.match(setting.notice, /^Invitation email is on/); + for (const missing of Object.keys(resend)) { + const partial = invitationEmailSetting({ ...resend, LOCAL_API_SEND_EMAIL: "yes", [missing]: "" }); + assert.equal(partial.send, false, `sent without ${missing}`); + assert.match(partial.notice, /also needs RESEND_API_KEY, RESEND_FROM and APP_URL/); + } +}); + +test("only the exact opt-in counts", () => { + for (const value of ["1", "true", "YES", " yes", "no", ""]) { + assert.equal(invitationEmailSetting({ ...resend, LOCAL_API_SEND_EMAIL: value }).send, false, `sent with ${JSON.stringify(value)}`); + } +}); + +test("off by default", () => { + assert.deepEqual(invitationEmailSetting({}), { send: false, notice: "Invitation email is off. Invitation links can still be copied and shared." }); +}); From 4ce9f7decae01e32ce990a0b9456adbbd5845b2d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 13:08:43 +0000 Subject: [PATCH 04/10] Pin the pnpm version the repository installs with The root package.json now names pnpm 10.34.6 in packageManager, so CI, corepack and pnpm itself all use one version instead of whatever is installed. 10.34.6 reads the current lockfile unchanged. The hash lets corepack check the download. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index e1e903f3..859078b8 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "workspace", "version": "0.0.0", "license": "MIT", + "packageManager": "pnpm@10.34.6+sha512.7e1ae66a83a9143c118cc1944f37cc4f2b8acddeda4b001cfa78908f6e46e45e15292739ca6c9d026426cc757e9318cc98c5551d6a16c165097b0028ba87b4e6", "scripts": { "preinstall": "sh -c 'rm -f package-lock.json yarn.lock; case \"$npm_config_user_agent\" in pnpm/*) ;; *) echo \"Use pnpm instead\" >&2; exit 1 ;; esac'", "build": "pnpm run typecheck && pnpm -r --filter \"./apps/**\" --if-present run build", From 96702d818c60404a7865bb29636ff9adf32e35d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 13:08:55 +0000 Subject: [PATCH 05/10] Update vite to 7.3.7 vite 7.3.2 is in range of two advisories fixed in 7.3.5: a server.fs.deny bypass through Windows alternate paths, and the bundled launch-editor disclosing an NTLM hash through UNC paths. The catalog now asks for ^7.3.5, which resolves to 7.3.7, the current 7.3 release. The lockfile changes only vite and the references to it: the peer suffixes of the packages that depend on it, and vite's own fdir, picomatch and tinyglobby, now the versions vitest already uses. Nothing new is downloaded beyond vite itself. minimumReleaseAge is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS --- pnpm-lock.yaml | 100 ++++++++++++++++++++++---------------------- pnpm-workspace.yaml | 2 +- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca1c6c9e..fb96cdec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,8 +64,8 @@ catalogs: specifier: ^4.21.0 version: 4.21.0 vite: - specifier: ^7.3.2 - version: 7.3.2 + specifier: ^7.3.5 + version: 7.3.7 wouter: specifier: ^3.3.5 version: 3.9.0 @@ -530,7 +530,7 @@ importers: version: 5.3.0 '@neondatabase/neon-js': specifier: 0.7.0-beta - version: 0.7.0-beta(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@standard-schema/spec@1.1.0)(@tanstack/react-query@5.90.21(react@19.1.0))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(ajv@8.18.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(nanostores@1.5.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) + version: 0.7.0-beta(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@standard-schema/spec@1.1.0)(@tanstack/react-query@5.90.21(react@19.1.0))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(ajv@8.18.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(nanostores@1.5.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) lucide-react: specifier: 'catalog:' version: 0.545.0(react@19.1.0) @@ -558,7 +558,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: 'catalog:' - version: 5.1.4(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.1.4(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) jsdom: specifier: ^30.1.1 version: 30.1.1(@noble/hashes@2.4.0) @@ -567,10 +567,10 @@ importers: version: 5.9.3 vite: specifier: 'catalog:' - version: 7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) vitest: specifier: ^5.0.2 - version: 5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) artifacts/api-server: dependencies: @@ -724,7 +724,7 @@ importers: version: 0.5.19(tailwindcss@4.2.1) '@tailwindcss/vite': specifier: 'catalog:' - version: 4.2.1(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.2.1(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/react-query': specifier: 'catalog:' version: 5.90.21(react@19.1.0) @@ -739,7 +739,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: 'catalog:' - version: 5.1.4(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.1.4(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) '@workspace/api-client-react': specifier: workspace:* version: link:../../lib/api-client-react @@ -808,7 +808,7 @@ importers: version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) vite: specifier: 'catalog:' - version: 7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) wouter: specifier: ^3.3.5 version: 3.9.0(react@19.1.0) @@ -910,7 +910,7 @@ importers: version: 0.0.6 '@tailwindcss/vite': specifier: 'catalog:' - version: 4.2.1(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.2.1(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) '@types/node': specifier: 'catalog:' version: 25.3.5 @@ -922,7 +922,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: 'catalog:' - version: 5.1.4(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.1.4(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -994,7 +994,7 @@ importers: version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) vite: specifier: 'catalog:' - version: 7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) zod: specifier: 'catalog:' version: 3.25.76 @@ -1012,7 +1012,7 @@ importers: version: 0.0.6 '@tailwindcss/vite': specifier: 'catalog:' - version: 4.2.1(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.2.1(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) '@types/node': specifier: 'catalog:' version: 25.3.5 @@ -1024,7 +1024,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: 'catalog:' - version: 5.1.4(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.1.4(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) react: specifier: 'catalog:' version: 19.1.0 @@ -1039,7 +1039,7 @@ importers: version: 4.21.0 vite: specifier: 'catalog:' - version: 7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) wouter: specifier: 'catalog:' version: 3.9.0(react@19.1.0) @@ -11279,8 +11279,8 @@ packages: yaml: optional: true - vite@7.3.2: - resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} + vite@7.3.7: + resolution: {integrity: sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -12377,11 +12377,11 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@better-auth/api-key@1.7.5(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(better-auth@1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))))(better-call@2.0.2(zod@4.3.6))': + '@better-auth/api-key@1.7.5(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(better-auth@1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))))(better-call@2.0.2(zod@4.3.6))': dependencies: '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3) '@better-auth/utils': 0.4.2 - better-auth: 1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) + better-auth: 1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) better-call: 2.0.2(zod@4.3.6) zod: 4.6.5 @@ -12421,14 +12421,14 @@ snapshots: '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3) '@better-auth/utils': 0.4.2 - '@better-auth/passkey@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))))(better-call@1.3.7(zod@4.6.5))(nanostores@1.5.3)': + '@better-auth/passkey@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))))(better-call@1.3.7(zod@4.6.5))(nanostores@1.5.3)': dependencies: '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3) '@better-auth/utils': 0.4.2 '@better-fetch/fetch': 1.3.1 '@simplewebauthn/browser': 13.3.0 '@simplewebauthn/server': 13.3.3 - better-auth: 1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) + better-auth: 1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) better-call: 1.3.7(zod@4.6.5) nanostores: 1.5.3 zod: 4.3.6 @@ -12518,10 +12518,10 @@ snapshots: '@date-fns/tz@1.4.1': {} - '@daveyplate/better-auth-ui@3.4.0(e3c124b483afe493503f54b7cb398ba3)': + '@daveyplate/better-auth-ui@3.4.0(357ce37a9a6e0027d2c14043549f51de)': dependencies: - '@better-auth/api-key': 1.7.5(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(better-auth@1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))))(better-call@2.0.2(zod@4.3.6)) - '@better-auth/passkey': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))))(better-call@1.3.7(zod@4.6.5))(nanostores@1.5.3) + '@better-auth/api-key': 1.7.5(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(better-auth@1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))))(better-call@2.0.2(zod@4.3.6)) + '@better-auth/passkey': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))))(better-call@1.3.7(zod@4.6.5))(nanostores@1.5.3) '@better-fetch/fetch': 1.3.1 '@captchafox/react': 1.13.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@hcaptcha/react-hcaptcha': 2.2.0 @@ -12545,7 +12545,7 @@ snapshots: '@react-email/components': 1.0.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-query': 5.90.21(react@19.1.0) '@wojtekmaj/react-recaptcha-v3': 0.1.4(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - better-auth: 1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) + better-auth: 1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) better-call: 2.0.2(zod@4.3.6) bowser: 2.14.1 class-variance-authority: 0.7.1 @@ -14060,15 +14060,15 @@ snapshots: '@neon/functions@0.11.0': {} - '@neondatabase/auth-ui@0.3.0-beta(f5b4828dc0f43eb0535364a260cbfc97)': + '@neondatabase/auth-ui@0.3.0-beta(3e0f700d598dfec41f6056c7399623ce)': dependencies: - '@better-auth/passkey': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))))(better-call@1.3.7(zod@4.6.5))(nanostores@1.5.3) + '@better-auth/passkey': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))))(better-call@1.3.7(zod@4.6.5))(nanostores@1.5.3) '@captchafox/react': 1.13.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@daveyplate/better-auth-ui': 3.4.0(e3c124b483afe493503f54b7cb398ba3) + '@daveyplate/better-auth-ui': 3.4.0(357ce37a9a6e0027d2c14043549f51de) '@hcaptcha/react-hcaptcha': 1.17.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@hookform/resolvers': 5.9.1(@standard-schema/spec@1.1.0)(ajv@8.18.0)(react-hook-form@7.71.2(react@19.1.0))(zod@4.3.6) '@marsidev/react-turnstile': 1.6.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@neondatabase/auth': 0.5.0-beta(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@standard-schema/spec@1.1.0)(@tanstack/react-query@5.90.21(react@19.1.0))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(ajv@8.18.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(nanostores@1.5.3)(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) + '@neondatabase/auth': 0.5.0-beta(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@standard-schema/spec@1.1.0)(@tanstack/react-query@5.90.21(react@19.1.0))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(ajv@8.18.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(nanostores@1.5.3)(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) '@radix-ui/react-avatar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-context': 1.1.3(@types/react@19.2.14)(react@19.1.0) @@ -14083,7 +14083,7 @@ snapshots: '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.1.0) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0) '@wojtekmaj/react-recaptcha-v3': 0.1.4(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - better-auth: 1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) + better-auth: 1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) better-call: 1.3.7(zod@4.3.6) class-variance-authority: 0.7.1 clsx: 2.1.1 @@ -14158,12 +14158,12 @@ snapshots: - vue - yup - '@neondatabase/auth@0.5.0-beta(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@standard-schema/spec@1.1.0)(@tanstack/react-query@5.90.21(react@19.1.0))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(ajv@8.18.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(nanostores@1.5.3)(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))': + '@neondatabase/auth@0.5.0-beta(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@standard-schema/spec@1.1.0)(@tanstack/react-query@5.90.21(react@19.1.0))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(ajv@8.18.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(nanostores@1.5.3)(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))': dependencies: '@better-fetch/fetch': 1.3.1 - '@neondatabase/auth-ui': 0.3.0-beta(f5b4828dc0f43eb0535364a260cbfc97) + '@neondatabase/auth-ui': 0.3.0-beta(3e0f700d598dfec41f6056c7399623ce) '@supabase/auth-js': 2.79.0 - better-auth: 1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) + better-auth: 1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) jose: 6.2.5 zod: 4.3.6 optionalDependencies: @@ -14223,9 +14223,9 @@ snapshots: - vue - yup - '@neondatabase/neon-js@0.7.0-beta(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@standard-schema/spec@1.1.0)(@tanstack/react-query@5.90.21(react@19.1.0))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(ajv@8.18.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(nanostores@1.5.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))': + '@neondatabase/neon-js@0.7.0-beta(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@standard-schema/spec@1.1.0)(@tanstack/react-query@5.90.21(react@19.1.0))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(ajv@8.18.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(nanostores@1.5.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))': dependencies: - '@neondatabase/auth': 0.5.0-beta(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@standard-schema/spec@1.1.0)(@tanstack/react-query@5.90.21(react@19.1.0))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(ajv@8.18.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(nanostores@1.5.3)(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) + '@neondatabase/auth': 0.5.0-beta(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@standard-schema/spec@1.1.0)(@tanstack/react-query@5.90.21(react@19.1.0))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(ajv@8.18.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(nanostores@1.5.3)(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) '@neondatabase/postgrest-js': 0.2.0-beta meow: 14.0.0 pg: 8.20.0 @@ -16644,12 +16644,12 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.2.1 - '@tailwindcss/vite@4.2.1(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tailwindcss/vite@4.2.1(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@tailwindcss/node': 4.2.1 '@tailwindcss/oxide': 4.2.1 tailwindcss: 4.2.1 - vite: 7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) '@tanstack/history@1.161.6': {} @@ -17201,7 +17201,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@5.1.4(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@5.1.4(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -17209,7 +17209,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -17229,14 +17229,14 @@ snapshots: optionalDependencies: vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@5.0.2(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@5.0.2(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@jridgewell/trace-mapping': 0.3.31 '@vitest/spy': 5.0.2 estree-walker: 3.0.3 magic-string: 1.4.2 optionalDependencies: - vite: 7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -17729,7 +17729,7 @@ snapshots: baseline-browser-mapping@2.10.8: {} - better-auth@1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))): + better-auth@1.6.23(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0))(pg@8.20.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))): dependencies: '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3) '@better-auth/drizzle-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.7(zod@4.6.5))(jose@6.2.5)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@types/pg@8.18.0)(kysely@0.29.6)(pg@8.20.0)) @@ -17754,7 +17754,7 @@ snapshots: pg: 8.20.0 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - vitest: 5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + vitest: 5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -24245,14 +24245,14 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 postcss: 8.5.8 rollup: 4.59.0 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 25.3.5 fsevents: 2.3.3 @@ -24303,10 +24303,10 @@ snapshots: - tsx - yaml - vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)): + vitest@5.0.2(@types/node@25.3.5)(jsdom@30.1.1(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@types/chai': 5.2.3 - '@vitest/mocker': 5.0.2(vite@7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 5.0.2(vite@7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) chai: 6.2.2 es-module-lexer: 2.3.2 expect-type: 1.4.0 @@ -24317,7 +24317,7 @@ snapshots: tinybench: 6.2.0 tinyexec: 1.3.1 tinyglobby: 0.2.17 - vite: 7.3.2(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.7(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 3.2.2 optionalDependencies: '@types/node': 25.3.5 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f30831da..017e0d70 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -63,7 +63,7 @@ catalog: tailwind-merge: ^3.3.1 tailwindcss: ^4.1.14 tsx: ^4.21.0 - vite: ^7.3.2 + vite: ^7.3.5 wouter: ^3.3.5 zod: ^3.25.76 From 09a8a6198f9c820c3bda03307fcd1942abc741d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 13:19:07 +0000 Subject: [PATCH 06/10] Count the migrations in run-local.sh without ls ShellCheck prefers a glob to parsing ls. The count is only for the progress line. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS --- infra/neon/tests/run-local.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infra/neon/tests/run-local.sh b/infra/neon/tests/run-local.sh index 6024b0e4..7cd98841 100755 --- a/infra/neon/tests/run-local.sh +++ b/infra/neon/tests/run-local.sh @@ -127,7 +127,8 @@ if [ -n "$(run_databases "$candidate")" ]; then fi run="$candidate" -echo "Building the local and stub databases from $(ls "$root"/infra/neon/migrations/*.sql | wc -l | tr -d ' ') migrations" +migrations=("$root"/infra/neon/migrations/*.sql) +echo "Building the local and stub databases from ${#migrations[@]} migrations" # Local first: on a fresh server the local database then sees only the roles # bootstrap.sql makes, as the local API does. for mode in local stub; do From f47ba0f6eaa6858ed7eda78a9a0c1613fedc68df Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 13:22:05 +0000 Subject: [PATCH 07/10] Check every pull request and push to main in GitHub Actions Finding 20 from the app review: nothing ran the tests automatically. .github/workflows/ci.yml has three jobs, each with a time limit and read-only access to the repository: - web: installs the web app, local API and reminder worker with the frozen lockfile, typechecks all three, runs the web tests and builds. - database: on a PostgreSQL 16.15 service, runs every test in infra/neon/tests through run-local.sh, starts the local API on its own database and runs its HTTP tests, then runs the reminder worker's tests, each on a fresh database. The service sorts text as C.UTF-8, like the local servers the tests were written against. - secrets: gitleaks, built from source with go install, scans the files and the commits the pull request or push adds, with findings redacted. Older history is left out: it still holds the key from finding 1. Actions are pinned to full commit hashes. Nothing deploys or uses a secret. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS --- .github/workflows/ci.yml | 183 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..cbcb5ea7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,183 @@ +# Checks every pull request and every push to main. +# +# web typechecks the web app, local API and reminder worker, runs the +# web tests and builds the web app +# database builds disposable databases from the migrations on PostgreSQL 16 +# and runs the database tests, the local API's HTTP tests and the +# reminder worker's tests on them +# secrets scans the files, and the commits this change adds, for secrets +# +# Nothing here deploys, uses a secret or reaches Neon, Vercel or Resend. Each +# step is a command you can also run locally. +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +# A new push to a pull request cancels its older run. Every push to main runs. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + web: + name: Web app + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + # Installs the pnpm version named by packageManager in package.json. + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + cache: pnpm + - name: Install + run: pnpm install --frozen-lockfile --filter @commune/web --filter @commune/local-api --filter @commune/reminder-worker + - name: Typecheck + run: pnpm --filter @commune/web --filter @commune/local-api --filter @commune/reminder-worker run typecheck + - name: Web tests + run: pnpm --filter @commune/web test + - name: Build + run: pnpm --filter @commune/web run build + + database: + name: Database, local API and worker + runs-on: ubuntu-24.04 + timeout-minutes: 20 + services: + postgres: + image: postgres:16.15 + env: + POSTGRES_USER: commune + POSTGRES_PASSWORD: commune + # Sort text as the local servers the tests are written against do + # (the image would otherwise use en_US.utf8). + POSTGRES_INITDB_ARGS: --locale=C.UTF-8 + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 5s + --health-timeout 5s + --health-retries 12 + env: + PGHOST: 127.0.0.1 + PGPORT: 5432 + PGUSER: commune + PGPASSWORD: commune + # Every database this job makes has a name starting with this. + SQL_TEST_PREFIX: commune_ci + # The local API's tests refuse its everyday port, 3002. + API_PORT: 3102 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + cache: pnpm + # The same install as the web job, so the two share one pnpm cache. + - name: Install + run: pnpm install --frozen-lockfile --filter @commune/web --filter @commune/local-api --filter @commune/reminder-worker + - name: PostgreSQL client + run: | + command -v psql >/dev/null || { sudo apt-get update -q && sudo apt-get install -y -q postgresql-client; } + psql --version + # Every test in infra/neon/tests, the lock order test included, each on + # a fresh database. The script's header explains the rest. + - name: Database tests + run: infra/neon/tests/run-local.sh + - name: Local API tests + run: | + db="${SQL_TEST_PREFIX}_api" + infra/neon/tests/run-local.sh --create "$db" local + log="$RUNNER_TEMP/local-api.log" + (cd apps/local-api && DATABASE_URL="postgres://127.0.0.1:$PGPORT/$db" exec node --import tsx src/server.ts) >"$log" 2>&1 & + api=$! + trap 'kill "$api" 2>/dev/null || true; wait "$api" 2>/dev/null || true; dropdb --if-exists --force "$db" || true' EXIT + for _ in $(seq 1 60); do + curl -sf "http://127.0.0.1:$API_PORT/api/health" >/dev/null && break + sleep 0.5 + done + if ! curl -sf "http://127.0.0.1:$API_PORT/api/health" >/dev/null; then + echo "The local API did not start:"; cat "$log"; exit 1 + fi + status=0 + for test in apps/local-api/tests/*.mjs; do + name=$(basename "$test" .mjs) + case "$name" in *fixture*) continue ;; esac + echo "::group::$name" + if API_BASE="http://127.0.0.1:$API_PORT" timeout 300 node "$test"; then result="PASS $name"; else result="FAIL $name"; status=1; fi + echo "::endgroup::" + echo "$result" + done + if [ "$status" -ne 0 ]; then echo "::group::Local API log"; cat "$log"; echo "::endgroup::"; fi + exit "$status" + - name: Reminder worker tests + working-directory: apps/reminder-worker + run: | + node --test tests/email.mjs tests/unsubscribe.mjs + status=0 + for test in integration invitations webhooks monthly-limits-concurrency; do + db="${SQL_TEST_PREFIX}_worker_${test//-/_}" + # The invitations test runs only on a database named for it. + if [ "$test" = invitations ]; then db="${SQL_TEST_PREFIX}_invite_email_test"; fi + ../../infra/neon/tests/run-local.sh --create "$db" stub + echo "::group::$test" + if DATABASE_URL="postgres://127.0.0.1:$PGPORT/$db" timeout 300 node "tests/$test.mjs"; then result="PASS $test"; else result="FAIL $test"; status=1; fi + echo "::endgroup::" + echo "$result" + dropdb --force "$db" + done + exit "$status" + + secrets: + name: Secret scan + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: '1.27.x' + cache: false + # The gitleaks command line tool, built from its published source, which + # Go checks against its checksum database. Unlike the gitleaks GitHub + # Action it needs no licence for an organisation's repository. + - name: Install gitleaks + run: go install github.com/zricethezav/gitleaks/v8@v8.30.1 + # A finding shows the file, line and rule, with the secret redacted. + - name: Scan the files + run: gitleaks dir . --redact --no-banner --verbose + # Only the commits this pull request or push adds. Older history still + # holds the Google API key from app review finding 1, so scanning all of + # it would fail every run. + - name: Scan the new commits + env: + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.after }} + run: | + if git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null; then + range="$BASE_SHA..$HEAD_SHA" + else + # A new branch, or a force push whose old commit is gone. + range="-1 $HEAD_SHA" + fi + gitleaks git --log-opts="$range" --redact --no-banner --verbose . From 47d6e89be379f7109cb17c161a5cb2ef4c46903e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 13:47:37 +0000 Subject: [PATCH 08/10] Document the CI workflow, run-local.sh and the email opt-in The database README points at run-local.sh, the local README says invitation email needs LOCAL_API_SEND_EMAIL=yes as well as the Resend settings, the local API README covers the two new tests, and the handoff records CI and the pnpm and vite pins. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS --- apps/local-api/README.md | 4 ++++ docs/HANDOFF.md | 4 ++++ infra/local/README.md | 14 ++++++++------ infra/neon/README.md | 4 ++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/local-api/README.md b/apps/local-api/README.md index 1c36ffe1..5e2c696e 100644 --- a/apps/local-api/README.md +++ b/apps/local-api/README.md @@ -120,6 +120,10 @@ request ids, and pledge limits and notices. It also refuses port 3002. signed-in one (`X-Commune-Viewer`, sent by each tab) is refused with 401, while the signed-in account and requests without the header are still served. +`sign-in-lockout.mjs` takes about 40 seconds (1,000 failed sign-ins). +`invitation-email-opt-in.mjs` needs no API: +`node --test apps/local-api/tests/invitation-email-opt-in.mjs`. + Search integration requires migration 0014. It covers all eight result types, active membership, selected/all-commune scopes, removed-member denial, literal wildcards, result caps and private-note masking. Its loopback guard refuses diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index d5b19518..2382a137 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -121,6 +121,10 @@ install was run. - Avoid `pnpm run` and `pnpm install` here for now. pnpm 11 checks the whole workspace first, the untracked apps are not in the lockfile, and the last attempt ran out of disk. +- CI: `.github/workflows/ci.yml` (web, database, secrets) runs on pull + requests and pushes to main. pnpm is pinned to 10.34.6 through + `packageManager`; pnpm 11 hands over to it here. vite is 7.3.7; reinstall + `.deps-web` to pick it up. - A proper `pnpm install` needs a decision about the untracked workspace apps and several GB free. **Free disk fell to about 1.6 GB during this session** (from 4.4 GB); this work accounts for under 200 MB of that. diff --git a/infra/local/README.md b/infra/local/README.md index a7ff2904..46b4e0bc 100644 --- a/infra/local/README.md +++ b/infra/local/README.md @@ -61,12 +61,14 @@ suppressed locally, so no email is claimed or sent. Contributions and settled shares are confirmations recorded by the relevant admin/payer, not payment transfers; Stripe is not yet connected. -Invitation emails now have a separate explicit send action. The local server -returns an honest unavailable message unless server-only `RESEND_API_KEY`, -`RESEND_FROM`, and `APP_URL` are configured. It still creates a real copyable -invite link. Never point `APP_URL` at production while testing local invites: -the email's HTTPS origin must serve this same database. Default local setup -sends no email. Delivery service tests use a mocked provider and isolated DB. +Invitation emails now have a separate explicit send action. The local API +emails invitations only when `LOCAL_API_SEND_EMAIL=yes` is set as well as +server-only `RESEND_API_KEY`, `RESEND_FROM` and `APP_URL`, and says at start-up +which way it is set. Otherwise it returns an honest unavailable message and +still creates a real invite link to copy and share. Never point `APP_URL` at +production while testing local invites: the email's HTTPS origin must serve +this same database. Default local setup sends no email. Delivery service tests +use a mocked provider and isolated DB. ## 2 October local workflow verification diff --git a/infra/neon/README.md b/infra/neon/README.md index 59dd1655..b65b5c92 100644 --- a/infra/neon/README.md +++ b/infra/neon/README.md @@ -385,6 +385,10 @@ rate limits) are recorded in [`docs/AUTH-SETTINGS.md`](../../docs/AUTH-SETTINGS. ## Local database verification +Run every database test locally with `infra/neon/tests/run-local.sh` (PG* +variables, loopback only; its header explains). CI runs it on every pull +request. + On a disposable local PostgreSQL database, run `tests/local_auth_stub.sql`, migrations 0001 through 0011, then `tests/reminders.sql`, `tests/reminder_monthly_limits.sql`, From 4b14ace6db5d6ee7d88839c29e8ef553d81d3891 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 14:31:09 +0000 Subject: [PATCH 09/10] Scan the whole history for secrets and close run-local.sh's host loophole The secret scan read only the commits a change added, so after a force push, or on the first push of a branch, it read the newest commit alone and a secret added and removed within the push went unseen. It now reads every commit behind the checkout, which takes about eight seconds. .gitleaksignore leaves out the two old commits that hold the Google API key from app review finding 1, by fingerprint only. That key still has to be revoked. run-local.sh checked PGHOST, but libpq connects to PGHOSTADDR or a PGSERVICE entry instead when they are set. It now refuses to start while PGHOSTADDR, PGSERVICE or PGSERVICEFILE is set. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS --- .github/workflows/ci.yml | 25 +++++++++---------------- .gitleaksignore | 6 ++++++ infra/neon/tests/run-local.sh | 10 ++++++++-- 3 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 .gitleaksignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cbcb5ea7..2c06c6a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ # database builds disposable databases from the migrations on PostgreSQL 16 # and runs the database tests, the local API's HTTP tests and the # reminder worker's tests on them -# secrets scans the files, and the commits this change adds, for secrets +# secrets scans the files and every commit behind them for secrets # # Nothing here deploys, uses a secret or reaches Neon, Vercel or Resend. Each # step is a command you can also run locally. @@ -166,18 +166,11 @@ jobs: # A finding shows the file, line and rule, with the secret redacted. - name: Scan the files run: gitleaks dir . --redact --no-banner --verbose - # Only the commits this pull request or push adds. Older history still - # holds the Google API key from app review finding 1, so scanning all of - # it would fail every run. - - name: Scan the new commits - env: - BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.after }} - run: | - if git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null; then - range="$BASE_SHA..$HEAD_SHA" - else - # A new branch, or a force push whose old commit is gone. - range="-1 $HEAD_SHA" - fi - gitleaks git --log-opts="$range" --redact --no-banner --verbose . + # Every commit behind this checkout, so a secret added and removed again + # is caught however the change was pushed. These are gitleaks' own + # history options, limited to this checkout rather than every branch. + # .gitleaksignore lists the two old commits that hold the Google API key + # from app review finding 1, by fingerprint (commit, file, rule and line; + # no secret). Revoking that key is still needed. + - name: Scan the history + run: gitleaks git --log-opts="--full-history --diff-filter=tuxdb HEAD" --redact --no-banner --verbose . diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 00000000..bfab557d --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,6 @@ +# Findings the secret scan (.github/workflows/ci.yml) leaves out, by +# fingerprint: commit:file:rule:line. Both are the Google API key from app +# review finding 1, in two old commits. It is no longer in the files, but it +# stays in history, so it must be revoked in Google Cloud. +23b3eeb5a19ccc38fb74b2755ce754496111e06b:.migration-backup/docs/COMMUNE-BUILD-BIBLE.md:gcp-api-key:619 +da953a1499af8b6c0d3bd8df2f6d2ea8ec4040e3:docs/COMMUNE-BUILD-BIBLE.md:gcp-api-key:619 diff --git a/infra/neon/tests/run-local.sh b/infra/neon/tests/run-local.sh index 7cd98841..8c21e351 100755 --- a/infra/neon/tests/run-local.sh +++ b/infra/neon/tests/run-local.sh @@ -12,8 +12,9 @@ # # The connection comes from the usual PG* variables. PGHOST defaults to # 127.0.0.1 and must be a loopback address: the auth stand-ins below create -# the authenticated and anonymous roles, which must never reach Neon. The role -# needs to create databases and roles (a local superuser, as in CI). +# the authenticated and anonymous roles, which must never reach Neon. PGHOSTADDR, +# PGSERVICE and PGSERVICEFILE must be unset, as libpq would follow them instead. +# The role needs to create databases and roles (a local superuser, as in CI). # # Each test runs on a copy of one of two databases built from the migrations, # one per local auth mode: @@ -42,6 +43,11 @@ case "$PGHOST" in 127.0.0.1|localhost) ;; *) echo "run-local.sh: PGHOST must be 127.0.0.1 or localhost, not $PGHOST" >&2; exit 2 ;; esac +for setting in PGHOSTADDR PGSERVICE PGSERVICEFILE; do + if [ -n "${!setting:-}" ]; then + echo "run-local.sh: unset $setting; it would take the connection somewhere other than PGHOST" >&2; exit 2 + fi +done valid_name() { [[ "$1" =~ ^[a-z_][a-z0-9_]*$ ]] && [ "${#1}" -le 63 ] From d44e76bdb31f202d469c04ba72bfa58e29edcad8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 16:50:01 +0000 Subject: [PATCH 10/10] List the two older secrets the full history holds The first run on GitHub failed the history scan. This container's clone was shallow, so the scan here had seen 318 commits; GitHub's checkout holds all 658, and two more findings sit in the older ones. The Google API key from app review finding 1 is also in docs/SESSION-REFERENCE.md at c5fae88 (the same key, compared by hash), and the earlier Supabase app's web push (VAPID) private key is in apps/web/.env.example at fb1d3c8. Neither is in the files today. Both join .gitleaksignore by fingerprint (commit, file, rule and line; no secret), so any other secret, old or new, still fails the scan. With the full history fetched, the scan here now sees the same 658 commits and passes. The Google key is the one already to be revoked; the web push key belongs to the old app's send-notification function and should be revoked there too. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS --- .github/workflows/ci.yml | 6 +++--- .gitleaksignore | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c06c6a2..e4a7277a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -169,8 +169,8 @@ jobs: # Every commit behind this checkout, so a secret added and removed again # is caught however the change was pushed. These are gitleaks' own # history options, limited to this checkout rather than every branch. - # .gitleaksignore lists the two old commits that hold the Google API key - # from app review finding 1, by fingerprint (commit, file, rule and line; - # no secret). Revoking that key is still needed. + # .gitleaksignore lists the old commits that hold the Google API key from + # app review finding 1 and the earlier app's web push key, by fingerprint + # (commit, file, rule and line; no secret). Revoking both is still needed. - name: Scan the history run: gitleaks git --log-opts="--full-history --diff-filter=tuxdb HEAD" --redact --no-banner --verbose . diff --git a/.gitleaksignore b/.gitleaksignore index bfab557d..a61b4e29 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -1,6 +1,11 @@ # Findings the secret scan (.github/workflows/ci.yml) leaves out, by -# fingerprint: commit:file:rule:line. Both are the Google API key from app -# review finding 1, in two old commits. It is no longer in the files, but it -# stays in history, so it must be revoked in Google Cloud. +# fingerprint: commit:file:rule:line. None of them is in the files any more, +# but history keeps them, so each must be revoked where it was issued. +# +# The Google API key from app review finding 1, in three old commits. 23b3eeb5a19ccc38fb74b2755ce754496111e06b:.migration-backup/docs/COMMUNE-BUILD-BIBLE.md:gcp-api-key:619 +c5fae888cafd3074cfa4891184a3d61790b7eb2d:docs/SESSION-REFERENCE.md:gcp-api-key:619 da953a1499af8b6c0d3bd8df2f6d2ea8ec4040e3:docs/COMMUNE-BUILD-BIBLE.md:gcp-api-key:619 +# The web push (VAPID) private key of the earlier Supabase app, committed to an +# example environment file in March 2026. The current app sends no web push. +fb1d3c8dd0cea11791c140db10159592bbddcb66:apps/web/.env.example:generic-api-key:7