Skip to content

Fix CLI/template issues; harden auth, migrate/generate flows and realtime#1

Merged
Helal-maker merged 3 commits intomainfrom
codex/review-and-fix-code-warnings-and-issues
Feb 20, 2026
Merged

Fix CLI/template issues; harden auth, migrate/generate flows and realtime#1
Helal-maker merged 3 commits intomainfrom
codex/review-and-fix-code-warnings-and-issues

Conversation

@Helal-maker
Copy link
Copy Markdown
Owner

@Helal-maker Helal-maker commented Feb 20, 2026

Motivation

  • Fix a set of reviewed issues across the CLI and base template to make generated projects cross-platform and safer (TSX compile option, cross-platform scripts, template .gitignore).
  • Harden auth scaffolding and middleware to avoid leaking secrets and to return structured 400 errors for validation failures.
  • Make codegen/migration routines more robust (safe realtime path resolution, quote-aware SQL splitting, safer route injection).
  • Improve realtime server security and reliability by adding auth/authorization, message validation and subscription caps.

Description

  • TypeScript / scripts and template hygiene: added "jsx": "react-jsx" to tsconfig.base.json, updated root typecheck script quoting for cross-platform use, added .gitignore to the base template, and bumped template zod to v4; generator now writes build/start scripts in produced package.json and uses validated env.DB_PATH for generated DB scripts.
  • Auth hardening: changed signup/login flows to use safeParse with structured 400 responses, guarded against empty .returning() results before dereference, changed validateSession to select only id/email/name, replaced fragile regex injection with a safe parser-like injector, and made ensureRoutesIndexHook check anchors and only write when safe (logs warnings otherwise).
  • Generator / scanner / router improvements: fixed canonical realtime template path, added Zod-validated pagination and limit+1/hasMore semantics in generated CRUD routes (with a comment about filter allowlists), made ensureZodValidatorInstalled detect an existing install before running bun add, hoisted the HTTP methods set in the route scanner, and strengthened the schema scanner to return Zod-validated TableInfo records while detecting index calls in chained expressions and keying tables by SQL name.
  • Migrations & realtime: replaced naive semicolon splitting with a quote-aware splitStatements, surfaced a CLI notice that drizzle-kit generate output is for preview while push applies changes, and rewrote the realtime utility to accept authenticated websocket upgrades, validate messages with Zod, enforce subscription/clients caps, and use deep-equality for filter matching.

Testing

  • Ran git diff --check which passed successfully.
  • Attempted bun install in this environment but it failed due to external registry 403 responses, so dependency install and full typecheck could not be completed here.
  • Attempted bun run typecheck / turbo workflows but they were blocked by the failed install / missing tooling in this environment, so no full typecheck run was completed.

Codex Task

Summary by CodeRabbit

  • New Features

    • Validated pagination for list endpoints (limit/offset + hasMore)
    • WebSocket realtime support with token-based connections and subscription limits
    • Added production build and start scripts
  • Bug Fixes

    • Strengthened signup/login payload validation with clear error responses
    • Improved SQL migration parsing to respect string literals
  • Documentation

    • CLI highlights documenting auth setup and CRUD generation
    • Realtime docs referencing the WebSocket endpoint
  • Chores

    • Bumped TypeScript and Zod versions
    • Reorganized migrate commands under a nested structure
  • Tests

    • Expanded tests for route metadata and initialized empty routes object

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

Adds defensive validation and safer parsing across the CLI, restructures migrate commands and signal handling, and modernizes the base template with WebSocket token extraction, a token-aware realtime server, pagination validation, and dependency bumps.

Changes

Cohort / File(s) Summary
Docs & template metadata
betterbase/README.md, betterbase/templates/base/README.md, betterbase/templates/base/.gitignore, betterbase/tsconfig.base.json
Updated README with CLI highlights and realtime docs, added base .gitignore, enabled JSX runtime in tsconfig.
Root & package manifests
betterbase/package.json, betterbase/packages/cli/package.json, betterbase/templates/base/package.json
Adjusted typecheck quoting in root script; bumped CLI TypeScript to ^5.8.0; upgraded template Zod to ^4.0.0.
Auth command & schema injection
betterbase/packages/cli/src/commands/auth.ts
Added defensive JSON parsing and zod safeParse for signup/login, explicit 400/500 responses, safer sqliteTable/passwordHash injection with robust parsing and anchor-based route/index insertions plus warnings.
Generate routes & pagination
betterbase/packages/cli/src/commands/generate.ts
Added zod pagination validation (limit/offset), hasMore detection via fetchLimit+1, updated realtime template path resolution, and conditional installer for @hono/zod-validator.
Init & DB handling
betterbase/packages/cli/src/commands/init.ts, betterbase/templates/base/src/db/migrate.ts
Added build/start scripts for generated package.json; prefer DB_PATH env for local DB; migrations use env.DB_PATH and include success/failure logging.
Migrations parsing
betterbase/packages/cli/src/commands/migrate.ts
Replaced naive SQL splitter with a parser that respects single/double/backtick literals and escapes; added diagnostic logging about preview vs push.
CLI wiring & shutdown
betterbase/packages/cli/src/index.ts
Restructured migrate subcommands under a migrate container; added cleanup routine and explicit SIGINT/SIGTERM/exit handlers for graceful teardown.
Context & route utilities
betterbase/packages/cli/src/utils/context-generator.ts, betterbase/packages/cli/src/utils/route-scanner.ts
Switched console.log to logger.success on context write; moved httpMethods Set to module scope to avoid per-loop allocation.
Schema scanner & types
betterbase/packages/cli/src/utils/scanner.ts
Introduced zod schemas (ColumnTypeSchema, ColumnInfoSchema, TableInfoSchema, TablesRecordSchema) and inferred exported types; validate parsed tables at runtime; hardened index/column parsing with guards.
Realtime server & WebSocket handling
betterbase/templates/base/src/index.ts, betterbase/templates/base/src/lib/realtime.ts
Wrapped upgradeWebSocket to extract token (Authorization header or ?token); added RealtimeServer with authenticate/authorize, per-client/table limits, zod-backed message validation, safeSend/broadcast snapshots, richer error handling, and client identity fields.
Tests
betterbase/packages/cli/test/context-generator.test.ts, betterbase/packages/cli/test/route-scanner.test.ts
Added assertions expecting context.routes = {} for empty/missing cases and asserted HTTP methods for scanned routes.
Template package & index
betterbase/templates/base/package.json, betterbase/templates/base/src/index.ts
Bumped template zod dependency and wired Bun websocket option using token-aware upgrade handler.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant WS as WS Upgrade Handler
    participant Realtime as RealtimeServer
    participant DB as Database

    Client->>WS: Open WS connection (Authorization header or ?token)
    WS->>WS: extract token from header or query
    WS->>Realtime: handleConnection(ws, token)
    Realtime->>Realtime: authenticate(token)
    alt auth fails
        Realtime-->>Client: close with auth error
    else auth succeeds
        Realtime-->>Client: open/ack
        Client->>Realtime: send subscribe/unsubscribe (validated by zod)
        Realtime->>Realtime: authorize(userId, claims, table)
        alt unauthorized or limits hit
            Realtime-->>Client: error message
        else accepted
            Realtime->>DB: (optional) fetch initial data
            Realtime-->>Client: subscribed confirmation
        end
    end
Loading
sequenceDiagram
    participant Client
    participant CLI as Auth Command
    participant DB as Database

    Client->>CLI: POST /signup or /login (JSON)
    CLI->>CLI: try parse JSON, safeParse with zod
    alt invalid payload
        CLI-->>Client: 400 with validation errors
    else valid payload
        CLI->>DB: create or verify user
        alt DB failure or no created user
            CLI-->>Client: 500
        else success
            CLI-->>Client: 200 with user fields (id, email, name)
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble through zod, I stitch each gate,
Tokens and sockets keep the burrow safe,
Parsers that mind each quoted line,
Migrations hum and routes align,
A hoppity cheer for this tidy update! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the primary changes across CLI auth hardening, migration/generate flow improvements, and realtime server security enhancements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/review-and-fix-code-warnings-and-issues

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

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 (6)
betterbase/packages/cli/src/commands/migrate.ts (1)

289-310: Missing escape handling for doubled double-quotes and backticks.

The function correctly handles '' as an escape sequence inside single-quoted strings (lines 291-295), but doesn't apply the same logic to double-quoted identifiers ("") or backtick identifiers (``). Standard SQL uses doubled quotes to escape the quote character itself within identifiers.

For example, "table""name" or `col``name` would be incorrectly parsed as exiting and re-entering the quoted context.

♻️ Proposed fix to handle doubled escape sequences consistently
     if (!inSingle && !inBacktick && ch === '"') {
+      current += ch;
+      if (inDouble && next === '"') {
+        current += next;
+        i += 1;
+        continue;
+      }
       inDouble = !inDouble;
-      current += ch;
       continue;
     }

     if (!inSingle && !inDouble && ch === '`') {
+      current += ch;
+      if (inBacktick && next === '`') {
+        current += next;
+        i += 1;
+        continue;
+      }
       inBacktick = !inBacktick;
-      current += ch;
       continue;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@betterbase/packages/cli/src/commands/migrate.ts` around lines 289 - 310, The
parser currently only treats doubled single quotes as escapes; update the
double-quote and backtick branches to mirror that logic: when encountering ch
=== '"' or ch === '`' (and not in the other quote types) append ch, and if
already inDouble/inBacktick and next equals the same quote char then append
next, increment i, and continue; otherwise toggle inDouble/inBacktick and
continue. Use the existing variables (inDouble, inSingle, inBacktick, current,
ch, next, i) and add the doubled-quote/backtick checks inside the blocks
handling double quotes and backticks.
betterbase/templates/base/src/lib/realtime.ts (3)

76-79: Placeholder authentication returns token as userId.

The authenticate method simply returns the trimmed token as the userId. While this may be intentional as a stub, it conflates authentication tokens with user identity. In production, this should validate the token (e.g., JWT verification) and extract the actual user ID.

If this is a placeholder, consider adding a TODO comment or documenting the expected production implementation.

📝 Suggested documentation
  authenticate(token: string | undefined): { userId: string; claims: string[] } | null {
+   // TODO: Replace with actual token validation (e.g., JWT verification)
+   // This stub returns the token as userId for development purposes only
    if (!token || !token.trim()) return null;
    return { userId: token.trim(), claims: ['realtime:*'] };
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@betterbase/templates/base/src/lib/realtime.ts` around lines 76 - 79, The
authenticate function currently returns the raw trimmed token as userId, which
conflates auth tokens with identity; update authenticate(token) to validate the
token (e.g., verify JWT or call your auth service), extract and return the real
user id and claims instead of the token, and if you intend this to remain a
stub, add a clear TODO comment explaining it is a placeholder and what
production behavior should be (verify signature, check expiry, map claims) so
callers don't rely on token-as-userId; refer to the authenticate method for
changes.

171-183: Consider defensive copy of subscribers set during iteration.

The spread [...subscribers] is good, but modifications to subscribers via handleClose during iteration could cause issues if handleClose triggers other side effects. The current implementation looks safe, but consider documenting this invariant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@betterbase/templates/base/src/lib/realtime.ts` around lines 171 - 183, Make
the iteration over the subscribers set explicitly defensive by materializing a
copy before the loop (e.g., const subs = Array.from(subscribers)) and iterate
over that copy instead of using [...subscribers] inline; this ensures that
concurrent removals triggered by this.handleClose(ws) or other side-effects (and
uses of this.safeSend, this.clients.get, subscription.filter checks via
this.matchesFilter) cannot affect the active iterator. Alternatively, if you
prefer to keep the current behavior, add a clear comment above the loop
documenting the invariant that handleClose may mutate subscribers but iteration
uses a snapshot to avoid concurrent-modification issues.

30-33: Consider adding max length constraints to table names in the schema.

The table field uses .min(1) but has no upper bound, which could allow excessively long table names that consume memory in the subscription maps.

🛡️ Suggested constraint
 const messageSchema = z.union([
-  z.object({ type: z.literal('subscribe'), table: z.string().min(1), filter: z.record(z.string(), z.unknown()).optional() }),
-  z.object({ type: z.literal('unsubscribe'), table: z.string().min(1) }),
+  z.object({ type: z.literal('subscribe'), table: z.string().min(1).max(128), filter: z.record(z.string(), z.unknown()).optional() }),
+  z.object({ type: z.literal('unsubscribe'), table: z.string().min(1).max(128) }),
 ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@betterbase/templates/base/src/lib/realtime.ts` around lines 30 - 33, The
schema for messageSchema allows unbounded table names (z.string().min(1)) which
can enable extremely long names; update both branch schemas for the 'subscribe'
and 'unsubscribe' objects to add a reasonable upper bound (e.g., .max(255) or
.max(100)) on the table field so the Zod validators reject excessively long
table names and protect subscription maps; modify the z.string().min(1)
occurrences inside messageSchema to z.string().min(1).max(<chosenLimit>) for
both the subscribe and unsubscribe variants.
betterbase/packages/cli/src/utils/scanner.ts (2)

96-99: Clarify table key fallback logic.

The code uses tableObj.name || declaration.name.text as the key. This means if the parsed table name is an empty string, it falls back to the declaration name. Consider whether this is the intended behavior or if only undefined/missing names should trigger the fallback.

📝 More explicit fallback
             const tableObj = this.parseTable(initializer);
-            const tableKey = tableObj.name || declaration.name.text;
+            const tableKey = tableObj.name.trim() || declaration.name.text;
             tables[tableKey] = tableObj;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@betterbase/packages/cli/src/utils/scanner.ts` around lines 96 - 99, The
current key uses a falsy check so an empty string tableObj.name falls back to
declaration.name.text; update the fallback so only undefined or null trigger it:
when handling sqliteTable/pgTable/mysqlTable (the block that calls
this.parseTable and sets tables[tableKey] = tableObj), compute tableKey by
treating tableObj.name as missing only if it is nullish (use a null/undefined
check or nullish coalescing) rather than any falsy value, or explicitly prefer
non-empty strings (e.g., prefer tableObj.name when !== ''), to preserve an
intentionally empty name if that is valid.

169-188: Potential infinite loop in parseIndexes if AST is malformed.

The while (ts.isCallExpression(value)) loop relies on breaking conditions that may not be met for all AST shapes. If value.expression is a CallExpression but not a PropertyAccessExpression, and the call name doesn't match index/uniqueIndex, the loop breaks correctly. However, consider adding a max iteration guard for defensive coding.

🛡️ Defensive iteration limit
         let value = unwrapExpression(property.initializer);
+        let iterations = 0;
+        const maxIterations = 100;
-        while (ts.isCallExpression(value)) {
+        while (ts.isCallExpression(value) && iterations++ < maxIterations) {
           const callName = getCallName(value);
           // ... rest of loop
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@betterbase/packages/cli/src/utils/scanner.ts` around lines 169 - 188, The
while loop in parseIndexes that traverses call expressions using
unwrapExpression/getCallName can spin indefinitely on malformed AST; add a
defensive max-iteration guard (e.g., const MAX_ITER = 50) and an iteration
counter that increments each loop iteration and forces a break (and optionally
logs a warning) if exceeded, placing the counter check at the top or bottom of
the while block so functions unwrapExpression, getCallName and the existing
break/continue logic remain unchanged except for the added counter and forced
exit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@betterbase/packages/cli/src/commands/auth.ts`:
- Around line 46-52: Wrap the await c.req.json() call in a try/catch before
calling signupSchema.safeParse so malformed JSON returns a 400 instead of
throwing; inside the catch return c.json({ error: 'Invalid JSON', details:
err.message }, 400). Apply the same pattern to the other handler(s) (e.g., the
handler around lines 78-85) where c.req.json() is used before a safeParse,
ensuring you only call signupSchema.safeParse (or the appropriate schema) with a
successfully parsed body.

---

Nitpick comments:
In `@betterbase/packages/cli/src/commands/migrate.ts`:
- Around line 289-310: The parser currently only treats doubled single quotes as
escapes; update the double-quote and backtick branches to mirror that logic:
when encountering ch === '"' or ch === '`' (and not in the other quote types)
append ch, and if already inDouble/inBacktick and next equals the same quote
char then append next, increment i, and continue; otherwise toggle
inDouble/inBacktick and continue. Use the existing variables (inDouble,
inSingle, inBacktick, current, ch, next, i) and add the doubled-quote/backtick
checks inside the blocks handling double quotes and backticks.

In `@betterbase/packages/cli/src/utils/scanner.ts`:
- Around line 96-99: The current key uses a falsy check so an empty string
tableObj.name falls back to declaration.name.text; update the fallback so only
undefined or null trigger it: when handling sqliteTable/pgTable/mysqlTable (the
block that calls this.parseTable and sets tables[tableKey] = tableObj), compute
tableKey by treating tableObj.name as missing only if it is nullish (use a
null/undefined check or nullish coalescing) rather than any falsy value, or
explicitly prefer non-empty strings (e.g., prefer tableObj.name when !== ''), to
preserve an intentionally empty name if that is valid.
- Around line 169-188: The while loop in parseIndexes that traverses call
expressions using unwrapExpression/getCallName can spin indefinitely on
malformed AST; add a defensive max-iteration guard (e.g., const MAX_ITER = 50)
and an iteration counter that increments each loop iteration and forces a break
(and optionally logs a warning) if exceeded, placing the counter check at the
top or bottom of the while block so functions unwrapExpression, getCallName and
the existing break/continue logic remain unchanged except for the added counter
and forced exit.

In `@betterbase/templates/base/src/lib/realtime.ts`:
- Around line 76-79: The authenticate function currently returns the raw trimmed
token as userId, which conflates auth tokens with identity; update
authenticate(token) to validate the token (e.g., verify JWT or call your auth
service), extract and return the real user id and claims instead of the token,
and if you intend this to remain a stub, add a clear TODO comment explaining it
is a placeholder and what production behavior should be (verify signature, check
expiry, map claims) so callers don't rely on token-as-userId; refer to the
authenticate method for changes.
- Around line 171-183: Make the iteration over the subscribers set explicitly
defensive by materializing a copy before the loop (e.g., const subs =
Array.from(subscribers)) and iterate over that copy instead of using
[...subscribers] inline; this ensures that concurrent removals triggered by
this.handleClose(ws) or other side-effects (and uses of this.safeSend,
this.clients.get, subscription.filter checks via this.matchesFilter) cannot
affect the active iterator. Alternatively, if you prefer to keep the current
behavior, add a clear comment above the loop documenting the invariant that
handleClose may mutate subscribers but iteration uses a snapshot to avoid
concurrent-modification issues.
- Around line 30-33: The schema for messageSchema allows unbounded table names
(z.string().min(1)) which can enable extremely long names; update both branch
schemas for the 'subscribe' and 'unsubscribe' objects to add a reasonable upper
bound (e.g., .max(255) or .max(100)) on the table field so the Zod validators
reject excessively long table names and protect subscription maps; modify the
z.string().min(1) occurrences inside messageSchema to
z.string().min(1).max(<chosenLimit>) for both the subscribe and unsubscribe
variants.

Comment thread betterbase/packages/cli/src/commands/auth.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d1506487b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (existsSync(realtimePath)) return;

const canonicalRealtimePath = path.resolve(import.meta.dir, '../../../templates/base/src/lib/realtime.ts');
const canonicalRealtimePath = path.resolve(import.meta.dir, '../../../../templates/base/src/lib/realtime.ts');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resolve realtime template path for bundled CLI

ensureRealtimeUtility resolves the canonical template from import.meta.dir using four .. segments, which only works when running from src in dev mode. The shipped bb binary runs from packages/cli/dist (bin points to ./dist/index.js), so this path resolves outside the repo and bb generate crud ... throws Canonical realtime template not found whenever src/lib/realtime.ts is missing.

Useful? React with 👍 / 👎.

Comment on lines +78 to +82
return { userId: token.trim(), claims: ['realtime:*'] };
}

authorize(userId: string, claims: string[], table: string): boolean {
return Boolean(userId) && (claims.includes('realtime:*') || claims.includes(`realtime:${table}`));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate websocket tokens before granting wildcard claims

The new auth path accepts any non-empty token and immediately assigns realtime:*, and authorize trusts that claim for every table. In practice, an unauthenticated client can connect with any arbitrary ?token= or Authorization value and subscribe to all realtime streams, so this does not enforce actual authentication/authorization despite the new checks.

Useful? React with 👍 / 👎.

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

🧹 Nitpick comments (3)
betterbase/packages/cli/src/commands/migrate.ts (1)

396-397: Consider simplifying the diagnostic message.

The note is helpful but verbose. A more concise version improves readability:

♻️ Suggested wording
   logger.info('Applying migrations with drizzle-kit push...');
-  logger.info('Note: drizzle-kit generate produced files in drizzle/ for preview/diff analysis only; push is what applied changes in this run.');
+  logger.info('Note: generated files in drizzle/ are for preview only; push applies the actual changes.');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@betterbase/packages/cli/src/commands/migrate.ts` around lines 396 - 397,
Replace the verbose diagnostic logged in migrate.ts with a more concise message:
update the second logger.info call (the one after logger.info('Applying
migrations with drizzle-kit push...')) to a shorter string such as "drizzle/
files are for preview; push applied changes." so the intent remains but the
output is more readable.
betterbase/packages/cli/src/utils/scanner.ts (2)

109-114: Consider using parse() instead of safeParse() when throwing on failure.

Since invalid output always throws, TablesRecordSchema.parse(tables) would be simpler and produce the same behavior with built-in Zod error formatting.

♻️ Suggested simplification
-    const validated = TablesRecordSchema.safeParse(tables);
-    if (!validated.success) {
-      throw new Error(`Schema scanner produced invalid output: ${JSON.stringify(validated.error.format())}`);
-    }
-
-    return validated.data;
+    return TablesRecordSchema.parse(tables);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@betterbase/packages/cli/src/utils/scanner.ts` around lines 109 - 114, Replace
the safeParse + manual throw pattern with a direct parse call on
TablesRecordSchema: instead of calling TablesRecordSchema.safeParse(tables),
inspecting validated.success and throwing with
JSON.stringify(validated.error.format()), call TablesRecordSchema.parse(tables)
and return its result; this preserves the throw-on-failure behavior and uses
Zod's built-in error formatting (update the return to return the parse result
and remove the validated variable and custom throw logic).

170-177: Silent break on MAX_ITER may hide parsing issues.

When MAX_ITER is exceeded, the loop breaks without any indication. For complex schemas, this could silently skip indexes. Consider logging a warning for debuggability.

♻️ Add warning when limit is reached
         if (iter > MAX_ITER) {
+          console.warn(`parseIndexes: exceeded MAX_ITER (${MAX_ITER}) for property, skipping further traversal`);
           break;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@betterbase/packages/cli/src/utils/scanner.ts` around lines 170 - 177, The
loop that walks nested call expressions (while (ts.isCallExpression(value))
using MAX_ITER/iter) silently breaks when iter > MAX_ITER; change this to emit a
visible warning instead of silently breaking: after detecting iter > MAX_ITER,
call the project's logger (or console.warn if no logger) with context (mention
MAX_ITER was hit, include the node's text or identifier such as value.getText()
and any parent/index info) so callers can debug skipped indexes; keep the
existing break/return behavior but ensure the warning message is informative and
uses the same logging facility as the surrounding code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@betterbase/templates/base/src/lib/realtime.ts`:
- Around line 207-222: The cap checks in the subscription flow incorrectly block
re-subscribes/updates by the same client when caps are reached; change the
checks in the subscription handler to bypass the per-client and per-table limits
if the client is already subscribed to that table: before enforcing
this.config.maxSubscriptionsPerClient, check client.subscriptions.has(table) and
skip the client-cap error if true; before enforcing
this.config.maxSubscribersPerTable, check tableSet.has(ws) and skip the
table-cap error if true; then update the existing entry in client.subscriptions
(and ensure tableSubscribers still contains ws) instead of treating it as a new
subscription.
- Around line 76-90: The authenticate method currently grants a wildcard claim
when rawClaims is missing which gives full access for tokens like "user" or
"user:"; change authenticate (function authenticate) to avoid defaulting to
['realtime:*'] — parse rawClaims into claims only when present and non-empty
(e.g., produce an empty array when rawClaims is undefined/empty) and ensure
authorize (method authorize) continues to check for 'realtime:*' or
`realtime:${table}` against the claims array so that tokens without explicit
claims have no privileges.

---

Nitpick comments:
In `@betterbase/packages/cli/src/commands/migrate.ts`:
- Around line 396-397: Replace the verbose diagnostic logged in migrate.ts with
a more concise message: update the second logger.info call (the one after
logger.info('Applying migrations with drizzle-kit push...')) to a shorter string
such as "drizzle/ files are for preview; push applied changes." so the intent
remains but the output is more readable.

In `@betterbase/packages/cli/src/utils/scanner.ts`:
- Around line 109-114: Replace the safeParse + manual throw pattern with a
direct parse call on TablesRecordSchema: instead of calling
TablesRecordSchema.safeParse(tables), inspecting validated.success and throwing
with JSON.stringify(validated.error.format()), call
TablesRecordSchema.parse(tables) and return its result; this preserves the
throw-on-failure behavior and uses Zod's built-in error formatting (update the
return to return the parse result and remove the validated variable and custom
throw logic).
- Around line 170-177: The loop that walks nested call expressions (while
(ts.isCallExpression(value)) using MAX_ITER/iter) silently breaks when iter >
MAX_ITER; change this to emit a visible warning instead of silently breaking:
after detecting iter > MAX_ITER, call the project's logger (or console.warn if
no logger) with context (mention MAX_ITER was hit, include the node's text or
identifier such as value.getText() and any parent/index info) so callers can
debug skipped indexes; keep the existing break/return behavior but ensure the
warning message is informative and uses the same logging facility as the
surrounding code.

Comment on lines +239 to +331
const usersExportIdx = current.search(/export\s+const\s+users\s*=\s*sqliteTable\s*\(/);
if (usersExportIdx === -1) {
logger.warn('Could not find sqlite users table block; skipping passwordHash injection.');
return;
}

const replacement = usersBlock[0].replace(/\n\}\);$/, "\n passwordHash: text('password_hash').notNull(),\n});");
writeFileSync(schemaPath, current.replace(usersBlock[0], replacement));
const callStart = current.indexOf('sqliteTable(', usersExportIdx);
if (callStart === -1) {
logger.warn('Could not locate sqliteTable call for users; skipping passwordHash injection.');
return;
}

let i = callStart;
let parenDepth = 0;
let inSingle = false;
let inDouble = false;
let inBacktick = false;
let escaped = false;

while (i < current.length) {
const ch = current[i];
const next = current[i + 1];

if (escaped) {
escaped = false;
i += 1;
continue;
}

if ((inSingle || inDouble || inBacktick) && ch === '\\') {
escaped = true;
i += 1;
continue;
}

if (!inDouble && !inBacktick && ch === "'") {
if (inSingle && next === "'") {
i += 2;
continue;
}
inSingle = !inSingle;
i += 1;
continue;
}

if (!inSingle && !inBacktick && ch === '"') {
inDouble = !inDouble;
i += 1;
continue;
}

if (!inSingle && !inDouble && ch === '`') {
inBacktick = !inBacktick;
i += 1;
continue;
}

if (inSingle || inDouble || inBacktick) {
i += 1;
continue;
}

if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
break;
}
}

i += 1;
}

if (i >= current.length || parenDepth !== 0) {
logger.warn('Could not safely parse users sqliteTable block; skipping passwordHash injection.');
return;
}

const statementEnd = current.indexOf(';', i);
if (statementEnd === -1) {
logger.warn('Could not locate end of users sqliteTable statement; skipping passwordHash injection.');
return;
}

const usersBlock = current.slice(usersExportIdx, statementEnd + 1);
const replacement = usersBlock.replace(/\n\}\);\s*$/, "\n passwordHash: text('password_hash').notNull(),\n});");
if (replacement === usersBlock) {
logger.warn('Could not inject passwordHash into users table; block layout was unexpected.');
return;
}

writeFileSync(schemaPath, `${current.slice(0, usersExportIdx)}${replacement}${current.slice(statementEnd + 1)}`);
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:

# First, let's find the auth.ts file and examine the context
find . -name "auth.ts" -path "*/cli/src/commands/*" 2>/dev/null | head -5

Repository: Helal-maker/Betterbase

Length of output: 112


🏁 Script executed:

# Search for sqliteTable calls in the codebase to see actual usage patterns
rg "sqliteTable\s*\(" -A 3 -B 1 --max-count=20

Repository: Helal-maker/Betterbase

Length of output: 5858


🏁 Script executed:

# More specifically, search for 3-argument sqliteTable patterns (with callbacks)
rg "sqliteTable\s*\(\s*['\"]" -A 5 | head -50

Repository: Helal-maker/Betterbase

Length of output: 3631


🏁 Script executed:

# Read the exact lines 239-331 in auth.ts
sed -n '239,331p' betterbase/packages/cli/src/commands/auth.ts

Repository: Helal-maker/Betterbase

Length of output: 2444


🏁 Script executed:

# Also search for all sqliteTable calls with 3 arguments to confirm they exist
rg "sqliteTable\s*\(['\"].*?['\"],\s*\{[^}]*\},\s*\(" -A 3 --max-count=10

Repository: Helal-maker/Betterbase

Length of output: 48


🏁 Script executed:

# Let me verify the exact pattern when a 3-arg sqliteTable call is used
cat -n betterbase/packages/cli/test/scanner.test.ts | sed -n '20,35p'

Repository: Helal-maker/Betterbase

Length of output: 822


🏁 Script executed:

# Extract a more complete view of the 3-arg sqliteTable usage in the test
rg -A 10 "export const users = sqliteTable\('users'" betterbase/packages/cli/test/scanner.test.ts | head -15

Repository: Helal-maker/Betterbase

Length of output: 539


🏁 Script executed:

# Test the proposed regex patterns to see if they correctly detect callbacks
python3 << 'EOF'
import re

# Sample 3-arg sqliteTable code
code_3arg = """}), (table) => ({
    usersEmailIdx: index('users_email_idx').on(table.email),
  }));"""

code_2arg = """}
);"""

# Test the proposed patterns
pattern1 = r'/\},\s*\(\s*\w+\s*\)\s*=>/'
pattern1_fixed = r'\},\s*\(\s*\w+\s*\)\s*=>'
pattern2_fixed = r'\},\s*function\s*\('

print("Testing with 3-arg callback:")
print(f"Pattern 1 match: {bool(re.search(pattern1_fixed, code_3arg))}")
print(f"Pattern 2 match: {bool(re.search(pattern2_fixed, code_3arg))}")

print("\nTesting with 2-arg (no callback):")
print(f"Pattern 1 match: {bool(re.search(pattern1_fixed, code_2arg))}")
print(f"Pattern 2 match: {bool(re.search(pattern2_fixed, code_2arg))}")

# Test the replacement pattern
replacement_pattern = r'\n\}\);\s*$'
test_block = "export const users = sqliteTable('users', {\n  id: text('id'),\n});"
print(f"\nReplacement pattern matches 2-arg: {bool(re.search(replacement_pattern, test_block))}")

test_block_3arg = "export const users = sqliteTable('users', {\n  id: text('id'),\n}), (table) => ({\n  idx: index('idx')\n}));"
print(f"Replacement pattern matches 3-arg: {bool(re.search(replacement_pattern, test_block_3arg))}")
if re.search(replacement_pattern, test_block_3arg):
    print(f"  Would match at: {repr(re.search(replacement_pattern, test_block_3arg).group())}")
EOF

Repository: Helal-maker/Betterbase

Length of output: 303


🏁 Script executed:

# Test corrected patterns for the actual 3-arg sqliteTable format
python3 << 'EOF'
import re

# Real 3-arg sqliteTable code from the test file
code_3arg = """          }, (table) => ({
            usersEmailIdx: index('users_email_idx').on(table.email),
          }));"""

code_2arg = """          }
        );"""

# The actual pattern in the code: }, (table) => ({
pattern_correct = r'\}\),\s*\(\s*\w+\s*\)\s*=>'

print("Testing with actual 3-arg callback pattern:")
print(f"Corrected pattern match: {bool(re.search(pattern_correct, code_3arg))}")
print(f"Match found: {re.search(pattern_correct, code_3arg).group() if re.search(pattern_correct, code_3arg) else 'None'}")

print("\nTesting with 2-arg (no callback):")
print(f"Corrected pattern match: {bool(re.search(pattern_correct, code_2arg))}")

# Test the replacement patterns
print("\n--- Testing replacement patterns ---")
block_2arg = """export const users = sqliteTable('users', {
  id: text('id'),
});"""

block_3arg = """export const users = sqliteTable('users', {
  id: text('id'),
}), (table) => ({
  idx: index('idx')
}));"""

# The code uses this pattern:
replacement_pattern = r'\n\}\);\s*$'

print(f"\n2-arg block ends with: {repr(block_2arg[-15:])}")
print(f"Pattern matches 2-arg: {bool(re.search(replacement_pattern, block_2arg))}")

print(f"\n3-arg block ends with: {repr(block_3arg[-15:])}")
print(f"Pattern matches 3-arg: {bool(re.search(replacement_pattern, block_3arg))}")

# What would happen if we try to inject into 3-arg?
if re.search(replacement_pattern, block_2arg):
    result_2arg = re.sub(replacement_pattern, "\n  passwordHash: text('password_hash').notNull(),\n});", block_2arg)
    print(f"\n2-arg injection result:\n{result_2arg}")

if re.search(replacement_pattern, block_3arg):
    result_3arg = re.sub(replacement_pattern, "\n  passwordHash: text('password_hash').notNull(),\n});", block_3arg)
    print(f"\n3-arg injection result:\n{result_3arg}")
else:
    print(f"\n3-arg block: Pattern does NOT match (good - injection won't happen incorrectly)")
EOF

Repository: Helal-maker/Betterbase

Length of output: 626


🏁 Script executed:

# Let's verify exactly what happens with the actual usersBlock when we have a 3-arg call
python3 << 'EOF'
import re

# Simulate what the code extracts as usersBlock for a 3-arg call
usersBlock_3arg = """export const users = sqliteTable('users', {
  id: text('id'),
  email: text('email').notNull().unique(),
  age: integer('age').default(18),
}, (table) => ({
  usersEmailIdx: index('users_email_idx').on(table.email),
}));"""

usersBlock_2arg = """export const users = sqliteTable('users', {
  id: text('id'),
  email: text('email').notNull().unique(),
  age: integer('age').default(18),
});"""

# The actual replacement pattern from the code
replacement_pattern = r'\n\}\);\s*$'
replacement_text = "\n  passwordHash: text('password_hash').notNull(),\n});"

# Test with 2-arg
result_2arg = usersBlock_2arg.replace(replacement_pattern, replacement_text)
print("2-arg replacement:")
print(f"Changed: {result_2arg != usersBlock_2arg}")
print(f"Result ends with: {repr(result_2arg[-50:])}\n")

# Test with 3-arg
result_3arg = usersBlock_3arg.replace(replacement_pattern, replacement_text)
print("3-arg replacement:")
print(f"Changed: {result_3arg != usersBlock_3arg}")
print(f"Original ends with: {repr(usersBlock_3arg[-50:])}")
if result_3arg == usersBlock_3arg:
    print("⚠️  REPLACEMENT FAILED - would trigger the warning and return early")
else:
    print(f"Result ends with: {repr(result_3arg[-50:])}")

# Now test the proposed detection patterns from the review
print("\n--- Testing proposed detection patterns ---")
pattern1 = r'\},\s*\(\s*\w+\s*\)\s*=>'
pattern2 = r'\},\s*function\s*\('

print(f"Pattern 1 (arrow fn) matches 3-arg: {bool(re.search(pattern1, usersBlock_3arg))}")
print(f"Pattern 2 (function) matches 3-arg: {bool(re.search(pattern2, usersBlock_3arg))}")
EOF

Repository: Helal-maker/Betterbase

Length of output: 477


Provide clearer diagnostics for unsupported 3-arg sqliteTable callbacks.

The code already prevents schema corruption when encountering 3-arg sqliteTable calls with callbacks—the replacement pattern /\n\}\);\s*$/ only matches the 2-arg syntax ending with });, so injection safely fails and logs a generic warning. However, a more specific warning would improve debugging. The suggested detection fix is partially correct: the arrow function pattern (\},\s*\(\s*\w+\s*\)\s*=>) does match the callback syntax, but the function pattern alone is incomplete. Consider adding an explicit callback detection to distinguish this case from other layout failures.

Comment thread betterbase/templates/base/src/lib/realtime.ts
Comment thread betterbase/templates/base/src/lib/realtime.ts Outdated
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.

🧹 Nitpick comments (1)
betterbase/packages/cli/src/utils/scanner.ts (1)

98-101: The table-key keying concern is valid for edge cases, but they do not occur in this codebase.

All table declarations in the codebase use string literals (e.g., sqliteTable('users', {...})), so tableObj.name always contains the correct value. The edge case you've identified—where a non-string-literal argument like sqliteTable(tableName, {...}) would cause tableObj.name to contain raw expression text instead of the intended table name—has no real-world instances here. The current fallback to declaration.name.text would only matter in such edge cases, and they don't exist. The suggested fix is a defensive improvement but adds complexity for a theoretical scenario.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@betterbase/packages/cli/src/utils/scanner.ts` around lines 98 - 101, The
current code uses tableObj.name ?? declaration.name.text to key the tables map,
which can capture raw expression text for non-string-literal table initializers;
update the logic in the block that calls parseTable (around where tableObj,
tableKey and tables[...] are set) to only use tableObj.name if it is a string
literal (or otherwise a safe string), otherwise skip adding the entry (or log a
warning) instead of falling back to declaration.name.text; reference the
parseTable result (tableObj.name), the declaration identifier
(declaration.name.text) and the tables map when implementing the guarded check
so the map only gets valid literal table names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@betterbase/packages/cli/src/utils/scanner.ts`:
- Around line 98-101: The current code uses tableObj.name ??
declaration.name.text to key the tables map, which can capture raw expression
text for non-string-literal table initializers; update the logic in the block
that calls parseTable (around where tableObj, tableKey and tables[...] are set)
to only use tableObj.name if it is a string literal (or otherwise a safe
string), otherwise skip adding the entry (or log a warning) instead of falling
back to declaration.name.text; reference the parseTable result (tableObj.name),
the declaration identifier (declaration.name.text) and the tables map when
implementing the guarded check so the map only gets valid literal table names.

@Helal-maker Helal-maker merged commit ab25087 into main Feb 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant