Skip to content

[PROJ-1816] Login allowlist + waitlist intake (dark behind LOGIN_ALLOWLIST_ENABLED) - #353

Merged
AndrewNordstrom merged 4 commits into
mainfrom
dev/PROJ-1816-login-allowlist-waitlist-backend
Jul 13, 2026
Merged

AndrewNordstrom merged 4 commits into
mainfrom
dev/PROJ-1816-login-allowlist-waitlist-backend

Conversation

@AndrewNordstrom

Copy link
Copy Markdown
Collaborator

What this PR does

Turns Corgi voting into an approved-accounts pilot, dark behind LOGIN_ALLOWLIST_ENABLED (default false) — merging this changes no behavior until the flag is flipped on the VPS.

  • Login gate (src/governance/routes/auth.ts): after credentials verify, admins (BOT_ADMIN_DIDS) bypass; everyone else needs an active approved_participants row (isParticipantApproved, fail-closed, 300s Redis cache). Deny invalidates the just-minted Redis session (it's persisted before the gate can run) and returns 403 {error:"NotApproved", waitlist:true} — the discriminator is declared in the response schema so Fastify's serializer doesn't strip it. No session cookie on deny.
  • Waitlist intake (POST /api/governance/waitlist, public): zod-normalized handle (trim, strip @, lowercase, domain regex; DIDs rejected), optional note ≤500. ON CONFLICT (handle) DO NOTHING — sticky rejections, first-note-wins. Identical generic 200 for new/duplicate/approved/rejected so the endpoint can't be used to probe who's approved. Rate-limited at the login tier (10/min/IP) via a branch placed above the governance-mutation catch-all in rate-limit-config.ts (which would otherwise key it by DID with vote limits).
  • Admin review (/api/admin/waitlist, inherits requireAdmin): list (status filter, oldest-first queue), approve (resolve handle→DID via resolve-handle.ts — extracted from participants.ts, both now share it; upsert into approved_participants with re-activation; invalidateParticipantCache so approval takes effect immediately; audit waitlist_approved), reject (audit waitlist_rejected). 404/409/400; unresolvable handles leave the row pending — the participants API remains the add-by-DID escape hatch.
  • Migration 033_waitlist.sql: waitlist_requests with unique normalized handle + status CHECK.
  • GET /api/admin/status now reports loginAllowlistEnabled (for the upcoming admin Access panel).

Why

Security review found the only real gap in the auth story: any Bluesky account with an app password could log in and vote. The transparency surfaces stay public by design; this scopes participation to a pilot allowlist with a public waitlist, reusing the existing approved_participants infra (migration 016, cache, admin CRUD, audit log) rather than new machinery.

Linear: Fixes PROJ-1816

Testing performed

  • 3 new vitest suites, 25 tests: handle normalization (@Alice.Example.COM → alice.example.com), custom domains, enumeration-proof duplicate body, flag-off regression (unapproved still logs in), admin bypass without a participant lookup, 403 body + minted-session invalidation + no-cookie-leak assertion, fail-closed lookup, approve lifecycle (upsert + cache invalidation + audit), reject, 404/409/400 paths.
  • Full suite: 135 files / 1460 tests pass (.env.example env, same as CI). tsc --noEmit clean.

Reviewer focus

  • The deny path in auth.ts — session invalidation ordering and the 403 schema.
  • The generic-response contract in waitlist.ts (intentionally never varies by row state).
  • Rate-limit branch placement in rate-limit-config.ts (must precede the governance catch-all).

Deployment notes (post-merge, before flag flip)

deploy.yml does not run migrations — npm run migrate on the VPS applies 033. Flag flip happens only after the frontend packet (waitlist UX) lands: verify BOT_ADMIN_DIDS contains the founder DID, then set LOGIN_ALLOWLIST_ENABLED=true + restart. Rollback = flag off + restart. (.env.example line omitted — local policy hook blocks .env* edits; the flag is documented in config.ts.)

… waitlist intake

Dark behind LOGIN_ALLOWLIST_ENABLED (default false). When enabled, login
requires the DID to be an admin (BOT_ADMIN_DIDS) or an active
approved_participants row; the deny path invalidates the just-minted
session and returns 403 {error:NotApproved, waitlist:true} with the
discriminator declared in the response schema.

Waitlist intake: POST /api/governance/waitlist stores a normalized handle
(+ optional note <=500) with ON CONFLICT DO NOTHING and an identical
generic 200 for every state, so the endpoint cannot be used to probe
approval status. Rate-limited at the login tier (10/min/IP), placed above
the governance mutation catch-all.

Admin review: GET /api/admin/waitlist, POST /:id/approve (resolves handle
to DID via the shared resolve-handle helper extracted from participants,
upserts approved_participants, invalidates the participant cache so
approval is immediate, audit-logs), POST /:id/reject. Unresolvable
handles leave the row pending; participants API remains the DID escape
hatch. Admin status now reports loginAllowlistEnabled.

Migration 033 creates waitlist_requests. Tests: 25 new across three
suites (normalization, enumeration-proof responses, flag-off regression,
admin bypass, session invalidation on deny, fail-closed, approve/reject
lifecycle). Full suite 135 files / 1460 tests green.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6c65f161-e262-43c6-b9b1-801c6807ca66

📥 Commits

Reviewing files that changed from the base of the PR and between 14f149f and acac7f6.

📒 Files selected for processing (2)
  • tests/admin-waitlist.test.ts
  • tests/governance-waitlist-route.test.ts

Summary by CodeRabbit

  • New Features
    • Added a public voting-pilot waitlist for submitting Bluesky handles and optional notes.
    • Added admin tools to list, approve, and reject waitlist requests.
    • Added an optional login allowlist gate for approved participants (admins unaffected).
    • Added admin status reporting for whether login allowlisting is enabled.
  • Bug Fixes
    • Centralized handle-to-DID resolution and improved approval/rejection writes for consistency under race conditions.
    • Updated rate limiting so waitlist intake follows the login-tier policy.
  • Tests
    • Expanded coverage for waitlist submission, admin approval/rejection flows, allowlist gating, and rate limiting.

Walkthrough

Adds a public voting-pilot waitlist, admin approval and rejection routes, login allowlist enforcement behind a default-disabled flag, waitlist persistence, rate limiting, status reporting, shared handle resolution, and route and authentication tests.

Changes

Waitlist and login allowlist

Layer / File(s) Summary
Waitlist persistence and pilot configuration
src/db/migrations/033_waitlist.sql, src/config.ts, src/feed/rate-limit-config.ts, tests/rate-limit-config.test.ts
Adds waitlist storage, the LOGIN_ALLOWLIST_ENABLED flag, login-tier rate limits, and rate-limit coverage.
Public waitlist intake
src/governance/routes/waitlist.ts, src/governance/server.ts, tests/governance-waitlist-route.test.ts
Adds normalized, deduplicated intake with generic responses, validation, persistence error handling, route wiring, and tests.
Login allowlist gate
src/governance/routes/auth.ts, tests/governance-auth-allowlist.test.ts
Restricts non-admin login completion to approved participants when enabled, invalidates denied sessions, documents the 403 response, and tests bypass and fail-closed behavior.
Admin waitlist management and participant wiring
src/admin/routes/*, tests/admin-waitlist.test.ts
Adds admin listing, atomic approval and rejection, shared handle resolution, participant upsert reuse, audit logging, cache invalidation, status reporting, route registration, and concurrency/error-path tests.

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

Possibly related issues

  • PROJ-1816 — Directly covers the login allowlist, waitlist intake, admin workflows, persistence, rate limiting, and verification in this change.

Possibly related PRs

Suggested labels: javascript

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% 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
Title check ✅ Passed The title is concise and accurately summarizes the main change: a login allowlist and waitlist intake behind a flag.
Description check ✅ Passed The description is directly about the same backend allowlist and waitlist changes described in the diff.
Linked Issues check ✅ Passed The changes implement the linked objectives: flag, login gating, waitlist intake, admin review, migration, status field, and tests.
Out of Scope Changes check ✅ Passed The diff stays focused on the allowlist/waitlist feature and related test coverage without unrelated feature work.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev/PROJ-1816-login-allowlist-waitlist-backend
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch dev/PROJ-1816-login-allowlist-waitlist-backend

Warning

Review ran into problems

🔥 Problems

These MCP integrations need to be re-authenticated in the Integrations settings: Notion


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

…ction

Review follow-up. The approve handler read the row, checked pending, then
did an unconditional UPDATE — a TOCTOU where two concurrent approves could
both pass the check, both resolve the handle, and both write an audit row.
And its four writes weren't transactional, so a partial failure could leave
the account approved in approved_participants while the queue still showed
pending.

Now: a fast 404/409 pre-check (skips the network resolve for bad ids),
then the pending-claim UPDATE ... WHERE id = $1 AND status = 'pending'
RETURNING runs inside a transaction with the participant upsert and audit
insert — atomic claim (no double-approve) and no cross-table drift. Cache
invalidation moved after commit. Reject already used the atomic pattern.

Adds a rate-limit-config regression test asserting the public waitlist POST
matches the login-tier IP-keyed branch (guards the ordering vs the generic
governance catch-all). Full suite 135 files / 1462 tests green.
@coderabbitai coderabbitai Bot added the javascript Pull requests that update javascript code label Jul 13, 2026

@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: 8

🤖 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/admin/routes/resolve-handle.ts`:
- Around line 9-13: Update resolveHandleToDid to create an AbortController,
abort the signal after the configured timeout, and pass that signal to
agent.resolveHandle so hung bsky.social requests are bounded. Add coverage
verifying both explicit abort handling and a slow upstream exceeding the
timeout.

In `@src/admin/routes/waitlist.ts`:
- Around line 190-201: Extract the duplicated approved-participant upsert into a
shared upsertApprovedParticipant helper accepting a database client and { did,
handle, addedBy, notes }. Replace the inline SQL in both the waitlist route and
participants route with this helper, preserving the existing conflict handling,
handle COALESCE behavior, and transaction client usage.
- Around line 237-292: Make the reject handler’s status update and governance
audit insertion atomic by wrapping both queries in a transaction, following the
transaction pattern used by the approve handler. Update the `waitlist_requests`
and `governance_audit_log` operations in the `/waitlist/:id/reject` route to use
the same transaction client, committing only after both succeed and rolling back
on failure; preserve the existing not-found and conflict handling.

In `@src/governance/routes/auth.ts`:
- Around line 141-161: Update the denied-account branch in the authentication
handler to let invalidateSession(session.accessJwt) failures propagate to the
existing outer error handling instead of catching and logging them locally.
Preserve the 403 response only when invalidation succeeds, and ensure
SessionStoreUnavailableError follows the established 503 SessionStoreUnavailable
behavior used elsewhere in the handler.

In `@tests/admin-waitlist.test.ts`:
- Around line 105-192: The approval tests cover the lost-claim path but not
errors after the transaction begins or invalid route IDs. Extend the approve
test suite around the existing transaction tests to reject the participant
upsert or audit query after a successful claim, then assert a non-2xx response,
ROLLBACK, no cache invalidation, and exactly one client release; also add
400-status cases for non-numeric, zero, and negative IDs to cover the route’s
positive integer validation.

In `@tests/governance-auth-allowlist.test.ts`:
- Around line 126-133: Update the deny-path cookie assertion in the governance
authorization test to require that the response has no set-cookie header at all,
rather than conditionally inspecting cookie contents. Remove the conditional
iteration and assert direct absence on response.headers['set-cookie'],
preserving the existing deny-path behavior.
- Around line 136-147: Update the “flag on: allowlist lookup failure” test
around login(buildApp()) to make isParticipantApprovedMock reject with a storage
error instead of resolving false. Keep the existing 403 status and NotApproved
assertions so the route’s catch path is verified to fail closed, while leaving
the separate unapproved-account test unchanged.

In `@tests/governance-waitlist-route.test.ts`:
- Around line 65-79: Extend the governance waitlist validation tests with a
successful boundary case: submit a valid 253-character handle and a note exactly
500 characters long, then assert the request succeeds and the expected query
path is invoked. Keep the existing malformed and oversized-input rejection
coverage unchanged.
🪄 Autofix (Beta)

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: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b058a651-5a5d-4c50-b0bb-02ca85a60464

📥 Commits

Reviewing files that changed from the base of the PR and between 0298aa4 and 90c9c77.

📒 Files selected for processing (15)
  • src/admin/routes/index.ts
  • src/admin/routes/participants.ts
  • src/admin/routes/resolve-handle.ts
  • src/admin/routes/status.ts
  • src/admin/routes/waitlist.ts
  • src/config.ts
  • src/db/migrations/033_waitlist.sql
  • src/feed/rate-limit-config.ts
  • src/governance/routes/auth.ts
  • src/governance/routes/waitlist.ts
  • src/governance/server.ts
  • tests/admin-waitlist.test.ts
  • tests/governance-auth-allowlist.test.ts
  • tests/governance-waitlist-route.test.ts
  • tests/rate-limit-config.test.ts

Comment thread src/admin/routes/resolve-handle.ts
Comment thread src/admin/routes/waitlist.ts Outdated
Comment thread src/admin/routes/waitlist.ts
Comment thread src/governance/routes/auth.ts
Comment thread tests/admin-waitlist.test.ts
Comment thread tests/governance-auth-allowlist.test.ts Outdated
Comment thread tests/governance-auth-allowlist.test.ts Outdated
Comment thread tests/governance-waitlist-route.test.ts
…transactional reject, harden deny

- resolve-handle: bound the external bsky.social resolveHandle with a 10s
  AbortController timeout so a hung upstream can't pin admin requests open.
- Extract upsertApprovedParticipant() shared by the participants and waitlist
  approve routes — the allowlist write path no longer has two copies to drift.
- Make the reject handler transactional (status flip + audit commit together),
  matching approve; a rejected request can't lose its audit row on partial failure.
- Login deny: if invalidating the just-minted session throws, return 503
  (consistent with this file's SessionStoreUnavailable handling) instead of a
  403 that would leave a live session — the 'token must be dead' invariant holds.
- Tests: mid-transaction-throw rollback coverage; unconditional cookie-absence
  assertion; a real fail-closed case (isParticipantApproved throws) + the new
  invalidation-failure 503 path; note/handle boundary-pass cases.

Full suite 135 files / 1465 tests green.

@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

Caution

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

⚠️ Outside diff range comments (3)
src/admin/routes/participants.ts (1)

162-182: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Participant add: upsert + audit insert aren't atomic — same bug class just fixed for waitlist reject.

This is the identical shape of issue that was fixed elsewhere in this PR ("audit log can silently be lost" for waitlist.ts reject): the upsert (163-168) and the governance_audit_log insert (174-182) are two independent db.query calls. If the audit insert throws after the upsert commits (constraint violation, pool exhaustion, network blip), the participant is live in approved_participants with zero audit trail, and the caller gets an uncaught error despite the write having actually succeeded. The DELETE /participants/:did handler below (235-268) has the same pattern.

🔒 Proposed fix: commit upsert + audit together
-    // Insert (or re-activate if previously removed)
-    await upsertApprovedParticipant(db, {
-      did: resolvedDid,
-      handle: resolvedHandle,
-      addedBy: adminDid,
-      notes: notes ?? null,
-    });
-
-    // Invalidate cache so next feed request picks up the change
-    await invalidateParticipantCache(resolvedDid);
-
-    // Audit log
-    await db.query(
-      `INSERT INTO governance_audit_log (action, actor_did, details)
-       VALUES ($1, $2, $3)`,
-      [
-        'participant_added',
-        adminDid,
-        JSON.stringify({ did: resolvedDid, handle: resolvedHandle, notes }),
-      ]
-    );
+    // Upsert + audit commit together, or not at all.
+    const client = await db.connect();
+    try {
+      await client.query('BEGIN');
+      await upsertApprovedParticipant(client, {
+        did: resolvedDid,
+        handle: resolvedHandle,
+        addedBy: adminDid,
+        notes: notes ?? null,
+      });
+      await client.query(
+        `INSERT INTO governance_audit_log (action, actor_did, details)
+         VALUES ($1, $2, $3)`,
+        ['participant_added', adminDid, JSON.stringify({ did: resolvedDid, handle: resolvedHandle, notes })]
+      );
+      await client.query('COMMIT');
+    } catch (err) {
+      await client.query('ROLLBACK').catch(() => {});
+      throw err;
+    } finally {
+      client.release();
+    }
+
+    // Invalidate cache so next feed request picks up the change
+    await invalidateParticipantCache(resolvedDid);
🤖 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/admin/routes/participants.ts` around lines 162 - 182, Wrap the
upsertApprovedParticipant call and the governance_audit_log insert in a single
transaction using db.connect()/BEGIN/COMMIT with ROLLBACK-on-error and
client.release() in finally, mirroring the pattern used in
src/admin/routes/waitlist.ts's approve/reject handlers. Keep
invalidateParticipantCache after the commit, outside the transaction.
🤖 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/admin/routes/participants.ts` around lines 162 - 182, Wrap
upsertApprovedParticipant and the participant_added governance_audit_log insert
in the add-participant handler’s single transaction using db.connect(), BEGIN,
COMMIT, rollback on errors, and client.release() in finally, mirroring the
waitlist transaction pattern. Keep invalidateParticipantCache(resolvedDid) after
the transaction commits and outside it; apply the same atomicity fix to the
DELETE /participants/:did handler’s participant removal and audit insert.

Source: Path instructions

src/governance/routes/auth.ts (1)

141-167: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Approval-check throwing skips session invalidation entirely — the "must be dead" invariant only covers half the failure surface.

The fix for the invalidateSession catch (Lines 151-158) is correct and matches the file's established SessionStoreUnavailable pattern — that part resolves the prior review comment.

But invalidateSession is only reachable after isParticipantApproved resolves to false (Line 146-148). If isParticipantApproved(session.did) itself throws, execution jumps straight to the outer catch (500/503) and invalidateSession is never called — the just-minted session for that account stays live in the store. That directly contradicts the comment on Lines 141-144: "the minted token must be dead, not merely unreturned." The client never gets the cookie either way, but a valid, undead session for an unapproved/undetermined account now sits in the store until its natural TTL.

🔒 Proposed fix: fail closed on the approval-check itself, not just on invalidation
       if (config.LOGIN_ALLOWLIST_ENABLED && !isAdmin(session.did)) {
-        const approved = await isParticipantApproved(session.did);
+        let approved: boolean;
+        try {
+          approved = await isParticipantApproved(session.did);
+        } catch (approvalErr) {
+          // Fail closed here too — don't leave a freshly-minted session
+          // alive just because we couldn't decide whether to deny it.
+          try {
+            await invalidateSession(session.accessJwt);
+          } catch (invalidateErr) {
+            logger.error({ err: invalidateErr, did: session.did }, 'Failed to invalidate session after approval-check failure');
+          }
+          throw approvalErr;
+        }
         if (!approved) {

Suggested test in tests/governance-auth-allowlist.test.ts: in the existing "allowlist lookup that throws" case, additionally assert deleteSessionMock/invalidateSession was called with the minted accessJwt — today that assertion would fail, exposing this exact gap.

🤖 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/governance/routes/auth.ts` around lines 141 - 167, Wrap the
isParticipantApproved(session.did) call in its own try/catch so that if it
throws, the handler attempts to invalidate the just-minted session
(session.accessJwt) before rethrowing, logging any invalidation failure at
error level. Preserve the existing behavior for the resolved-false branch
(invalidate → 403/503) unchanged.
🤖 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/governance/routes/auth.ts` around lines 141 - 167, Wrap the
isParticipantApproved(session.did) call in its own try/catch within the
allowlist gate; when approval lookup throws, attempt
invalidateSession(session.accessJwt), log any invalidation failure at error
level, then rethrow the original approval error so existing outer handling
applies. Preserve the resolved-false path’s current invalidation and 403/503
behavior.

Source: Path instructions

src/admin/routes/waitlist.ts (1)

213-224: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Post-commit cache-invalidation failure turns a successful approval into a false 500.

By Line 220 the DB transaction has already committed — waitlist_requests is approved, approved_participants is upserted, and the audit row exists. invalidateParticipantCache(did) isn't wrapped in try/catch, so if it throws (Redis blip), the whole request 500s even though the approval fully succeeded. Per the comment on Line 219, the account then stays cache-locked-out of login for up to 300s, and a retrying admin hits 409 Request already decided with no way to tell the approval actually worked.

🔒 Proposed fix: don't fail the response for a stale-cache-only failure
     // After commit: a login attempt made before approval leaves a cached
     // negative for up to 300s, so clear it or the account stays locked out.
-    await invalidateParticipantCache(did);
+    // The DB write already committed — a cache-invalidation hiccup shouldn't
+    // present a successful approval to the admin as a failure.
+    try {
+      await invalidateParticipantCache(did);
+    } catch (err) {
+      logger.error({ err, did }, 'Waitlist approve: cache invalidation failed after commit');
+    }

Suggested test: mock invalidateParticipantCache to reject and assert the response is still 200 { success: true, did, handle }.

🤖 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/admin/routes/waitlist.ts` around lines 213 - 224, Wrap the
invalidateParticipantCache(did) call in try/catch, logging a warning/error on
failure instead of letting it propagate, since the approval transaction has
already committed by this point. The route should still return the 200 success
response even if cache invalidation fails.
🤖 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/admin/routes/waitlist.ts` around lines 213 - 224, Wrap the post-commit
invalidateParticipantCache(did) call in a try/catch so cache failures are logged
as a warning or error without propagating. Keep the successful approval response
through the existing logger.info and reply.send path, ensuring the route still
returns 200 after the database transaction commits.

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 `@tests/admin-waitlist.test.ts`:
- Around line 215-231: Add a reject-path test alongside the existing reject
test, configuring clientQueryMock so the returning UPDATE in the reject handler
succeeds while the governance_audit_log query rejects. Assert the request
returns a 500-or-greater status, the recorded SQL contains ROLLBACK but not
COMMIT, and releaseMock is called exactly once.

In `@tests/governance-waitlist-route.test.ts`:
- Around line 65-78: Update the boundary test around longHandle to construct an
exactly 253-character valid handle, assert its length is 253, and verify the
second queryMock call received the normalized lowercase handle with a null note.
Keep the existing success assertion and first submission checks unchanged.

---

Outside diff comments:
In `@src/admin/routes/participants.ts`:
- Around line 162-182: Wrap upsertApprovedParticipant and the participant_added
governance_audit_log insert in the add-participant handler’s single transaction
using db.connect(), BEGIN, COMMIT, rollback on errors, and client.release() in
finally, mirroring the waitlist transaction pattern. Keep
invalidateParticipantCache(resolvedDid) after the transaction commits and
outside it; apply the same atomicity fix to the DELETE /participants/:did
handler’s participant removal and audit insert.

In `@src/admin/routes/waitlist.ts`:
- Around line 213-224: Wrap the post-commit invalidateParticipantCache(did) call
in a try/catch so cache failures are logged as a warning or error without
propagating. Keep the successful approval response through the existing
logger.info and reply.send path, ensuring the route still returns 200 after the
database transaction commits.

In `@src/governance/routes/auth.ts`:
- Around line 141-167: Wrap the isParticipantApproved(session.did) call in its
own try/catch within the allowlist gate; when approval lookup throws, attempt
invalidateSession(session.accessJwt), log any invalidation failure at error
level, then rethrow the original approval error so existing outer handling
applies. Preserve the resolved-false path’s current invalidation and 403/503
behavior.
🪄 Autofix (Beta)

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: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 23dbef94-71c9-4811-ad49-3b92874529ef

📥 Commits

Reviewing files that changed from the base of the PR and between 90c9c77 and 14f149f.

📒 Files selected for processing (8)
  • src/admin/routes/participant-upsert.ts
  • src/admin/routes/participants.ts
  • src/admin/routes/resolve-handle.ts
  • src/admin/routes/waitlist.ts
  • src/governance/routes/auth.ts
  • tests/admin-waitlist.test.ts
  • tests/governance-auth-allowlist.test.ts
  • tests/governance-waitlist-route.test.ts

Comment thread tests/admin-waitlist.test.ts
Comment thread tests/governance-waitlist-route.test.ts Outdated
…andle boundary

- Add the reject-side mirror of the approve mid-transaction-throw test: force
  the audit insert to fail after a winning claim, assert ROLLBACK / no COMMIT /
  release once.
- Boundary test now builds valid domain-format handles of exactly 253 (pass)
  and 254 (400) chars, so a .max() regression on the handle schema is caught
  instead of only exercising ~77% of the cap.
@AndrewNordstrom
AndrewNordstrom merged commit 883701c into main Jul 13, 2026
22 of 24 checks passed
@AndrewNordstrom
AndrewNordstrom deleted the dev/PROJ-1816-login-allowlist-waitlist-backend branch July 13, 2026 05:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant