Skip to content

Harden the account system: taken addresses, abandoned guests, and a turnstile - #12

Merged
boubou666 merged 6 commits into
mainfrom
worktree-harden-accounts
Aug 5, 2026
Merged

boubou666 merged 6 commits into
mainfrom
worktree-harden-accounts

Conversation

@boubou666

@boubou666 boubou666 commented Aug 5, 2026 •

Copy link
Copy Markdown
Owner

Three things the account system needed once it had real users, in the order they bite.

Say what a taken address costs

Registering with an address that already has an account printed "User already registered" and stopped. Accurate and useless: the person reading it is a guest with a browser full of boards, and the account they are being told about is reachable only by signing in — which leaves those boards with the guest, exactly as signing out would. The form sent them round the same loop with no way through.

register() now answers taken, with a message naming the cost, and the form offers to sign in as that address — next to the error rather than in place of it, because a button on its own is an invitation without the price on it.

The two paths disagree on the word for it: attaching to a guest gives email_exists, signing up with no session gives user_already_exists. Both are matched, neither assumed to cover the other, and this was checked against the live project rather than guessed.

The browser suite's after-hook now allows a 422 alongside the 400 it already allowed — Chrome logs every failed request at error level, and a taken address is a 422 by design. Still filtered by status, so a 500 from the stack fails the suite as before.

Sweep the guests who left nothing behind

Every first visit is a real row in auth.users and Supabase has no automatic cleanup: ordinary traffic grows the table forever, and anyone who wants to can grow it faster by asking the signup endpoint for guests in a loop.

sweep_anonymous_users deletes anonymous users who own no board, are on nobody else's board, and have not been seen for a while; pg_cron runs it daily. The interesting half is what it keeps, so every test is paired — a guest with a board and a guest without, swept together. A registered user never being swept has its own test: is_anonymous is the whole guard between housekeeping and deleting the customers.

Only the schedule is conditional, not the function. These migrations reach a hosted project, a local stack, and a bare Postgres that apply.js --stub fits out with just enough of GoTrue; pg_cron is absent from the third and create extension on a missing extension is an error, not a no-op.

test:db now runs its files one at a time. There is one database behind all of them and rls.test.js clears public.boards before each of its own tests; run concurrently that wipe lands mid-fixture and reads as a foreign key bug in the sweep rather than as two suites sharing a table.

A turnstile in front of the one door anyone can open

Anonymous sign-in takes nothing and answers with a row in auth.users. The sweep is a mop; this is a lock.

The site key is a value that can be absent, exactly as the Supabase config is — no key, no widget, no script, no token, which is what every test and every local dev server runs as. The token is taken per sign-in rather than at construction: it is single-use and short-lived, so one fetched at load would be spent by the time a retry wanted it. A CAPTCHA that will not issue one fails the start answered rather than thrown, so the gate shows it and keeps its retry.

config.toml stops describing somewhere a push would break

site_url is the deployed site now, which is safe in both directions because nothing reads it: there is no redirect-based auth flow here — no OAuth, no magic link, no emailRedirectTo, no password reset — and with confirmations off no mail carrying a link is sent either. env(...) would be the answer if that changed, and the file says so, along with which values remain unaudited against the hosted project.

Deploying this safely

The two halves of the CAPTCHA move in one direction only. Merge and deploy first, then set VITE_TURNSTILE_SITE_KEY, confirm sign-in still works on the live site, and only then enable CAPTCHA protection in Supabase. Enabling the project setting while the site sends no token rejects every sign-in.

Setting the variable is itself a change in failure modes: the live site starts depending on Cloudflare's script loading, where today it cannot fail that way. Deliberately left for a human to decide.

Verified, and not

Node suite 338/338, db suite 42/42 against a real local stack, browser account suite green including the new case, coverage 99.1%.

Not verified: anything involving real Cloudflare. The Turnstile tests drive a fake widget, so what an actual challenge does in an actual browser is unproven.

Found, not fixed

  • apply.js --stub against a bare Postgres is already broken on main: it fails at 20260804154000_board_broadcast.sql with schema "realtime" does not exist, an earlier migration than any here. The README's promise that RLS runs against any Postgres does not currently hold.
  • test:node has the same parallel-suites-share-one-database flakiness fixed here for test:db — sync.test.js and the supabase repository suite each failed once and passed on re-run.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TbSwVkGcNQZohV9VoeACRT

Summary by CodeRabbit

  • New Features

    • Added optional CAPTCHA protection during sign-in and registration.
    • Added clearer handling for registrations using an existing email, including a direct sign-in option.
    • Added automatic cleanup of inactive anonymous accounts.
  • Bug Fixes

    • Improved authentication error handling, CAPTCHA failures, and redirect configuration.
    • Preserved helpful messages when registration encounters an existing account.
  • Tests

    • Expanded coverage for authentication, CAPTCHA behavior, and anonymous account cleanup.

Janacek and others added 3 commits August 5, 2026 20:28
Registering with an address that already has an account printed
"User already registered" and stopped there. That is accurate and
useless: the person reading it is a guest with a browser full of boards,
and the account they are being told about is reachable only by signing
in — which leaves those boards with the guest, exactly as signing out
would. The form sent them round the same loop with no way through.

register() now recognises the case and answers `taken`, with a message
that names the cost. The two paths disagree on the word for it —
attaching to a guest gives `email_exists`, signing up with no session
gives `user_already_exists` — so both are matched, and neither is
assumed to cover the other. Checked against the live project rather
than guessed. A signed-out caller has no boards to strand, so it gets
the shorter sentence.

The form offers to sign in as that address, next to the error rather
than in place of it: a button on its own would be an invitation without
the price on it.

The browser suite's after-hook allows a 422 alongside the 400 it already
allowed. Chrome logs every failed request at error level, and a taken
address is a 422 by design. Still filtered by status rather than by
dropping network noise, so a 500 from the stack fails the suite as before.

config.toml stops describing somewhere a push would break. site_url is
the deployed site now, not the dev server, which is safe in both
directions because nothing reads it: there is no redirect-based auth
flow here — no OAuth, no magic link, no emailRedirectTo, no password
reset — and with confirmations off no mail carrying a link is sent
either. additional_redirect_urls lists both origins for the same reason.
`env(...)` would be the answer if that changed, and the file says so,
along with which values are still unaudited against the hosted project.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbSwVkGcNQZohV9VoeACRT
Every first visit is a real row in auth.users, and Supabase has no
automatic cleanup for them: ordinary traffic grows the table forever,
and anyone who wants to can grow it faster by asking the signup endpoint
for guests in a loop.

sweep_anonymous_users deletes anonymous users who own no board, are on
nobody else's board, and have not been seen for a while — pg_cron runs
it daily. The interesting half is what it keeps, so every test is paired:
a guest with a board and a guest without, swept together. The one that
would be a catastrophe rather than a bug has its own test, because
is_anonymous is the whole guard between housekeeping and deleting the
customers.

Only the schedule is conditional, not the function. These migrations are
applied to a hosted project, a local stack, and a bare Postgres that
`apply.js --stub` fits out with just enough of GoTrue; pg_cron is absent
from the third and `create extension` on a missing extension is an error
rather than a no-op. The stub grows the three columns the sweep reads,
added with `if not exists` so it stays a no-op against a real project.

test:db now runs its files one at a time. There is one database behind
all of them and rls.test.js clears public.boards before each of its own
tests; run concurrently that wipe lands mid-fixture here, and the failure
reads as a foreign key bug in the sweep rather than as two suites sharing
a table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbSwVkGcNQZohV9VoeACRT
Anonymous sign-in is the only endpoint here that takes nothing and
answers with a row in auth.users, and the nightly sweep is a mop rather
than a lock. A Turnstile token is now sent with it.

The site key is a value that can be absent, exactly as the Supabase
config is: with no key there is no widget, no script and no token, which
is what every test and every local dev server runs as. The project only
demands a token once CAPTCHA protection is switched on for it, so the two
halves move in one direction — the key ships first, the setting is turned
on afterwards. The reverse rejects every sign-in.

The token is taken per sign-in rather than at construction: it is
single-use and short-lived, so one fetched at load would be spent or
stale by the time a retry wanted it. A CAPTCHA that will not issue one
fails the start the way a refused sign-in does — answered, not thrown —
so the gate shows it and keeps its retry.

The script loader is memoised on the document rather than in a module
variable, because two apps on one page share the document and would
otherwise fetch it twice; a failed load clears the memo so a retry is a
retry. An onload that leaves no global rejects rather than failing later
on undefined — that is a proxy serving an error page with a 200.

Not verified against Cloudflare: the tests drive a fake widget. What a
real challenge does in a real browser is still unproven.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbSwVkGcNQZohV9VoeACRT
@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@boubou666, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d63d34f-6fb8-43d5-a9f9-1ff64ac08807

📥 Commits

Reviewing files that changed from the base of the PR and between bbd1a58 and 5230352.

📒 Files selected for processing (2)
  • src/platform/turnstile.js
  • test/node/turnstile.test.js
📝 Walkthrough

Walkthrough

The PR adds optional Turnstile CAPTCHA support, duplicate-email registration handling, deployed Supabase Auth URLs, and a secured scheduled sweep for stale anonymous users. It also adds browser, Node, and database coverage.

Changes

Authentication flows

Layer / File(s) Summary
Turnstile CAPTCHA provider
src/platform/turnstile.js, test/node/turnstile.test.js
Adds configuration parsing, shared script loading, token creation, cancellation, cleanup, timeout handling, and retry behavior.
Supabase CAPTCHA and duplicate-email handling
src/platform/auth.js, src/shell/auth.js, test/node/auth.test.js, supabase/config.toml
Passes optional CAPTCHA tokens during anonymous sign-in, maps known duplicate-email errors to structured results, and updates deployed Auth URL settings.
Duplicate-email registration UI
src/components/AccountForm.jsx, test/browser/auth.test.js
Tracks duplicate-email failures, displays account-conflict messaging, and provides a sign-in action for taken email addresses.

Anonymous-user cleanup

Layer / File(s) Summary
Anonymous-user sweep function
supabase/migrations/20260805190000_sweep_anonymous_users.sql
Adds a restricted cleanup function that removes eligible stale anonymous users and schedules an optional daily pg_cron job.
Sweep schema and integration validation
test/db/stub-auth.sql, test/db/sweep.test.js, package.json
Adds required auth columns, validates cleanup behavior and permissions, and runs database tests serially.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • boubou666/peerthink#5: Extends the same authentication implementations with duplicate-email handling and CAPTCHA support.

Sequence Diagram(s)

sequenceDiagram
  participant AccountForm
  participant createSupabaseAuth
  participant Turnstile
  participant Supabase
  AccountForm->>createSupabaseAuth: submit authentication request
  createSupabaseAuth->>Turnstile: request CAPTCHA token
  Turnstile-->>createSupabaseAuth: return token or failure
  createSupabaseAuth->>Supabase: send request with captchaToken
  Supabase-->>createSupabaseAuth: return success or duplicate-email error
  createSupabaseAuth-->>AccountForm: return structured result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: duplicate-email handling, anonymous-user cleanup, and Turnstile CAPTCHA integration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-harden-accounts

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/AccountForm.jsx`:
- Around line 76-89: Update the generic mode-switch handler in AccountForm to
reset taken alongside error whenever switching modes. Preserve the existing
duplicate-address action reset and ensure returning to registration after
switching through sign-in does not retain stale taken state.

In `@src/platform/turnstile.js`:
- Line 38: Update loadTurnstile and createTurnstileCaptcha to require and use
the document supplied by the factory caller instead of reading
globalThis.document. At the composition boundary in src/shell/auth.js, pass the
browser document explicitly when creating the Turnstile instance, ensuring every
document dependency arrives through factory arguments.
- Around line 44-56: Add a destroy() method to the returned CAPTCHA provider and
invoke it from the owning authentication lifecycle’s destroy path. Track the
script created in the loading flow, clear its onload and onerror handlers during
cleanup, and remove it from the document only while loading is pending. Ensure
repeated or post-settlement cleanup is safe.

In `@test/db/sweep.test.js`:
- Line 24: Update the test lifecycle around the database setup to begin a
transaction in beforeEach and roll it back in afterEach, preventing the global
sweep from persisting deletions. In the permission test’s nested transaction,
replace transaction usage with savepoints while preserving its existing
permission assertions and cleanup behavior.
- Line 22: Remove the direct pg import from the sweep test and update its
database access to use an approved existing helper or access path. If no
approved path exists, document an explicit test dependency exception instead of
retaining the import.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c57cdf1-fe17-48be-8c55-4d1c6d839f01

📥 Commits

Reviewing files that changed from the base of the PR and between cce1349 and b977e16.

📒 Files selected for processing (12)
  • package.json
  • src/components/AccountForm.jsx
  • src/platform/auth.js
  • src/platform/turnstile.js
  • src/shell/auth.js
  • supabase/config.toml
  • supabase/migrations/20260805190000_sweep_anonymous_users.sql
  • test/browser/auth.test.js
  • test/db/stub-auth.sql
  • test/db/sweep.test.js
  • test/node/auth.test.js
  • test/node/turnstile.test.js

Comment thread src/components/AccountForm.jsx
Comment thread src/platform/turnstile.js Outdated
Comment thread src/platform/turnstile.js
Comment thread test/db/sweep.test.js
Comment thread test/db/sweep.test.js
…in flight

All three were real.

The generic mode switch cleared `error` but not `taken`, so a trip to
sign-in and back put the "sign in as …" button under a form with nothing
wrong with it. Cleared with the error it belongs to.

turnstile.js reached for globalThis.document instead of taking it. The
document now arrives through the factory, from the composition boundary
in shell/auth.js, the same way the Supabase client and the storage do.

A script still loading kept its handlers and its tag after whoever asked
for it had gone. createTurnstileCaptcha grows a destroy() that drops a
load in flight — handlers cleared, tag removed — and the auth port calls
it from the destroy() it already had. Safe twice, and safe once the load
has landed, both tested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbSwVkGcNQZohV9VoeACRT

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/platform/turnstile.js (1)

108-128: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clean up active Turnstile widgets in destroy().

After turnstile.render() returns a widget ID, cancel(doc) does nothing. Store the ID, call turnstile.remove(widgetId), remove the container, and reject the pending CAPTCHA promise. Add a test for destruction after render() and before any callback.

loadTurnstile is document-scoped. Ensure one CAPTCHA instance cannot cancel another instance's pending load.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/turnstile.js` around lines 108 - 128, Update the captcha flow
and captcha.destroy around turnstile.render so the returned widget ID is
retained, the active widget is removed via turnstile.remove(widgetId), the
container is cleaned up, and any pending CAPTCHA promise is rejected when
destroyed before a callback. Add coverage for destruction after render but
before callback, and scope pending-load cancellation in loadTurnstile to the
owning document so one CAPTCHA instance cannot cancel another’s load.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/platform/turnstile.js`:
- Line 128: Update the loadTurnstile/captcha.destroy flow so cancellation is
scoped to the provider that initiated the shared document load. Track active
consumers per doc and only invoke cancel(doc) after the final consumer is
destroyed, preserving the pending loader and script for other providers.

---

Outside diff comments:
In `@src/platform/turnstile.js`:
- Around line 108-128: Update the captcha flow and captcha.destroy around
turnstile.render so the returned widget ID is retained, the active widget is
removed via turnstile.remove(widgetId), the container is cleaned up, and any
pending CAPTCHA promise is rejected when destroyed before a callback. Add
coverage for destruction after render but before callback, and scope
pending-load cancellation in loadTurnstile to the owning document so one CAPTCHA
instance cannot cancel another’s load.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e9d00e0b-c5ef-4533-9eac-f1ddafbe606b

📥 Commits

Reviewing files that changed from the base of the PR and between b977e16 and 1cc5ff4.

📒 Files selected for processing (5)
  • src/components/AccountForm.jsx
  • src/platform/auth.js
  • src/platform/turnstile.js
  • src/shell/auth.js
  • test/node/turnstile.test.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/platform/auth.js
  • src/shell/auth.js
  • src/components/AccountForm.jsx

Comment thread src/platform/turnstile.js Outdated
Two more from the review, both right.

sweep.test.js called sweep_anonymous_users() with autocommit on. The
function deletes every eligible row in the database, not only the ones
the test made — so pointed at the hosted project, an idle `npm run
test:db` would delete real visitors' guests for real. Each test now runs
in a transaction that is rolled back, and the permission test uses
savepoints rather than a transaction inside the transaction. The sweep is
still exercised; its effects no longer outlive the assertion.

The destroy() added last commit cancelled per document, and the script is
shared per document. Two apps on one page share it, so destroying either
provider pulled the script out from under the other and left it awaiting
a promise that could never settle — a worse leak than the one being
fixed. The loader now tracks its consumers and cancels once the last one
has withdrawn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbSwVkGcNQZohV9VoeACRT

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/platform/turnstile.js`:
- Around line 98-101: Update the cancellation logic in the pending-load cleanup
block so it returns immediately when pending.consumers.delete(consumer) is
false, ensuring only registered consumers can cancel the load. Add coverage for
a consumerless direct load followed by destruction of an unrelated provider,
verifying the direct caller’s Promise remains pending.
- Line 149: Update the provider cleanup around captcha.destroy to track rendered
widget IDs and pending Promise reject callbacks, then have destroy call
turnstile.remove for each widget, reject pending Promises, and remove each
container owned by the provider. Add coverage for destroying the provider after
render() but before its callback executes.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6205dcfc-9a9c-4069-9237-d6de72467bab

📥 Commits

Reviewing files that changed from the base of the PR and between 1cc5ff4 and bbd1a58.

📒 Files selected for processing (3)
  • src/platform/turnstile.js
  • test/db/sweep.test.js
  • test/node/turnstile.test.js

Comment thread src/platform/turnstile.js
Comment thread src/platform/turnstile.js
Set.delete() answers false when the consumer never joined, and the
previous check read that as "nobody left" — so a provider destroyed
before it ever asked for a token, or a direct loadTurnstile(doc) with no
consumer at all, could pull the script out from under whoever was
actually waiting. Standing to cancel now requires having joined.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbSwVkGcNQZohV9VoeACRT
@boubou666

Copy link
Copy Markdown
Owner Author

Answering the three open threads.

turnstile.js:101 — fixed in 5230352. Correct and worth catching: Set.delete() answers false when the consumer never joined, and the check read that as "nobody left". So a provider destroyed before it ever called captcha(), or a direct loadTurnstile(doc) started with no consumer, could cancel a load somebody else was waiting on. Standing to cancel now requires having joined, with a test for the consumerless case.

turnstile.js:149 and test/db/sweep.test.js:22 — both arrived as analysis chains with no finding stated in the body, so there is nothing here to act on. If either was meant to carry a recommendation it did not survive into the comment. Re-run the review and I will take a second look at whatever comes back with a claim attached.

For the record, the substantive findings from the earlier rounds are all in:

  • taken outliving its error on the generic mode switch — fixed in 1cc5ff4
  • globalThis.document reached for rather than injected — fixed in 1cc5ff4, document now arrives from the composition boundary
  • no cleanup for a pending script load — fixed in 1cc5ff4, destroy() wired into the auth port's existing one
  • the sweep test running a global delete under autocommit — fixed in bbd1a58, each test now in a rolled-back transaction with savepoints for the permission case. This was the important one: pointed at the hosted project it would have deleted real visitors' guest accounts.
  • destroy() cancelling a shared per-document load — fixed in bbd1a58, consumers tracked, cancelled once the last withdraws

342 node tests, 42 db tests, coverage 99.1%.

@boubou666

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@boubou666
boubou666 merged commit 575926b into main Aug 5, 2026
2 checks passed
@boubou666
boubou666 deleted the worktree-harden-accounts branch August 5, 2026 20:24
boubou666 added a commit that referenced this pull request Aug 5, 2026
#12 added the CAPTCHA and its site key variable, and never gave the
deploy workflow the line that passes one to the other. Vite inlines what
is in the environment at build time, so the key sat on the repository
while every build shipped without it — readTurnstileConfig answered null
and the deployed site quietly carried no CAPTCHA at all.

Which is the failure mode that feature was designed to have, and it hid
the mistake perfectly: nothing broke, nothing logged, and the only way to
notice was to look for the key in the bundle and not find it.


Claude-Session: https://claude.ai/code/session_01TbSwVkGcNQZohV9VoeACRT

Co-authored-by: boubou666 <thomas.epitech@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants