Skip to content

Account linking#53

Merged
InfinityBowman merged 16 commits into
mainfrom
account-linking
Dec 15, 2025
Merged

Account linking#53
InfinityBowman merged 16 commits into
mainfrom
account-linking

Conversation

@InfinityBowman
Copy link
Copy Markdown
Owner

@InfinityBowman InfinityBowman commented Dec 15, 2025

Summary by CodeRabbit

  • New Features

    • Self-service account merge flow (initiate, verify, complete, cancel) with verification codes and merge UI
    • Protected-route wrapper for authenticated pages; PIN input component for verification
  • UI

    • Linked accounts management (link/unlink, merge dialog), provider icons in admin users list
    • Account provider card and Integrations section on Settings
  • Backend

    • Account-merge API, email templates, unlinked timestamp on accounts, atomic user delete/merge operations
  • Documentation

    • Documentation tooling reworked; a local Better Auth doc was removed
  • Chores

    • Local admin scripts added, tests and MCP tooling/version updates

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 15, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds a self‑service account-merge feature (frontend + backend + email), modularizes MCP tooling into register* modules with caching and tests, introduces linked‑accounts UI and PIN input, updates DB schema and admin tooling/scripts, and adds route guards and centralized account‑linking error handling.

Changes

Cohort / File(s) Summary
Account merge backend
packages/workers/src/routes/account-merge.js, packages/workers/src/index.js, packages/workers/src/auth/config.js, packages/workers/src/auth/emailTemplates.js
New /api/accounts/merge routes (initiate, verify, complete, cancel); verification code persistence, email templates, account-linking config enabled, and route mounting.
Frontend account-merge client
packages/web/src/api/account-merge.js
New client API: initiateMerge, verifyMergeCode, completeMerge, getMergeStatus, cancelMerge — fetch wrappers with JSON handling and error propagation.
Linked accounts UI
packages/web/src/components/profile-ui/LinkedAccountsSection.jsx, packages/web/src/components/profile-ui/MergeAccountsDialog.jsx, packages/web/src/components/profile-ui/AccountProviderCard.jsx, packages/web/src/components/profile-ui/SettingsPage.jsx
New LinkedAccountsSection, MergeAccountsDialog, AccountProviderCard; LinkedAccountsSection mounted into SettingsPage; OAuth link/unlink flows and merge UI implemented.
Protected routing & auth layout
packages/web/src/components/auth-ui/ProtectedGuard.jsx, packages/web/src/components/auth-ui/AuthLayout.jsx, packages/web/src/Routes.jsx
Added ProtectedGuard component; refined AuthLayout load/redirect logic; protected routes wrapped with ProtectedGuard.
Account-linking errors & admin UI
packages/web/src/lib/account-linking-errors.js, packages/web/src/components/admin-ui/UserTable.jsx
Centralized OAuth/linking error mapping and parser; UserTable now displays provider icons and a Providers column.
Zag PIN input
packages/web/src/components/zag/PinInput.jsx
New 6-digit PIN input component using @zag-js/pin-input with onInput/onComplete callbacks and error styling.
MCP refactor & tools
packages/mcp/server.js, packages/mcp/tools/* (icons.js,lint.js,local-docs.js,better-auth.js,drizzle.js,zag.js), packages/mcp/constants.js, packages/mcp/package.json, packages/mcp/vitest.config.js, packages/mcp/__tests__/*, .vscode/mcp.json, mcp.json
Replaced inline tool registrations with modular register* functions; added icon/drizzle/zag/local-docs/lint/better-auth tools, CACHE_TTL constant, bumped MCP version to 2.0.0, vitest config, and tests; added VSCode MCP server entry and empty mcp.json.
MCP docs / deleted file
.github/copilot-instructions.md, docs/better-auth/llms.txt
Updated copilot instructions to reference MCP changes; removed docs/better-auth/llms.txt.
Workers DB & migrations
packages/workers/migrations/0001_init.sql, packages/workers/src/__tests__/admin.test.js
Added unlinkedAt column to account table; test setup now creates an account table for admin tests.
Admin & users routes: atomic operations
packages/workers/src/routes/admin.js, packages/workers/src/routes/users.js
Admin endpoints include account/provider data; destructive operations consolidated into db.batch transactions for atomic deletes/updates.
Workers scripts & package scripts
packages/workers/scripts/make-admin-local.mjs, packages/workers/scripts/make-admin-prod.mjs, packages/workers/package.json, packages/workers/.env.example, package.json
Added local make-admin script and npm script entries (user:make-admin:local), prod script now loads .env and requires ADMIN_EMAIL, and example env renamed ADMIN_EMAIL.
Formatting & tests
.prettierignore, packages/mcp/__tests__/*
Added package-lock.json to .prettierignore and added MCP unit tests and vitest config.
Misc constants & config
packages/mcp/constants.js, packages/mcp/package.json, packages/mcp/vitest.config.js
Added CACHE_TTL, bumped package version, and added vitest devDependency + test scripts.
sequenceDiagram
    autonumber
    actor User
    participant Frontend as Web Frontend
    participant Server as Workers API
    participant DB as D1 Database
    participant Email as Email Service

    User->>Frontend: Submit target email (initiate)
    Frontend->>Server: POST /api/accounts/merge/initiate { targetEmail }
    activate Server
    Server->>DB: lookup users & linked accounts
    Server->>DB: create verification record (mergeToken + code + expiry)
    Server->>Email: send verification email (HTML/text)
    Email-->>User: deliver code
    Server-->>Frontend: { mergeToken, preview }
    deactivate Server

    User->>Frontend: Enter code (verify)
    Frontend->>Server: POST /api/accounts/merge/verify { mergeToken, code }
    activate Server
    Server->>DB: validate code, mark verified
    Server-->>Frontend: success
    deactivate Server

    User->>Frontend: Confirm merge (complete)
    Frontend->>Server: POST /api/accounts/merge/complete { mergeToken }
    activate Server
    Server->>DB: validate token & verified
    Server->>DB: transfer non-duplicate accounts, reassign projects/memberships/media
    Server->>DB: merge subscriptions, delete secondary user, cleanup
    Server-->>Frontend: success + mergedProviders
    deactivate Server
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Review focus:
    • packages/workers/src/routes/account-merge.js — transactional merge logic, edge cases, rate limiting, and rollback.
    • packages/mcp/server.js and packages/mcp/tools/* — registration wiring, caching/TTL, input validation, and test coverage.
    • packages/web/src/components/profile-ui/* — multi-step dialog flows, state transitions, and error handling.
    • packages/workers/src/routes/admin.js & packages/workers/src/routes/users.js — db.batch operations and referential integrity.
    • packages/workers/scripts/make-admin-local.mjs & make-admin-prod.mjs — CLI parsing and Wrangler D1 execution.

"
I nibbled code and hopped about, 🐇
Pins that click and emails bright,
Two accounts merged beneath the light,
Docs and tools arranged just right,
Hooray — I bounded off into night!
"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.59% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Account linking' directly reflects the main feature addition across the changeset: OAuth provider linking/unlinking, account merge flows, and related infrastructure updates.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d78953a and 89c60f3.

📒 Files selected for processing (2)
  • packages/workers/src/routes/account-merge.js (1 hunks)
  • packages/workers/src/routes/admin.js (5 hunks)

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

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Dec 15, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
corates 89c60f3 Dec 15 2025, 09:41 PM

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (1)
packages/workers/src/routes/admin.js (1)

106-116: Duplicated search filter logic between count query and main query.

The search filter logic in the count query (lines 106-116) doesn't include all the same fields as the main query (lines 93-103). The main query filters on email, name, displayName, and username, but the count query only filters on email and name. This will cause pagination to report incorrect totals when users match on displayName or username.

     // Get total count for pagination
     const [totalResult] = await db
       .select({ count: count() })
       .from(user)
       .where(
         search ?
           or(
             like(sql`lower(${user.email})`, `%${search.toLowerCase()}%`),
             like(sql`lower(${user.name})`, `%${search.toLowerCase()}%`),
+            like(sql`lower(${user.displayName})`, `%${search.toLowerCase()}%`),
+            like(sql`lower(${user.username})`, `%${search.toLowerCase()}%`),
           )
         : undefined,
       );

Consider extracting the search condition into a shared helper to avoid duplication:

const buildSearchCondition = (searchTerm) => {
  if (!searchTerm) return undefined;
  const pattern = `%${searchTerm.toLowerCase()}%`;
  return or(
    like(sql`lower(${user.email})`, pattern),
    like(sql`lower(${user.name})`, pattern),
    like(sql`lower(${user.displayName})`, pattern),
    like(sql`lower(${user.username})`, pattern),
  );
};
🧹 Nitpick comments (12)
packages/mcp/server.js (1)

227-286: Well-structured tool implementation.

The better_auth_docs tool is cleanly implemented with good error handling for both HTTP errors and exceptions. The descriptive parameter documentation and helpful guidance in response messages are nice touches.

Optional consideration: The fetch call has no timeout, which could cause the tool to hang if the external server is unresponsive. Consider adding an AbortController with a timeout:

  async ({ path: docPath }) => {
    try {
      // Build the URL - if no path provided, fetch the index
      const url =
        docPath ?
          `${BETTER_AUTH_BASE_URL}/llms.txt/${docPath}`
        : `${BETTER_AUTH_BASE_URL}/llms.txt`;

-      const response = await fetch(url);
+      const controller = new AbortController();
+      const timeoutId = setTimeout(() => controller.abort(), 30000);
+      
+      const response = await fetch(url, { signal: controller.signal });
+      clearTimeout(timeoutId);
packages/web/src/components/admin-ui/UserTable.jsx (1)

28-33: Consider centralizing provider metadata

PROVIDER_INFO is likely to be reused across the account-linking UI (settings, merge dialogs, etc.). To avoid drift when adding/removing providers, consider moving this map into a shared constants/module (e.g., authProviders.js) and importing it here and in related components.

As per coding guidelines, favor shared config over repeating hardcoded values.

packages/workers/migrations/0001_init.sql (1)

70-70: Consider adding an index for unlinkedAt if querying unlinked accounts.

The new unlinkedAt column supports soft-delete/grace period semantics for account unlinking. If your backend queries for accounts where unlinkedAt IS NOT NULL or filters by unlink timestamp ranges, consider adding an index:

CREATE INDEX idx_account_unlinkedAt ON account(unlinkedAt);

Only add this if you anticipate frequent queries filtering or ordering by this column.

Based on coding guidelines: all migrations should go in this single file (0001_init.sql).

packages/web/src/components/profile-ui/AccountProviderCard.jsx (1)

37-49: Consider using user's locale for date formatting.

The date is hardcoded to 'en-US' locale. Per coding guidelines, prefer using config files rather than hardcoding values. Consider using the user's browser locale or a configurable app-wide setting.

   const linkedDate = createMemo(() => {
     if (!props.account.createdAt) return null;
     const date = new Date(
       typeof props.account.createdAt === 'number' ?
         props.account.createdAt * 1000 // Unix timestamp in seconds
       : props.account.createdAt,
     );
-    return date.toLocaleDateString('en-US', {
+    return date.toLocaleDateString(undefined, { // Uses browser's locale
       month: 'short',
       day: 'numeric',
       year: 'numeric',
     });
   });
packages/web/src/components/profile-ui/MergeAccountsDialog.jsx (1)

1-384: Consider extracting step components if this grows further.

At ~384 lines, this component exceeds the 200-300 line guideline. However, since it's a cohesive multi-step wizard with clear separation between handlers and UI steps, the current structure is acceptable. If more steps or complexity are added, consider extracting each step into separate components under a MergeAccountsDialog/ folder.

packages/web/src/components/profile-ui/LinkedAccountsSection.jsx (1)

90-95: Consider a more robust approach than setTimeout.

The 100ms delay is a workaround for Zag dialog initialization timing. While this works, it could cause flaky behavior if initialization takes longer. Consider using queueMicrotask or ensuring the Dialog component handles this internally.

-        setTimeout(() => {
-          setShowMergeDialog(true);
-        }, 100);
+        queueMicrotask(() => {
+          setShowMergeDialog(true);
+        });
packages/workers/src/routes/account-merge.js (2)

43-45: Consider using crypto.getRandomValues for code generation.

Math.random() is not cryptographically secure. While this is a verification code with short expiry, using crypto.getRandomValues would be more robust.

 function generateCode() {
-  return Math.floor(100000 + Math.random() * 900000).toString();
+  const array = new Uint32Array(1);
+  crypto.getRandomValues(array);
+  return String(100000 + (array[0] % 900000)).padStart(6, '0');
 }

265-383: Acknowledge partial merge risk due to lack of transactions.

The code correctly notes that D1 doesn't support transactions. If a failure occurs mid-merge (e.g., after moving accounts but before deleting the secondary user), the database will be in an inconsistent state. Consider:

  1. Adding idempotency markers to allow retry/recovery
  2. Ordering operations from least to most destructive (current order is reasonable)
  3. Adding a status field to track merge progress

For now, ensure monitoring/alerting is in place for the [AccountMerge] Error during merge: log message.

packages/web/src/api/account-merge.js (1)

8-8: Solid API surface; consider a small fetch helper + defensive JSON parsing

The overall structure looks good: consistent base URL, credentialed requests, and clear error messages returned as thrown Errors. Route paths and payload shapes align with the described backend flow.

Two optional improvements you might consider:

  1. Factor out common fetch/error logic
    All five functions repeat the same pattern (fetchresponse.json()!response.ok guard). A tiny helper would reduce duplication and keep future behavior changes (e.g., headers, error mapping) in one place:

    +async function request(path, options = {}) {
    +  const response = await fetch(`${API_BASE}${path}`, {
    +    credentials: 'include',
    +    headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
    +    ...options,
    +  });
    +
    +  // Be defensive if a route ever returns non‑JSON or 204
    +  let data;
    +  try {
    +    data = await response.json();
    +  } catch {
    +    data = {};
    +  }
    +
    +  if (!response.ok) {
    +    throw new Error(data.error || 'Request failed');
    +  }
    +
    +  return data;
    +}
    +
    export async function initiateMerge(targetEmail) {
    -  const response = await fetch(`${API_BASE}/api/accounts/merge/initiate`, {
    -    method: 'POST',
    -    credentials: 'include',
    -    headers: { 'Content-Type': 'application/json' },
    -    body: JSON.stringify({ targetEmail }),
    -  });
    -
    -  const data = await response.json();
    -
    -  if (!response.ok) {
    -    throw new Error(data.error || 'Failed to initiate merge');
    -  }
    -
    -  return data;
    +  return request('/api/accounts/merge/initiate', {
    +    method: 'POST',
    +    body: JSON.stringify({ targetEmail }),
    +  });
    }

    You could similarly adapt verifyMergeCode, completeMerge, getMergeStatus, and cancelMerge to use request.

  2. Clarify expectations around JSON responses
    Right now every function assumes response.json() will succeed, even on error. If any endpoint ever returns non‑JSON (e.g., empty 204, HTML error), these will throw before reaching your !response.ok branch. If the backend always returns JSON this is fine; otherwise the defensive try/catch pattern above will keep errors surfaced as clean Errors.

These are quality-of-life improvements rather than blockers; behavior is otherwise sound.

Also applies to: 15-30, 38-53, 60-75, 82-98, 105-120

ACCOUNT_LINKING_PLAN.md (3)

61-94: Add languages to fenced code blocks to satisfy markdownlint

Several of the diagram blocks use bare triple backticks without a language, which triggers MD040 in markdownlint-cli2. Since these are ASCII flow diagrams rather than code, you can tag them with a neutral language like text to keep rendering the same while appeasing the linter.

For example, for one of the flow diagrams:

-```
+```text
 ┌─────────────────────────────────────────────────────────────────┐
 │                        FLOW DIAGRAM                             │
 ...
-```
+```

Applying this pattern to the diagram-style fences around the flows (e.g., roughly lines 61–94, 131–160, 165–175, 197–221, 248–290, 323–363, 399–435, 451–480, 495–531, 554–584, 595–624, 639–656, 812–846, and 1147–1162) should clear the MD040 warnings without changing the document semantics.

Also applies to: 131-160, 165-175, 197-221, 248-290, 323-363, 399-435, 451-480, 495-531, 554-584, 595-624, 639-656, 787-795, 812-846, 1147-1162


235-238: Replace bare ORCID URL with a markdown link to satisfy MD034

The ORCID profile reference is currently a bare URL, which markdownlint flags (MD034). Converting it to a proper markdown link keeps the text readable and resolves the warning:

-- Consider linking to ORCID profile: `https://orcid.org/0000-0001-2345-6789`
+- Consider linking to the ORCID profile:
+- [https://orcid.org/0000-0001-2345-6789](https://orcid.org/0000-0001-2345-6789)

You can pick any link text you prefer; the key is avoiding the bare URL form.


1411-1450: Update implementation checklist to match current implementation status

The checklist at the end still marks several items as unchecked (e.g., creating LinkedAccountsSection.jsx, AccountProviderCard.jsx, and account-linking-errors.js), but this PR appears to implement those artifacts already. Keeping the checklist in sync with reality will prevent confusion for future maintainers using this document as a source of truth.

Recommend reviewing the checklist and checking off items that this PR actually delivers (or adjusting wording if the implementation differs slightly from the original plan).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c779b71 and 696b4b1.

📒 Files selected for processing (25)
  • .github/copilot-instructions.md (2 hunks)
  • ACCOUNT_LINKING_PLAN.md (1 hunks)
  • docs/better-auth/llms.txt (0 hunks)
  • mcp.json (1 hunks)
  • package.json (1 hunks)
  • packages/mcp/server.js (2 hunks)
  • packages/web/src/Routes.jsx (1 hunks)
  • packages/web/src/api/account-merge.js (1 hunks)
  • packages/web/src/components/admin-ui/UserTable.jsx (5 hunks)
  • packages/web/src/components/auth-ui/AuthGuard.jsx (1 hunks)
  • packages/web/src/components/auth-ui/AuthLayout.jsx (1 hunks)
  • packages/web/src/components/profile-ui/AccountProviderCard.jsx (1 hunks)
  • packages/web/src/components/profile-ui/LinkedAccountsSection.jsx (1 hunks)
  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx (1 hunks)
  • packages/web/src/components/profile-ui/SettingsPage.jsx (2 hunks)
  • packages/web/src/components/zag/PinInput.jsx (1 hunks)
  • packages/web/src/lib/account-linking-errors.js (1 hunks)
  • packages/workers/migrations/0001_init.sql (1 hunks)
  • packages/workers/package.json (1 hunks)
  • packages/workers/scripts/make-admin-local.mjs (1 hunks)
  • packages/workers/src/auth/config.js (1 hunks)
  • packages/workers/src/auth/emailTemplates.js (1 hunks)
  • packages/workers/src/index.js (2 hunks)
  • packages/workers/src/routes/account-merge.js (1 hunks)
  • packages/workers/src/routes/admin.js (3 hunks)
💤 Files with no reviewable changes (1)
  • docs/better-auth/llms.txt
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{js,jsx,ts,tsx}: Prefer modern ES6+ syntax and features
Use aliases for imports when appropriate to improve readability

**/*.{js,jsx,ts,tsx}: For UI icons, use the solid-icons library or SVGs only. Do not use emojis.
Prefer modern ES6+ syntax and features
Use aliases for imports when appropriate to improve readability
Prefer using config files rather than hardcoding values
Each file should handle one coherent responsibility
Group related components in subdirectories with an index.js barrel export

Files:

  • packages/web/src/components/profile-ui/AccountProviderCard.jsx
  • packages/web/src/components/auth-ui/AuthLayout.jsx
  • packages/workers/src/auth/config.js
  • packages/web/src/components/profile-ui/LinkedAccountsSection.jsx
  • packages/web/src/components/profile-ui/SettingsPage.jsx
  • packages/workers/src/routes/account-merge.js
  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/components/zag/PinInput.jsx
  • packages/workers/src/index.js
  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
  • packages/web/src/components/admin-ui/UserTable.jsx
  • packages/workers/src/auth/emailTemplates.js
  • packages/web/src/lib/account-linking-errors.js
  • packages/workers/src/routes/admin.js
  • packages/web/src/api/account-merge.js
  • packages/web/src/Routes.jsx
  • packages/mcp/server.js
**/components/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/components/**/*.{jsx,tsx}: Keep component files small, focused, and modular, aiming for ~200-300 lines max per component file
When a component exceeds ~300 lines, extract sub-components into a folder (e.g., ComponentName/ with index.jsx and helper components)
Move complex logic from component files into separate utility files or primitives
In SolidJS components, access props directly from the props object or wrap them in a function instead of destructuring to maintain reactivity
Use createMemo to compute derived values based on props or state in SolidJS components

Files:

  • packages/web/src/components/profile-ui/AccountProviderCard.jsx
  • packages/web/src/components/auth-ui/AuthLayout.jsx
  • packages/web/src/components/profile-ui/LinkedAccountsSection.jsx
  • packages/web/src/components/profile-ui/SettingsPage.jsx
  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/components/zag/PinInput.jsx
  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
  • packages/web/src/components/admin-ui/UserTable.jsx
**/*.{jsx,tsx,js,ts}

📄 CodeRabbit inference engine (.cursorrules)

Use Solid's createStore for complex state or state objects in SolidJS components

Files:

  • packages/web/src/components/profile-ui/AccountProviderCard.jsx
  • packages/web/src/components/auth-ui/AuthLayout.jsx
  • packages/workers/src/auth/config.js
  • packages/web/src/components/profile-ui/LinkedAccountsSection.jsx
  • packages/web/src/components/profile-ui/SettingsPage.jsx
  • packages/workers/src/routes/account-merge.js
  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/components/zag/PinInput.jsx
  • packages/workers/src/index.js
  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
  • packages/web/src/components/admin-ui/UserTable.jsx
  • packages/workers/src/auth/emailTemplates.js
  • packages/web/src/lib/account-linking-errors.js
  • packages/workers/src/routes/admin.js
  • packages/web/src/api/account-merge.js
  • packages/web/src/Routes.jsx
  • packages/mcp/server.js
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{jsx,tsx}: Use responsive design principles for UI components
Keep files small, focused, and modular. Extract sub-modules into a folder (e.g., ComponentName/ with index.jsx and helper components). Move complex logic into separate utility files or primitives. Split large forms into section components.

Files:

  • packages/web/src/components/profile-ui/AccountProviderCard.jsx
  • packages/web/src/components/auth-ui/AuthLayout.jsx
  • packages/web/src/components/profile-ui/LinkedAccountsSection.jsx
  • packages/web/src/components/profile-ui/SettingsPage.jsx
  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/components/zag/PinInput.jsx
  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
  • packages/web/src/components/admin-ui/UserTable.jsx
  • packages/web/src/Routes.jsx
packages/web/src/components/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/web/src/components/**/*.{jsx,tsx}: When implementing UI components, use zag.js from the Zag.js documentation at /docs/zag
Reuse existing Zag components before creating new ones or using plain HTML inputs. Existing components include: Avatar, Checkbox, Collapsible, Dialog, FileUpload, FloatingPanel, PasswordInput, Splitter, Switch, Tabs, Toast, and Tooltip

Files:

  • packages/web/src/components/profile-ui/AccountProviderCard.jsx
  • packages/web/src/components/auth-ui/AuthLayout.jsx
  • packages/web/src/components/profile-ui/LinkedAccountsSection.jsx
  • packages/web/src/components/profile-ui/SettingsPage.jsx
  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/components/zag/PinInput.jsx
  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
  • packages/web/src/components/admin-ui/UserTable.jsx
packages/web/src/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/web/src/**/*.{jsx,tsx}: Do NOT prop-drill application state. Shared or cross-feature state must live in external stores under packages/web/src/stores/ or relative to the component file. Import stores directly where needed instead of passing values through multiple components.
Components should receive at most 1–5 props, and only for local configuration, not shared state
If a component would need more than 5 props, move the shared data into an external store, a primitive, or Solid context (when scoped to a feature)
Do not destructure props in SolidJS components as it breaks reactivity. Instead, access props directly from the props object (e.g., props.name) or wrap them in a function (e.g., () => props.name)
Use createMemo for derived values based on props or state to ensure reactive updates in SolidJS
Use Solid's createStore for complex state objects in SolidJS for better performance and reactivity
Components should be lean and focused. Do not implement business logic; move that into stores, utilities, or primitives
Never have a component act as a 'God component' coordinating multiple large concerns

Files:

  • packages/web/src/components/profile-ui/AccountProviderCard.jsx
  • packages/web/src/components/auth-ui/AuthLayout.jsx
  • packages/web/src/components/profile-ui/LinkedAccountsSection.jsx
  • packages/web/src/components/profile-ui/SettingsPage.jsx
  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/components/zag/PinInput.jsx
  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
  • packages/web/src/components/admin-ui/UserTable.jsx
  • packages/web/src/Routes.jsx
packages/web/src/components/zag/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Place new zag components into packages/web/src/components/zag/* files

Files:

  • packages/web/src/components/zag/PinInput.jsx
packages/workers/migrations/0001_init.sql

📄 CodeRabbit inference engine (.cursorrules)

All database migrations should go in a single file: packages/workers/migrations/0001_init.sql

When adding new tables or schema changes, edit the existing 0001_init.sql file directly rather than creating new migration files

Files:

  • packages/workers/migrations/0001_init.sql
packages/workers/migrations/*.sql

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All database migrations should go in a single file: packages/workers/migrations/0001_init.sql. Do NOT create separate migration files.

Files:

  • packages/workers/migrations/0001_init.sql
🧠 Learnings (18)
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : Components should be lean and focused. Do not implement business logic; move that into stores, utilities, or primitives

Applied to files:

  • packages/web/src/components/auth-ui/AuthLayout.jsx
📚 Learning: 2025-12-15T16:38:07.301Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-15T16:38:07.301Z
Learning: Applies to **/components/**/*.{jsx,tsx} : Move complex logic from component files into separate utility files or primitives

Applied to files:

  • packages/web/src/components/auth-ui/AuthLayout.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : Do not destructure props in SolidJS components as it breaks reactivity. Instead, access props directly from the props object (e.g., `props.name`) or wrap them in a function (e.g., `() => props.name`)

Applied to files:

  • packages/web/src/components/auth-ui/AuthLayout.jsx
  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/components/zag/PinInput.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : Use Solid's `createStore` for complex state objects in SolidJS for better performance and reactivity

Applied to files:

  • packages/web/src/components/auth-ui/AuthLayout.jsx
  • packages/web/src/components/auth-ui/AuthGuard.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/components/**/*.{jsx,tsx} : Reuse existing Zag components before creating new ones or using plain HTML inputs. Existing components include: Avatar, Checkbox, Collapsible, Dialog, FileUpload, FloatingPanel, PasswordInput, Splitter, Switch, Tabs, Toast, and Tooltip

Applied to files:

  • packages/web/src/components/profile-ui/SettingsPage.jsx
  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/components/zag/PinInput.jsx
  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
  • packages/web/src/components/admin-ui/UserTable.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : Use `createMemo` for derived values based on props or state to ensure reactive updates in SolidJS

Applied to files:

  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : If a component would need more than 5 props, move the shared data into an external store, a primitive, or Solid context (when scoped to a feature)

Applied to files:

  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/components/zag/PinInput.jsx
  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
📚 Learning: 2025-12-15T16:38:07.301Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-15T16:38:07.301Z
Learning: Applies to **/*.{jsx,tsx,js,ts} : Use Solid's `createStore` for complex state or state objects in SolidJS components

Applied to files:

  • packages/web/src/components/auth-ui/AuthGuard.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/components/**/*.{jsx,tsx} : When implementing UI components, use zag.js from the Zag.js documentation at /docs/zag

Applied to files:

  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/components/zag/PinInput.jsx
  • .github/copilot-instructions.md
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/components/zag/**/*.{jsx,tsx} : Place new zag components into `packages/web/src/components/zag/*` files

Applied to files:

  • packages/web/src/components/zag/PinInput.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : For UI icons, use the `solid-icons` library or SVGs only. Do not use emojis.

Applied to files:

  • packages/web/src/components/admin-ui/UserTable.jsx
📚 Learning: 2025-12-15T16:38:07.301Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-15T16:38:07.301Z
Learning: Reuse existing Zag components: Checkbox, Collapsible, Dialog, FileUpload, PasswordInput, Splitter, Switch, Tabs, Toast, and Tooltip from `packages/web/src/components/zag/`

Applied to files:

  • packages/web/src/components/admin-ui/UserTable.jsx
  • .github/copilot-instructions.md
📚 Learning: 2025-12-15T16:38:07.301Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-15T16:38:07.301Z
Learning: When implementing UI components, use zag.js and reference the Zag.js documentation in `docs/zag/llms.txt`

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/workers/migrations/*.sql : All database migrations should go in a single file: `packages/workers/migrations/0001_init.sql`. Do NOT create separate migration files.

Applied to files:

  • .github/copilot-instructions.md
  • packages/workers/migrations/0001_init.sql
📚 Learning: 2025-12-15T16:38:07.301Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-15T16:38:07.301Z
Learning: Applies to packages/workers/migrations/0001_init.sql : All database migrations should go in a single file: `packages/workers/migrations/0001_init.sql`

Applied to files:

  • .github/copilot-instructions.md
  • packages/workers/migrations/0001_init.sql
📚 Learning: 2025-12-15T16:38:07.301Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-15T16:38:07.301Z
Learning: Do NOT create separate migration files (e.g., 0002_xxx.sql) since this project is not yet in production

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/workers/migrations/0001_init.sql : When adding new tables or schema changes, edit the existing 0001_init.sql file directly rather than creating new migration files

Applied to files:

  • packages/workers/migrations/0001_init.sql
📚 Learning: 2025-12-15T16:38:07.301Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-15T16:38:07.301Z
Learning: When adding new tables or schema changes, edit the existing 0001_init.sql file directly

Applied to files:

  • packages/workers/migrations/0001_init.sql
🧬 Code graph analysis (14)
packages/web/src/components/profile-ui/AccountProviderCard.jsx (1)
packages/web/src/components/zag/Tooltip.jsx (1)
  • Tooltip (5-37)
packages/web/src/components/profile-ui/LinkedAccountsSection.jsx (2)
packages/web/src/lib/account-linking-errors.js (4)
  • error (65-65)
  • parseOAuthError (64-101)
  • message (94-94)
  • getLinkErrorMessage (48-53)
packages/web/src/components/profile-ui/AccountProviderCard.jsx (1)
  • AccountProviderCard (25-131)
packages/web/src/components/profile-ui/SettingsPage.jsx (2)
packages/web/src/components/profile-ui/LinkedAccountsSection.jsx (1)
  • LinkedAccountsSection (54-353)
packages/web/src/components/profile-ui/GoogleDriveSettings.jsx (1)
  • GoogleDriveSettings (14-109)
packages/workers/src/routes/account-merge.js (4)
packages/workers/src/middleware/auth.js (1)
  • requireAuth (34-53)
packages/workers/src/db/client.js (1)
  • createDb (9-11)
packages/workers/src/auth/email.js (1)
  • createEmailService (21-110)
packages/workers/src/auth/emailTemplates.js (2)
  • getAccountMergeEmailHtml (151-177)
  • getAccountMergeEmailText (179-194)
packages/web/src/components/auth-ui/AuthGuard.jsx (3)
packages/web/src/components/Navbar.jsx (1)
  • Navbar (8-189)
packages/web/src/components/HomePage.jsx (1)
  • HomePage (19-221)
packages/web/src/api/better-auth-store.js (1)
  • createBetterAuthStore (12-668)
packages/web/src/components/zag/PinInput.jsx (2)
packages/web/src/components/zag/PasswordInput.jsx (8)
  • props (8-8)
  • PasswordInput (6-56)
  • passwordInput (22-22)
  • props (7-7)
  • props (9-9)
  • props (12-12)
  • password (18-18)
  • e (40-40)
packages/web/src/components/zag/Checkbox.jsx (1)
  • Checkbox (22-83)
packages/workers/src/index.js (1)
packages/workers/src/routes/account-merge.js (1)
  • accountMergeRoutes (32-32)
packages/web/src/components/profile-ui/MergeAccountsDialog.jsx (3)
packages/web/src/api/account-merge.js (4)
  • initiateMerge (15-30)
  • verifyMergeCode (38-53)
  • completeMerge (60-75)
  • cancelMerge (105-120)
packages/web/src/components/zag/Dialog.jsx (3)
  • Dialog (18-92)
  • details (30-30)
  • details (123-123)
packages/web/src/components/zag/PinInput.jsx (1)
  • PinInput (5-54)
packages/web/src/components/admin-ui/UserTable.jsx (1)
packages/web/src/components/zag/Tooltip.jsx (1)
  • Tooltip (5-37)
packages/workers/scripts/make-admin-local.mjs (4)
packages/workers/src/index.js (1)
  • result (55-55)
packages/workers/src/db/schema.js (2)
  • user (103-103)
  • user (37-37)
packages/workers/src/routes/users.js (1)
  • c (184-253)
packages/workers/src/routes/members.js (2)
  • c (112-237)
  • c (243-300)
packages/workers/src/auth/emailTemplates.js (2)
packages/workers/src/auth/templates.js (3)
  • getEmailVerificationSuccessPage (5-104)
  • getEmailVerificationErrorPage (196-284)
  • getEmailVerificationFailurePage (106-194)
packages/workers/src/auth/email.js (1)
  • sendMagicLink (91-101)
packages/workers/src/routes/admin.js (1)
packages/workers/src/db/schema.js (3)
  • account (42-58)
  • account (42-58)
  • user (103-103)
packages/web/src/api/account-merge.js (2)
packages/web/src/components/profile-ui/MergeAccountsDialog.jsx (2)
  • targetEmail (37-37)
  • mergeToken (39-39)
packages/workers/src/routes/account-merge.js (1)
  • mergeToken (99-99)
packages/web/src/Routes.jsx (2)
packages/web/src/config/api.js (2)
  • BASEPATH (13-13)
  • BASEPATH (13-13)
packages/web/src/components/auth-ui/AuthGuard.jsx (1)
  • AuthGuard (5-69)
🪛 LanguageTool
ACCOUNT_LINKING_PLAN.md

[uncategorized] ~101-~101: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... | Full page redirect | | 3...

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[style] ~1252-~1252: The words ‘explain’ and ‘explanation’ are quite similar. Consider replacing ‘explain’ with a different word.
Context: ... 2025" - Disabled unlink buttons should explain why: aria-describedby pointing to exp...

(VERB_NOUN_SENT_LEVEL_REP)

🪛 markdownlint-cli2 (0.18.1)
ACCOUNT_LINKING_PLAN.md

61-61: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


131-131: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


164-164: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


196-196: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


225-225: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


247-247: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


323-323: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


398-398: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


450-450: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


494-494: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


553-553: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


594-594: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


638-638: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


787-787: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


812-812: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


1146-1146: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


1251-1251: Bare URL used

(MD034, no-bare-urls)


1311-1311: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Workers Builds: corates
🔇 Additional comments (37)
mcp.json (1)

1-1: Empty configuration file - is this intentional?

This file contains only an empty JSON object. If this is a placeholder for MCP configuration, consider adding a comment in the PR description or removing it until actual configuration is needed.

.github/copilot-instructions.md (2)

34-34: LGTM!

Clear instruction to use the MCP tool for Better-Auth documentation, aligning with the new better_auth_docs tool added to the MCP server.


68-68: LGTM!

Good addition to centralize documentation access through the Corates MCP.

packages/mcp/server.js (1)

69-69: LGTM!

Version bump to 1.3.0 is appropriate for the new better_auth_docs tool addition.

packages/web/src/components/admin-ui/UserTable.jsx (2)

15-16: Good use of icon + Zag Tooltip primitives

Importing FiMail for the fallback and reusing the shared Zag Tooltip component is consistent with the icon and component guidelines for this repo.

Based on learnings, this correctly reuses existing Zag components and solid-icons.

Also applies to: 26-27


148-150: Table header + fallback colspan update look consistent

Adding the “Providers” header and updating the “No users found” row to colspan='6' correctly keeps the header/body structure aligned with the new column.

Also applies to: 167-167

package.json (1)

30-31: LGTM!

The new local admin script follows the existing pattern and properly delegates to the workers package.

packages/web/src/components/auth-ui/AuthLayout.jsx (1)

1-7: LGTM! Excellent separation of concerns.

The simplified AuthLayout now focuses solely on layout presentation, delegating authentication logic to AuthGuard at the route level. This follows the Single Responsibility Principle and makes the component more maintainable.

As per coding guidelines: Components should be lean and focused, moving business logic into stores, utilities, or primitives.

packages/web/src/lib/account-linking-errors.js (2)

48-53: Message fallback logic is correct.

The getLinkErrorMessage function always returns a string (never null), using a generic fallback message for unknown error codes. This is appropriate for this helper function.

The parseOAuthError function on line 99 correctly returns message ?? null to preserve the null values for silent errors (like user cancellations) that are explicitly set to null in the LINK_ERROR_MESSAGES map.


64-101: Robust OAuth error parsing implementation.

The error parsing logic properly normalizes various error code formats, maps them to standardized internal codes, and handles silent errors appropriately. The comprehensive error mappings cover common OAuth and Better Auth scenarios.

packages/workers/package.json (2)

19-19: LGTM!

The new local admin script follows the existing pattern for the production script.


30-30: The review comment incorrectly attributes Tailwind CSS breaking changes to Zod. The listed changes (e.g., text-3xl!, bg-blue-500/20, bg-(--brand-color)) are Tailwind CSS syntax updates, not Zod schema validation library changes.

The codebase's Zod 4.1.13 usage is compatible: it uses the error parameter in .enum(), .refine() with message, .safeParse(), and .optional()—all standard Zod 4 patterns. No deprecated APIs (.flatten(), .format(), .nonstrict()) are present.

No compatibility verification is needed for this dependency upgrade.

Likely an incorrect or invalid review comment.

packages/workers/src/index.js (2)

28-28: LGTM!

Import follows the established pattern for route modules.


174-176: Account merge routes are properly protected with authentication middleware.

The /api/accounts/merge endpoints apply requireAuth middleware at the router level (accountMergeRoutes.use('*', requireAuth)), ensuring all routes—/initiate, /verify, /complete, and /cancel—require authentication. Each route handler additionally retrieves the authenticated user via getAuth(c), confirming the user context is available and protected.

packages/web/src/components/profile-ui/SettingsPage.jsx (4)

20-20: LGTM!

Import follows proper module conventions.


147-149: Good placement for account linking section.

Positioning the LinkedAccountsSection before the Integrations section creates a logical flow: authentication providers → third-party integrations → security settings.


167-170: Smart UX enhancement with data-section attribute.

The data-section="security" attribute enables the LinkedAccountsSection to scroll users directly to the password/credential section when they need to add a password before unlinking accounts. This creates a seamless user experience.


150-160: Clear section naming and description.

Renaming from "Connected Services" to "Integrations" with an explanatory description provides better clarity about the purpose of this section (third-party services) vs. the new Linked Accounts section (authentication providers).

packages/workers/src/auth/config.js (1)

137-148: Account unlinking safeguards are properly enforced.

The configuration's allowUnlinkingAll: true setting is safe because:

  1. Last account protection: Better Auth prevents unlinking a user's final sign-in method. The codebase enforces this with canUnlink() checks (preventing the unlink button from displaying) and returns CANNOT_UNLINK_ONLY_ACCOUNT error if attempted. The comment about magic link fallback is correct—email verification is an additional fallback.

  2. allowDifferentEmails: true is an intentional feature with proper safeguards: email mismatches trigger EMAIL_DOESNT_MATCH errors, and the UI requires an active session (allowDifferentEmails requires prior authentication per the comment).

No action required—the implementation includes sufficient protections.

packages/workers/src/routes/admin.js (2)

121-151: LGTM!

The linked accounts fetching logic correctly handles the empty array edge case and efficiently batches the query for all users in the result set. The reduce operation for grouping accounts by userId is clean and appropriate.


208-224: LGTM!

The linked accounts query for the user details endpoint is clean and correctly ordered by createdAt descending. The response structure with the new accounts field aligns with the admin UI requirements.

packages/web/src/components/zag/PinInput.jsx (2)

35-53: LGTM with minor suggestions.

The component implementation follows the Zag.js patterns. A few optional improvements:

  1. The required attribute on each input may be redundant since Zag's getInputProps already handles this based on the machine config.
  2. Consider adding focus-visible: variants for better keyboard navigation accessibility.

These are minor and don't block the implementation.


20-31: Refactor to use Zag's recommended splitProps pattern for reactive prop handling.

The current configuration passes evaluated prop values (required()) instead of accessors. Zag.js documentation recommends using splitProps to separate machine-driven props from local props, then passing accessors directly:

-import { createMemo, createUniqueId, mergeProps } from 'solid-js';
+import { createMemo, createUniqueId, splitProps } from 'solid-js';
 
 export default function PinInput(props) {
-  const merged = mergeProps(
-    {
-      required: true,
-      otp: true,
-      autoComplete: 'one-time-code',
-    },
-    props,
-  );
-
-  const required = () => merged.required;
-  const otp = () => merged.otp;
-  const autoComplete = () => merged.autoComplete;
-  const isError = () => merged.isError;
+  const [machineProps, local] = splitProps(
+    props,
+    ['required', 'otp', 'autoComplete', 'onInput', 'onComplete']
+  );
+  const isError = () => local.isError;
 
-  const service = useMachine(pinInput.machine, () => ({
-    id: createUniqueId(),
-    required: required(),
-    autoComplete: autoComplete(),
-    otp: otp(),
+  const service = useMachine(pinInput.machine, {
+    id: createUniqueId(),
+    required: machineProps.required ?? true,
+    autoComplete: machineProps.autoComplete ?? 'one-time-code',
+    otp: machineProps.otp ?? true,
     onValueChange(value) {
-      merged.onInput?.(value.valueAsString);
+      machineProps.onInput?.(value.valueAsString);
     },
     onValueComplete(details) {
-      merged.onComplete?.(details.valueAsString);
+      machineProps.onComplete?.(details.valueAsString);
     },
-  }));
+  });

This ensures the machine receives reactive values and can observe prop changes, matching the library's recommended pattern.

⛔ Skipped due to learnings
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/components/**/*.{jsx,tsx} : Reuse existing Zag components before creating new ones or using plain HTML inputs. Existing components include: Avatar, Checkbox, Collapsible, Dialog, FileUpload, FloatingPanel, PasswordInput, Splitter, Switch, Tabs, Toast, and Tooltip
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : If a component would need more than 5 props, move the shared data into an external store, a primitive, or Solid context (when scoped to a feature)
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/components/**/*.{jsx,tsx} : When implementing UI components, use zag.js from the Zag.js documentation at /docs/zag
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : Do not destructure props in SolidJS components as it breaks reactivity. Instead, access props directly from the props object (e.g., `props.name`) or wrap them in a function (e.g., `() => props.name`)
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : Components should receive at most 1–5 props, and only for local configuration, not shared state
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-15T16:38:07.301Z
Learning: Applies to **/components/**/*.{jsx,tsx} : In SolidJS components, access props directly from the props object or wrap them in a function instead of destructuring to maintain reactivity
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-15T16:38:07.301Z
Learning: Components should receive at most 1–5 props for local configuration only, not shared state
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-15T16:38:07.301Z
Learning: Reuse existing Zag components: Checkbox, Collapsible, Dialog, FileUpload, PasswordInput, Splitter, Switch, Tabs, Toast, and Tooltip from `packages/web/src/components/zag/`
packages/web/src/Routes.jsx (1)

27-33: LGTM!

The auth routes are correctly wrapped with component={AuthGuard}, which allows the router to pass nested routes as children. This pattern correctly gates the sign-in/sign-up flows.

packages/web/src/components/profile-ui/AccountProviderCard.jsx (1)

61-129: LGTM!

The component demonstrates good practices:

  • Proper use of createMemo for derived values per coding guidelines
  • Props accessed directly without destructuring to maintain SolidJS reactivity
  • Reuses existing Zag Tooltip component as recommended
  • Good accessibility with aria-label on interactive elements
  • Clean conditional rendering with Show components
packages/web/src/components/auth-ui/AuthGuard.jsx (2)

10-17: LGTM!

The initial load tracking pattern is clean and correctly distinguishes between the first auth state resolution and subsequent background refetches. This prevents showing loading spinners on every auth state check.


56-67: LGTM!

The loading indicator and content gating using Show components follows SolidJS patterns. The spinner animation is clean.

packages/workers/scripts/make-admin-local.mjs (2)

63-65: Escaping approach is adequate for this local-only script.

The sqlString function properly escapes single quotes. For a local development utility this is sufficient.


124-189: LGTM!

The main function has good safety checks: refusing to proceed with multiple matched users, checking if already admin, and verifying the update. Error handling is appropriate for a CLI tool.

packages/workers/src/auth/emailTemplates.js (2)

151-177: LGTM!

The HTML template follows the established pattern of other templates in this file, with consistent styling and proper parameterization. The security warning about ignoring unexpected requests is appropriate.


179-194: LGTM!

The plain text template provides a clear fallback matching the HTML version's content and formatting conventions.

packages/web/src/components/profile-ui/MergeAccountsDialog.jsx (2)

33-55: LGTM!

Good use of SolidJS patterns: signals for local state, createEffect for reset on dialog open, and props accessed directly without destructuring to maintain reactivity. State is appropriately scoped to this component.


80-102: Good double-submission guard.

The if (loading()) return; check at Line 82 prevents concurrent submissions. The fallback from codeFromPinInput to verificationCode() signal properly handles both the onComplete callback and manual button click paths.

packages/web/src/components/profile-ui/LinkedAccountsSection.jsx (3)

54-56: LGTM!

Good use of createResource for data fetching with automatic loading/error states. The refetch function is properly used after mutations.


193-197: LGTM!

The canUnlink logic properly prevents users from locking themselves out - they can only unlink if they have another linked account or a verified email (for magic link sign-in). Good use of createMemo for this derived value.


240-250: LGTM!

Good use of the For directive for rendering the accounts list, with proper passing of props to AccountProviderCard. The reactive values (canUnlink(), unlinkingId()) are correctly evaluated at render time.

packages/workers/src/routes/account-merge.js (1)

392-414: LGTM!

The cancel route is appropriately permissive (allows cancel without token since it's scoped to the current user's requests) and idempotent (always returns success). This is good practice for cancellation endpoints.

Comment thread packages/web/src/components/admin-ui/UserTable.jsx
Comment thread packages/web/src/components/auth-ui/AuthGuard.jsx Outdated
Comment thread packages/web/src/components/profile-ui/AccountProviderCard.jsx
Comment thread packages/web/src/lib/account-linking-errors.js Outdated
Comment thread packages/web/src/Routes.jsx Outdated
Comment on lines +40 to +45
<Route path='/' component={() => <AuthGuard redirect='dashboard' />}>
<Route path='/profile' component={ProfilePage} />
<Route path='/settings' component={SettingsPage} />
<Route path='/admin' component={AdminDashboard} />
<Route path='/settings/billing' component={BillingPage} />
</Route>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the Routes.jsx file to see the full context
cd packages/web/src && wc -l Routes.jsx && head -60 Routes.jsx

Repository: InfinityBowman/corates

Length of output: 2903


🏁 Script executed:

# Find the AuthGuard component
find packages/web/src -name "*AuthGuard*" -o -name "*authGuard*" | head -20

Repository: InfinityBowman/corates

Length of output: 115


🏁 Script executed:

# Search for AuthGuard import/usage patterns
rg -n "AuthGuard" packages/web/src --type jsx --type tsx

Repository: InfinityBowman/corates

Length of output: 93


🏁 Script executed:

# Read AuthGuard component
cat -n packages/web/src/components/auth-ui/AuthGuard.jsx

Repository: InfinityBowman/corates

Length of output: 2897


🏁 Script executed:

# Also check how AuthGuard is used on line 27 (the working pattern)
sed -n '27,34p' packages/web/src/Routes.jsx

Repository: InfinityBowman/corates

Length of output: 457


Protected routes won't render - AuthGuard doesn't receive children.

The inline arrow function () => <AuthGuard redirect='dashboard' /> ignores the children prop passed by SolidJS Router. Nested routes need to be forwarded to AuthGuard via props.children for them to render. The profile, settings, admin, and billing routes on lines 41-44 are currently inaccessible.

Use the same pattern as the working auth routes on line 27:

-        <Route path='/' component={() => <AuthGuard redirect='dashboard' />}>
+        <Route path='/' component={props => <AuthGuard redirect='dashboard'>{props.children}</AuthGuard>}>
           <Route path='/profile' component={ProfilePage} />
           <Route path='/settings' component={SettingsPage} />
           <Route path='/admin' component={AdminDashboard} />
           <Route path='/settings/billing' component={BillingPage} />
         </Route>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Route path='/' component={() => <AuthGuard redirect='dashboard' />}>
<Route path='/profile' component={ProfilePage} />
<Route path='/settings' component={SettingsPage} />
<Route path='/admin' component={AdminDashboard} />
<Route path='/settings/billing' component={BillingPage} />
</Route>
<Route path='/' component={props => <AuthGuard redirect='dashboard'>{props.children}</AuthGuard>}>
<Route path='/profile' component={ProfilePage} />
<Route path='/settings' component={SettingsPage} />
<Route path='/admin' component={AdminDashboard} />
<Route path='/settings/billing' component={BillingPage} />
</Route>
🤖 Prompt for AI Agents
In packages/web/src/Routes.jsx around lines 40 to 45, the inline component for
the protected Route returns <AuthGuard redirect='dashboard' /> without
forwarding nested route children, so the nested profile/settings/admin/billing
routes never render; change the Route to accept props and pass props.children
into AuthGuard (use the same pattern as the working auth routes on line 27) so
AuthGuard wraps and renders its child routes.

Comment thread packages/workers/scripts/make-admin-local.mjs Outdated
Comment thread packages/workers/src/routes/account-merge.js
Comment thread packages/workers/src/routes/account-merge.js
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 (1)
packages/workers/scripts/make-admin-prod.mjs (1)

5-6: Update header comment to clarify ADMIN_EMAIL fallback behavior.

The header comment at lines 5–6 refers to "default email" but doesn't explain what that default is. Since the code now uses process.env.ADMIN_EMAIL as the fallback (line 143), the comment should clarify this instead of leaving it vague.

Additionally, the usage text at line 126 mentions setting ADMIN_EMAIL but doesn't explicitly state that this value is used when --email is omitted. Consider updating it to say something like: "If --email is omitted, uses the ADMIN_EMAIL environment variable (from .env or environment)."

Also confirmed that the old ADMIN_EMAILS (plural) key is fully retired—only ADMIN_EMAIL (singular) is referenced in the codebase.

♻️ Duplicate comments (1)
packages/workers/src/routes/account-merge.js (1)

180-185: Rate limiting implementation addresses previous concern.

The per-token rate limiting (5 attempts per 15 minutes) adequately protects against brute-force attacks on the 6-digit verification code.

🧹 Nitpick comments (9)
packages/web/src/components/profile-ui/AccountProviderCard.jsx (2)

37-49: Avoid hard-coding en-US for linked date formatting

If you care about i18n, consider using the browser locale (e.g. navigator.language) or accepting a locale/formatOptions prop instead of hard-coding 'en-US', so the date renders naturally for non–US users.


106-128: Validate unlink UI logic and harden aria-label fallback

  • With Show when={props.canUnlink} and inner Show when={!isCredential()}, credential providers get no unlink button and no tooltip explanation when they can’t be unlinked. Confirm that this “no UI at all for credentials” state is intentional.
  • If props.provider?.name is missing, the button’s aria-label becomes “Unlink undefined account”. Prefer a safer fallback.
-            aria-label={`Unlink ${props.provider?.name} account`}
+            aria-label={`Unlink ${(props.provider?.name || props.account.providerId)} account`}
packages/web/src/lib/account-linking-errors.js (1)

64-98: Minor: Redundant nullish coalescing on line 96.

The expression message ?? null is redundant since getLinkErrorMessage already returns either a string or null (for silent errors). The ?? null doesn't change the result.

   return {
     code,
-    message: message ?? null,
+    message,
   };
packages/workers/src/routes/account-merge.js (2)

44-46: Consider using crypto for code generation.

Math.random() is not cryptographically secure. While the rate limiting (5 attempts) and short expiry (15 minutes) provide adequate protection, using crypto.getRandomValues() would be more robust.

 function generateCode() {
-  return Math.floor(100000 + Math.random() * 900000).toString();
+  const array = new Uint32Array(1);
+  crypto.getRandomValues(array);
+  return (100000 + (array[0] % 900000)).toString();
 }

288-291: Acknowledge data consistency risk without transactions.

The comment correctly notes D1's lack of transaction support. A partial merge failure could leave data in an inconsistent state. Consider adding a more defensive approach: either implement idempotent operations that can be retried, or add a cleanup mechanism for failed merges.

For production reliability, consider:

  1. Adding a mergeStatus field to track progress through each step
  2. Implementing a retry/resume mechanism for failed merges
  3. Logging each successful step for debugging partial failures
packages/workers/.env.example (1)

22-22: ADMIN_EMAIL name matches scripts; consider adding trailing newline for linter.

The ADMIN_EMAIL key aligns with the new local/prod admin scripts that read process.env.ADMIN_EMAIL, so the rename from the plural form looks correct. dotenv-linter is only flagging the missing blank line at EOF; you can optionally add a trailing newline to silence it, but behavior is otherwise fine.

packages/workers/scripts/make-admin-local.mjs (1)

1-189: Local admin script looks solid; consider sharing helpers with prod and clarifying JSON/"no rows" handling.

This script mirrors the prod admin tool well: argument parsing, email normalization/validation, SQL construction, and D1 execution all look correct for the local DB case, and the ADMIN_EMAIL fallback is consistent with the prod script.

Two optional refinements you might consider:

  1. Share common helpers with make-admin-prod.mjs.
    parseArgs, normalizeEmail, isValidEmail, sqlString, runWranglerD1Execute, and extractRowsFromWranglerJson are nearly identical between the scripts (differing only in DB config and the --yes flag in prod). Extracting these into a shared module (e.g., ./admin-utils.mjs) would reduce duplication and keep behavior in sync.

  2. Differentiate JSON parse failures from "no rows found".
    When JSON.parse throws in extractRowsFromWranglerJson, you return { rows: null, raw }, and the caller treats !before.rows as "No user found". This conflates "Wrangler output changed / wasn't JSON" with a legitimate no-match. A simple check for rows === null with a distinct error message (or logging the raw output) would make failures easier to diagnose.

The current implementation is safe and functionally correct for local admin operations.

packages/web/src/components/auth-ui/AuthGuard.jsx (1)

33-41: Spinner UI is duplicated with AuthLayout – consider a shared loader component.

The loading block here is essentially the same as the one in AuthLayout.jsx. To keep things DRY and make future styling changes easier, consider extracting a small <AuthLoadingSpinner /> (or similar) component and reusing it in both guards.

packages/web/src/components/auth-ui/AuthLayout.jsx (1)

22-51: AuthLayout guest guard and visibility logic are solid; only minor DRY opportunity.

The redirect effect plus showContent logic correctly:

  • Keeps logged‑in users off signin/signup/check‑email,
  • Allows /complete-profile and /reset-password for both logged‑in and guests as intended, and
  • Avoids doing any navigation from within render.

The only minor nit is that the loading spinner + layout here duplicate the markup in ProtectedGuard.jsx; extracting a shared <AuthLoadingSpinner /> (or similar) used by both guards would make future changes to the auth loading UI simpler.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 696b4b1 and 303c492.

📒 Files selected for processing (11)
  • .prettierignore (1 hunks)
  • packages/web/src/Routes.jsx (2 hunks)
  • packages/web/src/components/admin-ui/UserTable.jsx (5 hunks)
  • packages/web/src/components/auth-ui/AuthGuard.jsx (1 hunks)
  • packages/web/src/components/auth-ui/AuthLayout.jsx (4 hunks)
  • packages/web/src/components/profile-ui/AccountProviderCard.jsx (1 hunks)
  • packages/web/src/lib/account-linking-errors.js (1 hunks)
  • packages/workers/.env.example (1 hunks)
  • packages/workers/scripts/make-admin-local.mjs (1 hunks)
  • packages/workers/scripts/make-admin-prod.mjs (3 hunks)
  • packages/workers/src/routes/account-merge.js (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .prettierignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/web/src/components/admin-ui/UserTable.jsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{js,jsx,ts,tsx}: Prefer modern ES6+ syntax and features
Use aliases for imports when appropriate to improve readability

**/*.{js,jsx,ts,tsx}: For UI icons, use the solid-icons library or SVGs only. Do not use emojis.
Prefer modern ES6+ syntax and features
Use aliases for imports when appropriate to improve readability
Prefer using config files rather than hardcoding values
Each file should handle one coherent responsibility
Group related components in subdirectories with an index.js barrel export

Files:

  • packages/web/src/components/profile-ui/AccountProviderCard.jsx
  • packages/workers/src/routes/account-merge.js
  • packages/web/src/lib/account-linking-errors.js
  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/Routes.jsx
  • packages/web/src/components/auth-ui/AuthLayout.jsx
**/components/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/components/**/*.{jsx,tsx}: Keep component files small, focused, and modular, aiming for ~200-300 lines max per component file
When a component exceeds ~300 lines, extract sub-components into a folder (e.g., ComponentName/ with index.jsx and helper components)
Move complex logic from component files into separate utility files or primitives
In SolidJS components, access props directly from the props object or wrap them in a function instead of destructuring to maintain reactivity
Use createMemo to compute derived values based on props or state in SolidJS components

Files:

  • packages/web/src/components/profile-ui/AccountProviderCard.jsx
  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/components/auth-ui/AuthLayout.jsx
**/*.{jsx,tsx,js,ts}

📄 CodeRabbit inference engine (.cursorrules)

Use Solid's createStore for complex state or state objects in SolidJS components

Files:

  • packages/web/src/components/profile-ui/AccountProviderCard.jsx
  • packages/workers/src/routes/account-merge.js
  • packages/web/src/lib/account-linking-errors.js
  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/Routes.jsx
  • packages/web/src/components/auth-ui/AuthLayout.jsx
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{jsx,tsx}: Use responsive design principles for UI components
Keep files small, focused, and modular. Extract sub-modules into a folder (e.g., ComponentName/ with index.jsx and helper components). Move complex logic into separate utility files or primitives. Split large forms into section components.

Files:

  • packages/web/src/components/profile-ui/AccountProviderCard.jsx
  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/Routes.jsx
  • packages/web/src/components/auth-ui/AuthLayout.jsx
packages/web/src/components/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/web/src/components/**/*.{jsx,tsx}: When implementing UI components, use zag.js from the Zag.js documentation at /docs/zag
Reuse existing Zag components before creating new ones or using plain HTML inputs. Existing components include: Avatar, Checkbox, Collapsible, Dialog, FileUpload, FloatingPanel, PasswordInput, Splitter, Switch, Tabs, Toast, and Tooltip

Files:

  • packages/web/src/components/profile-ui/AccountProviderCard.jsx
  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/components/auth-ui/AuthLayout.jsx
packages/web/src/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/web/src/**/*.{jsx,tsx}: Do NOT prop-drill application state. Shared or cross-feature state must live in external stores under packages/web/src/stores/ or relative to the component file. Import stores directly where needed instead of passing values through multiple components.
Components should receive at most 1–5 props, and only for local configuration, not shared state
If a component would need more than 5 props, move the shared data into an external store, a primitive, or Solid context (when scoped to a feature)
Do not destructure props in SolidJS components as it breaks reactivity. Instead, access props directly from the props object (e.g., props.name) or wrap them in a function (e.g., () => props.name)
Use createMemo for derived values based on props or state to ensure reactive updates in SolidJS
Use Solid's createStore for complex state objects in SolidJS for better performance and reactivity
Components should be lean and focused. Do not implement business logic; move that into stores, utilities, or primitives
Never have a component act as a 'God component' coordinating multiple large concerns

Files:

  • packages/web/src/components/profile-ui/AccountProviderCard.jsx
  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/Routes.jsx
  • packages/web/src/components/auth-ui/AuthLayout.jsx
🧠 Learnings (4)
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : Use Solid's `createStore` for complex state objects in SolidJS for better performance and reactivity

Applied to files:

  • packages/web/src/components/auth-ui/AuthGuard.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : Do not destructure props in SolidJS components as it breaks reactivity. Instead, access props directly from the props object (e.g., `props.name`) or wrap them in a function (e.g., `() => props.name`)

Applied to files:

  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/Routes.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : If a component would need more than 5 props, move the shared data into an external store, a primitive, or Solid context (when scoped to a feature)

Applied to files:

  • packages/web/src/components/auth-ui/AuthGuard.jsx
  • packages/web/src/Routes.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : Use `createMemo` for derived values based on props or state to ensure reactive updates in SolidJS

Applied to files:

  • packages/web/src/components/auth-ui/AuthGuard.jsx
🧬 Code graph analysis (6)
packages/web/src/components/profile-ui/AccountProviderCard.jsx (1)
packages/web/src/components/zag/Tooltip.jsx (1)
  • Tooltip (5-37)
packages/workers/scripts/make-admin-prod.mjs (1)
packages/workers/scripts/make-admin-local.mjs (4)
  • email (134-134)
  • args (15-18)
  • args (68-68)
  • args (125-125)
packages/workers/src/routes/account-merge.js (5)
packages/workers/src/middleware/auth.js (1)
  • requireAuth (34-53)
packages/workers/src/db/client.js (1)
  • createDb (9-11)
packages/workers/src/auth/email.js (1)
  • createEmailService (21-110)
packages/workers/src/auth/emailTemplates.js (2)
  • getAccountMergeEmailHtml (151-177)
  • getAccountMergeEmailText (179-194)
packages/workers/src/middleware/rateLimit.js (1)
  • rateLimit (51-114)
packages/workers/scripts/make-admin-local.mjs (1)
packages/workers/scripts/make-admin-prod.mjs (12)
  • args (15-19)
  • args (75-75)
  • args (134-134)
  • i (23-23)
  • email (143-143)
  • stderr (85-85)
  • stdout (86-86)
  • message (87-87)
  • selectSql (157-159)
  • updateSql (161-163)
  • beforeJson (173-173)
  • before (174-174)
packages/web/src/components/auth-ui/AuthGuard.jsx (2)
packages/web/src/components/auth-ui/AuthLayout.jsx (2)
  • useBetterAuth (10-10)
  • navigate (11-11)
packages/web/src/components/Navbar.jsx (1)
  • Navbar (8-189)
packages/web/src/Routes.jsx (2)
packages/web/src/components/auth-ui/AuthGuard.jsx (1)
  • ProtectedGuard (9-44)
packages/web/src/Layout.jsx (1)
  • MainLayout (10-41)
🪛 dotenv-linter (4.0.0)
packages/workers/.env.example

[warning] 22-22: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Workers Builds: corates
  • GitHub Check: Workers Builds: corates-workers-prod
🔇 Additional comments (9)
packages/web/src/components/profile-ui/AccountProviderCard.jsx (2)

17-23: ORCID formatter correctly handles checksum and preformatted IDs

The helper cleanly no-ops when hyphens are already present and the regex now supports 15 digits plus a final digit/X/x, which covers the ORCID checksum case without over-formatting invalid inputs.


8-11: > Likely an incorrect or invalid review comment.

packages/web/src/lib/account-linking-errors.js (2)

8-41: LGTM! Well-structured error message mapping.

The error code mapping is comprehensive, covering OAuth errors, session issues, network errors, and provider configuration problems. The use of null values for silent errors (user-cancelled scenarios) is a good pattern for UX.


48-53: LGTM!

The function correctly handles both defined error codes (including null silent errors) and unknown codes with a sensible fallback.

packages/workers/src/routes/account-merge.js (4)

1-36: LGTM! Clean module setup with appropriate imports.

The imports are well-organized, including authentication middleware, rate limiting, and email services. Using requireAuth middleware on all routes is the correct security approach.


95-96: Use eq for identifier cleanup to avoid unintended deletions.

Using like with a pattern that ends with % could potentially match unintended records if the identifier format changes. Since the identifier format is merge:{userId}:{targetId}, using a more precise match would be safer.

   // Delete any existing merge requests for this user
-  await db.delete(verification).where(like(verification.identifier, `merge:${currentUser.id}:%`));
+  await db.delete(verification).where(sql`${verification.identifier} LIKE ${'merge:' + currentUser.id + ':%'}`);

Actually, the current like usage is appropriate here since you want to delete all merge requests initiated by this user regardless of target. The pattern is safe given the structured identifier format.


130-139: Email failure handling properly implemented.

The rollback of the verification record on email failure and returning 502 status correctly addresses the previous review concern about email failures.


427-434: Verify intentional behavior for token-less cancellation.

Line 431 allows cancellation without providing a mergeToken (!mergeToken is truthy when token is missing). This enables users to cancel their own merge requests from the UI without tracking the token, which may be intentional for UX. Since the query is scoped to currentUser.id, this is secure.

Confirm this is the intended behavior for the cancel flow.

packages/web/src/Routes.jsx (1)

20-44: ProtectedGuard wiring for nested routes looks correct.

Importing ProtectedGuard and using it directly as the component for the nested / route ensures that the /profile, /settings, /admin, and /settings/billing routes render as children and are properly gated by auth, while still sharing Layout. This fixes the prior “children not forwarded” pattern and looks good.

Comment thread packages/web/src/components/auth-ui/ProtectedGuard.jsx
Comment thread packages/workers/src/routes/account-merge.js
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
packages/web/src/components/auth-ui/ProtectedGuard.jsx (1)

5-8: Clarify hardcoded redirect behavior in documentation.

The doc comment states "Redirects guests to dashboard" but doesn't clarify that this path is hardcoded. This mirrors a similar documentation issue flagged in the related AuthGuard.jsx component. Consider either:

  • Updating the comment to explicitly state "Redirects guests to /dashboard" to match the current implementation, or
  • Adding a redirectTo prop (with /dashboard as default) if you anticipate reusing this guard with different redirect targets.

Given the current usage in Routes.jsx, this is primarily a documentation clarity issue.

🤖 Optional: Add redirectTo prop for flexibility

If you'd like to make this configurable:

 /**
  * ProtectedGuard - For authenticated pages (profile, settings, admin, etc.)
- * Redirects guests to dashboard
+ * Redirects guests to the specified path (default: /dashboard)
  */
-export default function ProtectedGuard(props) {
+export default function ProtectedGuard(props) {
+  const redirectTo = () => props.redirectTo || '/dashboard';
   const { isLoggedIn, authLoading } = useBetterAuth();
   const navigate = useNavigate();

   // ...

   createEffect(() => {
     if (!authLoading() && !isLoggedIn()) {
-      navigate('/dashboard', { replace: true });
+      navigate(redirectTo(), { replace: true });
     }
   });
🧹 Nitpick comments (4)
packages/web/src/components/auth-ui/ProtectedGuard.jsx (1)

15-26: Consider simplifying auth state tracking logic.

The current implementation uses two separate effects and an initialLoadComplete signal to coordinate auth state and redirect logic. While this works correctly, you might consider consolidating the logic into a single effect or moving this coordination into a primitive/hook for better reusability.

However, the current separation of concerns (tracking load completion vs. handling redirects) is clear and maintainable, so this is purely optional.

🤖 Alternative: Single effect approach
-  const [initialLoadComplete, setInitialLoadComplete] = createSignal(false);
-
-  createEffect(() => {
-    if (!authLoading()) {
-      setInitialLoadComplete(true);
-    }
-  });
-
-  // Redirect non-logged-in users to dashboard
+  // Track initial load and redirect non-logged-in users
   createEffect(() => {
     if (!authLoading() && !isLoggedIn()) {
       navigate('/dashboard', { replace: true });
     }
   });

-  const showLoading = () => !initialLoadComplete() && authLoading();
-  const showContent = () => initialLoadComplete() && isLoggedIn();
+  const showLoading = () => authLoading();
+  const showContent = () => !authLoading() && isLoggedIn();

This simplification assumes the redirect happens fast enough that we don't need separate tracking of "initial load complete."

packages/workers/src/__tests__/admin.test.js (2)

67-84: Consider adding unique constraints to prevent duplicate account links.

The account table lacks unique constraints on provider-account combinations. Typically, OAuth account tables include constraints like UNIQUE(providerId, accountId) or UNIQUE(userId, providerId) to prevent duplicate account linkings at the database level.

Apply this diff to add a unique constraint:

     password TEXT,
     createdAt INTEGER DEFAULT (unixepoch()),
     updatedAt INTEGER DEFAULT (unixepoch()),
-    FOREIGN KEY(userId) REFERENCES user(id) ON DELETE CASCADE
+    FOREIGN KEY(userId) REFERENCES user(id) ON DELETE CASCADE,
+    UNIQUE(userId, providerId, accountId)
   )

163-189: Consider adding a seedAccount helper for future test maintainability.

While the account table has been added to the schema, there's no corresponding seedAccount helper function like those provided for other tables (seedUser, seedProject, seedSession). Adding this helper now would make it easier to write account-related tests in the future.

Example implementation to add after the seedSession function:

async function seedAccount({ 
  id, 
  userId, 
  accountId, 
  providerId, 
  accessToken = null,
  refreshToken = null,
  scope = null,
  createdAt,
  updatedAt 
}) {
  await env.DB.prepare(
    `INSERT INTO account (id, userId, accountId, providerId, accessToken, refreshToken, scope, createdAt, updatedAt)
     VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)`
  )
    .bind(id, userId, accountId, providerId, accessToken, refreshToken, scope, createdAt, updatedAt)
    .run();
}
packages/workers/src/routes/account-merge.js (1)

44-46: Use crypto-secure randomness for verification codes

generateCode relies on Math.random, which is not cryptographically secure. For verification codes, prefer a CSPRNG to avoid predictable codes, even with rate limiting in place.

Example refactor:

-function generateCode() {
-  return Math.floor(100000 + Math.random() * 900000).toString();
-}
+function generateCode() {
+  const array = new Uint32Array(1);
+  crypto.getRandomValues(array);
+  // 0–899999 mapped to 100000–999999
+  const code = 100000 + (array[0] % 900000);
+  return code.toString();
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 303c492 and c485c55.

📒 Files selected for processing (4)
  • packages/web/src/Routes.jsx (2 hunks)
  • packages/web/src/components/auth-ui/ProtectedGuard.jsx (1 hunks)
  • packages/workers/src/__tests__/admin.test.js (2 hunks)
  • packages/workers/src/routes/account-merge.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/web/src/Routes.jsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{js,jsx,ts,tsx}: Prefer modern ES6+ syntax and features
Use aliases for imports when appropriate to improve readability

**/*.{js,jsx,ts,tsx}: For UI icons, use the solid-icons library or SVGs only. Do not use emojis.
Prefer modern ES6+ syntax and features
Use aliases for imports when appropriate to improve readability
Prefer using config files rather than hardcoding values
Each file should handle one coherent responsibility
Group related components in subdirectories with an index.js barrel export

Files:

  • packages/web/src/components/auth-ui/ProtectedGuard.jsx
  • packages/workers/src/__tests__/admin.test.js
  • packages/workers/src/routes/account-merge.js
**/components/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/components/**/*.{jsx,tsx}: Keep component files small, focused, and modular, aiming for ~200-300 lines max per component file
When a component exceeds ~300 lines, extract sub-components into a folder (e.g., ComponentName/ with index.jsx and helper components)
Move complex logic from component files into separate utility files or primitives
In SolidJS components, access props directly from the props object or wrap them in a function instead of destructuring to maintain reactivity
Use createMemo to compute derived values based on props or state in SolidJS components

Files:

  • packages/web/src/components/auth-ui/ProtectedGuard.jsx
**/*.{jsx,tsx,js,ts}

📄 CodeRabbit inference engine (.cursorrules)

Use Solid's createStore for complex state or state objects in SolidJS components

Files:

  • packages/web/src/components/auth-ui/ProtectedGuard.jsx
  • packages/workers/src/__tests__/admin.test.js
  • packages/workers/src/routes/account-merge.js
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{jsx,tsx}: Use responsive design principles for UI components
Keep files small, focused, and modular. Extract sub-modules into a folder (e.g., ComponentName/ with index.jsx and helper components). Move complex logic into separate utility files or primitives. Split large forms into section components.

Files:

  • packages/web/src/components/auth-ui/ProtectedGuard.jsx
packages/web/src/components/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/web/src/components/**/*.{jsx,tsx}: When implementing UI components, use zag.js from the Zag.js documentation at /docs/zag
Reuse existing Zag components before creating new ones or using plain HTML inputs. Existing components include: Avatar, Checkbox, Collapsible, Dialog, FileUpload, FloatingPanel, PasswordInput, Splitter, Switch, Tabs, Toast, and Tooltip

Files:

  • packages/web/src/components/auth-ui/ProtectedGuard.jsx
packages/web/src/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/web/src/**/*.{jsx,tsx}: Do NOT prop-drill application state. Shared or cross-feature state must live in external stores under packages/web/src/stores/ or relative to the component file. Import stores directly where needed instead of passing values through multiple components.
Components should receive at most 1–5 props, and only for local configuration, not shared state
If a component would need more than 5 props, move the shared data into an external store, a primitive, or Solid context (when scoped to a feature)
Do not destructure props in SolidJS components as it breaks reactivity. Instead, access props directly from the props object (e.g., props.name) or wrap them in a function (e.g., () => props.name)
Use createMemo for derived values based on props or state to ensure reactive updates in SolidJS
Use Solid's createStore for complex state objects in SolidJS for better performance and reactivity
Components should be lean and focused. Do not implement business logic; move that into stores, utilities, or primitives
Never have a component act as a 'God component' coordinating multiple large concerns

Files:

  • packages/web/src/components/auth-ui/ProtectedGuard.jsx
🧠 Learnings (4)
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : Do not destructure props in SolidJS components as it breaks reactivity. Instead, access props directly from the props object (e.g., `props.name`) or wrap them in a function (e.g., `() => props.name`)

Applied to files:

  • packages/web/src/components/auth-ui/ProtectedGuard.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : Use Solid's `createStore` for complex state objects in SolidJS for better performance and reactivity

Applied to files:

  • packages/web/src/components/auth-ui/ProtectedGuard.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : Use `createMemo` for derived values based on props or state to ensure reactive updates in SolidJS

Applied to files:

  • packages/web/src/components/auth-ui/ProtectedGuard.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : If a component would need more than 5 props, move the shared data into an external store, a primitive, or Solid context (when scoped to a feature)

Applied to files:

  • packages/web/src/components/auth-ui/ProtectedGuard.jsx
🧬 Code graph analysis (1)
packages/workers/src/__tests__/admin.test.js (1)
packages/workers/src/db/schema.js (1)
  • user (103-103)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Workers Builds: corates
  • GitHub Check: Workers Builds: corates-workers-prod
🔇 Additional comments (3)
packages/workers/src/__tests__/admin.test.js (1)

41-41: LGTM!

The account table drop is correctly positioned before the user table to respect foreign key constraints.

packages/workers/src/routes/account-merge.js (2)

352-383: Subscription merge logic looks consistent and resolves earlier stripeCustomerId edge case

The updated subscription merge section now:

  • Compares tiers purely via TIER_PRIORITY, regardless of stripeCustomerId.
  • Moves a higher-priority secondary subscription to the primary user (deleting the primary’s old sub if present).
  • Deletes the secondary subscription when the primary is same-or-better.

This aligns with the intention to handle gifts/manual upgrades and avoids discarding a better plan just because it lacks a Stripe customer ID. The use of updatedAt: new Date() on transfer is also good.


415-437: Cancel endpoint behavior is straightforward and idempotent

The /cancel handler:

  • Scopes cancellations to the authenticated user via the merge:${currentUser.id}:% identifier pattern.
  • Optionally validates mergeToken when provided, but still allows “cancel any active merge for me” when omitted.
  • Safely no-ops when there is no merge request.

This is simple, idempotent, and avoids leaking information about other users’ merges.

Comment on lines +28 to +29
const showLoading = () => !initialLoadComplete() && authLoading();
const showContent = () => initialLoadComplete() && isLoggedIn();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use createMemo for derived values.

The showLoading and showContent functions compute derived values from reactive signals. Per SolidJS best practices and project guidelines, these should use createMemo to explicitly memoize computations and make reactive dependencies clearer.

Based on learnings, derived values based on props or state should use createMemo for reactive updates.

Apply this diff:

+  const showLoading = createMemo(() => !initialLoadComplete() && authLoading());
+  const showContent = createMemo(() => initialLoadComplete() && isLoggedIn());
-  const showLoading = () => !initialLoadComplete() && authLoading();
-  const showContent = () => initialLoadComplete() && isLoggedIn();

   return (
     <>
-      <Show when={showLoading()}>
+      <Show when={showLoading()}>
         <div class='w-full max-w-md sm:max-w-xl flex items-center justify-center'>
           <div class='text-center'>
             <div class='animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto' />
             <p class='mt-4 text-gray-600'>Loading...</p>
           </div>
         </div>
       </Show>
-      <Show when={showContent()}>{props.children}</Show>
+      <Show when={showContent()}>{props.children}</Show>
     </>
   );

Note: The imports already include createMemo would need to be added:

-import { createEffect, Show, createSignal } from 'solid-js';
+import { createEffect, Show, createSignal, createMemo } from 'solid-js';

Comment on lines +67 to +84
await run(`
CREATE TABLE account (
id TEXT PRIMARY KEY,
userId TEXT NOT NULL,
accountId TEXT NOT NULL,
providerId TEXT NOT NULL,
accessToken TEXT,
refreshToken TEXT,
accessTokenExpiresAt INTEGER,
refreshTokenExpiresAt INTEGER,
scope TEXT,
idToken TEXT,
password TEXT,
createdAt INTEGER DEFAULT (unixepoch()),
updatedAt INTEGER DEFAULT (unixepoch()),
FOREIGN KEY(userId) REFERENCES user(id) ON DELETE CASCADE
)
`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if unlinkedAt field exists in migration file
rg -n "unlinkedAt" packages/workers/migrations/0001_init.sql

Repository: InfinityBowman/corates

Length of output: 144


🏁 Script executed:

#!/bin/bash
# Get the full account table schema from migration file
rg -A 20 "CREATE TABLE account" packages/workers/migrations/0001_init.sql

Repository: InfinityBowman/corates

Length of output: 687


🏁 Script executed:

#!/bin/bash
# Get the full test account table schema (lines 67-84 context)
sed -n '60,90p' packages/workers/src/__tests__/admin.test.js

Repository: InfinityBowman/corates

Length of output: 829


Apply the unlinkedAt field to match the migration schema.

The test schema is missing the unlinkedAt field defined in packages/workers/migrations/0001_init.sql (line 70). This discrepancy causes the test environment to diverge from the actual database schema, potentially allowing tests to miss issues related to soft-delete handling.

     scope TEXT,
     idToken TEXT,
     password TEXT,
+    unlinkedAt INTEGER DEFAULT NULL,
     createdAt INTEGER DEFAULT (unixepoch()),
     updatedAt INTEGER DEFAULT (unixepoch()),
     FOREIGN KEY(userId) REFERENCES user(id) ON DELETE CASCADE
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await run(`
CREATE TABLE account (
id TEXT PRIMARY KEY,
userId TEXT NOT NULL,
accountId TEXT NOT NULL,
providerId TEXT NOT NULL,
accessToken TEXT,
refreshToken TEXT,
accessTokenExpiresAt INTEGER,
refreshTokenExpiresAt INTEGER,
scope TEXT,
idToken TEXT,
password TEXT,
createdAt INTEGER DEFAULT (unixepoch()),
updatedAt INTEGER DEFAULT (unixepoch()),
FOREIGN KEY(userId) REFERENCES user(id) ON DELETE CASCADE
)
`);
await run(`
CREATE TABLE account (
id TEXT PRIMARY KEY,
userId TEXT NOT NULL,
accountId TEXT NOT NULL,
providerId TEXT NOT NULL,
accessToken TEXT,
refreshToken TEXT,
accessTokenExpiresAt INTEGER,
refreshTokenExpiresAt INTEGER,
scope TEXT,
idToken TEXT,
password TEXT,
unlinkedAt INTEGER DEFAULT NULL,
createdAt INTEGER DEFAULT (unixepoch()),
updatedAt INTEGER DEFAULT (unixepoch()),
FOREIGN KEY(userId) REFERENCES user(id) ON DELETE CASCADE
)
`);
🤖 Prompt for AI Agents
In packages/workers/src/__tests__/admin.test.js around lines 67 to 84, the
CREATE TABLE account test schema is missing the unlinkedAt column from the real
migration; update the CREATE TABLE account block to include the unlinkedAt field
(match the migration by adding an unlinkedAt column of the same type and
default, e.g., unlinkedAt INTEGER DEFAULT NULL) inside the column list before
the createdAt/updatedAt columns so the test DB schema matches
packages/workers/migrations/0001_init.sql.

Comment thread packages/workers/src/routes/account-merge.js
Comment thread packages/workers/src/routes/account-merge.js
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
packages/workers/src/routes/account-merge.js (2)

61-161: Add rate limiting to prevent merge email spam.

The /initiate endpoint lacks rate limiting. An authenticated user can trigger unlimited merge emails to arbitrary addresses by repeatedly calling this endpoint with different target emails. Consider applying rate limiting keyed by currentUser.id:

+// Rate limiter for merge initiation (3 attempts per hour per user)
+const mergeInitiateRateLimiter = rateLimit({
+  limit: 3,
+  windowMs: 60 * 60 * 1000, // 1 hour
+  keyPrefix: 'merge-initiate',
+  keyGenerator: c => c.get('user')?.id || 'unknown',
+});
+
-accountMergeRoutes.post('/initiate', async c => {
+accountMergeRoutes.post('/initiate', mergeInitiateRateLimiter, async c => {

Additionally, returning targetProviders in the preview (lines 156-159) before email verification allows probing which providers are linked to arbitrary emails. Consider deferring this information until after verification.


178-191: Add type validation for mergeToken and code to prevent runtime errors.

If a malicious client sends a non-string value (e.g., {"code": 123}), calling code.trim() on line 220 will throw a TypeError, resulting in a 500 error instead of a clean 400.

 accountMergeRoutes.post('/verify', async c => {
   const { user: currentUser } = getAuth(c);
   const { mergeToken, code } = await c.req.json();

-  if (!mergeToken || !code) {
+  if (typeof mergeToken !== 'string' || typeof code !== 'string') {
     return c.json({ error: 'Merge token and code are required' }, 400);
   }
+
+  const trimmedToken = mergeToken.trim();
+  const trimmedCode = code.trim();
+
+  if (!trimmedToken || !trimmedCode) {
+    return c.json({ error: 'Merge token and code are required' }, 400);
+  }

   // Apply rate limiting keyed by mergeToken
-  c.set('mergeTokenKey', mergeToken);
+  c.set('mergeTokenKey', trimmedToken);

Then use trimmedToken and trimmedCode in subsequent comparisons.

🧹 Nitpick comments (11)
packages/web/src/lib/account-linking-errors.js (2)

72-84: Consider removing redundant self-mappings.

Some entries in errorMappings map codes to themselves (e.g., ACCOUNT_ALREADY_LINKED: 'ACCOUNT_ALREADY_LINKED'). These are redundant since unmapped codes are used as-is. Removing them would reduce noise:

 const errorMappings = {
   ACCESS_DENIED: 'OAUTH_CANCELLED',
   USER_CANCELLED: 'OAUTH_CANCELLED',
-  ACCOUNT_ALREADY_LINKED: 'ACCOUNT_ALREADY_LINKED',
   ACCOUNT_EXISTS: 'ACCOUNT_ALREADY_LINKED',
   ACCOUNT_ALREADY_LINKED_TO_DIFFERENT_USER: 'ACCOUNT_ALREADY_LINKED_TO_DIFFERENT_USER',
-  EMAIL_DOESNT_MATCH: 'EMAIL_DOESNT_MATCH',
-  STATE_MISMATCH: 'STATE_MISMATCH',
-  STATE_NOT_FOUND: 'STATE_NOT_FOUND',
-  INVALID_CALLBACK_REQUEST: 'INVALID_CALLBACK_REQUEST',
-  OAUTH_PROVIDER_NOT_FOUND: 'OAUTH_PROVIDER_NOT_FOUND',
-  UNABLE_TO_LINK_ACCOUNT: 'UNABLE_TO_LINK_ACCOUNT',
 };

48-53: Use the existing UNKNOWN constant for the fallback message.

The fallback message on line 52 duplicates LINK_ERROR_MESSAGES.UNKNOWN. Using the constant ensures consistency if the message is updated later.

 export function getLinkErrorMessage(code) {
   if (code in LINK_ERROR_MESSAGES) {
     return LINK_ERROR_MESSAGES[code];
   }
-  return 'An unexpected error occurred. Please try again.';
+  return LINK_ERROR_MESSAGES.UNKNOWN;
 }
packages/mcp/__tests__/icons.test.js (1)

9-41: Consider extracting the manifest path to a shared constant.

The manifest path is constructed identically on lines 11, 17, and 33. While test isolation is good, extracting this to a module-level constant would reduce duplication.

You could apply this pattern:

+const manifestPath = path.join(__dirname, '..', 'icon-manifest.json');
+
 describe('icons tool', () => {
   it('should have icon manifest file', async () => {
-    const manifestPath = path.join(__dirname, '..', 'icon-manifest.json');
     const stat = await fs.stat(manifestPath);
packages/mcp/__tests__/better-auth.test.js (1)

1-32: Consider importing BETTER_AUTH_BASE_URL from the production code.

The constant on line 3 duplicates the one defined in packages/mcp/tools/better-auth.js. Importing it from the production module would ensure consistency if the URL changes.

Example:

 import { describe, it, expect } from 'vitest';
-
-const BETTER_AUTH_BASE_URL = 'https://www.better-auth.com';
+import { BETTER_AUTH_BASE_URL } from '../tools/better-auth.js';

Note: This would require exporting the constant from tools/better-auth.js.

packages/mcp/__tests__/drizzle.test.js (1)

1-31: Consider importing DRIZZLE_DOCS_URL from the production code.

The constant on line 3 duplicates the one in packages/mcp/tools/drizzle.js. Importing it would ensure consistency.

Example:

 import { describe, it, expect } from 'vitest';
-
-const DRIZZLE_DOCS_URL = 'https://orm.drizzle.team/llms-full.txt';
+import { DRIZZLE_DOCS_URL } from '../tools/drizzle.js';

Note: This would require exporting the constant from tools/drizzle.js.

packages/mcp/__tests__/zag.test.js (2)

3-3: Consider importing ZAG_DOCS_URL from the source module.

The URL constant is duplicated here and in packages/mcp/tools/zag.js. Export it from the source module to keep them in sync.

-const ZAG_DOCS_URL = 'https://zagjs.com/llms-solid.txt';
+import { ZAG_DOCS_URL } from '../tools/zag.js';

This requires exporting ZAG_DOCS_URL from packages/mcp/tools/zag.js.


6-36: Reduce redundant fetches by caching the response in beforeAll.

Each test case independently fetches the same remote URL, resulting in 4 network requests per test run. This increases test execution time and potential flakiness from network issues.

+let fetchedText;
+
 describe('zag tool', () => {
+  beforeAll(async () => {
+    const response = await fetch(ZAG_DOCS_URL);
+    expect(response.ok).toBe(true);
+    fetchedText = await response.text();
+  });
+
   it('should fetch zag docs from remote URL', async () => {
-    const response = await fetch(ZAG_DOCS_URL);
-    expect(response.ok).toBe(true);
-
-    const text = await response.text();
-    expect(text.length).toBeGreaterThan(1000);
+    expect(fetchedText.length).toBeGreaterThan(1000);
   });

   it('should contain ## Resources markers for parsing', async () => {
-    const response = await fetch(ZAG_DOCS_URL);
-    const text = await response.text();
-
-    expect(text).toContain('## Resources');
+    expect(fetchedText).toContain('## Resources');
   });
   // ... apply similar pattern to remaining tests
packages/mcp/tools/icons.js (1)

12-18: Add error handling for manifest loading failures.

If icon-manifest.json is missing or malformed, the unhandled exception will crash the tool. Consider graceful error handling with a user-friendly message.

 async function loadManifest() {
   if (!manifestCache) {
     const manifestPath = path.join(__dirname, '..', 'icon-manifest.json');
-    manifestCache = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
+    try {
+      manifestCache = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
+    } catch (err) {
+      throw new Error(`Failed to load icon manifest: ${err.message}`);
+    }
   }
   return manifestCache;
 }
packages/mcp/tools/zag.js (1)

30-41: Consider documenting the magic values for maintainability.

The lookahead limit of 10 lines and the excluded package names ('solid', 'i') are undocumented. Adding brief comments would help future maintainers understand why these values were chosen.

+  // Look ahead for npm link (within next 10 lines - typical spacing in doc format)
   for (let i = 0; i < lines.length; i++) {
     if (lines[i] === '## Resources') {
-      // Look ahead for npm link (within next 10 lines)
       for (let j = i + 1; j < Math.min(i + 10, lines.length); j++) {
         const npmMatch = lines[j].match(/@zag-js\/([a-z-]+)/);
+        // Skip 'solid' (framework binding) and 'i' (false positive from npm link formatting)
         if (npmMatch && npmMatch[1] !== 'solid' && npmMatch[1] !== 'i') {
packages/mcp/tools/drizzle.js (1)

6-18: Opportunity to extract shared caching logic.

The cache pattern (module-level cache variable + TTL check + fetch + store) is duplicated across better-auth.js, drizzle.js, and zag.js. Consider extracting a reusable createCachedFetcher(url, parser) utility.

This is a nice-to-have refactor that could reduce duplication and ensure consistent caching behavior across all doc tools.

packages/mcp/tools/local-docs.js (1)

32-34: Silent error handling may hide configuration issues.

Returning an empty array on any error (including permission issues or invalid docsRoot path) makes debugging harder. Consider logging the error or distinguishing between "no docs found" and "error reading directory".

   } catch {
-    return [];
+    // Log for debugging but return empty to avoid breaking the tool
+    console.error('Failed to list local docs:', docsRoot);
+    return [];
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c485c55 and 0c56424.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (20)
  • .github/copilot-instructions.md (2 hunks)
  • .vscode/mcp.json (1 hunks)
  • packages/mcp/__tests__/better-auth.test.js (1 hunks)
  • packages/mcp/__tests__/drizzle.test.js (1 hunks)
  • packages/mcp/__tests__/icons.test.js (1 hunks)
  • packages/mcp/__tests__/zag.test.js (1 hunks)
  • packages/mcp/constants.js (1 hunks)
  • packages/mcp/package.json (1 hunks)
  • packages/mcp/server.js (1 hunks)
  • packages/mcp/tools/better-auth.js (1 hunks)
  • packages/mcp/tools/drizzle.js (1 hunks)
  • packages/mcp/tools/icons.js (1 hunks)
  • packages/mcp/tools/lint.js (1 hunks)
  • packages/mcp/tools/local-docs.js (1 hunks)
  • packages/mcp/tools/zag.js (1 hunks)
  • packages/mcp/vitest.config.js (1 hunks)
  • packages/web/src/lib/account-linking-errors.js (1 hunks)
  • packages/workers/src/routes/account-merge.js (1 hunks)
  • packages/workers/src/routes/admin.js (5 hunks)
  • packages/workers/src/routes/users.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/copilot-instructions.md
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{js,jsx,ts,tsx}: Prefer modern ES6+ syntax and features
Use aliases for imports when appropriate to improve readability

**/*.{js,jsx,ts,tsx}: For UI icons, use the solid-icons library or SVGs only. Do not use emojis.
Prefer modern ES6+ syntax and features
Use aliases for imports when appropriate to improve readability
Prefer using config files rather than hardcoding values
Each file should handle one coherent responsibility
Group related components in subdirectories with an index.js barrel export

Files:

  • packages/mcp/constants.js
  • packages/mcp/__tests__/better-auth.test.js
  • packages/mcp/tools/better-auth.js
  • packages/mcp/__tests__/icons.test.js
  • packages/mcp/tools/zag.js
  • packages/mcp/__tests__/drizzle.test.js
  • packages/mcp/tools/icons.js
  • packages/mcp/tools/drizzle.js
  • packages/workers/src/routes/admin.js
  • packages/mcp/tools/lint.js
  • packages/workers/src/routes/users.js
  • packages/mcp/tools/local-docs.js
  • packages/mcp/vitest.config.js
  • packages/web/src/lib/account-linking-errors.js
  • packages/workers/src/routes/account-merge.js
  • packages/mcp/__tests__/zag.test.js
  • packages/mcp/server.js
**/*.{jsx,tsx,js,ts}

📄 CodeRabbit inference engine (.cursorrules)

Use Solid's createStore for complex state or state objects in SolidJS components

Files:

  • packages/mcp/constants.js
  • packages/mcp/__tests__/better-auth.test.js
  • packages/mcp/tools/better-auth.js
  • packages/mcp/__tests__/icons.test.js
  • packages/mcp/tools/zag.js
  • packages/mcp/__tests__/drizzle.test.js
  • packages/mcp/tools/icons.js
  • packages/mcp/tools/drizzle.js
  • packages/workers/src/routes/admin.js
  • packages/mcp/tools/lint.js
  • packages/workers/src/routes/users.js
  • packages/mcp/tools/local-docs.js
  • packages/mcp/vitest.config.js
  • packages/web/src/lib/account-linking-errors.js
  • packages/workers/src/routes/account-merge.js
  • packages/mcp/__tests__/zag.test.js
  • packages/mcp/server.js
🧠 Learnings (3)
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/components/**/*.{jsx,tsx} : When implementing UI components, use zag.js from the Zag.js documentation at /docs/zag

Applied to files:

  • packages/mcp/tools/zag.js
  • packages/mcp/__tests__/zag.test.js
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/components/zag/**/*.{jsx,tsx} : Place new zag components into `packages/web/src/components/zag/*` files

Applied to files:

  • packages/mcp/tools/zag.js
  • packages/mcp/__tests__/zag.test.js
📚 Learning: 2025-12-15T16:38:07.301Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-15T16:38:07.301Z
Learning: When implementing UI components, use zag.js and reference the Zag.js documentation in `docs/zag/llms.txt`

Applied to files:

  • packages/mcp/tools/zag.js
  • packages/mcp/__tests__/zag.test.js
🧬 Code graph analysis (12)
packages/mcp/__tests__/better-auth.test.js (1)
packages/mcp/tools/better-auth.js (3)
  • BETTER_AUTH_BASE_URL (4-4)
  • response (23-23)
  • text (28-28)
packages/mcp/tools/better-auth.js (1)
packages/mcp/constants.js (2)
  • CACHE_TTL (1-1)
  • CACHE_TTL (1-1)
packages/mcp/__tests__/icons.test.js (3)
packages/mcp/tools/icons.js (3)
  • __filename (6-6)
  • __dirname (7-7)
  • manifestPath (14-14)
packages/mcp/iconSearch.js (1)
  • loadManifest (9-15)
packages/mcp/scrape-icons.js (1)
  • main (62-78)
packages/mcp/__tests__/drizzle.test.js (1)
packages/mcp/tools/drizzle.js (2)
  • DRIZZLE_DOCS_URL (4-4)
  • response (20-20)
packages/mcp/tools/icons.js (2)
packages/mcp/iconSearch.js (2)
  • loadManifest (9-15)
  • console (36-38)
packages/mcp/scrape-icons.js (1)
  • main (62-78)
packages/mcp/tools/drizzle.js (2)
packages/mcp/__tests__/drizzle.test.js (1)
  • DRIZZLE_DOCS_URL (3-3)
packages/mcp/constants.js (2)
  • CACHE_TTL (1-1)
  • CACHE_TTL (1-1)
packages/workers/src/routes/admin.js (1)
packages/workers/src/db/schema.js (5)
  • account (42-58)
  • account (42-58)
  • user (5-24)
  • user (5-24)
  • user (103-103)
packages/mcp/tools/lint.js (2)
packages/mcp/server.js (2)
  • server (26-29)
  • repoRoot (22-22)
packages/web/src/lib/account-linking-errors.js (1)
  • error (65-65)
packages/workers/src/routes/users.js (1)
packages/workers/src/db/schema.js (13)
  • projectMembers (85-95)
  • projectMembers (85-95)
  • projects (73-82)
  • projects (73-82)
  • verification (61-68)
  • verification (61-68)
  • account (42-58)
  • account (42-58)
  • session (27-39)
  • session (27-39)
  • user (5-24)
  • user (5-24)
  • user (103-103)
packages/mcp/tools/local-docs.js (1)
packages/mcp/server.js (2)
  • docsRoot (23-23)
  • server (26-29)
packages/workers/src/routes/account-merge.js (5)
packages/workers/src/middleware/auth.js (1)
  • requireAuth (34-53)
packages/workers/src/db/client.js (1)
  • createDb (9-11)
packages/workers/src/auth/email.js (1)
  • createEmailService (21-110)
packages/workers/src/auth/emailTemplates.js (2)
  • getAccountMergeEmailHtml (151-177)
  • getAccountMergeEmailText (179-194)
packages/workers/src/middleware/rateLimit.js (1)
  • rateLimit (51-114)
packages/mcp/__tests__/zag.test.js (1)
packages/mcp/tools/zag.js (2)
  • ZAG_DOCS_URL (4-4)
  • response (20-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Workers Builds: corates
🔇 Additional comments (24)
packages/workers/src/routes/admin.js (2)

121-145: LGTM - Good approach for fetching linked accounts.

Using sql.join for the IN clause and grouping by userId is efficient. The accountsMap pattern avoids N+1 queries.


248-260: Good use of atomic batch for ban operation.

Atomically updating the ban status and invalidating sessions ensures the user can't continue using existing sessions after being banned.

packages/workers/src/routes/account-merge.js (3)

48-52: LGTM - Secure code generation using crypto.

Using crypto.getRandomValues instead of Math.random is the correct approach for security-sensitive codes.


358-429: Well-structured atomic batch for account merge.

The batch operation handles all aspects of the merge comprehensively:

  • Account deduplication and transfer
  • Project ownership and membership transfer
  • Subscription tier-based merging
  • Media file ownership transfer
  • Cleanup of secondary user and verification record

The atomic batch ensures data consistency if any operation fails.


449-471: LGTM - Cancel endpoint handles both explicit and implicit cancellation.

The logic allows cancelling either by providing a specific mergeToken or by omitting it to cancel any pending merge for the current user. This provides flexibility for the UI.

.vscode/mcp.json (1)

7-11: LGTM!

The Stripe MCP server configuration is correctly structured for HTTP-based external service integration.

packages/mcp/constants.js (1)

1-1: LGTM!

Centralized cache TTL constant with clear calculation and documentation.

packages/mcp/__tests__/icons.test.js (1)

1-8: LGTM!

Standard Vitest test setup with proper ESM module path handling.

packages/mcp/vitest.config.js (1)

1-9: LGTM!

The Vitest configuration appropriately enables globals and sets a reasonable 10-second timeout for tests making network requests.

packages/mcp/package.json (3)

3-3: Major version bump to 2.0.0.

The major version bump appropriately signals breaking changes from the modular tool registration refactor mentioned in the PR summary.


12-13: LGTM!

The test scripts and Vitest devDependency properly support the new test infrastructure introduced in this PR.

Also applies to: 19-20


15-18: Zod version 4.1.13 is valid and stable. The version exists on npm, was published on 2025-11-24, and is marked as the current latest stable release. No action required.

Likely an incorrect or invalid review comment.

packages/mcp/tools/lint.js (2)

1-16: LGTM!

The tool registration follows the modular pattern established in this PR. Command construction is safe since fix is a boolean parameter validated by Zod.


17-41: LGTM!

Comprehensive error handling that captures both success and failure cases. The 4MB buffer size is appropriate for lint output, and the consistent content structure makes the tool output predictable.

packages/mcp/tools/icons.js (2)

20-60: LGTM!

The search_icons tool implementation is clean: case-insensitive search, configurable limit with early termination, and appropriate response formatting.


62-79: LGTM!

The icon-libraries resource properly lists available libraries with their icon counts.

packages/mcp/tools/better-auth.js (2)

7-17: Global TTL clears entire cache on expiration.

The current design uses a single cacheTime for all entries. When TTL expires, the entire cache is cleared even if some entries were cached more recently. This is acceptable for infrequently changing docs but worth noting.


33-71: LGTM!

The tool registration is well-structured with clear documentation, proper error handling, and consistent response formatting.

packages/mcp/tools/zag.js (2)

149-152: Bidirectional partial match may produce unexpected results.

The condition c.includes(normalizedName) || normalizedName.includes(c) means searching for "a" would match any component containing "a", but also any component whose name is contained in "a". Consider if bidirectional matching is intentional.

For example, searching for "tabs" would match "tabs" (intended) but the reverse condition normalizedName.includes(c) seems unnecessary unless short component names exist.


114-187: LGTM!

The tool registration provides good UX with exact match, partial match, and helpful error messages guiding users to the index.

packages/mcp/tools/drizzle.js (1)

100-168: LGTM!

The tool implementation is well-structured with proper index generation, exact/partial matching, and informative error responses.

packages/mcp/tools/local-docs.js (2)

5-10: Good security practice with input validation.

The assertSafeDocName function properly prevents path traversal attacks by restricting characters to alphanumeric, dot, underscore, and dash.


65-91: Good defense-in-depth with allowlist validation.

The tool validates the doc name format with assertSafeDocName, then verifies against the discovered allowlist before reading. This prevents accessing arbitrary files even with a valid-looking name.

packages/mcp/server.js (1)

12-37: Clean modular refactor.

The shift from inline tool definitions to modular registrations improves maintainability and testability. Each tool module is now self-contained with its own caching, parsing, and error handling.

Comment thread packages/workers/src/routes/account-merge.js
Comment on lines +151 to +160
// Delete all user data atomically using batch transaction
// Order matters for foreign key constraints
await db.batch([
db.delete(projectMembers).where(eq(projectMembers.userId, userId)),
db.delete(projects).where(eq(projects.createdBy, userId)),
db.delete(verification).where(eq(verification.identifier, currentUser.email)),
db.delete(account).where(eq(account.userId, userId)),
db.delete(session).where(eq(session.userId, userId)),
db.delete(user).where(eq(user.id, userId)),
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's find and verify the files exist
echo "=== Checking files ==="
ls -la packages/workers/src/routes/users.js 2>/dev/null && echo "✓ users.js found" || echo "✗ users.js not found"
ls -la packages/workers/src/routes/admin.js 2>/dev/null && echo "✓ admin.js found" || echo "✗ admin.js not found"

# Get the actual code at specified lines in users.js
echo ""
echo "=== users.js lines 151-160 ==="
sed -n '151,160p' packages/workers/src/routes/users.js

# Get more context around line 151 in users.js
echo ""
echo "=== users.js lines 145-165 (more context) ==="
sed -n '145,165p' packages/workers/src/routes/users.js

# Get the actual code at specified lines in admin.js
echo ""
echo "=== admin.js lines 387-394 ==="
sed -n '387,394p' packages/workers/src/routes/admin.js 2>/dev/null || echo "Line range might differ"

# Get more context around potential delete operation in admin.js
echo ""
echo "=== Searching for DELETE /users/:userId in admin.js ==="
rg -n "DELETE.*users" packages/workers/src/routes/admin.js -A 5 -B 2

Repository: InfinityBowman/corates

Length of output: 2741


🏁 Script executed:

#!/bin/bash
# Now let's verify the schema to see the cascade delete configuration
echo "=== Finding schema files ==="
find packages/workers -name "*.ts" -o -name "*.js" | grep -i schema | head -10

# Look for table definitions
echo ""
echo "=== Searching for table schemas ==="
rg -l "verification\|projectMembers\|projects\|account\|session\|user" packages/workers/src --type ts --type js | grep -i schema

# Let's check common schema locations
echo ""
echo "=== Checking db schema locations ==="
ls -la packages/workers/src/db/ 2>/dev/null || echo "db/ not found"
ls -la packages/workers/src/schema/ 2>/dev/null || echo "schema/ not found"
find packages/workers -name "schema.ts" -o -name "schema.js" -o -name "*schema*" 2>/dev/null | head -20

Repository: InfinityBowman/corates

Length of output: 551


🏁 Script executed:

#!/bin/bash
# Read the schema file to verify cascade delete configurations
echo "=== Schema file content ==="
cat packages/workers/src/db/schema.js

Repository: InfinityBowman/corates

Length of output: 6251


Batch deletion is good, but note inconsistencies between endpoints and missing tables.

The batch transaction in users.js correctly uses atomic operations with proper foreign key ordering. However, there are significant inconsistencies with the admin endpoint's deletion logic in admin.js (lines 387-394):

  • users.js deletes: projectMembers, projects, verification, account, session, user
  • admin.js deletes: projectMembers, projects, session, user (missing: verification, account)

This means admin deletion of a user leaves orphaned verification and account records. Additionally, both endpoints are missing deletions for:

  • twoFactor (has onDelete: 'cascade' constraint)
  • subscriptions (has onDelete: 'cascade' constraint)

While cascade deletes in the schema will handle these automatically, explicit deletion provides defensive redundancy and ensures intentional cleanup. The verification table deletion in users.js is correctly done by identifier since it lacks a direct userId foreign key.

Align admin.js deletion with users.js, and consider adding twoFactor and subscriptions to both for complete data removal.

🤖 Prompt for AI Agents
In packages/workers/src/routes/users.js around lines 151 to 160 and in
packages/workers/src/routes/admin.js around lines 387 to 394, the admin delete
flow is missing verification and account deletions and both endpoints omit
twoFactor and subscriptions; update admin.js to match users.js by adding
deletions for verification (filter by identifier), account (by userId), and also
add explicit deletions for twoFactor and subscriptions in both users.js and
admin.js, preserving the current foreign-key-aware ordering so deletes occur
before the user row is removed.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/web/src/components/profile-ui/MergeAccountsDialog.jsx (2)

146-152: Use createMemo for derived values from props.

Both providerName and isOpen are derived values based on props. Per SolidJS best practices and coding guidelines, these should use createMemo to ensure proper reactive tracking and updates.

Apply this diff:

-  const providerName = () => {
+  const providerName = createMemo(() => {
     const names = { google: 'Google', orcid: 'ORCID' };
     return names[props.conflictProvider] || props.conflictProvider;
-  };
+  });

   // Preserve reactivity for open prop
-  const isOpen = () => props.open;
+  const isOpen = createMemo(() => props.open);

As per coding guidelines: "Use createMemo for derived values based on props or state to ensure reactive updates in SolidJS."


154-387: Consider extracting step panels if the component grows further.

The Dialog structure and step implementations are well-organized with proper conditional rendering and user feedback. At 388 lines, this component slightly exceeds the 200-300 line guideline, but remains cohesive with a single clear responsibility. If additional features are added, consider extracting individual step panels into separate sub-components.

Note: The plain HTML email input (lines 206-214) is appropriate since no generic TextInput Zag component exists in the documented list.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c56424 and f74df93.

📒 Files selected for processing (3)
  • packages/web/src/api/account-merge.js (1 hunks)
  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx (1 hunks)
  • packages/workers/src/routes/account-merge.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/web/src/api/account-merge.js
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{js,jsx,ts,tsx}: Prefer modern ES6+ syntax and features
Use aliases for imports when appropriate to improve readability

**/*.{js,jsx,ts,tsx}: For UI icons, use the solid-icons library or SVGs only. Do not use emojis.
Prefer modern ES6+ syntax and features
Use aliases for imports when appropriate to improve readability
Prefer using config files rather than hardcoding values
Each file should handle one coherent responsibility
Group related components in subdirectories with an index.js barrel export

Files:

  • packages/workers/src/routes/account-merge.js
  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
**/*.{jsx,tsx,js,ts}

📄 CodeRabbit inference engine (.cursorrules)

Use Solid's createStore for complex state or state objects in SolidJS components

Files:

  • packages/workers/src/routes/account-merge.js
  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
**/components/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/components/**/*.{jsx,tsx}: Keep component files small, focused, and modular, aiming for ~200-300 lines max per component file
When a component exceeds ~300 lines, extract sub-components into a folder (e.g., ComponentName/ with index.jsx and helper components)
Move complex logic from component files into separate utility files or primitives
In SolidJS components, access props directly from the props object or wrap them in a function instead of destructuring to maintain reactivity
Use createMemo to compute derived values based on props or state in SolidJS components

Files:

  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{jsx,tsx}: Use responsive design principles for UI components
Keep files small, focused, and modular. Extract sub-modules into a folder (e.g., ComponentName/ with index.jsx and helper components). Move complex logic into separate utility files or primitives. Split large forms into section components.

Files:

  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
packages/web/src/components/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/web/src/components/**/*.{jsx,tsx}: When implementing UI components, use zag.js from the Zag.js documentation at /docs/zag
Reuse existing Zag components before creating new ones or using plain HTML inputs. Existing components include: Avatar, Checkbox, Collapsible, Dialog, FileUpload, FloatingPanel, PasswordInput, Splitter, Switch, Tabs, Toast, and Tooltip

Files:

  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
packages/web/src/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/web/src/**/*.{jsx,tsx}: Do NOT prop-drill application state. Shared or cross-feature state must live in external stores under packages/web/src/stores/ or relative to the component file. Import stores directly where needed instead of passing values through multiple components.
Components should receive at most 1–5 props, and only for local configuration, not shared state
If a component would need more than 5 props, move the shared data into an external store, a primitive, or Solid context (when scoped to a feature)
Do not destructure props in SolidJS components as it breaks reactivity. Instead, access props directly from the props object (e.g., props.name) or wrap them in a function (e.g., () => props.name)
Use createMemo for derived values based on props or state to ensure reactive updates in SolidJS
Use Solid's createStore for complex state objects in SolidJS for better performance and reactivity
Components should be lean and focused. Do not implement business logic; move that into stores, utilities, or primitives
Never have a component act as a 'God component' coordinating multiple large concerns

Files:

  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
🧠 Learnings (3)
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/components/**/*.{jsx,tsx} : Reuse existing Zag components before creating new ones or using plain HTML inputs. Existing components include: Avatar, Checkbox, Collapsible, Dialog, FileUpload, FloatingPanel, PasswordInput, Splitter, Switch, Tabs, Toast, and Tooltip

Applied to files:

  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : If a component would need more than 5 props, move the shared data into an external store, a primitive, or Solid context (when scoped to a feature)

Applied to files:

  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
📚 Learning: 2025-12-15T16:38:32.106Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-15T16:38:32.106Z
Learning: Applies to packages/web/src/**/*.{jsx,tsx} : Use `createMemo` for derived values based on props or state to ensure reactive updates in SolidJS

Applied to files:

  • packages/web/src/components/profile-ui/MergeAccountsDialog.jsx
🧬 Code graph analysis (1)
packages/workers/src/routes/account-merge.js (5)
packages/workers/src/middleware/auth.js (1)
  • requireAuth (34-53)
packages/workers/src/middleware/rateLimit.js (1)
  • rateLimit (51-114)
packages/workers/src/db/client.js (1)
  • createDb (9-11)
packages/workers/src/auth/email.js (1)
  • createEmailService (21-110)
packages/workers/src/auth/emailTemplates.js (2)
  • getAccountMergeEmailHtml (151-177)
  • getAccountMergeEmailText (179-194)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Workers Builds: corates
  • GitHub Check: Workers Builds: corates-workers-prod
🔇 Additional comments (8)
packages/web/src/components/profile-ui/MergeAccountsDialog.jsx (3)

1-31: LGTM!

The imports and constants are well-organized. The component correctly uses existing Zag components (Dialog, PinInput) and icons from solid-icons rather than emojis.


33-55: LGTM!

Props are correctly accessed without destructuring to maintain SolidJS reactivity, and local state management with createSignal is appropriate. The createEffect properly resets state when the dialog opens.


57-144: LGTM!

All handler functions demonstrate proper error handling, loading state management, and user feedback through toasts. The use of optional chaining for callbacks and the dual-parameter approach in handleVerifyCode show thoughtful design.

packages/workers/src/routes/account-merge.js (5)

1-60: LGTM!

The setup is solid with proper middleware, cryptographically secure code generation using crypto.getRandomValues, and rate limiting configured for the initiate endpoint.


69-176: LGTM!

The /initiate endpoint properly addresses previous review concerns: email failures now roll back the verification record and return an error response (lines 151-160), rate limiting is applied per user+email (lines 84-88), and targetProviders are deferred until after code verification for security (line 173 comment). Type validation and self-merge prevention are correctly implemented.


178-283: LGTM!

The /verify endpoint successfully addresses all previous review concerns: rate limiting prevents brute force attacks (lines 179-184, 210-215), type validation ensures safe string handling (lines 201-208), and provider information is now properly returned after verification (lines 264-282).


334-480: LGTM!

The merge logic is comprehensive and correctly handles all data migration scenarios: accounts, projects, memberships, subscriptions, and media files. The subscription merge logic now properly uses only tier priority without the stripeCustomerId requirement, and the atomic batch transaction ensures data integrity. Error handling is appropriate.


482-511: LGTM!

The /cancel endpoint safely handles cleanup of pending merge requests. The lenient token matching (line 505) allows users to cancel without providing a token, which is acceptable for this non-destructive operation since it's already scoped to the current user.

Comment thread packages/workers/src/routes/account-merge.js
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/workers/src/routes/admin.js (1)

130-161: Provider aggregation works correctly; consider inArray for cleaner code.

The logic correctly fetches linked accounts and attaches provider arrays to each user. The manual SQL IN-clause construction (lines 142-146) is functional but could be simplified using Drizzle's inArray operator.

Optional refactoring to use inArray:

+import { eq, desc, sql, like, or, count, inArray } from 'drizzle-orm';

     if (userIds.length > 0) {
       const accounts = await db
         .select({
           userId: account.userId,
           providerId: account.providerId,
         })
         .from(account)
-        .where(
-          sql`${account.userId} IN (${sql.join(
-            userIds.map(id => sql`${id}`),
-            sql`, `,
-          )})`,
-        );
+        .where(inArray(account.userId, userIds));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f74df93 and d78953a.

📒 Files selected for processing (4)
  • packages/mcp/__tests__/icons.test.js (1 hunks)
  • packages/workers/src/routes/account-merge.js (1 hunks)
  • packages/workers/src/routes/admin.js (5 hunks)
  • packages/workers/src/routes/users.js (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/workers/src/routes/account-merge.js
  • packages/mcp/tests/icons.test.js
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{js,jsx,ts,tsx}: Prefer modern ES6+ syntax and features
Use aliases for imports when appropriate to improve readability

**/*.{js,jsx,ts,tsx}: For UI icons, use the solid-icons library or SVGs only. Do not use emojis.
Prefer modern ES6+ syntax and features
Use aliases for imports when appropriate to improve readability
Prefer using config files rather than hardcoding values
Each file should handle one coherent responsibility
Group related components in subdirectories with an index.js barrel export

Files:

  • packages/workers/src/routes/admin.js
  • packages/workers/src/routes/users.js
**/*.{jsx,tsx,js,ts}

📄 CodeRabbit inference engine (.cursorrules)

Use Solid's createStore for complex state or state objects in SolidJS components

Files:

  • packages/workers/src/routes/admin.js
  • packages/workers/src/routes/users.js
🧬 Code graph analysis (1)
packages/workers/src/routes/admin.js (1)
packages/workers/src/db/schema.js (12)
  • account (42-58)
  • account (42-58)
  • user (5-24)
  • user (5-24)
  • session (27-39)
  • session (27-39)
  • subscriptions (109-124)
  • subscriptions (109-124)
  • twoFactor (127-136)
  • twoFactor (127-136)
  • verification (61-68)
  • verification (61-68)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Workers Builds: corates-workers-prod
  • GitHub Check: Workers Builds: corates
🔇 Additional comments (5)
packages/workers/src/routes/users.js (2)

8-17: LGTM: Comprehensive imports for atomic deletion.

The extended imports correctly include twoFactor and subscriptions, enabling complete user data cleanup in the batch deletion operation below.


160-171: Excellent atomic deletion, but note inconsistency with admin endpoint.

The batch transaction correctly ensures all-or-nothing deletion with proper foreign key ordering. However, line 169 uses eq(verification.identifier, currentUser.email) while the admin endpoint (admin.js line 414) uses like(verification.identifier, \%${userToDelete.email}%`)`.

The eq approach here is more precise and safer — it matches the exact email without wildcards that could accidentally match similar addresses (e.g., pattern "%user@example.com%" would also match "otheruser@example.com").

The admin endpoint should adopt this eq pattern for consistency and correctness.

packages/workers/src/routes/admin.js (3)

8-17: LGTM: Complete schema imports for enhanced admin operations.

The extended imports correctly support both the new provider/account aggregation features and the comprehensive atomic deletion operations.


217-227: LGTM: Clean account fetching for user details.

The linked accounts query correctly retrieves and orders account provider information for the user detail endpoint.


257-269: Excellent use of atomic batch for ban operation.

The batch transaction correctly ensures the user ban and session invalidation happen atomically, preventing inconsistent states where a user might be marked as banned but still have active sessions.

Comment thread packages/workers/src/routes/admin.js
@InfinityBowman InfinityBowman merged commit 56e4de0 into main Dec 15, 2025
2 of 4 checks passed
@InfinityBowman InfinityBowman deleted the account-linking branch December 15, 2025 21:40
@coderabbitai coderabbitai Bot mentioned this pull request Dec 16, 2025
InfinityBowman added a commit that referenced this pull request Dec 17, 2025
This was referenced Dec 19, 2025
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